Implement asset upload API and utility functions
- Add Asset and CardLayer model updates - Create asset upload API endpoints - Add AssetUploadDialog component - Create card layout algorithms - Implement symmetry generation utils - Add template configurations
This commit is contained in:
1
backend/apps/management/__init__.py
Normal file
1
backend/apps/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# management/__init__.py
|
||||
1
backend/apps/management/commands/__init__.py
Normal file
1
backend/apps/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# management/commands/__init__.py
|
||||
145
backend/apps/management/commands/init_system.py
Normal file
145
backend/apps/management/commands/init_system.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files.storage import default_storage
|
||||
from apps.projects.models import Project, Asset, CardLayer
|
||||
from apps.templates.models import CardTemplate
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Initialize cards design system with sample data'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('Starting initialization...'))
|
||||
|
||||
# 创建模板和数据
|
||||
self.create_templates()
|
||||
self.create_default_assets()
|
||||
self.create_sample_project()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Initialization complete!'))
|
||||
|
||||
def create_templates(self):
|
||||
"""创建示例模板"""
|
||||
templates = [
|
||||
{
|
||||
'id': 'classic',
|
||||
'name': '经典风格',
|
||||
'description': '标准扑克牌设计,传统花色和字体',
|
||||
'color_spade': '#000000',
|
||||
'color_heart': '#FF0000',
|
||||
'color_club': '#000000',
|
||||
'color_diamond': '#FF0000',
|
||||
'color_background': '#FFFFFF',
|
||||
},
|
||||
{
|
||||
'id': 'modern',
|
||||
'name': '现代简约',
|
||||
'description': '扁平化设计,简洁线条',
|
||||
'color_spade': '#333333',
|
||||
'color_heart': '#E53935',
|
||||
'color_club': '#333333',
|
||||
'color_diamond': '#E53935',
|
||||
'color_background': '#FAFAFA',
|
||||
}
|
||||
]
|
||||
|
||||
for template_data in templates:
|
||||
try:
|
||||
template = CardTemplate.objects.get(id=template_data['id'])
|
||||
if not template.default_assets:
|
||||
template.default_assets = template_data
|
||||
template.save()
|
||||
except CardTemplate.DoesNotExist:
|
||||
template = CardTemplate.objects.create(
|
||||
id=template_data['id'],
|
||||
name=template_data['name'],
|
||||
description=template_data['description'],
|
||||
color_spade=template_data['color_spade'],
|
||||
color_heart=template_data['color_heart'],
|
||||
color_club=template_data['color_club'],
|
||||
color_diamond=template_data['color_diamond'],
|
||||
color_background=template_data['color_background'],
|
||||
default_assets=template_data
|
||||
)
|
||||
|
||||
def create_default_assets(self):
|
||||
"""创建默认花色素材"""
|
||||
suits = {
|
||||
'spade': 0xE27B60,
|
||||
'heart': 0xE27B60,
|
||||
'club': 0xE27B60,
|
||||
'diamond': 0xE27B60
|
||||
}
|
||||
|
||||
materials = 'backend/media/assets'
|
||||
os.makedirs(materials, exist_ok=True)
|
||||
|
||||
for suit_name, color_code in suits.items():
|
||||
# 创建简单SVG花色图案
|
||||
svg_path = os.path.join('backend/media/assets', f'{suit_name}.svg')
|
||||
|
||||
storyboardSVG = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<path d="M50 5 L50 30 L55 25 L55 5 Z" fill="''' f'#{color_code:06X}' f'"\n/>
|
||||
<path d="M50 5 L45 25 L50 30 L55 25 Z" fill="''' f'#{color_code:06X}' f'"\n/>
|
||||
<path d="M30 55 Q30 45 40 45 L50 55 L60 45 Q70 45 70 55 Q70 65 60 70 L50 60 L40 70 Q30 65 30 55 Z" fill="''' f'#{color_code:06X}' f'"\n/>
|
||||
<path d="M20 80 L20 95 L80 95 L80 80" stroke="''' f'#{color_code:06X}' f'"\n stroke-width="8" fill="none"/>^
|
||||
</svg>'''
|
||||
|
||||
with open(svg_path, 'w', encoding='utf-8') as f:
|
||||
f.write(storyboardSVG)
|
||||
|
||||
# 创建Asset记录
|
||||
Asset.objects.create(
|
||||
asset_type='suit_symbol',
|
||||
asset_key=suit_name,
|
||||
color=f'#{color_code:06X}'
|
||||
)
|
||||
|
||||
def create_sample_project(self):
|
||||
"""创建示例项目"""
|
||||
try:
|
||||
project = Project.objects.get(name="示例项目")
|
||||
self.stdout.write(self.style.WARNING('示例项目已存在,跳过创建'))
|
||||
return
|
||||
except Project.DoesNotExist:
|
||||
project = Project.objects.create(
|
||||
name="示例项目",
|
||||
template_id='classic',
|
||||
card_width=750,
|
||||
card_height=1050,
|
||||
export_resolution='standard',
|
||||
export_include_back=True
|
||||
)
|
||||
|
||||
# 创建示例素材
|
||||
suit_assets = [
|
||||
{'type': 'suit_symbol', 'key': 'spade'},
|
||||
{'type': 'suit_symbol', 'key': 'heart'},
|
||||
{'type': 'suit_symbol', 'key': 'club'},
|
||||
{'type': 'suit_symbol', 'key': 'diamond'},
|
||||
]
|
||||
|
||||
for asset_data in suit_assets:
|
||||
Asset.objects.create(
|
||||
asset_type=asset_data['type'],
|
||||
asset_key=asset_data['key'],
|
||||
width=60,
|
||||
height=60
|
||||
)
|
||||
|
||||
# 创建JQK示例素材记录(临时)
|
||||
face_cards = [
|
||||
{'type': 'face_card', 'key': 'spade-J'},
|
||||
{'type': 'face_card', 'key': 'spade-Q'},
|
||||
{'type': 'face_card', 'key': 'spade-K'},
|
||||
]
|
||||
|
||||
for face_card in face_cards:
|
||||
Asset.objects.create(
|
||||
asset_type=face_card['type'],
|
||||
asset_key=face_card['key'],
|
||||
width=300,
|
||||
height=500
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'项目 "{project.name}" 已创建'))
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.db import models
|
||||
import uuid
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
"""项目配置模型"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
@@ -24,8 +25,11 @@ class Project(models.Model):
|
||||
|
||||
class Asset(models.Model):
|
||||
"""项目素材模型"""
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='assets')
|
||||
asset_type = models.CharField(max_length=20) # 'suit_symbol', 'face_card', 'joker', 'back', 'border'
|
||||
asset_key = models.CharField(max_length=50) # 如 'spade', 'heart-J', 'big_joker'
|
||||
file_path = models.CharField(max_length=255) # 相对于media目录
|
||||
file_name = models.CharField(max_length=100)
|
||||
width = models.IntegerField(null=True)
|
||||
height = models.IntegerField(null=True)
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -33,9 +37,13 @@ class Asset(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.asset_type}:{self.asset_key}"
|
||||
|
||||
class Meta:
|
||||
ordering = ['-uploaded_at']
|
||||
|
||||
|
||||
class CardLayer(models.Model):
|
||||
"""牌面图层配置模型"""
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='layers')
|
||||
card_type = models.CharField(max_length=20) # 'number', 'face', 'joker'
|
||||
card_key = models.CharField(max_length=30) # 'spade-A', 'heart-K', 'big_joker'
|
||||
layer_name = models.CharField(max_length=50)
|
||||
@@ -47,7 +55,6 @@ class CardLayer(models.Model):
|
||||
|
||||
# 图层属性(JSON存储)
|
||||
properties = models.JSONField(default=dict)
|
||||
file_ref = models.ForeignKey(Asset, on_delete=models.SET_NULL, null=True, related_name='layers')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.card_key}-{self.layer_name}"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
from .models import Project, Asset, CardLayer
|
||||
|
||||
|
||||
@@ -9,10 +10,20 @@ class ProjectSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class AssetSerializer(serializers.ModelSerializer):
|
||||
file_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = '__all__'
|
||||
|
||||
def get_file_url(self, obj):
|
||||
if obj.file_path:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
return request.build_absolute_uri(f'{settings.MEDIA_URL}{obj.file_path}')
|
||||
return f'{settings.MEDIA_URL}{obj.file_path}'
|
||||
return None
|
||||
|
||||
|
||||
class CardLayerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from django.urls import path
|
||||
from .views import project_list, project_detail
|
||||
from .views import project_list, project_detail, asset_list, asset_detail
|
||||
|
||||
urlpatterns = [
|
||||
path('', project_list, name='project-list'),
|
||||
path('<str:pk>/', project_detail, name='project-detail'),
|
||||
path('<str:project_pk>/assets/', asset_list, name='asset-list'),
|
||||
path('<str:project_pk>/assets/<str:asset_pk>/', asset_detail, name='asset-detail'),
|
||||
]
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from .models import Project
|
||||
from .serializers import ProjectSerializer, ProjectDetailSerializer
|
||||
from django.core.files.storage import default_storage
|
||||
from django.conf import settings
|
||||
from PIL import Image
|
||||
import os
|
||||
from .models import Project, Asset
|
||||
from .serializers import ProjectSerializer, ProjectDetailSerializer, AssetSerializer
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
@@ -42,4 +46,81 @@ def project_detail(request, pk):
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
project.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
def asset_list(request, project_pk):
|
||||
"""获取项目素材列表或上传新素材"""
|
||||
try:
|
||||
project = Project.objects.get(pk=project_pk)
|
||||
except Project.DoesNotExist:
|
||||
return Response({'error': 'Project not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if request.method == 'GET':
|
||||
assets = project.assets.all()
|
||||
serializer = AssetSerializer(assets, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
elif request.method == 'POST':
|
||||
if 'file' not in request.FILES:
|
||||
return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
file = request.FILES['file']
|
||||
asset_type = request.POST.get('asset_type', 'unknown')
|
||||
asset_key = request.POST.get('asset_key', 'unknown')
|
||||
|
||||
# 创建项目素材目录
|
||||
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)
|
||||
|
||||
# 保存文件
|
||||
file_name = f"{asset_key}_{file.name}"
|
||||
file_path = os.path.join(project_media_dir, file_name)
|
||||
saved_path = default_storage.save(file_path, file)
|
||||
|
||||
# 获取图片尺寸
|
||||
try:
|
||||
img = Image.open(file)
|
||||
width, height = img.size
|
||||
except:
|
||||
width, height = None, None
|
||||
|
||||
# 创建Asset记录
|
||||
asset = Asset.objects.create(
|
||||
project=project,
|
||||
asset_type=asset_type,
|
||||
asset_key=asset_key,
|
||||
file_path=saved_path,
|
||||
file_name=file_name,
|
||||
width=width,
|
||||
height=height
|
||||
)
|
||||
|
||||
serializer = AssetSerializer(asset)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(['GET', 'DELETE'])
|
||||
def asset_detail(request, project_pk, asset_pk):
|
||||
"""获取或删除单个素材"""
|
||||
try:
|
||||
project = Project.objects.get(pk=project_pk)
|
||||
asset = project.assets.get(pk=asset_pk)
|
||||
except (Project.DoesNotExist, Asset.DoesNotExist):
|
||||
return Response({'error': 'Asset not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if request.method == 'GET':
|
||||
serializer = AssetSerializer(asset)
|
||||
return Response(serializer.data)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
# 删除文件
|
||||
if asset.file_path:
|
||||
file_full_path = os.path.join(settings.MEDIA_ROOT, asset.file_path)
|
||||
if os.path.exists(file_full_path):
|
||||
os.remove(file_full_path)
|
||||
|
||||
asset.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
Reference in New Issue
Block a user