feat: 现代化翻页组件 + 完整项目更新
- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航 - PickModal 和 SwapModal 集成分页功能,每页20项 - 搜索/筛选切换自动回到第1页 - 现代化CSS样式:hover浮起动效、主色高亮、移动端适配 - 新增 dashboard、admin 管理模板 - 菜系(cuisine)模型和迁移 - 构建脚本 build.py 支持 PyInstaller 打包 - 前端资源重新构建
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user