Implement Django backend and Vue frontend structure
- Django backend with projects, templates, exports apps - SQLite database models for Project, Asset, CardLayer - REST API endpoints for project management - Vue frontend with Vite, Element Plus, Fabric.js - Home page for project selection - Editor page with Fabric.js canvas integration
This commit is contained in:
1
backend/apps/__init__.py
Normal file
1
backend/apps/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# apps/__init__.py
|
||||
1
backend/apps/exports/__init__.py
Normal file
1
backend/apps/exports/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# apps/exports/__init__.py
|
||||
7
backend/apps/exports/urls.py
Normal file
7
backend/apps/exports/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from .views import export_project, export_single_card
|
||||
|
||||
urlpatterns = [
|
||||
path('projects/<str:pk>/export/', export_project, name='export-project'),
|
||||
path('projects/<str:pk>/export/<str:card_key>/', export_single_card, name='export-single-card'),
|
||||
]
|
||||
196
backend/apps/exports/utils.py
Normal file
196
backend/apps/exports/utils.py
Normal file
@@ -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]
|
||||
|
||||
# 解析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)
|
||||
|
||||
# 解析字体和颜色
|
||||
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
|
||||
96
backend/apps/exports/views.py
Normal file
96
backend/apps/exports/views.py
Normal file
@@ -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)
|
||||
|
||||
# 保存到media目录
|
||||
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):
|
||||
"""
|
||||
导出单张牌PNG
|
||||
"""
|
||||
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)
|
||||
1
backend/apps/projects/__init__.py
Normal file
1
backend/apps/projects/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# apps/projects/__init__.py
|
||||
56
backend/apps/projects/models.py
Normal file
56
backend/apps/projects/models.py
Normal file
@@ -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']
|
||||
29
backend/apps/projects/serializers.py
Normal file
29
backend/apps/projects/serializers.py
Normal file
@@ -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__'
|
||||
7
backend/apps/projects/urls.py
Normal file
7
backend/apps/projects/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from .views import project_list, project_detail
|
||||
|
||||
urlpatterns = [
|
||||
path('', project_list, name='project-list'),
|
||||
path('<str:pk>/', project_detail, name='project-detail'),
|
||||
]
|
||||
45
backend/apps/projects/views.py
Normal file
45
backend/apps/projects/views.py
Normal file
@@ -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)
|
||||
1
backend/apps/templates/__init__.py
Normal file
1
backend/apps/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# apps/templates/__init__.py
|
||||
38
backend/apps/templates/models.py
Normal file
38
backend/apps/templates/models.py
Normal file
@@ -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')
|
||||
|
||||
# 默认素材路径(JSON)
|
||||
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}"
|
||||
58
backend/apps/templates/views.py
Normal file
58
backend/apps/templates/views.py
Normal file
@@ -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)
|
||||
22
backend/manage.py
Normal file
22
backend/manage.py
Normal file
@@ -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()
|
||||
1
backend/poker_api/__init__.py
Normal file
1
backend/poker_api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# poker_api/__init__.py
|
||||
79
backend/poker_api/settings.py
Normal file
79
backend/poker_api/settings.py
Normal file
@@ -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,
|
||||
}
|
||||
14
backend/poker_api/urls.py
Normal file
14
backend/poker_api/urls.py
Normal file
@@ -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)
|
||||
9
backend/poker_api/wsgi.py
Normal file
9
backend/poker_api/wsgi.py
Normal file
@@ -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()
|
||||
10
backend/requirements.txt
Normal file
10
backend/requirements.txt
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user