import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { apiClient, errorMessage, HealthDay } from '../services/api'; import Chart from '../components/charts/Chart'; import StatTile from '../components/charts/StatTile'; import Skeleton from '../components/Skeleton'; import './Pages.css'; const RANGES = [7, 14, 30, 90]; const H = 3600; function avg(values: Array): number | null { const present = values.filter((v): v is number => v != null); return present.length ? present.reduce((a, b) => a + b, 0) / present.length : null; } function Sleep() { const [days, setDays] = useState([]); // 14 by default: the stacked chart needs bars wide enough to read the // thinnest stage and to give hover a ~24px hit target. Longer windows stay // available for the trend, where density matters less. const [range, setRange] = useState(14); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { const load = async () => { setLoading(true); try { const end = new Date(); const start = new Date(end.getTime() - (range - 1) * 86400000); setDays( await apiClient.getHealthSummary( start.toISOString().slice(0, 10), end.toISOString().slice(0, 10) ) ); } catch (err: any) { setError(errorMessage(err, '加载睡眠数据失败')); } finally { setLoading(false); } }; load(); }, [range]); const nights = days.filter((d) => d.sleepDuration != null); // Stage seconds are converted to hours here so the stacked bar and the // duration chart share one y-scale — a chart never carries two scales. const rows = nights.map((d) => ({ date: d.date.slice(5), deep: d.sleep?.deepSeconds != null ? d.sleep.deepSeconds / H : null, light: d.sleep?.lightSeconds != null ? d.sleep.lightSeconds / H : null, rem: d.sleep?.remSeconds != null ? d.sleep.remSeconds / H : null, awake: d.sleep?.awakeSeconds != null ? d.sleep.awakeSeconds / H : null, quality: d.sleepQuality, spo2: d.sleepSpo2Avg, respiration: d.sleepRespirationAvg, stress: d.sleepStressAvg, })); const avgDeep = avg(rows.map((r) => r.deep)); const avgRem = avg(rows.map((r) => r.rem)); const avgLight = avg(rows.map((r) => r.light)); const avgAwake = avg(rows.map((r) => r.awake)); const avgDuration = avg(nights.map((d) => d.sleepDuration)); const avgQuality = avg(nights.map((d) => d.sleepQuality)); const totalStages = [avgDeep, avgLight, avgRem].reduce( (sum, v) => sum + (v ?? 0), 0 ); const share = (v: number | null) => v == null || totalStages === 0 ? undefined : `占 ${Math.round((v / totalStages) * 100)}%`; const hrs = (v: number | null, d = 1) => (v == null ? null : Math.round(v * 10 ** d) / 10 ** d); return (

睡眠

分期、评分与夜间生理指标

{RANGES.map((r) => ( ))}
{error &&
{error}
} {loading && ( <>
)} {!loading && !error && nights.length === 0 && (

所选区间内没有睡眠记录。

去同步数据
)} {!loading && !error && nights.length > 0 && ( <>

{nights.length} 晚平均

)}
); } export default Sleep;