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

@@ -0,0 +1,23 @@
# Generated by Django 5.2.14 on 2026-06-02 06:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('templates', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='cardtemplate',
name='design_override',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='cardtemplate',
name='theme_id',
field=models.CharField(blank=True, default='classical', max_length=50),
),
]

View File

@@ -1,8 +1,9 @@
from django.db import models
import uuid
class CardTemplate(models.Model):
"""扑克牌模板模型"""
"""扑克牌模板模型(经典/现代/卡通/复古 4 套预设)"""
id = models.CharField(max_length=50, primary_key=True) # 'classic', 'modern', etc.
name = models.CharField(max_length=100)
description = models.TextField()
@@ -15,7 +16,15 @@ class CardTemplate(models.Model):
color_diamond = models.CharField(max_length=20, default='#FF0000')
color_background = models.CharField(max_length=20, default='#FFFFFF')
# 默认素材路径JSON
# 模板绑定的预设主题(指向 LibraryAsset.theme_id
# 例如 'classical' 模板 → 使用 LibraryAsset.theme_id='classical' 的所有素材
theme_id = models.CharField(max_length=50, blank=True, default='classical')
# 模板级 design 覆盖background_color / border_color / pip_size_ratio 等
# 不填则走全局默认
design_override = models.JSONField(default=dict)
# 默认素材路径JSON保留字段兼容老逻辑
default_assets = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)

View File

@@ -0,0 +1,110 @@
"""
模板应用工具:把 CardTemplate 的预设theme + background + design应用到 Project。
- 复制 LibraryAsset 中的素材到 projects/<pid>/<asset_type>/<asset_key>_xxx.png
- 在 Project 的 design 中写 background_color / border_color / suit_colors 等
"""
import os
import shutil
from time import time
from django.conf import settings
from apps.projects.models import Project, Asset, LibraryAsset
def _copy_lib_to_project(project, lib, asset_type, asset_key):
"""把 LibraryAsset 的文件复制到 projects/<pid>/<asset_type>/ 下,并在 Project.assets 建记录"""
project_media_dir = f'projects/{project.id}/{asset_type}'
full_dir = os.path.join(settings.MEDIA_ROOT, project_media_dir)
os.makedirs(full_dir, exist_ok=True)
src = os.path.join(settings.MEDIA_ROOT, lib.file_path)
if not os.path.exists(src):
return None
ts = int(time() * 1000)
fn = f'{asset_key}_{ts}_{lib.file_name}'
dst_rel = f'{project_media_dir}/{fn}'
shutil.copy2(src, os.path.join(settings.MEDIA_ROOT, dst_rel))
# 读图尺寸
width = height = None
try:
from PIL import Image
with Image.open(os.path.join(settings.MEDIA_ROOT, dst_rel)) as im:
width, height = im.size
except Exception:
pass
# 删除同 (asset_type, asset_key) 的旧记录
Asset.objects.filter(project=project, asset_type=asset_type, asset_key=asset_key).delete()
return Asset.objects.create(
project=project,
asset_type=asset_type,
asset_key=asset_key,
file_path=dst_rel,
file_name=fn,
width=width,
height=height,
)
def apply_template_to_project(project, template):
"""根据 CardTemplate 的预设填充项目:写 design + 复制 JQK/Joker 素材
默认行为:
- 整副牌 background_color / border_color 等设计项按模板的 design_override 写入
- 模板绑定的 theme_id 对应的所有 LibraryAsset 复制成 JQK/Joker 资产
- 应用到所有 4 个花色 + 大小王
"""
# 1. 应用 design 覆盖
base_design = dict(project.design or {})
override = template.design_override or {}
base_design.update(override)
# 同步花色颜色
if 'suit_symbols' not in base_design:
base_design['suit_symbols'] = {}
base_design['suit_symbols']['spade'] = {'type': 'text', 'value': '', 'color': template.color_spade, 'asset_id': None}
base_design['suit_symbols']['heart'] = {'type': 'text', 'value': '', 'color': template.color_heart, 'asset_id': None}
base_design['suit_symbols']['club'] = {'type': 'text', 'value': '', 'color': template.color_club, 'asset_id': None}
base_design['suit_symbols']['diamond'] = {'type': 'text', 'value': '', 'color': template.color_diamond, 'asset_id': None}
# 背景色
base_design['background_color'] = template.color_background
project.design = base_design
project.save()
# 2. 复制主题素材(如果绑定了 theme_id
theme_id = template.theme_id
if not theme_id:
return {'applied': 0, 'theme_id': None}
libs = LibraryAsset.objects.filter(theme_id=theme_id)
if not libs.exists():
return {'applied': 0, 'theme_id': theme_id, 'warning': f'no library assets for theme {theme_id}'}
applied = []
for lib in libs:
# asset_type: J/Q/K -> face_cardjoker -> joker
if lib.role == 'joker':
asset_type = 'joker'
# 默认应用到 joker-big
asset_key = 'joker-big'
# 同主题如果有 small 的角色素材就分别处理
if lib.asset_id == 'small':
asset_key = 'joker-small'
else:
asset_type = 'face_card'
# 4 个花色都复制
for suit in ('spade', 'heart', 'club', 'diamond'):
asset_key = f'{suit}-{lib.role}'
a = _copy_lib_to_project(project, lib, asset_type, asset_key)
if a:
applied.append(a.id)
continue
a = _copy_lib_to_project(project, lib, asset_type, asset_key)
if a:
applied.append(a.id)
return {
'applied': len(applied),
'theme_id': theme_id,
'asset_ids': applied,
}

