营养校准、后台美化、静态走对象存储 CDN、标题可编辑、admin 拼音搜索

- 菜品营养按食物成分表+烹饪方式校准(calibrate_nutrition 命令)
- 后台接入 jazzmin 主题美化,移除 Google Fonts(模板覆盖)
- 前端/后台静态资源指向 qnycdn 对象存储 CDN(vite base + DJANGO_STATIC_URL)
- 菜单标题学校名称/周数可编辑,日期随周数计算
- admin 搜索支持拼音首字母/全拼(Dish.pinyin 索引,pypinyin)
- Dockerfile 使用清华 pip 镜像源
This commit is contained in:
root
2026-08-06 21:11:14 +08:00
parent 75f214dc40
commit 65b52b052f
16 changed files with 805 additions and 16 deletions
+31 -3
View File
@@ -1,4 +1,5 @@
from django.contrib import admin
from django.utils.html import format_html, mark_safe
from .models import Dish
@@ -15,10 +16,10 @@ def deactivate_dishes(modeladmin, request, queryset):
@admin.register(Dish)
class DishAdmin(admin.ModelAdmin):
list_display = ('name', 'cuisine', 'dish_type', 'ingredient_detail', 'protein', 'fat', 'calorie', 'is_active', 'created_at')
list_display = ('name', 'cuisine', 'dish_type_display', 'ingredient_detail', 'protein', 'fat', 'calorie', 'nutri_badge', 'is_active', 'created_at')
list_filter = ('dish_type', 'cuisine', 'is_active')
search_fields = ('name', 'ingredient_detail', 'cuisine')
list_editable = ('cuisine', 'dish_type', 'ingredient_detail', 'protein', 'fat', 'calorie', 'is_active')
search_fields = ('name', 'ingredient_detail', 'cuisine', 'pinyin')
list_editable = ('cuisine', 'ingredient_detail', 'protein', 'fat', 'calorie', 'is_active')
list_per_page = 50
actions = [activate_dishes, deactivate_dishes]
fieldsets = (
@@ -26,3 +27,30 @@ class DishAdmin(admin.ModelAdmin):
('营养信息', {'fields': ('ingredient_detail', 'protein', 'fat', 'calorie')}),
('其他', {'fields': ('description',), 'classes': ('collapse',)}),
)
@admin.display(description='营养提示', ordering='calorie')
def nutri_badge(self, obj):
badges = []
if obj.dish_type == 'meat' and obj.fat >= 20:
badges.append('<span class="badge badge-danger">高脂</span>')
if obj.dish_type == 'meat' and obj.calorie >= 300:
badges.append('<span class="badge badge-warning">高热量</span>')
if obj.dish_type in ('meat', 'veg') and obj.protein >= 15:
badges.append('<span class="badge badge-success">高蛋白</span>')
if obj.dish_type == 'veg' and obj.fat <= 3 and obj.calorie <= 60:
badges.append('<span class="badge badge-info">清淡</span>')
return mark_safe('&nbsp;'.join(badges)) if badges else '—'
@admin.display(description='类型')
def dish_type_display(self, obj):
colors = {'meat': '#d9534f', 'veg': '#5cb85c', 'soup': '#5bc0de', 'staple': '#f0ad4e'}
return format_html(
'<span style="color:{};font-weight:600">{}</span>',
colors.get(obj.dish_type, '#999'),
dict(Dish.TYPE_CHOICES).get(obj.dish_type, obj.dish_type),
)
admin.site.site_header = '学校订餐菜单管理系统'
admin.site.site_title = '学校订餐菜单管理'
admin.site.index_title = '菜单管理后台'
@@ -0,0 +1,27 @@
from django.db import migrations, models
from pypinyin import Style, lazy_pinyin
def fill_pinyin(apps, schema_editor):
Dish = apps.get_model('meals', 'Dish')
for dish in Dish.objects.all():
full = ''.join(lazy_pinyin(dish.name))
initials = ''.join(lazy_pinyin(dish.name, style=Style.FIRST_LETTER))
dish.pinyin = f'{full} {initials}'
dish.save(update_fields=['pinyin'])
class Migration(migrations.Migration):
dependencies = [
('meals', '0004_dish_cuisine'),
]
operations = [
migrations.AddField(
model_name='dish',
name='pinyin',
field=models.CharField(blank=True, editable=False, max_length=200, verbose_name='拼音索引'),
),
migrations.RunPython(fill_pinyin, migrations.RunPython.noop),
]
+10
View File
@@ -1,5 +1,7 @@
from django.db import models
from pypinyin import Style, lazy_pinyin
class Dish(models.Model):
TYPE_CHOICES = [
@@ -29,6 +31,7 @@ class Dish(models.Model):
calorie = models.IntegerField('热量(kcal)', default=0)
description = models.CharField('备注', max_length=200, blank=True)
is_active = models.BooleanField('启用', default=True)
pinyin = models.CharField('拼音索引', max_length=200, blank=True, editable=False)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
@@ -38,3 +41,10 @@ class Dish(models.Model):
def __str__(self):
return f'{self.get_dish_type_display()}: {self.name}'
def save(self, *args, **kwargs):
if self.name:
full = ''.join(lazy_pinyin(self.name))
initials = ''.join(lazy_pinyin(self.name, style=Style.FIRST_LETTER))
self.pinyin = f'{full} {initials}'
super().save(*args, **kwargs)
+19 -7
View File
@@ -223,23 +223,35 @@ def generate_week_menu(payload, menu_key='a'):
return week
def build_menu_title(label):
today = date.today()
week_no = today.isocalendar().week
monday = today - timedelta(days=today.weekday())
def build_menu_title(label, school_name=None, week_no=None):
school_name = school_name or SCHOOL_NAME
iso = date.today().isocalendar()
try:
week_no = int(week_no) if week_no is not None else iso.week
except (TypeError, ValueError):
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}'
return f'{SCHOOL_NAME}[第{week_no}周]{date_range}营养{label}菜单'
return f'{school_name}[第{week_no}周]{date_range}营养{label}菜单'
@api_view(['POST'])
def generate_menu(request):
data = request.data
school = data.get('school_name') or SCHOOL_NAME
week_no = data.get('week_no')
menus = {}
for label in ('A', 'B'):
key = label.lower()
menus[key] = {
'title': build_menu_title(label),
'week': generate_week_menu(request.data, menu_key=key),
'title': build_menu_title(label, school, week_no),
'week': generate_week_menu(data, menu_key=key),
}
return Response(menus)