- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航 - PickModal 和 SwapModal 集成分页功能,每页20项 - 搜索/筛选切换自动回到第1页 - 现代化CSS样式:hover浮起动效、主色高亮、移动端适配 - 新增 dashboard、admin 管理模板 - 菜系(cuisine)模型和迁移 - 构建脚本 build.py 支持 PyInstaller 打包 - 前端资源重新构建
91 lines
3.0 KiB
Python
91 lines
3.0 KiB
Python
"""Dashboard API & view – visualization panel for the school meal planner."""
|
||
import json
|
||
from collections import Counter
|
||
|
||
from django.http import JsonResponse
|
||
from django.shortcuts import render
|
||
from django.db.models import Avg, Count, Max, Min, Sum
|
||
|
||
from .models import Dish
|
||
|
||
|
||
def _dashboard_stats():
|
||
"""Return aggregate statistics for the dashboard."""
|
||
total = Dish.objects.count()
|
||
active = Dish.objects.filter(is_active=True).count()
|
||
inactive = total - active
|
||
|
||
# Count by type
|
||
type_counts = dict(
|
||
Dish.objects.values_list('dish_type').annotate(c=Count('id')).values_list('dish_type', 'c')
|
||
)
|
||
|
||
# Count by cuisine
|
||
cuisine_counts = dict(
|
||
Dish.objects.values_list('cuisine').annotate(c=Count('id')).values_list('cuisine', 'c')
|
||
)
|
||
|
||
# Nutrition averages (active dishes only, meat + veg only for meaningful stats)
|
||
nutrition = Dish.objects.filter(is_active=True, dish_type__in=('meat', 'veg')).aggregate(
|
||
avg_protein=Avg('protein'),
|
||
avg_fat=Avg('fat'),
|
||
avg_calorie=Avg('calorie'),
|
||
max_calorie=Max('calorie'),
|
||
min_calorie=Min('calorie'),
|
||
)
|
||
|
||
# Per-type nutrition averages
|
||
type_nutrition = {}
|
||
for dt in ('meat', 'veg', 'soup', 'staple'):
|
||
agg = Dish.objects.filter(is_active=True, dish_type=dt).aggregate(
|
||
avg_protein=Avg('protein'),
|
||
avg_fat=Avg('fat'),
|
||
avg_calorie=Avg('calorie'),
|
||
count=Count('id'),
|
||
)
|
||
type_nutrition[dt] = agg
|
||
|
||
# Recent 10 dishes
|
||
recent = list(
|
||
Dish.objects.order_by('-created_at')[:10].values(
|
||
'id', 'name', 'dish_type', 'cuisine', 'protein', 'fat', 'calorie', 'is_active', 'created_at',
|
||
)
|
||
)
|
||
for r in recent:
|
||
r['created_at'] = r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else ''
|
||
|
||
return {
|
||
'total': total,
|
||
'active': active,
|
||
'inactive': inactive,
|
||
'type_counts': type_counts,
|
||
'cuisine_counts': cuisine_counts,
|
||
'nutrition': {
|
||
'avg_protein': round(float(nutrition['avg_protein'] or 0), 1),
|
||
'avg_fat': round(float(nutrition['avg_fat'] or 0), 1),
|
||
'avg_calorie': round(float(nutrition['avg_calorie'] or 0)),
|
||
'max_calorie': int(nutrition['max_calorie'] or 0),
|
||
'min_calorie': int(nutrition['min_calorie'] or 0),
|
||
},
|
||
'type_nutrition': {
|
||
k: {
|
||
'avg_protein': round(float(v['avg_protein'] or 0), 1),
|
||
'avg_fat': round(float(v['avg_fat'] or 0), 1),
|
||
'avg_calorie': round(float(v['avg_calorie'] or 0)),
|
||
'count': v['count'],
|
||
}
|
||
for k, v in type_nutrition.items()
|
||
},
|
||
'recent': recent,
|
||
}
|
||
|
||
|
||
def dashboard_api(request):
|
||
"""GET /api/dashboard/ -> JSON stats for the dashboard."""
|
||
return JsonResponse(_dashboard_stats())
|
||
|
||
|
||
def dashboard_view(request):
|
||
"""GET /dashboard/ -> serve the dashboard HTML page."""
|
||
return render(request, 'dashboard.html')
|