View File

@@ -2,27 +2,49 @@ from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import CardTemplate
from apps.projects.models import LibraryAsset
def _template_to_dict(t):
return {
'id': t.id,
'name': t.name,
'description': t.description,
'preview_image': t.preview_image.url if t.preview_image else None,
'theme_id': t.theme_id,
'colors': {
'spade': t.color_spade,
'heart': t.color_heart,
'club': t.color_club,
'diamond': t.color_diamond,
'background': t.color_background,
},
'design_override': t.design_override or {},
}
def _template_with_preview(t):
"""附加 library 中 theme_id 对应的素材预览(前端可以画一组小图)"""
out = _template_to_dict(t)
libs = LibraryAsset.objects.filter(theme_id=t.theme_id)
out['library'] = [
{
'id': lib.id,
'role': lib.role,
'role_name': lib.role_name,
'label': lib.label,
'file_url': f'/media/{lib.file_path}',
}
for lib in libs
]
return out
@api_view(['GET'])
def template_list(request):
"""获取所有模板列表"""
"""获取所有模板列表(含每个模板的 theme 预览)"""
templates = CardTemplate.objects.all()
data = []
for template in templates:
data.append({
'id': template.id,
'name': template.name,
'description': template.description,
'preview_image': template.preview_image.url if template.preview_image else None,
'colors': {
'spade': template.color_spade,
'heart': template.color_heart,
'club': template.color_club,
'diamond': template.color_diamond,
'background': template.color_background,
},
})
data = [_template_with_preview(t) for t in templates]
return Response(data)
@@ -33,26 +55,4 @@ def template_detail(request, pk):
template = CardTemplate.objects.get(pk=pk)
except CardTemplate.DoesNotExist:
return Response({'error': 'Template not found'}, status=status.HTTP_404_NOT_FOUND)
data = {
'id': template.id,
'name': template.name,
'description': template.description,
'preview_image': template.preview_image.url if template.preview_image else None,
'colors': {
'spade': template.color_spade,
'heart': template.color_heart,
'club': template.color_club,
'diamond': template.color_diamond,
'background': template.color_background,
},
'default_assets': template.default_assets,
'suit_symbols': {
ss.suit_name: {
'svg_path': ss.svg_path,
'color': ss.color,
}
for ss in template.suit_symbols.all()
}
}
return Response(data)
return Response(_template_with_preview(template))