Files
school-meal/backend/templates/admin/index.html
T
Chunyu d7553520ce feat: 现代化翻页组件 + 完整项目更新
- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航
- PickModal 和 SwapModal 集成分页功能,每页20项
- 搜索/筛选切换自动回到第1页
- 现代化CSS样式:hover浮起动效、主色高亮、移动端适配
- 新增 dashboard、admin 管理模板
- 菜系(cuisine)模型和迁移
- 构建脚本 build.py 支持 PyInstaller 打包
- 前端资源重新构建
2026-08-28 13:44:21 +08:00

100 lines
8.7 KiB
HTML

{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block extrastyle %}
{{ block.super }}
<style>
.dash-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
.dash-stat{background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 6px rgba(0,0,0,.06);display:flex;align-items:center;gap:12px}
.dash-stat .icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px}
.dash-stat .icon.blue{background:#dbeafe;color:#2563eb}
.dash-stat .icon.green{background:#dcfce7;color:#16a34a}
.dash-stat .icon.amber{background:#fef3c7;color:#d97706}
.dash-stat .icon.red{background:#fee2e2;color:#dc2626}
.dash-stat .num{font-size:24px;font-weight:800;color:#1e293b;line-height:1}
.dash-stat .label{font-size:12px;color:#64748b;margin-top:2px}
.dash-charts{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
.dash-chart-box{background:#fff;border-radius:8px;padding:18px;box-shadow:0 1px 6px rgba(0,0,0,.06)}
.dash-chart-box h3{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #e2e8f0}
.dash-chart-container{position:relative;height:240px}
.dash-nutri-row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px}
.dash-nutri{background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 6px rgba(0,0,0,.06);text-align:center}
.dash-nutri .value{font-size:28px;font-weight:800;line-height:1.1}
.dash-nutri .unit{font-size:13px;color:#64748b}
.dash-nutri .sublabel{font-size:12px;color:#94a3b8;margin-top:6px}
.dash-nutri.protein .value{color:#2563eb}
.dash-nutri.fat .value{color:#d97706}
.dash-nutri.calorie .value{color:#dc2626}
.dash-table-wrap{background:#fff;border-radius:8px;padding:18px;box-shadow:0 1px 6px rgba(0,0,0,.06)}
.dash-table-wrap h3{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #e2e8f0}
.dash-table{width:100%;border-collapse:collapse;font-size:13px}
.dash-table th{text-align:left;padding:8px 10px;background:#f8fafc;color:#64748b;font-weight:600;border-bottom:2px solid #e2e8f0;font-size:11px;text-transform:uppercase;letter-spacing:.4px}
.dash-table td{padding:8px 10px;border-bottom:1px solid #f1f5f9}
.dash-table tr:hover td{background:#f8fafc}
.badge-t{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600}
.badge-t.meat{background:#fef3c7;color:#92400e}
.badge-t.veg{background:#dcfce7;color:#166534}
.badge-t.soup{background:#dbeafe;color:#1e40af}
.badge-t.staple{background:#fce7f3;color:#9d174d}
.badge-t.on{background:#dcfce7;color:#16a34a}
@media(max-width:900px){.dash-stats,.dash-charts,.dash-nutri-row{grid-template-columns:1fr}}
</style>
{% endblock %}
{% block coltype %}colMS{% endblock %}
{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
{% block footer %}{% endblock %}
{% block content %}
<div id="content-main">
<div class="dash-stats" id="dash-stats"></div>
<div class="dash-charts">
<div class="dash-chart-box"><h3>{% trans "菜品类型分布" %}</h3><div class="dash-chart-container"><canvas id="cType"></canvas></div></div>
<div class="dash-chart-box"><h3>{% trans "菜系分布" %}</h3><div class="dash-chart-container"><canvas id="cCuisine"></canvas></div></div>
</div>
<div class="dash-charts">
<div class="dash-chart-box"><h3>{% trans "各类型营养对比" %}</h3><div class="dash-chart-container"><canvas id="cRadar"></canvas></div></div>
<div class="dash-chart-box"><h3>{% trans "各类型菜品数量" %}</h3><div class="dash-chart-container"><canvas id="cBar"></canvas></div></div>
</div>
<div class="dash-nutri-row" id="dash-nutri"></div>
<div class="dash-table-wrap">
<h3>{% trans "最近添加的菜品" %}</h3>
<table class="dash-table"><thead><tr><th>{% trans "菜品名称" %}</th><th>{% trans "类型" %}</th><th>{% trans "菜系" %}</th><th>{% trans "蛋白质" %}</th><th>{% trans "脂肪" %}</th><th>{% trans "热量" %}</th><th>{% trans "状态" %}</th><th>{% trans "创建时间" %}</th></tr></thead><tbody id="dash-recent"></tbody></table>
</div>
</div>
{% endblock %}
{% block extrahead %}
{{ block.super }}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script>
(function(){
var TL={meat:'荤菜',veg:'素菜',soup:'营养汤',staple:'主食'};
var TC={meat:'#f59e0b',veg:'#22c55e',soup:'#3b82f6',staple:'#ec4899'};
var TB={meat:'rgba(245,158,11,.15)',veg:'rgba(34,197,94,.15)',soup:'rgba(59,130,246,.15)',staple:'rgba(236,72,153,.15)'};
fetch('/api/dashboard/').then(function(r){return r.json()}).then(function(d){
var ce=Object.entries(d.cuisine_counts).sort(function(a,b){return b[1]-a[1]});
var te=['meat','veg','soup','staple'].map(function(k){return[k,d.type_counts[k]||0]});
document.getElementById('dash-stats').innerHTML=
'<div class="dash-stat"><div class="icon blue">🍽️</div><div><div class="num">'+d.total+'</div><div class="label">菜品总数</div></div></div>'+
'<div class="dash-stat"><div class="icon green">✅</div><div><div class="num">'+d.active+'</div><div class="label">启用中</div></div></div>'+
'<div class="dash-stat"><div class="icon amber">⏸️</div><div><div class="num">'+d.inactive+'</div><div class="label">已停用</div></div></div>'+
'<div class="dash-stat"><div class="icon red">📊</div><div><div class="num">'+d.nutrition.avg_calorie+'</div><div class="label">平均热量(kcal)</div></div></div>';
document.getElementById('dash-nutri').innerHTML=
'<div class="dash-nutri protein"><div class="value">'+d.nutrition.avg_protein+'<span class="unit">g</span></div><div class="sublabel">荤菜/素菜平均蛋白质</div></div>'+
'<div class="dash-nutri fat"><div class="value">'+d.nutrition.avg_fat+'<span class="unit">g</span></div><div class="sublabel">荤菜/素菜平均脂肪</div></div>'+
'<div class="dash-nutri calorie"><div class="value">'+d.nutrition.avg_calorie+'<span class="unit">kcal</span></div><div class="sublabel">平均热量('+d.nutrition.min_calorie+'~'+d.nutrition.max_calorie+')</div></div>';
document.getElementById('dash-recent').innerHTML=d.recent.map(function(r){
return '<tr><td style="font-weight:600">'+r.name+'</td><td><span class="badge-t '+r.dish_type+'">'+(TL[r.dish_type]||r.dish_type)+'</span></td><td>'+(r.cuisine||'-')+'</td><td>'+Number(r.protein).toFixed(1)+'g</td><td>'+Number(r.fat).toFixed(1)+'g</td><td>'+r.calorie+'kcal</td><td><span class="badge-t '+(r.is_active?'on':'')+'">'+(r.is_active?'启用':'停用')+'</span></td><td style="color:#94a3b8">'+r.created_at+'</td></tr>';
}).join('');
setTimeout(function(){
new Chart(document.getElementById('cType'),{type:'doughnut',data:{labels:te.map(function(x){return TL[x[0]]}),datasets:[{data:te.map(function(x){return x[1]}),backgroundColor:te.map(function(x){return TC[x[0]]}),borderWidth:2,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'right',labels:{padding:12,font:{size:12}}}}}});
new Chart(document.getElementById('cCuisine'),{type:'bar',data:{labels:ce.map(function(x){return x[0]}),datasets:[{label:'菜品数',data:ce.map(function(x){return x[1]}),backgroundColor:'#3b82f6',borderRadius:5}]},options:{responsive:true,maintainAspectRatio:false,indexAxis:'y',plugins:{legend:{display:false}},scales:{x:{grid:{display:false}},y:{grid:{display:false}}}}});
new Chart(document.getElementById('cRadar'),{type:'radar',data:{labels:['蛋白质(g)','脂肪(g)','热量(kcal/10)'],datasets:te.filter(function(x){return d.type_nutrition[x[0]]&&d.type_nutrition[x[0]].count>0}).map(function(x){return{label:TL[x[0]],data:[d.type_nutrition[x[0]].avg_protein,d.type_nutrition[x[0]].avg_fat,Math.round(d.type_nutrition[x[0]].avg_calorie/10)],backgroundColor:TB[x[0]],borderColor:TC[x[0]],borderWidth:2,pointRadius:3}})},options:{responsive:true,maintainAspectRatio:false,scales:{r:{beginAtZero:true,grid:{color:'#e2e8f0'},ticks:{display:false}}},plugins:{legend:{position:'bottom',labels:{padding:10,font:{size:11}}}}}});
new Chart(document.getElementById('cBar'),{type:'bar',data:{labels:te.map(function(x){return TL[x[0]]}),datasets:[{label:'平均蛋白质(g)',data:te.map(function(x){return d.type_nutrition[x[0]]?d.type_nutrition[x[0]].avg_protein:0}),backgroundColor:'#3b82f6',borderRadius:3},{label:'平均脂肪(g)',data:te.map(function(x){return d.type_nutrition[x[0]]?d.type_nutrition[x[0]].avg_fat:0}),backgroundColor:'#f59e0b',borderRadius:3},{label:'平均热量(kcal/10)',data:te.map(function(x){return d.type_nutrition[x[0]]?Math.round(d.type_nutrition[x[0]].avg_calorie/10):0}),backgroundColor:'#ef4444',borderRadius:3}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'top',labels:{font:{size:11}}}},scales:{x:{grid:{display:false}},y:{beginAtZero:true,grid:{color:'#f1f5f9'}}}}});
},100);
});
})();
</script>
{% endblock %}