feat(ui): 饮食页面添加营养状况卡片与建议功能

添加今日营养状况卡片,显示热量、蛋白质、碳水、脂肪的摄入进度;
修改近7天摄入趋势图表,添加建议摄入参考线(虚线);
页面加载时调用营养建议API并展示晚餐建议。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 16:29:35 +08:00
parent 36bbd909bf
commit 02d7abfa63

View File

@@ -5507,6 +5507,14 @@ def get_meal_page_html() -> str:
</div>
</div>
<!-- 今日营养状况卡片 -->
<div class="chart-container" id="nutrition-status-card">
<h3>今日营养状况</h3>
<div id="nutrition-status-content">
<div class="empty">加载中...</div>
</div>
</div>
<div class="chart-container">
<h3>本月饮食日历</h3>
<div class="calendar" id="calendar">
@@ -5877,14 +5885,24 @@ def get_meal_page_html() -> str:
type: 'line',
data: {
labels: intakeLabels,
datasets: [{
label: '摄入卡路里',
data: intakeValues,
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.2)',
fill: true,
tension: 0.3,
}]
datasets: [
{
label: '实际摄入',
data: intakeValues,
borderColor: '#10B981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.3,
},
{
label: '建议摄入',
data: Array(intakeLabels.length).fill(window.nutritionTarget || 1800),
borderColor: '#F59E0B',
borderDash: [5, 5],
pointRadius: 0,
fill: false,
}
]
},
options: { responsive: true, scales: { y: { beginAtZero: true } } }
});
@@ -6000,7 +6018,81 @@ def get_meal_page_html() -> str:
}
}
async function loadNutritionRecommendations() {
try {
const res = await fetch('/api/nutrition/recommendations');
const data = await res.json();
const container = document.getElementById('nutrition-status-content');
const calcPercent = (actual, target) => target > 0 ? Math.min(Math.round(actual / target * 100), 100) : 0;
const getColor = (percent) => percent >= 80 ? '#10B981' : percent >= 50 ? '#F59E0B' : '#EF4444';
const nutrients = [
{ key: 'calories', label: '热量', unit: '', target: data.daily_targets.calories, actual: data.today_intake.calories },
{ key: 'protein', label: '蛋白质', unit: 'g', target: data.daily_targets.protein, actual: data.today_intake.protein },
{ key: 'carbs', label: '碳水', unit: 'g', target: data.daily_targets.carbs, actual: data.today_intake.carbs },
{ key: 'fat', label: '脂肪', unit: 'g', target: data.daily_targets.fat, actual: data.today_intake.fat },
];
let html = `
<div class="grid" style="grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;">
`;
nutrients.forEach(n => {
const pct = calcPercent(n.actual, n.target);
const color = getColor(pct);
html += `
<div style="text-align: center; padding: 12px; background: rgba(255,255,255,0.05); border-radius: 8px;">
<div style="font-size: 12px; color: #94A3B8;">${n.label}</div>
<div style="font-size: 20px; font-weight: 600; color: white;">${n.actual}</div>
<div style="font-size: 11px; color: #64748B;">/${n.target}${n.unit}</div>
<div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin: 8px 0;">
<div style="height: 100%; width: ${pct}%; background: ${color}; border-radius: 2px;"></div>
</div>
<div style="font-size: 11px; color: ${color};">${pct}%</div>
</div>
`;
});
html += '</div>';
// Add suggestions if any
if (data.suggestions && data.suggestions.length > 0) {
html += `
<div style="background: rgba(251,191,36,0.1); border: 1px solid rgba(251,191,36,0.3); border-radius: 8px; padding: 12px;">
<div style="font-size: 13px; font-weight: 500; color: #FBBF24; margin-bottom: 8px;">晚餐建议</div>
`;
data.suggestions.forEach(s => {
html += `<div style="font-size: 12px; color: #E2E8F0; margin-bottom: 4px;">${s.message}</div>`;
if (s.options && s.options.length > 0) {
html += '<ul style="margin: 4px 0 0 16px; padding: 0; font-size: 11px; color: #94A3B8;">';
s.options.forEach(o => {
html += `<li style="margin: 2px 0;">${o}</li>`;
});
html += '</ul>';
}
});
html += '</div>';
} else {
html += `
<div style="text-align: center; padding: 12px; color: #10B981; font-size: 13px;">
今日营养摄入均衡
</div>
`;
}
container.innerHTML = html;
// Store nutrition target for trend chart
window.nutritionTarget = data.daily_targets.calories;
} catch (e) {
console.error('加载营养建议失败:', e);
document.getElementById('nutrition-status-content').innerHTML = '<div class="empty">加载失败</div>';
}
}
document.getElementById('addMealForm').addEventListener('submit', submitMealForm);
loadNutritionRecommendations();
loadMealStats();
// 检查管理员状态