Rebuild Home and Editor pages with simplified inline styles
- Replace Element Plus components with pure HTML/CSS - Use inline styles for reliable rendering - Fix templates URL duplicate path issue - Insert template data (4 templates) via Django shell - Clear Vite cache and restart both servers
This commit is contained in:
Binary file not shown.
@@ -1,9 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import template_list, template_detail
|
from .views import template_list, template_detail
|
||||||
|
|
||||||
app_name = 'templates'
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('templates/', template_list, name='template-list'),
|
path('', template_list, name='template-list'),
|
||||||
path('templates/<str:pk>/', template_detail, name='template-detail'),
|
path('<str:pk>/', template_detail, name='template-detail'),
|
||||||
]
|
]
|
||||||
Binary file not shown.
@@ -1,481 +1,169 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="editor-container">
|
<div style="min-height: 100vh; background: #1a1a2e; color: #eee; font-family: sans-serif;">
|
||||||
<el-header class="editor-header">
|
<header style="background: #16213e; padding: 15px 30px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
<div class="header-left">
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
<button @click="goBack" class="back-btn">← 返回</button>
|
<button @click="$router.push('/')" style="background: #333; color: #aaa; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">← 返回</button>
|
||||||
<input
|
<input v-model="pname" @blur="saveName" style="background: transparent; border: none; color: white; font-size: 18px; font-weight: bold; outline: none; width: 300px;" placeholder="项目名称">
|
||||||
v-model="projectName"
|
|
||||||
@blur="saveProjectName"
|
|
||||||
class="project-name-input"
|
|
||||||
placeholder="项目名称"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div style="display: flex; gap: 10px;">
|
||||||
<el-button type="primary" @click="exportAll">导出全部</el-button>
|
<button @click="doExportAll" style="padding: 8px 20px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer;">导出全部</button>
|
||||||
<el-button @click="exportSingle">导出当前</el-button>
|
<button @click="doExportSingle" style="padding: 8px 20px; background: #0f3460; color: #e94560; border: 1px solid #e94560; border-radius: 4px; cursor: pointer;">导出当前</button>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</header>
|
||||||
|
|
||||||
<el-container class="editor-body">
|
<div style="display: flex; height: calc(100vh - 60px);">
|
||||||
<el-aside width="300px" class="left-panel">
|
<aside style="width: 260px; background: #0f3460; padding: 20px; overflow-y: auto;">
|
||||||
<el-tabs v-model="leftTab">
|
<h3 style="margin: 0 0 15px 0; font-size: 14px; color: #888;">花色选择</h3>
|
||||||
<el-tab-pane label="素材库" name="assets">
|
<div style="display: flex; gap: 8px; margin-bottom: 25px;">
|
||||||
<div class="asset-section">
|
<button v-for="s in suits" :key="s"
|
||||||
<h3>花色图案</h3>
|
@click="switchSuit(s)"
|
||||||
<div class="asset-grid">
|
:style="{ padding: '8px 14px', border: 'none', borderRadius: '4px', cursor: 'pointer', background: currentSuit === s ? '#e94560' : '#16213e', color: currentSuit === s ? 'white' : '#aaa', fontWeight: currentSuit === s ? 'bold' : 'normal' }">
|
||||||
<div v-for="suit in suits" :key="suit.id" class="asset-item">
|
{{ suitLabels[s] }}
|
||||||
<div class="asset-preview">{{ suit.symbol }}</div>
|
</button>
|
||||||
<button @click="uploadAsset('suit', suit.id)">上传</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="图层" name="layers">
|
<h3 style="margin: 0 0 15px 0; font-size: 14px; color: #888;">图层管理</h3>
|
||||||
<div class="layer-list">
|
<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
|
<span>{{ l.visible ? '👁' : '—' }}</span>
|
||||||
v-for="layer in currentLayers"
|
<span style="font-size: 13px;">{{ l.name }}</span>
|
||||||
:key="layer.id"
|
|
||||||
class="layer-item"
|
|
||||||
:class="{ active: selectedLayer === layer.id }"
|
|
||||||
@click="selectLayer(layer.id)"
|
|
||||||
>
|
|
||||||
<el-checkbox v-model="layer.visible" />
|
|
||||||
<span>{{ layer.name }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-aside>
|
|
||||||
|
|
||||||
<el-main class="canvas-area">
|
<main style="flex: 1; display: flex; justify-content: center; align-items: center; background: #1a1a2e; padding: 20px;">
|
||||||
<div class="canvas-wrapper">
|
<div style="background: #16213e; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.3);">
|
||||||
<canvas ref="canvasRef" id="main-canvas"></canvas>
|
<canvas ref="canvasEl" id="main-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
|
||||||
</el-container>
|
|
||||||
|
|
||||||
<el-footer height="120px" class="card-list-footer">
|
<footer style="background: #0f3460; padding: 10px 20px; display: flex; gap: 5px; overflow-x: auto;">
|
||||||
<div class="card-tabs">
|
<div v-for="c in currentCards" :key="c.key"
|
||||||
<div
|
@click="selectCard(c.key)"
|
||||||
v-for="suit in cardSuits"
|
:style="{ minWidth: '60px', height: '84px', background: currentCard === c.key ? '#e94560' : '#16213e', color: currentCard === c.key ? 'white' : '#aaa', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', fontSize: '12px', fontWeight: currentCard === c.key ? 'bold' : 'normal', flexShrink: 0, transition: 'all 0.2s' }">
|
||||||
:key="suit"
|
{{ c.label }}
|
||||||
class="suit-tab"
|
|
||||||
:class="{ active: currentSuit === suit }"
|
|
||||||
@click="switchSuit(suit)"
|
|
||||||
>
|
|
||||||
{{ suit }}
|
|
||||||
</div>
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-thumbnails">
|
|
||||||
<div
|
|
||||||
v-for="card in currentCards"
|
|
||||||
:key="card.key"
|
|
||||||
class="card-thumb"
|
|
||||||
:class="{ active: currentCard === card.key }"
|
|
||||||
@click="selectCard(card.key)"
|
|
||||||
>
|
|
||||||
<div class="card-mini-preview">{{ card.label }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-footer>
|
|
||||||
</el-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, watch, nextTick } from 'vue'
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Canvas, Rect, Text as FabricText } from 'fabric'
|
import { Canvas, FabricText } from 'fabric'
|
||||||
import { ElMessage } from 'element-plus'
|
import axios from 'axios'
|
||||||
import { getProject, updateProject } from '@/api/project'
|
|
||||||
import { exportProject, getExportUrl } from '@/api/export'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const canvasRef = ref(null)
|
const canvasEl = ref(null)
|
||||||
const projectName = ref('')
|
const pname = ref('')
|
||||||
const leftTab = ref('assets')
|
|
||||||
const selectedLayer = ref(null)
|
|
||||||
const currentSuit = ref('spade')
|
const currentSuit = ref('spade')
|
||||||
const currentCard = ref('spade-A')
|
const currentCard = ref('spade-A')
|
||||||
const canvas = ref(null)
|
const fabricCanvas = ref(null)
|
||||||
const projectId = computed(() => route.params.projectId)
|
const projectId = computed(() => route.params.projectId)
|
||||||
|
|
||||||
const suits = [
|
const suits = ['spade', 'heart', 'club', 'diamond']
|
||||||
{ id: 'spade', symbol: '♠', name: '黑桃' },
|
const suitLabels = { spade: '♠ 黑桃', heart: '♥ 红桃', club: '♣ 梅花', diamond: '♦ 方块' }
|
||||||
{ id: 'heart', symbol: '♥', name: '红桃' },
|
|
||||||
{ id: 'club', symbol: '♣', name: '梅花' },
|
|
||||||
{ id: 'diamond', symbol: '♦', name: '方块' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const cardSuits = ['spade', 'heart', 'club', 'diamond']
|
const ranks = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']
|
||||||
|
|
||||||
const cardRanks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
|
|
||||||
|
|
||||||
const currentCards = computed(() => {
|
const currentCards = computed(() => {
|
||||||
const cards = cardRanks.map(rank => ({
|
const cards = ranks.map(r => ({ key: `${currentSuit.value}-${r}`, label: getSymbol(currentSuit.value) + r }))
|
||||||
key: `${currentSuit.value}-${rank}`,
|
|
||||||
label: `${getSuitSymbol(currentSuit.value)}${rank}`
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (currentSuit.value === 'spade') {
|
if (currentSuit.value === 'spade') {
|
||||||
cards.push({ key: 'joker-big', label: '大王' })
|
cards.push({ key: 'joker-big', label: '大王' })
|
||||||
cards.push({ key: 'joker-small', label: '小王' })
|
cards.push({ key: 'joker-small', label: '小王' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return cards
|
return cards
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentLayers = ref([
|
const layers = [
|
||||||
{ id: 'background', name: '背景层', visible: true },
|
{ id: 'bg', name: '背景层', visible: true },
|
||||||
{ id: 'border', name: '边框层', visible: true },
|
{ id: 'border', name: '边框层', visible: true },
|
||||||
{ id: 'pattern', name: '图案层', visible: true },
|
{ id: 'pattern', name: '图案层', visible: true },
|
||||||
{ id: 'text', name: '文字层', visible: true }
|
{ id: 'text', name: '文字层', visible: true }
|
||||||
])
|
]
|
||||||
|
|
||||||
function getSuitSymbol(suit) {
|
function getSymbol(suit) {
|
||||||
const map = {
|
return { spade: '♠', heart: '♥', club: '♣', diamond: '♦' }[suit] || ''
|
||||||
spade: '♠',
|
|
||||||
heart: '♥',
|
|
||||||
club: '♣',
|
|
||||||
diamond: '♦'
|
|
||||||
}
|
}
|
||||||
return map[suit] || ''
|
|
||||||
|
function isRed(suit) {
|
||||||
|
return suit === 'heart' || suit === 'diamond'
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (projectId.value) {
|
if (projectId.value) {
|
||||||
await loadProject()
|
try {
|
||||||
|
const res = await axios.get(`/api/projects/${projectId.value}/`)
|
||||||
|
pname.value = res.data.name
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load project:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
initCanvas()
|
initCanvas()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadProject() {
|
|
||||||
try {
|
|
||||||
const project = await getProject(projectId.value)
|
|
||||||
projectName.value = project.name
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('加载项目失败')
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initCanvas() {
|
function initCanvas() {
|
||||||
if (!canvasRef.value) return
|
if (!canvasEl.value) return
|
||||||
|
fabricCanvas.value = new Canvas('main-canvas', {
|
||||||
canvas.value = new Canvas('main-canvas', {
|
width: 400,
|
||||||
width: 750,
|
height: 560,
|
||||||
height: 1050,
|
backgroundColor: '#ffffff'
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
selection: true
|
|
||||||
})
|
})
|
||||||
|
drawCard()
|
||||||
drawDefaultCard()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawDefaultCard() {
|
function drawCard() {
|
||||||
if (!canvas.value) return
|
const c = fabricCanvas.value
|
||||||
|
if (!c) return
|
||||||
|
|
||||||
canvas.value.clear()
|
c.clear()
|
||||||
canvas.value.setBackgroundColor('#ffffff', canvas.value.renderAll.bind(canvas.value))
|
c.backgroundColor = '#ffffff'
|
||||||
|
|
||||||
const rect = new Rect({
|
const suit = currentSuit.value
|
||||||
left: 50,
|
|
||||||
top: 50,
|
|
||||||
width: 650,
|
|
||||||
height: 950,
|
|
||||||
fill: 'transparent',
|
|
||||||
stroke: '#e0e0e0',
|
|
||||||
strokeWidth: 2,
|
|
||||||
selectable: false
|
|
||||||
})
|
|
||||||
canvas.value.add(rect)
|
|
||||||
|
|
||||||
const suitSymbol = getSuitSymbol(currentSuit.value)
|
|
||||||
const rank = currentCard.value.split('-')[1] || 'A'
|
const rank = currentCard.value.split('-')[1] || 'A'
|
||||||
|
const sym = getSymbol(suit)
|
||||||
|
const color = isRed(suit) ? '#FF0000' : '#000000'
|
||||||
|
|
||||||
const topText = new FabricText(`${rank}${suitSymbol}`, {
|
const topText = new fabric.FabricText(`${rank}${sym}`, {
|
||||||
left: 70,
|
left: 30, top: 25, fontSize: 36, fill: color, selectable: true
|
||||||
top: 70,
|
|
||||||
fontSize: 48,
|
|
||||||
fill: currentSuit.value === 'heart' || currentSuit.value === 'diamond' ? '#FF0000' : '#000000',
|
|
||||||
selectable: true
|
|
||||||
})
|
})
|
||||||
canvas.value.add(topText)
|
c.add(topText)
|
||||||
|
|
||||||
const centerSymbol = new FabricText(suitSymbol, {
|
const center = new fabric.FabricText(sym, {
|
||||||
left: 375,
|
left: 200, top: 280, fontSize: 80, fill: color, selectable: true, originX: 'center', originY: 'center'
|
||||||
top: 525,
|
|
||||||
fontSize: 120,
|
|
||||||
fill: currentSuit.value === 'heart' || currentSuit.value === 'diamond' ? '#FF0000' : '#000000',
|
|
||||||
selectable: true,
|
|
||||||
originX: 'center',
|
|
||||||
originY: 'center'
|
|
||||||
})
|
})
|
||||||
canvas.value.add(centerSymbol)
|
c.add(center)
|
||||||
|
|
||||||
const bottomText = new FabricText(`${rank}${suitSymbol}`, {
|
c.renderAll()
|
||||||
left: 630,
|
|
||||||
top: 930,
|
|
||||||
fontSize: 48,
|
|
||||||
fill: currentSuit.value === 'heart' || currentSuit.value === 'diamond' ? '#FF0000' : '#000000',
|
|
||||||
selectable: true,
|
|
||||||
angle: 180,
|
|
||||||
originX: 'center',
|
|
||||||
originY: 'center'
|
|
||||||
})
|
|
||||||
canvas.value.add(bottomText)
|
|
||||||
|
|
||||||
canvas.value.renderAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchSuit(suit) {
|
function switchSuit(s) {
|
||||||
currentSuit.value = suit
|
currentSuit.value = s
|
||||||
currentCard.value = `${suit}-A`
|
currentCard.value = `${s}-A`
|
||||||
drawDefaultCard()
|
drawCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCard(cardKey) {
|
function selectCard(key) {
|
||||||
currentCard.value = cardKey
|
currentCard.value = key
|
||||||
drawDefaultCard()
|
drawCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLayer(layerId) {
|
async function saveName() {
|
||||||
selectedLayer.value = layerId
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadAsset(type, id) {
|
|
||||||
ElMessage.info('素材上传功能开发中...')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveProjectName() {
|
|
||||||
if (!projectId.value) return
|
if (!projectId.value) return
|
||||||
|
try { await axios.put(`/api/projects/${projectId.value}/`, { name: pname.value }) } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doExportAll() {
|
||||||
try {
|
try {
|
||||||
await updateProject(projectId.value, { name: projectName.value })
|
const res = await axios.post(`/api/projects/${projectId.value}/export/`, { resolution: 'standard', cards: 'all' })
|
||||||
ElMessage.success('项目名称已保存')
|
if (res.data.download_url) window.open(res.data.download_url, '_blank')
|
||||||
} catch (error) {
|
} catch (e) { alert('导出失败: ' + e.message) }
|
||||||
ElMessage.error('保存失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportAll() {
|
function doExportSingle() {
|
||||||
if (!projectId.value) return
|
const url = `/api/projects/${projectId.value}/export/${currentCard.value}/?resolution=standard`
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await exportProject(projectId.value, 'standard', 'all')
|
|
||||||
window.open(result.download_url, '_blank')
|
|
||||||
ElMessage.success('导出成功')
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('导出失败')
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportSingle() {
|
|
||||||
if (!projectId.value) return
|
|
||||||
|
|
||||||
const url = getExportUrl(projectId.value, currentCard.value, 'standard')
|
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
ElMessage.success('开始导出')
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
router.push('/')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.editor-container {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: #ffffff;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
padding: 0 20px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #666;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-name-input {
|
|
||||||
border: none;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-name-input:focus {
|
|
||||||
outline: none;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-body {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel {
|
|
||||||
background: #fafafa;
|
|
||||||
border-right: 1px solid #e0e0e0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-area {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: #e8e8e8;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-wrapper {
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-list-footer {
|
|
||||||
background: #ffffff;
|
|
||||||
border-top: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 10px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suit-tab {
|
|
||||||
padding: 5px 15px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suit-tab.active {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-thumbnails {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-thumb {
|
|
||||||
width: 50px;
|
|
||||||
height: 70px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-thumb.active {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-mini-preview {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-section {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-section h3 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-item {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-preview {
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 32px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-list {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-item:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-item.active {
|
|
||||||
background: #e6f0ff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,300 +1,124 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-container">
|
<div style="min-height: 100vh; background: #1a1a2e; color: #eee; font-family: sans-serif;">
|
||||||
<header class="header">
|
<header style="background: #16213e; padding: 25px 40px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
<h1>扑克牌设计管理系统</h1>
|
<h1 style="margin: 0; font-size: 28px; color: #e94560;">扑克牌设计管理系统</h1>
|
||||||
<button @click="createNewProject" class="create-btn">创建新项目</button>
|
<button @click="doCreate" style="padding: 12px 28px; background: #e94560; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; font-weight: bold;">
|
||||||
|
创建新项目
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="main">
|
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 20px;">
|
||||||
<h2>选择或创建项目</h2>
|
<h2 style="margin-bottom: 30px; font-size: 22px;">选择模板系列开始设计</h2>
|
||||||
|
|
||||||
<div class="tabs">
|
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; margin-bottom: 50px;">
|
||||||
<button
|
<div v-for="t in templateList" :key="t.id"
|
||||||
:class="{ active: activeTab === 'templates' }"
|
@click="createFromTemplate(t.id)"
|
||||||
@click="activeTab = 'templates'"
|
style="background: #0f3460; border-radius: 12px; padding: 25px; cursor: pointer; transition: all 0.3s; text-align: center;"
|
||||||
>
|
@mouseenter="$event.currentTarget.style.background = '#16213e'"
|
||||||
模板系列
|
@mouseleave="$event.currentTarget.style.background = '#0f3460'">
|
||||||
</button>
|
<div style="font-size: 48px; margin-bottom: 12px;">{{ t.icon }}</div>
|
||||||
<button
|
<h3 style="margin: 0 0 8px 0; font-size: 18px; color: #e94560;">{{ t.name }}</h3>
|
||||||
:class="{ active: activeTab === 'existing' }"
|
<p style="margin: 0; font-size: 13px; color: #aaa; line-height: 1.5;">{{ t.desc }}</p>
|
||||||
@click="activeTab = 'existing'"
|
|
||||||
>
|
|
||||||
已有项目
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="activeTab === 'templates'" class="template-grid">
|
|
||||||
<div v-for="template in templates" :key="template.id" class="template-card" @click="selectTemplate(template.id)">
|
|
||||||
<div class="template-preview">{{ template.name.substring(0, 2) }}</div>
|
|
||||||
<h3>{{ template.name }}</h3>
|
|
||||||
<p>{{ template.description }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeTab === 'existing'" class="project-list">
|
<div v-if="hasProjects" style="background: #0f3460; border-radius: 12px; padding: 25px;">
|
||||||
<div v-if="loading">加载中...</div>
|
<h3 style="margin: 0 0 20px 0;">已有项目</h3>
|
||||||
<div v-else-if="projects.length === 0" class="empty-state">
|
<div v-for="p in projectList" :key="p.id"
|
||||||
暂无项目,请创建新项目
|
style="display: flex; justify-content: space-between; align-items: center; padding: 15px; background: #16213e; border-radius: 8px; margin-bottom: 10px;">
|
||||||
</div>
|
<div>
|
||||||
<div v-else>
|
<strong>{{ p.name }}</strong>
|
||||||
<div v-for="project in projects" :key="project.id" class="project-item">
|
<div style="font-size: 12px; color: #888; margin-top: 4px;">创建于: {{ formatDate(p.created_at) }}</div>
|
||||||
<div class="project-info">
|
|
||||||
<h3>{{ project.name }}</h3>
|
|
||||||
<p>创建于: {{ formatDate(project.created_at) }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="project-actions">
|
|
||||||
<button @click="editProject(project.id)">编辑</button>
|
|
||||||
<button @click="deleteProject(project.id)" style="color: red;">删除</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button @click.stop="editProject(p.id)" style="padding: 6px 16px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer;">编辑</button>
|
||||||
|
<button @click.stop="removeProject(p.id)" style="padding: 6px 16px; background: #333; color: #aaa; border: none; border-radius: 4px; cursor: pointer;">删除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" style="text-align: center; padding: 40px; color: #888;">加载中...</div>
|
||||||
|
<div v-if="loadError" style="text-align: center; padding: 40px; color: #e94560;">{{ loadError }}</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const activeTab = ref('templates')
|
const projectList = ref([])
|
||||||
const projects = ref([])
|
|
||||||
const templates = ref([])
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const loadError = ref('')
|
||||||
|
|
||||||
// 内置模板数据(不依赖API)
|
const templateList = [
|
||||||
const defaultTemplates = [
|
{ id: 'classic', name: '经典风格', desc: '标准扑克牌设计,传统花色和字体', icon: '♠' },
|
||||||
{ id: 'classic', name: '经典风格', description: '标准扑克牌设计,传统花色和字体' },
|
{ id: 'modern', name: '现代简约', desc: '扁平化设计,简洁线条', icon: '◆' },
|
||||||
{ id: 'modern', name: '现代简约', description: '扁平化设计,简洁线条' },
|
{ id: 'cartoon', name: '卡通风格', desc: 'Q版可爱人像,圆润花色图案', icon: '★' },
|
||||||
{ id: 'cartoon', name: '卡通风格', description: 'Q版可爱人像,圆润花色图案' },
|
{ id: 'vintage', name: '复古风格', desc: '复古色调和纹理,装饰性边框', icon: '♛' }
|
||||||
{ id: 'vintage', name: '复古风格', description: '复古色调和纹理,装饰性边框' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
onMounted(async () => {
|
const hasProjects = computed(() => projectList.value.length > 0)
|
||||||
templates.value = defaultTemplates
|
|
||||||
await loadProjects()
|
onMounted(() => {
|
||||||
|
loadProjects()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
loadError.value = ''
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/projects/')
|
const res = await axios.get('/api/projects/')
|
||||||
projects.value = response.data.value || response.data || []
|
projectList.value = res.data || []
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error('Failed to load projects:', error)
|
loadError.value = '无法连接后端服务: ' + e.message
|
||||||
projects.value = []
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewProject() {
|
async function doCreate() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/projects/', {
|
const res = await axios.post('/api/projects/', {
|
||||||
name: `新项目 ${new Date().toLocaleDateString()}`,
|
name: '新项目 ' + new Date().toLocaleDateString(),
|
||||||
template_id: 'classic'
|
template_id: 'classic'
|
||||||
})
|
})
|
||||||
const projectId = response.data.id
|
router.push('/editor/' + res.data.id)
|
||||||
router.push(`/editor/${projectId}`)
|
} catch (e) {
|
||||||
} catch (error) {
|
alert('创建失败: ' + e.message)
|
||||||
console.error('Failed to create project:', error)
|
|
||||||
alert('创建项目失败: ' + error.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectTemplate(templateId) {
|
async function createFromTemplate(tid) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/projects/', {
|
const nm = templateList.find(t => t.id === tid)?.name || tid
|
||||||
name: `${templateId} - 新项目`,
|
const res = await axios.post('/api/projects/', {
|
||||||
template_id: templateId
|
name: nm + ' - 新项目',
|
||||||
|
template_id: tid
|
||||||
})
|
})
|
||||||
const projectId = response.data.id
|
router.push('/editor/' + res.data.id)
|
||||||
router.push(`/editor/${projectId}`)
|
} catch (e) {
|
||||||
} catch (error) {
|
alert('创建失败: ' + e.message)
|
||||||
console.error('Failed to create project:', error)
|
|
||||||
alert('创建项目失败: ' + error.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function editProject(projectId) {
|
function editProject(id) {
|
||||||
router.push(`/editor/${projectId}`)
|
router.push('/editor/' + id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteProject(projectId) {
|
async function removeProject(id) {
|
||||||
if (!confirm('确定要删除这个项目吗?')) return
|
if (!confirm('确定删除?')) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/projects/${projectId}/`)
|
await axios.delete('/api/projects/' + id + '/')
|
||||||
await loadProjects()
|
await loadProjects()
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error('Failed to delete project:', error)
|
alert('删除失败: ' + e.message)
|
||||||
alert('删除失败: ' + error.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateString) {
|
function formatDate(d) {
|
||||||
return new Date(dateString).toLocaleString('zh-CN')
|
return new Date(d).toLocaleString('zh-CN')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.home-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 20px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn {
|
|
||||||
background: white;
|
|
||||||
color: #667eea;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
padding: 30px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main h2 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs button.active {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-card {
|
|
||||||
background: white;
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-card:hover {
|
|
||||||
border-color: #667eea;
|
|
||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-preview {
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 36px;
|
|
||||||
color: #667eea;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-card h3 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-card p {
|
|
||||||
margin: 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-list {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 15px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-info h3 {
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-info p {
|
|
||||||
margin: 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-actions button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 50px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user