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:
Poker Design Developer
2026-05-31 22:07:39 +08:00
parent 71193c30e6
commit 670ac0a917
5 changed files with 172 additions and 662 deletions

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>