主题:classical 古典宫廷(王子/皇后/国王/小丑)
modern 现代人物(小孩/女青年/男青年/小丑鱼)
astronomy 天文(星星/月亮/太阳/黑洞)
minimal 简笔符号(圆点/♀/♂/叉)
改动:
- 新增 LibraryAsset 模型(全局素材库,theme_id/role/asset_id 索引)
- 新增 seed_library 管理命令,用 Pillow 画 16 张 PNG 素材
- 新增 /api/projects/library/ 列表 API
- 新增 /api/projects/{pid}/library/{id}/apply/ 应用 API
把预设素材复制到 projects/<pid>/joker 或 face_card 下,作为该牌位的素材
- AssetPanel 加 tab 切换:「我的素材」+「预设主题」,主题可筛选、点击套用到当前牌
- 修复 generate_card_png 的 joker asset 匹配 bug:which 应该是 card_key(含前缀)才能匹配 asset_key
设计要点:
- 预设素材只画上半身(y=0~150),下半留空,让系统的'自动对称'流水线正确工作
- 预设素材 PNG 200×300,深色描边 + 半透明浅色填充,在任意牌面背景上叠加都清晰
115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
from rest_framework.decorators import api_view
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from django.conf import settings
|
||
from .models import LibraryAsset, Project, Asset
|
||
from .serializers import LibraryAssetSerializer
|
||
import os
|
||
|
||
|
||
@api_view(['GET'])
|
||
def library_list(request):
|
||
"""列出所有预设素材(按主题分组)"""
|
||
assets = LibraryAsset.objects.all()
|
||
serializer = LibraryAssetSerializer(assets, many=True, context={'request': request})
|
||
|
||
# 按 theme_id 分组
|
||
grouped = {}
|
||
for a in serializer.data:
|
||
a['file_url'] = f'{settings.MEDIA_URL}{a["file_path"]}'
|
||
grouped.setdefault(a['theme_id'], {
|
||
'theme_id': a['theme_id'],
|
||
'theme_name': a['theme_name'],
|
||
'description': a['description'],
|
||
'items': [],
|
||
})['items'].append(a)
|
||
|
||
return Response(list(grouped.values()))
|
||
|
||
|
||
@api_view(['GET'])
|
||
def library_themes(request):
|
||
"""返回所有主题的元信息(不含具体素材项,用于渲染主题筛选器)"""
|
||
themes = LibraryAsset.objects.values('theme_id', 'theme_name', 'description').distinct()
|
||
return Response(list(themes))
|
||
|
||
|
||
@api_view(['GET'])
|
||
def library_detail(request, pk):
|
||
"""单个预设素材的详情"""
|
||
try:
|
||
a = LibraryAsset.objects.get(pk=pk)
|
||
except LibraryAsset.DoesNotExist:
|
||
return Response({'error': 'Not found'}, status=status.HTTP_404_NOT_FOUND)
|
||
data = LibraryAssetSerializer(a, context={'request': request}).data
|
||
data['file_url'] = f'{settings.MEDIA_URL}{data["file_path"]}'
|
||
return Response(data)
|
||
|
||
|
||
@api_view(['POST'])
|
||
def library_apply(request, project_pk, pk):
|
||
"""把预设素材应用到项目某张牌(默认:spade-J / joker-big 等)
|
||
请求体: { card_key?: 'spade-J' } 可选;不传则用预设素材的 role + 默认 spade
|
||
"""
|
||
try:
|
||
project = Project.objects.get(pk=project_pk)
|
||
lib = LibraryAsset.objects.get(pk=pk)
|
||
except (Project.DoesNotExist, LibraryAsset.DoesNotExist):
|
||
return Response({'error': 'not found'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
card_key = request.data.get('card_key')
|
||
if not card_key:
|
||
# 默认: spade-{role} 或 joker-{which}
|
||
if lib.role == 'joker':
|
||
card_key = 'joker-big' # 默认应用到 big;用户可换
|
||
else:
|
||
card_key = f'spade-{lib.role}'
|
||
|
||
# 决定 asset_type: J/Q/K -> face_card;joker -> joker
|
||
asset_type = 'face_card' if lib.role in ('J', 'Q', 'K') else 'joker'
|
||
asset_key = card_key
|
||
|
||
# 删除该项目同 (asset_type, asset_key) 的旧记录(避免重复)
|
||
Asset.objects.filter(project=project, asset_type=asset_type, asset_key=asset_key).delete()
|
||
|
||
# 复制 library 文件到 projects/<pid>/ 下,避免污染原文件
|
||
import shutil
|
||
from time import time
|
||
project_media_dir = os.path.join('projects', str(project.id), asset_type)
|
||
full_dir = os.path.join(settings.MEDIA_ROOT, project_media_dir)
|
||
os.makedirs(full_dir, exist_ok=True)
|
||
|
||
src_path = os.path.join(settings.MEDIA_ROOT, lib.file_path)
|
||
ts = int(time() * 1000)
|
||
new_file_name = f'{asset_key}_{ts}_{lib.file_name}'
|
||
dst_rel = os.path.join(project_media_dir, new_file_name)
|
||
dst_abs = os.path.join(settings.MEDIA_ROOT, dst_rel)
|
||
shutil.copy2(src_path, dst_abs)
|
||
|
||
# 读 svg 尺寸
|
||
width = height = None
|
||
try:
|
||
from PIL import Image
|
||
with Image.open(dst_abs) as im:
|
||
width, height = im.size
|
||
except Exception:
|
||
pass
|
||
|
||
asset = Asset.objects.create(
|
||
project=project,
|
||
asset_type=asset_type,
|
||
asset_key=asset_key,
|
||
file_path=dst_rel,
|
||
file_name=new_file_name,
|
||
width=width,
|
||
height=height,
|
||
)
|
||
|
||
return Response({
|
||
'ok': True,
|
||
'asset_id': asset.id,
|
||
'card_key': card_key,
|
||
'asset_type': asset_type,
|
||
'file_url': f'{settings.MEDIA_URL}{dst_rel}',
|
||
}, status=status.HTTP_201_CREATED)
|