Add interactive layer management to Editor
- Click layer to toggle visibility and redraw canvas - Click ▲/▼ buttons to reorder layers (z-index) - Active layer highlighted with left red border - Each layer now actually renders: bg/white, border/frame, pattern/suit symbol, text/corner labels - Fix fabric.js imports (Canvas, FabricText, Rect)
This commit is contained in:
18
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
18
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,43 +1,43 @@
|
|||||||
{
|
{
|
||||||
"hash": "8c4b68ba",
|
"hash": "76c4bd08",
|
||||||
"configHash": "34f6f529",
|
"configHash": "3c200fce",
|
||||||
"lockfileHash": "99e89a35",
|
"lockfileHash": "99e89a35",
|
||||||
"browserHash": "6a02d3bf",
|
"browserHash": "df8b7ff8",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "9022993e",
|
"fileHash": "a369977c",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus": {
|
"element-plus": {
|
||||||
"src": "../../element-plus/es/index.mjs",
|
"src": "../../element-plus/es/index.mjs",
|
||||||
"file": "element-plus.js",
|
"file": "element-plus.js",
|
||||||
"fileHash": "ed152bc7",
|
"fileHash": "0a2f9603",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"fabric": {
|
"fabric": {
|
||||||
"src": "../../fabric/dist/index.min.mjs",
|
"src": "../../fabric/dist/index.min.mjs",
|
||||||
"file": "fabric.js",
|
"file": "fabric.js",
|
||||||
"fileHash": "14f1f947",
|
"fileHash": "a1369625",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"pinia": {
|
"pinia": {
|
||||||
"src": "../../pinia/dist/pinia.mjs",
|
"src": "../../pinia/dist/pinia.mjs",
|
||||||
"file": "pinia.js",
|
"file": "pinia.js",
|
||||||
"fileHash": "ecda6472",
|
"fileHash": "20bc890b",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"src": "../../vue-router/dist/vue-router.mjs",
|
"src": "../../vue-router/dist/vue-router.mjs",
|
||||||
"file": "vue-router.js",
|
"file": "vue-router.js",
|
||||||
"fileHash": "4a9d54f8",
|
"fileHash": "017f450e",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||||
"file": "vue.js",
|
"file": "vue.js",
|
||||||
"fileHash": "4a46bbb6",
|
"fileHash": "70ae02ae",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,9 +23,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 style="margin: 0 0 15px 0; font-size: 14px; color: #888;">图层管理</h3>
|
<h3 style="margin: 0 0 15px 0; font-size: 14px; color: #888;">图层管理</h3>
|
||||||
<div v-for="l in layers" :key="l.id" style="padding: 10px; margin-bottom: 5px; background: #16213e; border-radius: 4px; display: flex; align-items: center; gap: 10px; cursor: pointer;">
|
<div v-for="l in layers" :key="l.id"
|
||||||
<span>{{ l.visible ? '👁' : '—' }}</span>
|
@click="toggleLayer(l)"
|
||||||
<span style="font-size: 13px;">{{ l.name }}</span>
|
:style="{ padding: '10px 12px', marginBottom: '5px', background: activeLayer === l.id ? '#16213e' : '#0f3460', borderRadius: '4px', display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', borderLeft: activeLayer === l.id ? '3px solid #e94560' : '3px solid transparent', transition: 'all 0.2s' }">
|
||||||
|
<span style="width: 20px; text-align: center;">{{ l.visible ? '👁' : '—' }}</span>
|
||||||
|
<span style="font-size: 13px; flex: 1;">{{ l.name }}</span>
|
||||||
|
<span style="display: flex; flex-direction: column; gap: 2px;">
|
||||||
|
<button @click.stop="moveLayer(l, -1)" :disabled="l.zIndex <= 0"
|
||||||
|
:style="{ border: 'none', background: 'transparent', color: l.zIndex <= 0 ? '#333' : '#888', cursor: l.zIndex <= 0 ? 'default' : 'pointer', fontSize: '10px', padding: '0' }">▲</button>
|
||||||
|
<button @click.stop="moveLayer(l, 1)" :disabled="l.zIndex >= layers.length - 1"
|
||||||
|
:style="{ border: 'none', background: 'transparent', color: l.zIndex >= layers.length - 1 ? '#333' : '#888', cursor: l.zIndex >= layers.length - 1 ? 'default' : 'pointer', fontSize: '10px', padding: '0' }">▼</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -49,7 +57,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Canvas, FabricText } from 'fabric'
|
import { Canvas, FabricText, Rect } from 'fabric'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -60,6 +68,7 @@ const pname = ref('')
|
|||||||
const currentSuit = ref('spade')
|
const currentSuit = ref('spade')
|
||||||
const currentCard = ref('spade-A')
|
const currentCard = ref('spade-A')
|
||||||
const fabricCanvas = ref(null)
|
const fabricCanvas = ref(null)
|
||||||
|
const activeLayer = ref('bg')
|
||||||
const projectId = computed(() => route.params.projectId)
|
const projectId = computed(() => route.params.projectId)
|
||||||
|
|
||||||
const suits = ['spade', 'heart', 'club', 'diamond']
|
const suits = ['spade', 'heart', 'club', 'diamond']
|
||||||
@@ -76,12 +85,12 @@ const currentCards = computed(() => {
|
|||||||
return cards
|
return cards
|
||||||
})
|
})
|
||||||
|
|
||||||
const layers = [
|
const layers = ref([
|
||||||
{ id: 'bg', name: '背景层', visible: true },
|
{ id: 'bg', name: '背景层', visible: true, zIndex: 0 },
|
||||||
{ id: 'border', name: '边框层', visible: true },
|
{ id: 'border', name: '边框层', visible: true, zIndex: 1 },
|
||||||
{ id: 'pattern', name: '图案层', visible: true },
|
{ id: 'pattern', name: '图案层', visible: true, zIndex: 2 },
|
||||||
{ id: 'text', name: '文字层', visible: true }
|
{ id: 'text', name: '文字层', visible: true, zIndex: 3 }
|
||||||
]
|
])
|
||||||
|
|
||||||
function getSymbol(suit) {
|
function getSymbol(suit) {
|
||||||
return { spade: '♠', heart: '♥', club: '♣', diamond: '♦' }[suit] || ''
|
return { spade: '♠', heart: '♥', club: '♣', diamond: '♦' }[suit] || ''
|
||||||
@@ -91,6 +100,23 @@ function isRed(suit) {
|
|||||||
return suit === 'heart' || suit === 'diamond'
|
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)
|
||||||
|
if (swapped) {
|
||||||
|
swapped.zIndex = layer.zIndex
|
||||||
|
layer.zIndex = newZ
|
||||||
|
layers.value.sort((a, b) => a.zIndex - b.zIndex)
|
||||||
|
drawCard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (projectId.value) {
|
if (projectId.value) {
|
||||||
try {
|
try {
|
||||||
@@ -109,7 +135,7 @@ function initCanvas() {
|
|||||||
fabricCanvas.value = new Canvas('main-canvas', {
|
fabricCanvas.value = new Canvas('main-canvas', {
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 560,
|
height: 560,
|
||||||
backgroundColor: '#ffffff'
|
backgroundColor: '#f5f5f5'
|
||||||
})
|
})
|
||||||
drawCard()
|
drawCard()
|
||||||
}
|
}
|
||||||
@@ -119,22 +145,47 @@ function drawCard() {
|
|||||||
if (!c) return
|
if (!c) return
|
||||||
|
|
||||||
c.clear()
|
c.clear()
|
||||||
c.backgroundColor = '#ffffff'
|
|
||||||
|
|
||||||
|
const sortedLayers = [...layers.value].sort((a, b) => a.zIndex - b.zIndex)
|
||||||
const suit = currentSuit.value
|
const suit = currentSuit.value
|
||||||
const rank = currentCard.value.split('-')[1] || 'A'
|
const rank = currentCard.value.split('-')[1] || 'A'
|
||||||
const sym = getSymbol(suit)
|
const sym = getSymbol(suit)
|
||||||
const color = isRed(suit) ? '#FF0000' : '#000000'
|
const color = isRed(suit) ? '#FF0000' : '#000000'
|
||||||
|
const cardW = 400
|
||||||
|
const cardH = 560
|
||||||
|
|
||||||
const topText = new fabric.FabricText(`${rank}${sym}`, {
|
for (const layer of sortedLayers) {
|
||||||
left: 30, top: 25, fontSize: 36, fill: color, selectable: true
|
if (!layer.visible) continue
|
||||||
})
|
|
||||||
c.add(topText)
|
|
||||||
|
|
||||||
const center = new fabric.FabricText(sym, {
|
if (layer.id === 'bg') {
|
||||||
left: 200, top: 280, fontSize: 80, fill: color, selectable: true, originX: 'center', originY: 'center'
|
const bg = new Rect({ left: 0, top: 0, width: cardW, height: cardH, fill: '#ffffff', selectable: false })
|
||||||
})
|
c.add(bg)
|
||||||
c.add(center)
|
}
|
||||||
|
|
||||||
|
if (layer.id === 'border') {
|
||||||
|
const frame = new Rect({ left: 10, top: 10, width: cardW - 20, height: cardH - 20, fill: 'transparent', stroke: '#333', strokeWidth: 2, selectable: false })
|
||||||
|
c.add(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.id === 'pattern') {
|
||||||
|
const center = new FabricText(sym, {
|
||||||
|
left: cardW / 2, top: cardH / 2, fontSize: 80, fill: color, selectable: true, originX: 'center', originY: 'center'
|
||||||
|
})
|
||||||
|
c.add(center)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.id === 'text') {
|
||||||
|
const topLabel = new FabricText(`${rank}${sym}`, {
|
||||||
|
left: 18, top: 16, fontSize: 32, fill: color, selectable: true
|
||||||
|
})
|
||||||
|
c.add(topLabel)
|
||||||
|
|
||||||
|
const bottomLabel = new FabricText(`${sym}${rank}`, {
|
||||||
|
left: cardW - 18, top: cardH - 16, fontSize: 32, fill: color, selectable: true, originX: 'right', originY: 'bottom'
|
||||||
|
})
|
||||||
|
c.add(bottomLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.renderAll()
|
c.renderAll()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user