Add background image upload and real-time layer editing

- Background layer: color picker (real-time) + image upload + clear
- Image preview shows in sidebar after upload
- Uploaded image scales to fill the card dimensions
- All layer properties now update canvas in real-time (no apply button needed)
- Border: color + thickness slider real-time
- Text: font size slider real-time
- Click canvas objects to select layer and sync properties
This commit is contained in:
Poker Design Developer
2026-05-31 22:53:08 +08:00
parent ab7c6c4474
commit bde508dcfe

View File

@@ -37,21 +37,33 @@
</div>
<h3 style="margin: 25px 0 12px 0; font-size: 14px; color: #888;">属性编辑</h3>
<div v-if="activeLayer === 'bg'" style="display: flex; flex-direction: column; gap: 8px;">
<label style="font-size: 12px; color: #aaa;">背景颜色</label>
<input v-model="bgColor" type="color" @input="updateBgColor" style="width: 100%; height: 30px; border: none; border-radius: 4px; cursor: pointer;">
<button @click="applyBgColor" style="padding: 6px 12px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">应用背景色</button>
<div v-if="activeLayer === 'bg'" style="display: flex; flex-direction: column; gap: 10px;">
<label style="font-size: 12px; color: #aaa;">背景颜色实时预览</label>
<input v-model="bgColor" type="color" @input="onBgColorChange" style="width: 100%; height: 32px; border: none; border-radius: 4px; cursor: pointer;">
<div style="border-top: 1px solid #333; margin: 4px 0;"></div>
<label style="font-size: 12px; color: #aaa;">上传背景图片</label>
<input ref="bgFileInput" type="file" accept="image/*" @change="onBgImageUpload" style="display: none;">
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button @click="triggerBgUpload" style="flex: 1; padding: 8px 12px; background: #16213e; color: #aaa; border: 1px solid #444; border-radius: 4px; cursor: pointer; font-size: 12px; white-space: nowrap;">选择图片</button>
<button v-if="bgImageUrl" @click="clearBgImage" style="padding: 8px 12px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">清除</button>
</div>
<div v-if="bgImageUrl" style="margin-top: 4px;">
<div style="font-size: 10px; color: #666; margin-bottom: 4px;">图片预览:</div>
<img :src="bgImageUrl" style="width: 100%; max-height: 100px; object-fit: contain; border-radius: 4px; border: 1px solid #333;">
</div>
</div>
<div v-if="activeLayer === 'border'" style="display: flex; flex-direction: column; gap: 8px;">
<label style="font-size: 12px; color: #aaa;">边框颜色</label>
<input v-model="borderColor" type="color" @input="updateBorderColor" style="width: 100%; height: 30px; border: none; border-radius: 4px; cursor: pointer;">
<label style="font-size: 12px; color: #aaa;">边框颜色实时</label>
<input v-model="borderColor" type="color" @input="onBorderColorChange" style="width: 100%; height: 32px; border: none; border-radius: 4px; cursor: pointer;">
<label style="font-size: 12px; color: #aaa;">边框粗细: {{ borderWidth }}px</label>
<input v-model="borderWidth" type="range" min="0" max="10" step="1" @input="applyBorder" style="width: 100%; cursor: pointer;">
<button @click="applyBorder" style="padding: 6px 12px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">应用边框</button>
<input v-model="borderWidth" type="range" min="0" max="20" step="1" @input="onBorderChange" style="width: 100%; cursor: pointer;">
</div>
<div v-if="activeLayer === 'text'" style="display: flex; flex-direction: column; gap: 8px;">
<label style="font-size: 12px; color: #aaa;">字体大小: {{ textSize }}px</label>
<input v-model="textSize" type="range" min="12" max="120" step="2" @input="applyTextStyle" style="width: 100%; cursor: pointer;">
<label style="font-size: 12px; color: #aaa;">字体大小实时: {{ textSize }}px</label>
<input v-model="textSize" type="range" min="12" max="120" step="2" @input="onTextSizeChange" style="width: 100%; cursor: pointer;">
</div>
<div v-if="!['bg','border','text','pattern'].includes(activeLayer)" style="color: #888; font-size: 12px; padding: 10px;">
选中画布上的对象查看属性
@@ -78,7 +90,7 @@
<script setup>
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Canvas, FabricText, Rect } from 'fabric'
import { Canvas, FabricText, Rect, FabricImage } from 'fabric'
import axios from 'axios'
const route = useRoute()
@@ -107,39 +119,87 @@ const currentCards = computed(() => {
})
const layers = ref([
{ id: 'bg', name: '背景层', visible: true, zIndex: 0, fillColor: '#ffffff' },
{ id: 'bg', name: '背景层', visible: true, zIndex: 0, fillColor: '#ffffff', imageData: null },
{ id: 'border', name: '边框层', visible: true, zIndex: 1, strokeColor: '#333333', strokeWidth: 2 },
{ id: 'pattern', name: '图案层', visible: true, zIndex: 2 },
{ id: 'text', name: '文字层', visible: true, zIndex: 3, textSize: 32 }
])
const bgColor = ref('#ffffff')
const bgImageUrl = ref(null)
const borderColor = ref('#333333')
const borderWidth = ref(2)
const textSize = ref(32)
function updateBgColor() {
const l = layers.value.find(x => x.id === 'bg')
if (l) l.fillColor = bgColor.value
const bgFileInput = ref(null)
function triggerBgUpload() {
bgFileInput.value?.click()
}
function applyBgColor() {
updateBgColor()
function onBgImageUpload(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (ev) => {
bgImageUrl.value = ev.target.result
const l = layers.value.find(x => x.id === 'bg')
if (l) l.imageData = ev.target.result
drawCard()
}
reader.readAsDataURL(file)
}
function clearBgImage() {
bgImageUrl.value = null
const l = layers.value.find(x => x.id === 'bg')
if (l) l.imageData = null
drawCard()
}
function applyBorder() {
function onBgColorChange() {
const l = layers.value.find(x => x.id === 'bg')
if (l) l.fillColor = bgColor.value
drawCard()
}
function toggleLayer(layer) {
activeLayer.value = layer.id
layer.visible = !layer.visible
syncLayerProps()
drawCard()
}
function syncLayerProps() {
const bgLayer = layers.value.find(x => x.id === 'bg')
if (bgLayer) {
bgColor.value = bgLayer.fillColor
bgImageUrl.value = bgLayer.imageData
}
const bLayer = layers.value.find(x => x.id === 'border')
if (bLayer) {
borderColor.value = bLayer.strokeColor
borderWidth.value = bLayer.strokeWidth
}
const tLayer = layers.value.find(x => x.id === 'text')
if (tLayer) {
textSize.value = tLayer.textSize
}
}
function onBorderColorChange() {
const l = layers.value.find(x => x.id === 'border')
if (l) l.strokeColor = borderColor.value
drawCard()
}
function onBorderChange() {
const l = layers.value.find(x => x.id === 'border')
if (l) { l.strokeColor = borderColor.value; l.strokeWidth = parseInt(borderWidth.value) }
drawCard()
}
function updateBorderColor() {
const l = layers.value.find(x => x.id === 'border')
if (l) l.strokeColor = borderColor.value
}
function applyTextStyle() {
function onTextSizeChange() {
const l = layers.value.find(x => x.id === 'text')
if (l) l.textSize = parseInt(textSize.value)
drawCard()
@@ -153,12 +213,6 @@ function isRed(suit) {
return suit === 'heart' || suit === 'diamond'
}
function toggleLayer(layer) {
activeLayer.value = layer.id
layer.visible = !layer.visible
drawCard()
}
function moveLayer(layer, delta) {
const newZ = layer.zIndex + delta
const swapped = layers.value.find(l => l.zIndex === newZ)
@@ -196,6 +250,7 @@ function initCanvas() {
const obj = e.selected[0]
if (obj && obj.layerId) {
activeLayer.value = obj.layerId
syncLayerProps()
}
})
@@ -203,6 +258,7 @@ function initCanvas() {
const obj = e.selected[0]
if (obj && obj.layerId) {
activeLayer.value = obj.layerId
syncLayerProps()
}
})
@@ -227,9 +283,22 @@ function drawCard() {
if (!layer.visible) continue
if (layer.id === 'bg') {
const bg = new Rect({ left: 0, top: 0, width: cardW, height: cardH, fill: layer.fillColor || '#ffffff', selectable: true, hasControls: false, lockMovementX: true, lockMovementY: true, stroke: 'transparent', strokeWidth: 0 })
bg.layerId = 'bg'
c.add(bg)
if (layer.imageData) {
const bgImg = new Image()
bgImg.crossOrigin = 'anonymous'
bgImg.onload = () => {
const fImg = new FabricImage(bgImg, { left: 0, top: 0, selectable: true, hasControls: false, lockMovementX: true, lockMovementY: true })
fImg.scaleX = cardW / bgImg.width
fImg.scaleY = cardH / bgImg.height
fImg.layerId = 'bg'
c.insertAt(0, fImg)
c.renderAll()
}
bgImg.src = layer.imageData
}
const bgRect = new Rect({ left: 0, top: 0, width: cardW, height: cardH, fill: layer.imageData ? 'transparent' : (layer.fillColor || '#ffffff'), selectable: true, hasControls: false, lockMovementX: true, lockMovementY: true, stroke: 'transparent', strokeWidth: 0 })
bgRect.layerId = 'bg'
c.add(bgRect)
}
if (layer.id === 'border') {