feat(ui): 睡眠页面添加健康评估与影响卡片
添加睡眠健康评估卡片和睡眠影响卡片,显示近7天睡眠状况分析和健康提示。 同时修改睡眠时长趋势图,根据睡眠时长自动着色数据点。 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;">偏少 <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;">过多 >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') || '{}');
|
||||
|
||||
Reference in New Issue
Block a user