feat: 现代化翻页组件 + 完整项目更新

- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航
- PickModal 和 SwapModal 集成分页功能,每页20项
- 搜索/筛选切换自动回到第1页
- 现代化CSS样式:hover浮起动效、主色高亮、移动端适配
- 新增 dashboard、admin 管理模板
- 菜系(cuisine)模型和迁移
- 构建脚本 build.py 支持 PyInstaller 打包
- 前端资源重新构建
This commit is contained in:
Chunyu
2026-08-28 13:44:21 +08:00
parent 65b52b052f
commit d7553520ce
51 changed files with 4318 additions and 441 deletions
+36 -17
View File
@@ -8,13 +8,14 @@ from openpyxl.utils import get_column_letter
from rest_framework import status, viewsets
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAdminUser
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from .models import Dish
from .serializers import DishSerializer
from .models import Cuisine, Dish
from .serializers import DishSerializer, CuisineSerializer
WEEKDAYS = ['周一', '周二', '周三', '周四', '周五']
SCHOOL_NAME = '上海外国语大学附属宝山双语学校'
SCHOOL_NAME = '学校菜单'
COLOR_HEADER = PatternFill('solid', fgColor='2F5597')
COLOR_DAY = PatternFill('solid', fgColor='D9E2F3')
@@ -27,9 +28,16 @@ BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
CENTER = Alignment(horizontal='center', vertical='center', wrap_text=True)
class CuisineViewSet(viewsets.ModelViewSet):
queryset = Cuisine.objects.all()
serializer_class = CuisineSerializer
permission_classes = [AllowAny]
class DishViewSet(viewsets.ModelViewSet):
queryset = Dish.objects.all()
serializer_class = DishSerializer
permission_classes = [AllowAny]
def get_queryset(self):
qs = Dish.objects.all()
@@ -59,9 +67,14 @@ def bulk_create_dishes(request):
if not name or dish_type not in ('meat', 'veg', 'soup', 'staple'):
skipped.append(name or '未命名')
continue
# Resolve cuisine FK by name
cuisine_name = str(raw.get('cuisine', '') or '').strip()
cuisine_obj = None
if cuisine_name:
cuisine_obj, _ = Cuisine.objects.get_or_create(name=cuisine_name)
defaults = {
'dish_type': dish_type,
'cuisine': str(raw.get('cuisine', '家常菜') or '家常菜'),
'cuisine': cuisine_obj,
'ingredient_detail': str(raw.get('ingredient_detail', '') or raw.get('ingredient', '')),
'protein': float(raw.get('protein', 0) or 0),
'fat': float(raw.get('fat', 0) or 0),
@@ -94,7 +107,7 @@ def _pop_next(pool, shuffle_src):
def _serialize_dish(d):
return {
'id': d.id, 'name': d.name, 'ingredient': d.ingredient_detail, 'dish_type': d.dish_type,
'cuisine': d.cuisine, 'protein': float(d.protein), 'fat': float(d.fat), 'calorie': d.calorie,
'cuisine': d.cuisine_name, 'protein': float(d.protein), 'fat': float(d.fat), 'calorie': d.calorie,
}
@@ -211,19 +224,19 @@ def generate_week_menu(payload, menu_key='a'):
week.append({
'day': day,
'meats': [{'id': d.id, 'name': d.name, 'ingredient': d.ingredient_detail, 'dish_type': d.dish_type, 'cuisine': d.cuisine,
'meats': [{'id': d.id, 'name': d.name, 'ingredient': d.ingredient_detail, 'dish_type': d.dish_type, 'cuisine': d.cuisine_name,
'protein': float(d.protein), 'fat': float(d.fat), 'calorie': d.calorie} for d in day_meat_list],
'vegs': [{'id': d.id, 'name': d.name, 'ingredient': d.ingredient_detail, 'dish_type': d.dish_type, 'cuisine': d.cuisine,
'vegs': [{'id': d.id, 'name': d.name, 'ingredient': d.ingredient_detail, 'dish_type': d.dish_type, 'cuisine': d.cuisine_name,
'protein': float(d.protein), 'fat': float(d.fat), 'calorie': d.calorie} for d in day_veg],
'soup': {'id': soup.id, 'name': soup.name, 'ingredient': soup.ingredient_detail, 'dish_type': soup.dish_type, 'cuisine': soup.cuisine,
'soup': {'id': soup.id, 'name': soup.name, 'ingredient': soup.ingredient_detail, 'dish_type': soup.dish_type, 'cuisine': soup.cuisine_name,
'protein': float(soup.protein), 'fat': float(soup.fat), 'calorie': soup.calorie} if soup else None,
'staple': {'id': staple.id, 'name': staple.name, 'ingredient': staple.ingredient_detail, 'dish_type': staple.dish_type, 'cuisine': staple.cuisine,
'staple': {'id': staple.id, 'name': staple.name, 'ingredient': staple.ingredient_detail, 'dish_type': staple.dish_type, 'cuisine': staple.cuisine_name,
'protein': float(staple.protein), 'fat': float(staple.fat), 'calorie': staple.calorie} if staple else None,
})
return week
def build_menu_title(label, school_name=None, week_no=None):
def build_menu_title(label, school_name=None, week_no=None, date_start=None, date_end=None):
school_name = school_name or SCHOOL_NAME
iso = date.today().isocalendar()
try:
@@ -232,12 +245,16 @@ def build_menu_title(label, school_name=None, week_no=None):
week_no = iso.week
if not 1 <= week_no <= 53:
week_no = iso.week
try:
monday = date.fromisocalendar(iso.year, week_no, 1)
except ValueError:
monday = date.today() - timedelta(days=date.today().weekday())
friday = monday + timedelta(days=4)
date_range = f'{monday.month}.{monday.day}--{friday.month}.{friday.day}'
# Use custom dates if provided, otherwise compute from week number
if date_start and date_end:
date_range = f'{date_start}--{date_end}'
else:
try:
monday = date.fromisocalendar(iso.year, week_no, 1)
except ValueError:
monday = date.today() - timedelta(days=date.today().weekday())
friday = monday + timedelta(days=4)
date_range = f'{monday.month}.{monday.day}--{friday.month}.{friday.day}'
return f'{school_name}[第{week_no}周]{date_range}营养{label}菜单'
@@ -246,11 +263,13 @@ def generate_menu(request):
data = request.data
school = data.get('school_name') or SCHOOL_NAME
week_no = data.get('week_no')
date_start = data.get('date_start')
date_end = data.get('date_end')
menus = {}
for label in ('A', 'B'):
key = label.lower()
menus[key] = {
'title': build_menu_title(label, school, week_no),
'title': build_menu_title(label, school, week_no, date_start, date_end),
'week': generate_week_menu(data, menu_key=key),
}
return Response(menus)