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