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:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user