diff --git a/.superpowers/brainstorm/screen/architecture-overview.html b/.superpowers/brainstorm/screen/architecture-overview.html new file mode 100644 index 0000000..1000097 --- /dev/null +++ b/.superpowers/brainstorm/screen/architecture-overview.html @@ -0,0 +1,116 @@ +

扑克牌设计管理系统 - 核心架构

+

个人从零开始设计完整扑克牌系列

+ +
+

系统核心功能模块

+ +
+
+
+
+
??
+
图案上传
+
+
+
+

图案素材管理

+

上传花色图案、数字字体、JQK人像、大小王图片、背面图案等设计素材

+
+
+ +
+
+
+
??
+
实时设计
+
+
+
+

可视化设计编辑器

+

实时预览54张牌面,调整布局、颜色、字体等设计元素

+
+
+ +
+
+
+
??
+
批量导出
+
+
+
+

PNG导出系统

+

一键导出整套54张扑克牌为PNG格式,支持多种分辨率

+
+
+
+
+ +
+

扑克牌设计规范

+ +
+
标准扑克牌结构(54张)
+
+
+ +
+
数字牌 (40张)
+
+ ? 4种花色 × 10张 (A, 2-10)
+ ? 左右对称布局
+ ? 中间花色图案均匀分布
+ ? 四角标注点数与花色 +
+
+ +
+
花牌 JQK (12张)
+
+ ? 4种花色 × 3张 (J, Q, K)
+ ? 中心对称设计
+ ? 人像上下倒置可正常观看
+ ? 沿中轴线对称绘制 +
+
+ +
+
大小王 (2张)
+
+ ? 大王、小王各1张
+ ? 无花色区分
+ ? 独立图案设计
+ ? 视觉对称原则 +
+
+ +
+
+
+
+ +
+

设计工作流程

