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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user