feat(ui): 睡眠页面添加健康评估与影响卡片

添加睡眠健康评估卡片和睡眠影响卡片,显示近7天睡眠状况分析和健康提示。
同时修改睡眠时长趋势图,根据睡眠时长自动着色数据点。

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

View File

@@ -6374,6 +6374,19 @@ def get_sleep_page_html() -> str:
</div>
</div>
<!-- 睡眠健康评估卡片 -->
<div id="sleep-assessment-card" class="chart-container">
<h3>睡眠健康评估</h3>
<div id="sleep-assessment-content">
<div class="empty">加载中...</div>
</div>
</div>
<!-- 睡眠影响卡片(动态显示警告或益处) -->
<div id="sleep-impact-card" class="chart-container" style="display: none;">
<div id="sleep-impact-content"></div>
</div>
<div class="chart-container">
<h3>近 30 天睡眠时长趋势</h3>
<canvas id="duration-chart" height="120"></canvas>
@@ -6504,6 +6517,9 @@ def get_sleep_page_html() -> str:
backgroundColor: 'rgba(109, 40, 217, 0.2)',
fill: true,
tension: 0.3,
pointBackgroundColor: durationData.map(d =>
d >= 7 && d <= 9 ? '#10B981' : d < 6 ? '#EF4444' : '#F59E0B'
),
}]
},
options: { responsive: true, scales: { y: { beginAtZero: true } } }
@@ -6572,6 +6588,105 @@ def get_sleep_page_html() -> str:
}
}
async function loadSleepAssessment() {
try {
const res = await fetch('/api/sleep/assessment');
const data = await res.json();
// Render assessment card
const assessmentContainer = document.getElementById('sleep-assessment-content');
const statusColor = data.status.color;
const avgPercent = Math.min(Math.max((data.avg_duration - 4) / 6 * 100, 0), 100);
assessmentContainer.innerHTML = `
<div style="text-align: center; margin-bottom: 16px;">
<div style="font-size: 14px; color: #94A3B8;">近 7 天平均睡眠</div>
<div style="font-size: 36px; font-weight: 600; color: white;">${data.avg_duration}<span style="font-size: 18px; color: #94A3B8;"> 小时</span></div>
</div>
<div style="margin-bottom: 16px;">
<div style="display: flex; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 4px;">
<div style="flex: 1; background: #EF4444;"></div>
<div style="flex: 1; background: #F59E0B;"></div>
<div style="flex: 2; background: #10B981;"></div>
<div style="flex: 1; background: #3B82F6;"></div>
</div>
<div style="position: relative; height: 12px;">
<div style="position: absolute; left: ${avgPercent}%; transform: translateX(-50%); color: ${statusColor}; font-size: 12px;">▲</div>
</div>
<div style="display: flex; font-size: 10px; color: #64748B;">
<div style="flex: 1; text-align: center;">偏少 &lt;5h</div>
<div style="flex: 1; text-align: center;">不足 5-6h</div>
<div style="flex: 2; text-align: center;">理想 7-9h</div>
<div style="flex: 1; text-align: center;">过多 &gt;9h</div>
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding: 12px; background: rgba(255,255,255,0.05); border-radius: 8px;">
<div>
<span style="color: ${statusColor}; font-weight: 600;">${data.status.label}</span>
${data.gap_hours > 0 ? `<span style="color: #94A3B8; font-size: 12px; margin-left: 8px;">距理想还差 ${data.gap_hours} 小时/天</span>` : ''}
</div>
<div style="font-size: 12px; color: #64748B;">
本周: 理想 ${data.ideal_days_count}/${data.total_days}
</div>
</div>
`;
// Render impact card
const impactCard = document.getElementById('sleep-impact-card');
const impactContainer = document.getElementById('sleep-impact-content');
const isWarning = data.status.level === 'severe' || data.status.level === 'insufficient';
const bgColor = isWarning ? 'rgba(127, 29, 29, 0.2)' : 'rgba(6, 95, 70, 0.2)';
const borderColor = isWarning ? 'rgba(239, 68, 68, 0.3)' : 'rgba(16, 185, 129, 0.3)';
const titleColor = isWarning ? '#F87171' : '#34D399';
const title = isWarning ? '睡眠不足警告' : '睡眠状态良好';
const intro = isWarning
? `您近 7 天平均睡眠仅 ${data.avg_duration} 小时,低于健康标准 7 小时。长期睡眠不足会对身体造成以下影响:`
: `您近 7 天平均睡眠 ${data.avg_duration} 小时,处于健康范围内!充足睡眠为您带来以下益处:`;
impactCard.style.display = 'block';
impactCard.style.background = bgColor;
impactCard.style.border = `1px solid ${borderColor}`;
const impacts = data.health_impacts;
let impactHtml = `
<h3 style="color: ${titleColor}; margin-bottom: 12px;">${title}</h3>
<p style="font-size: 13px; color: #CBD5E1; margin-bottom: 16px;">${intro}</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
`;
Object.values(impacts).forEach(impact => {
impactHtml += `
<div style="background: rgba(255,255,255,0.05); border-radius: 8px; padding: 12px;">
<div style="font-size: 13px; font-weight: 500; color: #E2E8F0; margin-bottom: 8px;">${impact.title}</div>
<ul style="margin: 0; padding-left: 16px; font-size: 11px; color: #94A3B8;">
`;
impact.effects.forEach(e => {
impactHtml += `<li style="margin: 4px 0;">${e}</li>`;
});
impactHtml += `
</ul>
</div>
`;
});
impactHtml += `
</div>
<div style="margin-top: 16px; padding: 12px; background: rgba(255,255,255,0.05); border-radius: 8px; text-align: center;">
<span style="font-size: 13px; color: #E2E8F0;">${data.suggestion}</span>
</div>
`;
impactContainer.innerHTML = impactHtml;
} catch (e) {
console.error('加载睡眠评估失败:', e);
document.getElementById('sleep-assessment-content').innerHTML = '<div class="empty">加载失败</div>';
}
}
async function deleteSleep(id) {
if (!confirm('确定要删除这条睡眠记录吗?')) return;
try {
@@ -6615,6 +6730,7 @@ def get_sleep_page_html() -> str:
document.getElementById('addSleepForm').addEventListener('submit', submitSleepForm);
loadSleepData();
loadSleepAssessment();
// 检查管理员状态
const user = JSON.parse(localStorage.getItem('user') || '{}');