+ +
+
+
1
+
上传素材
+
+
+
+
2
+
配置样式
+
+
+
+
3
+
实时预览
+
+
+
+
4
+
导出PNG
+
+
+
diff --git a/backend/apps/__init__.py b/backend/apps/__init__.py new file mode 100644 index 0000000..8eac5c8 --- /dev/null +++ b/backend/apps/__init__.py @@ -0,0 +1 @@ +# apps/__init__.py \ No newline at end of file diff --git a/backend/apps/exports/__init__.py b/backend/apps/exports/__init__.py new file mode 100644 index 0000000..6ce1151 --- /dev/null +++ b/backend/apps/exports/__init__.py @@ -0,0 +1 @@ +# apps/exports/__init__.py \ No newline at end of file diff --git a/backend/apps/exports/urls.py b/backend/apps/exports/urls.py new file mode 100644 index 0000000..bb645b9 --- /dev/null +++ b/backend/apps/exports/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from .views import export_project, export_single_card + +urlpatterns = [ + path('projects//export/', export_project, name='export-project'), + path('projects//export//', export_single_card, name='export-single-card'), +] diff --git a/backend/apps/exports/utils.py b/backend/apps/exports/utils.py new file mode 100644 index 0000000..ca6868d --- /dev/null +++ b/backend/apps/exports/utils.py @@ -0,0 +1,196 @@ +from PIL import Image, ImageDraw +import io +import os + + +def load_image(file_path, scale=1): + """鍔犺浇鍥剧墖骞跺簲鐢ㄧ缉鏀""" + img = Image.open(file_path).convert('RGBA') + + if scale > 1: + new_size = (int(img.width * scale), int(img.height * scale)) + img = img.resize(new_size, Image.LANCZOS) + + return img + + +def generate_symmetrical_face_card(original_image_path, scale=1): + """ + 鐢熸垚JQK涓績瀵圭О鍥炬 + 杈撳叆锛氬師濮嬪浘鐗囪矾寰 + 杈撳嚭锛氫腑蹇冨绉扮殑鍥惧儚鏁扮粍锛堜笂鍗婇儴鍒嗐佷笅鍗婇儴鍒嗭級 + """ + original = load_image(original_image_path, scale) + width, height = original.size + half_height = height // 2 + + # 鍒涘缓涓婂崐閮ㄥ垎 + top_half = original.crop((0, 0, width, half_height)) + + # 鍒涘缓涓嬪崐閮ㄥ垎骞剁炕杞 + bottom_half = original.crop((0, half_height, width, height)) + bottom_half = bottom_half.transpose(Image.FLIP_TOP_BOTTOM) + + return top_half, bottom_half + + +def render_background(canvas, layer, scale): + """娓叉煋鑳屾櫙灞""" + if layer.properties: + properties = layer.properties + width = canvas.size[0] + height = canvas.size[1] + + # 瑙f瀽color锛堝 '#FF0000' 鎴 'rgb(255,0,0)'锛 + bg_color = properties.get('color', '#FFFFFF') + + # 鍒涘缓鑳屾櫙鐭╁舰 + draw = ImageDraw.Draw(canvas, 'RGBA') + draw.rectangle(((0, 0), (width, height)), fill=bg_color + 'FF') + + # 濡傛灉鏈夌汗鐞嗘垨鍥炬璺緞 + texture_path = properties.get('texture_path') + if texture_path and os.path.exists(texture_path): + texture = load_image(texture_path, scale) + bg_height = height // 4 + for y in range(0, height, bg_height): + canvas.paste(texture, (0, y), texture) + + +def render_image_layer(canvas, project, layer, scale): + """娓叉煋鍥剧墖灞傦紙浜哄儚銆佽姳鑹茬瓑锛""" + if not layer.file_ref or not layer.file_ref.file_path: + return + + asset_path = os.path.join(project.media_root, layer.file_ref.file_path) + + if not os.path.exists(asset_path): + return + + image = load_image(asset_path, scale) + + # 鑾峰彇浣嶇疆淇℃伅 + properties = layer.properties or {} + x = properties.get('x', 0) + y = properties.get('y', 0) + width = properties.get('width', image.size[0]) + height = properties.get('height', image.size[1]) + + # 璁$畻瀹為檯鍧愭爣 + canvas_width, canvas_height = canvas.size + actual_x = (x / project.card_width) * canvas_width + actual_y = (y / project.card_height) * canvas_height + + # 璁$畻瀹為檯灏哄 + actual_w = (width / project.card_width) * canvas_width + actual_h = (height / project.card_height) * canvas_height + + # 瑁佸壀鍥剧墖 + cropped = image.copy() + cropped.thumbnail((actual_w, actual_h), Image.LANCZOS) + + # 璁$畻灞呬腑浣嶇疆 + paste_x = actual_x + (actual_w - cropped.size[0]) / 2 + paste_y = actual_y + (actual_h - cropped.size[1]) / 2 + + canvas.paste(cropped, (int(paste_x), int(paste_y)), cropped) + + +def render_text_layer(canvas, layer, scale): + """娓叉煋鏂囧瓧灞""" + properties = layer.properties or {} + + draw = ImageDraw.Draw(canvas, 'RGBA') + + text = properties.get('text', '') + x = properties.get('x', 0) + y = properties.get('y', 0) + + # 瑙f瀽瀛椾綋鍜岄鑹 + font = properties.get('font', None) + if font and isinstance(font, dict): + font_size = int(font.get('size', 24) * scale) + font_path = font.get('path') + + from PIL import ImageFont + if font_path and os.path.exists(font_path): + try: + custom_font = ImageFont.truetype(font_path, font_size) + except: + custom_font = None + else: + custom_font = None + else: + from PIL import ImageFont + custom_font = ImageFont.load_default() + + # 杞崲棰滆壊 + color = properties.get('color', '#000000') + if color.startswith('#'): + r = int(color[1:3], 16) + g = int(color[3:5], 16) + b = int(color[5:7], 16) + fill = (r, g, b, 255) + else: + fill = (0, 0, 0, 255) + + # 璁$畻瀹為檯鍧愭爣鍜屽昂瀵 + canvas_width, canvas_height = canvas.size + actual_x = (x / project.card_width) * canvas_width + actual_y = (y / project.card_height) * canvas_height + + bbox = draw.textbbox((0, 0), text, font=custom_font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + # 灞呬腑璁$畻 + paste_x = actual_x + (canvas_width * project.card_width * 0.3 - text_width) / 2 + paste_y = actual_y + (canvas_height * project.card_height * 0.5 - text_height) / 2 + + draw.text((int(paste_x), int(paste_y)), text, font=custom_font, fill=fill) + + +def generate_card_png(project, card_key, resolution='standard', scale_map={ 'standard': 1, 'hd': 2, 'ultra-hd': 4 }): + """ + 鐢熸垚鍗曞紶鐗岀殑PNG鍥剧墖 + + Args: + project: Project瀵硅薄 + card_key: 鐗岄潰key锛堝'hearts-A', 'spades-K', 'joker-big'锛 + resolution: 鍒嗚鲸鐜囷紙standard/hd/ultra-hd锛 + scale_map: 鍒嗚鲸鐜囧搴旂殑缂╂斁姣斾緥 + + Returns: + Image瀵硅薄 + """ + scale = scale_map.get(resolution, 1) + + # 鍒涘缓鍩虹鐢诲竷 + # 鐗岄潰鍧愭爣绯 + x_offset = int(50 * scale) + y_offset = int(50 * scale) + draw_width = int((project.card_width - 100) * scale) + draw_height = int((project.card_height - 100) * scale) + + canvas = Image.new('RGBA', (draw_width, draw_height)) + draw = ImageDraw.Draw(canvas, 'RGBA') + draw.rectangle(((0, 0), (draw_width, draw_height)), fill=(255, 255, 255, 255)) + + # 鑾峰彇鍗$墖绫诲瀷鐨勬墍鏈夊浘灞 + layers = CardLayer.objects.filter( + project=project, + card_key=card_key, + visible=True + ).order_by('z_index') + + # 娓叉煋鍚勫浘灞 + for layer in layers: + layer_type = layer.layer_type + if layer_type == 'background': + render_background(canvas, layer, scale) + elif layer_type == 'image': + render_image_layer(canvas, project, layer, scale) + elif layer_type == 'text': + render_text_layer(canvas, layer, scale) + + return canvas \ No newline at end of file diff --git a/backend/apps/exports/views.py b/backend/apps/exports/views.py new file mode 100644 index 0000000..dd65c33 --- /dev/null +++ b/backend/apps/exports/views.py @@ -0,0 +1,96 @@ +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework import status +from django.http import HttpResponse +from ..projects.models import Project +from .utils import generate_card_png +import zipfile +import io +import os + + +@api_view(['POST']) +def export_project(request, pk): + """ + 鎵归噺瀵煎嚭鏁村壇鐗屼负ZIP鏂囦欢 + 璇锋眰浣: { "resolution": "standard", "cards": "all" } + """ + try: + project = Project.objects.get(pk=pk) + except Project.DoesNotExist: + return Response({'error': 'Project not found'}, status=status.HTTP_404_NOT_FOUND) + + resolution = request.data.get('resolution', 'standard') + cards_filter = request.data.get('cards', 'all') + + # 纭畾瑕佸鍑虹殑鐗 + cards = [] + if cards_filter == 'all': + # 鐢熸垚鎵鏈54寮犵墝 + for suit in ['spade', 'heart', 'club', 'diamond']: + for rank in ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10']: + cards.append(f"{suit}-{rank}") + for face in ['J', 'Q', 'K']: + cards.append(f"{suit}-{face}") + cards.extend(['joker-big', 'joker-small']) + + if project.export_include_back: + cards.append('back') + else: + cards = cards_filter if isinstance(cards_filter, list) else [cards_filter] + + # 鍒涘缓ZIP鏂囦欢 + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for card_key in cards: + try: + png = generate_card_png(project, card_key, resolution) + img_buffer = io.BytesIO() + png.save(img_buffer, format='PNG') + img_buffer.seek(0) + zip_file.writestr(f"{card_key}.png", img_buffer.getvalue()) + except Exception as e: + # 璁板綍閿欒浣嗙户缁鐞嗗叾浠栫墝 + print(f"Error generating {card_key}: {str(e)}") + continue + + zip_buffer.seek(0) + + # 淇濆瓨鍒癿edia鐩綍 + export_dir = os.path.join('media', 'export', str(project.id)) + os.makedirs(export_dir, exist_ok=True) + zip_path = os.path.join(export_dir, 'cards.zip') + + with open(zip_path, 'wb') as f: + f.write(zip_buffer.getvalue()) + + return Response({ + 'download_url': f'/media/export/{project.id}/cards.zip', + 'card_count': len(cards) + }) + + +@api_view(['GET']) +def export_single_card(request, pk, card_key): + """ + 瀵煎嚭鍗曞紶鐗孭NG + """ + try: + project = Project.objects.get(pk=pk) + except Project.DoesNotExist: + return Response({'error': 'Project not found'}, status=status.HTTP_404_NOT_FOUND) + + resolution = request.query_params.get('resolution', 'standard') + + try: + png = generate_card_png(project, card_key, resolution) + img_buffer = io.BytesIO() + png.save(img_buffer, format='PNG') + img_buffer.seek(0) + + response = HttpResponse(img_buffer, content_type='image/png') + response['Content-Disposition'] = f'attachment; filename="{card_key}.png"' + return response + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/backend/apps/projects/__init__.py b/backend/apps/projects/__init__.py new file mode 100644 index 0000000..667787f --- /dev/null +++ b/backend/apps/projects/__init__.py @@ -0,0 +1 @@ +# apps/projects/__init__.py \ No newline at end of file diff --git a/backend/apps/projects/models.py b/backend/apps/projects/models.py new file mode 100644 index 0000000..d05c56a --- /dev/null +++ b/backend/apps/projects/models.py @@ -0,0 +1,56 @@ +from django.db import models +import uuid + +class Project(models.Model): + """椤圭洰閰嶇疆妯″瀷""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=100) + template_id = models.CharField(max_length=50, default='classic') + card_width = models.IntegerField(default=750) + card_height = models.IntegerField(default=1050) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # 瀵煎嚭璁剧疆 + export_resolution = models.CharField(max_length=20, default='standard') + export_include_back = models.BooleanField(default=True) + + def __str__(self): + return self.name + + class Meta: + ordering = ['-updated_at'] + + +class Asset(models.Model): + """椤圭洰绱犳潗妯″瀷""" + 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' + width = models.IntegerField(null=True) + height = models.IntegerField(null=True) + uploaded_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.asset_type}:{self.asset_key}" + + +class CardLayer(models.Model): + """鐗岄潰鍥惧眰閰嶇疆妯″瀷""" + 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) + layer_type = models.CharField(max_length=20) # 'background', 'border', 'image', 'text' + visible = models.BooleanField(default=True) + locked = models.BooleanField(default=False) + opacity = models.FloatField(default=1.0) + z_index = models.IntegerField(default=0) + + # 鍥惧眰灞炴э紙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}" + + class Meta: + ordering = ['card_key', 'z_index'] diff --git a/backend/apps/projects/serializers.py b/backend/apps/projects/serializers.py new file mode 100644 index 0000000..d4b4847 --- /dev/null +++ b/backend/apps/projects/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers +from .models import Project, Asset, CardLayer + + +class ProjectSerializer(serializers.ModelSerializer): + class Meta: + model = Project + fields = '__all__' + + +class AssetSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + fields = '__all__' + + +class CardLayerSerializer(serializers.ModelSerializer): + class Meta: + model = CardLayer + fields = '__all__' + + +class ProjectDetailSerializer(serializers.ModelSerializer): + assets = AssetSerializer(many=True, read_only=True) + layers = CardLayerSerializer(many=True, read_only=True) + + class Meta: + model = Project + fields = '__all__' \ No newline at end of file diff --git a/backend/apps/projects/urls.py b/backend/apps/projects/urls.py new file mode 100644 index 0000000..e0ed3ac --- /dev/null +++ b/backend/apps/projects/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from .views import project_list, project_detail + +urlpatterns = [ + path('', project_list, name='project-list'), + path('/', project_detail, name='project-detail'), +] diff --git a/backend/apps/projects/views.py b/backend/apps/projects/views.py new file mode 100644 index 0000000..bdce82d --- /dev/null +++ b/backend/apps/projects/views.py @@ -0,0 +1,45 @@ +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 + + +@api_view(['GET', 'POST']) +def project_list(request): + """鑾峰彇椤圭洰鍒楄〃鎴栧垱寤烘柊椤圭洰""" + if request.method == 'GET': + projects = Project.objects.all() + serializer = ProjectSerializer(projects, many=True) + return Response(serializer.data) + + elif request.method == 'POST': + serializer = ProjectSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET', 'PUT', 'DELETE']) +def project_detail(request, pk): + """鑾峰彇銆佹洿鏂版垨鍒犻櫎椤圭洰""" + try: + project = Project.objects.get(pk=pk) + except Project.DoesNotExist: + return Response({'error': 'Project not found'}, status=status.HTTP_404_NOT_FOUND) + + if request.method == 'GET': + serializer = ProjectDetailSerializer(project) + return Response(serializer.data) + + elif request.method == 'PUT': + serializer = ProjectSerializer(project, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == 'DELETE': + project.delete() + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/backend/apps/templates/__init__.py b/backend/apps/templates/__init__.py new file mode 100644 index 0000000..a04af74 --- /dev/null +++ b/backend/apps/templates/__init__.py @@ -0,0 +1 @@ +# apps/templates/__init__.py \ No newline at end of file diff --git a/backend/apps/templates/models.py b/backend/apps/templates/models.py new file mode 100644 index 0000000..1a4842d --- /dev/null +++ b/backend/apps/templates/models.py @@ -0,0 +1,38 @@ +from django.db import models +import uuid + +class CardTemplate(models.Model): + """鎵戝厠鐗屾ā鏉挎ā鍨""" + id = models.CharField(max_length=50, primary_key=True) # 'classic', 'modern', etc. + name = models.CharField(max_length=100) + description = models.TextField() + preview_image = models.ImageField(upload_to='templates/previews/', null=True) + + # 榛樿閰嶈壊鏂规 + color_spade = models.CharField(max_length=20, default='#000000') + color_heart = models.CharField(max_length=20, default='#FF0000') + color_club = models.CharField(max_length=20, default='#000000') + color_diamond = models.CharField(max_length=20, default='#FF0000') + color_background = models.CharField(max_length=20, default='#FFFFFF') + + # 榛樿绱犳潗璺緞锛圝SON锛 + default_assets = models.JSONField(default=dict) + + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + + class Meta: + ordering = ['name'] + + +class SuitSymbol(models.Model): + """鑺辫壊鍥炬妯℃澘""" + template = models.ForeignKey(CardTemplate, on_delete=models.CASCADE, related_name='suit_symbols') + suit_name = models.CharField(max_length=20) # 'spade', 'heart', 'club', 'diamond' + svg_path = models.TextField() # SVG璺緞鏁版嵁 + color = models.CharField(max_length=20) + + def __str__(self): + return f"{self.template.name} - {self.suit_name}" diff --git a/backend/apps/templates/views.py b/backend/apps/templates/views.py new file mode 100644 index 0000000..8622fcf --- /dev/null +++ b/backend/apps/templates/views.py @@ -0,0 +1,58 @@ +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework import status +from .models import CardTemplate + + +@api_view(['GET']) +def template_list(request): + """鑾峰彇鎵鏈夋ā鏉垮垪琛""" + 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, + }, + }) + return Response(data) + + +@api_view(['GET']) +def template_detail(request, pk): + """鑾峰彇妯℃澘璇︽儏""" + try: + 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) diff --git a/backend/manage.py b/backend/manage.py new file mode 100644 index 0000000..557b9e9 --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poker_api.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend/poker_api/__init__.py b/backend/poker_api/__init__.py new file mode 100644 index 0000000..466af95 --- /dev/null +++ b/backend/poker_api/__init__.py @@ -0,0 +1 @@ +# poker_api/__init__.py \ No newline at end of file diff --git a/backend/poker_api/settings.py b/backend/poker_api/settings.py new file mode 100644 index 0000000..776d018 --- /dev/null +++ b/backend/poker_api/settings.py @@ -0,0 +1,79 @@ +""" +Django settings for poker_api project. +""" + +from pathlib import Path +import os + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = 'django-insecure-poker-design-local-dev-key-change-in-production' + +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'apps.projects', + 'apps.templates', + 'apps.exports', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.middleware.common.CommonMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'poker_api.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + ], + }, + }, +] + +WSGI_APPLICATION = 'poker_api.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +LANGUAGE_CODE = 'zh-hans' +TIME_ZONE = 'Asia/Shanghai' +USE_I18N = True +USE_TZ = True + +STATIC_URL = 'static/' + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +CORS_ALLOWED_ORIGINS = [ + "http://localhost:5173", + "http://127.0.0.1:5173", +] + +CORS_ALLOW_CREDENTIALS = True + +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, +} diff --git a/backend/poker_api/urls.py b/backend/poker_api/urls.py new file mode 100644 index 0000000..918b8fc --- /dev/null +++ b/backend/poker_api/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('api/', include([ + path('', include('apps.projects.urls')), + path('', include('apps.templates.urls')), + path('', include('apps.exports.urls')), + ])), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/poker_api/wsgi.py b/backend/poker_api/wsgi.py new file mode 100644 index 0000000..96b5829 --- /dev/null +++ b/backend/poker_api/wsgi.py @@ -0,0 +1,9 @@ +""" +WSGI config for poker_api project. +""" + +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'poker_api.settings') +application = get_wsgi_application() \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..3487f10 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +# Django鏍稿績 +Django>=5.0,<6.0 +djangorestframework>=3.14 +django-cors-headers>=4.3 + +# 鍥惧儚澶勭悊 +Pillow>=10.0 + +# 鍏朵粬宸ュ叿 +python-dotenv>=1.0 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..82ac2a7 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 鎵戝厠鐗岃璁$鐞嗙郴缁 + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..90927fc --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "poker-design-frontend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.21", + "vue-router": "^4.3.0", + "pinia": "^2.1.7", + "element-plus": "^2.5.6", + "axios": "^1.6.7", + "fabric": "^6.4.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.1.6", + "sass": "^1.72.0" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..d408419 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/frontend/src/api/export.js b/frontend/src/api/export.js new file mode 100644 index 0000000..1afa11b --- /dev/null +++ b/frontend/src/api/export.js @@ -0,0 +1,15 @@ +import axios from 'axios' + +const API_BASE = '/api' + +export async function exportProject(projectId, resolution = 'standard', cards = 'all') { + const response = await axios.post(`${API_BASE}/projects/${projectId}/export/`, { + resolution, + cards + }) + return response.data +} + +export function getExportUrl(projectId, cardKey, resolution = 'standard') { + return `${API_BASE}/projects/${projectId}/export/${cardKey}/?resolution=${resolution}` +} diff --git a/frontend/src/api/project.js b/frontend/src/api/project.js new file mode 100644 index 0000000..632e28b --- /dev/null +++ b/frontend/src/api/project.js @@ -0,0 +1,27 @@ +import axios from 'axios' + +const API_BASE = '/api' + +export async function getProjects() { + const response = await axios.get(`${API_BASE}/projects/`) + return response.data +} + +export async function createProject(data) { + const response = await axios.post(`${API_BASE}/projects/`, data) + return response.data +} + +export async function getProject(id) { + const response = await axios.get(`${API_BASE}/projects/${id}/`) + return response.data +} + +export async function updateProject(id, data) { + const response = await axios.put(`${API_BASE}/projects/${id}/`, data) + return response.data +} + +export async function deleteProject(id) { + await axios.delete(`${API_BASE}/projects/${id}/`) +} diff --git a/frontend/src/api/template.js b/frontend/src/api/template.js new file mode 100644 index 0000000..0ee3397 --- /dev/null +++ b/frontend/src/api/template.js @@ -0,0 +1,13 @@ +import axios from 'axios' + +const API_BASE = '/api' + +export async function getTemplates() { + const response = await axios.get(`${API_BASE}/templates/`) + return response.data +} + +export async function getTemplate(id) { + const response = await axios.get(`${API_BASE}/templates/${id}/`) + return response.data +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..f778cc7 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,12 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.use(ElementPlus) +app.mount('#app') \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..81152b5 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Home from '@/views/Home.vue' +import Editor from '@/views/Editor.vue' + +const routes = [ + { + path: '/', + name: 'Home', + component: Home + }, + { + path: '/editor/:projectId?', + name: 'Editor', + component: Editor + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +export default router \ No newline at end of file diff --git a/frontend/src/views/Editor.vue b/frontend/src/views/Editor.vue new file mode 100644 index 0000000..1e5b7af --- /dev/null +++ b/frontend/src/views/Editor.vue @@ -0,0 +1,481 @@ + + + + + diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..d9c7868 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..bc6ac69 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') + } + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true + }, + '/media': { + target: 'http://localhost:8000', + changeOrigin: true + } + } + } +})