feat: 模板预设机制 - 4 套模板各绑一组 JQK/Joker + 背景

经典 → 古典宫廷(王子/皇后/国王/小丑)
现代 → 现代人物(小孩/女青年/男青年/小丑鱼)
卡通 → 现代人物 + 暖色调 + 圆边框
复古 → 简笔符号 + 深色边框 + 米色背景

后端:
- CardTemplate 新增 theme_id(绑预设主题)+ design_override(背景/边框/字体等覆盖)
- 新增 apply_template_to_project():把 LibraryAsset 复制到项目素材 + 写 design
- 创建项目时支持传 template_id,自动套用整套预设
- 模板列表 API 附加 library 预览(4 张图缩略)

前端 Home.vue:
- 4 套模板卡片每张带 4 张缩略图(来自 library 预览)
- 点模板一键创建项目 + 跳转到编辑器
- '新建空白项目' 保留为独立按钮

init_system 同步:4 套模板配置 + 应用到示例项目
This commit is contained in:
Developer
2026-06-02 15:08:37 +08:00
parent 5ca000b8ab
commit 7417a4a893
7 changed files with 309 additions and 78 deletions

View File

@@ -8,15 +8,22 @@
<main>
<section>
<h2>选择模板系列开始设计</h2>
<div class="template-grid">
<div v-for="t in templateList" :key="t.id"
<p class="hint">点击下方任一模板自动创建项目并预填 JQK 人物 + 大小王 + 背景色</p>
<div class="template-grid" v-if="!templateLoading && templates.length">
<div v-for="t in templates" :key="t.id"
class="template-card"
@click="createFromTemplate(t.id)">
<div class="icon">{{ t.icon }}</div>
@click="createFromTemplate(t)">
<div class="icon">{{ suitIconFor(t.id) }}</div>
<h3>{{ t.name }}</h3>
<p>{{ t.desc }}</p>
<p class="desc">{{ t.description }}</p>
<div class="preview" v-if="t.library && t.library.length">
<img v-for="(lib, i) in t.library.slice(0, 4)" :key="i" :src="lib.file_url" :alt="lib.role_name" />
</div>
<div class="cardinality">{{ t.library?.length || 0 }} 张预设素材</div>
</div>
</div>
<div v-else-if="templateLoading" class="muted">加载模板中</div>
<div v-else class="muted">未获取到模板</div>
</section>
<section v-if="hasProjects" class="project-list">
@@ -45,22 +52,44 @@
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import axios from 'axios'
import { useProjectStore } from '@/stores/projectStore.js'
const router = useRouter()
const store = useProjectStore()
const { projects, loading, error } = storeToRefs(store)
const templateList = [
{ id: 'classic', name: '经典风格', desc: '标准扑克牌设计,传统花色和字体', icon: '♠' },
{ id: 'modern', name: '现代简约', desc: '扁平化设计,简洁线条', icon: '◆' },
{ id: 'cartoon', name: '卡通风格', desc: 'Q版可爱人像圆润花色图案', icon: '★' },
{ id: 'vintage', name: '复古风格', desc: '复古色调和纹理,装饰性边框', icon: '♛' },
]
const templates = ref([])
const templateLoading = ref(false)
const TEMPLATE_ICONS = {
classic: '♛',
modern: '◆',
cartoon: '★',
vintage: '♟',
}
function suitIconFor(tid) {
return TEMPLATE_ICONS[tid] || '♠'
}
const hasProjects = computed(() => projects.value.length > 0)
onMounted(() => store.fetchProjects())
onMounted(async () => {
store.fetchProjects()
loadTemplates()
})
async function loadTemplates() {
templateLoading.value = true
try {
const r = await axios.get('/api/templates/')
templates.value = r.data || []
} catch (e) {
console.error('load templates failed', e)
} finally {
templateLoading.value = false
}
}
async function doCreate() {
try {
@@ -71,10 +100,9 @@ async function doCreate() {
}
}
async function createFromTemplate(tid) {
async function createFromTemplate(t) {
try {
const nm = templateList.find(t => t.id === tid)?.name || tid
const p = await store.createProject(nm + ' - ' + new Date().toLocaleDateString(), tid)
const p = await store.createProject(t.name + ' - ' + new Date().toLocaleDateString(), t.id)
router.push('/editor/' + p.id)
} catch (e) {
alert('创建失败: ' + e.message)
@@ -105,13 +133,17 @@ function formatDate(d) {
.topbar h1 { margin: 0; font-size: 24px; color: #e94560; }
main { max-width: 1200px; margin: 0 auto; padding: 30px 20px; }
section { margin-bottom: 36px; }
h2 { margin: 0 0 18px 0; font-size: 18px; color: #ccc; }
h2 { margin: 0 0 8px 0; font-size: 18px; color: #ccc; }
.hint { margin: 0 0 18px 0; font-size: 12px; color: #888; }
.template-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
.template-card { background: #0f3460; border-radius: 12px; padding: 22px; cursor: pointer; transition: all 0.2s; text-align: center; }
.template-card:hover { background: #16213e; transform: translateY(-2px); }
.template-card:hover { background: #16213e; transform: translateY(-2px); box-shadow: 0 6px 20px rgba(233, 69, 96, 0.15); }
.template-card .icon { font-size: 44px; margin-bottom: 10px; color: #e94560; }
.template-card h3 { margin: 0 0 6px 0; font-size: 16px; }
.template-card p { margin: 0; font-size: 12px; color: #aaa; line-height: 1.5; }
.template-card .desc { margin: 0 0 12px 0; font-size: 12px; color: #aaa; line-height: 1.5; min-height: 36px; }
.template-card .preview { display: flex; gap: 4px; justify-content: center; margin-bottom: 10px; background: #fff; border-radius: 4px; padding: 4px; }
.template-card .preview img { width: 36px; height: 48px; object-fit: contain; }
.template-card .cardinality { font-size: 11px; color: #e94560; font-weight: bold; }
.project-list { background: #0f3460; border-radius: 12px; padding: 22px; }
.project-row { display: flex; justify-content: space-between; align-items: center; padding: 12px; background: #16213e; border-radius: 8px; margin-bottom: 8px; }
.project-row .meta { font-size: 12px; color: #888; margin-top: 4px; }