import { useCallback, useEffect, useMemo, useState } from 'react'; import { apiClient, Activity, errorMessage, HealthDay } from '../services/api'; import Skeleton from '../components/Skeleton'; import './Daily.css'; import './Pages.css'; /** Every stored metric, grouped the way the device groups them. */ interface Field { key: keyof HealthDay | 'sleepDeep' | 'sleepLight' | 'sleepRem' | 'sleepAwake'; label: string; unit?: string; /** Convert the raw stored value for display. */ transform?: (v: number) => number; decimals?: number; hint?: string; } const SECONDS_TO_HOURS = (v: number) => v / 3600; const SECONDS_TO_MINUTES = (v: number) => v / 60; const GROUPS: Array<{ title: string; fields: Field[] }> = [ { title: '活动', fields: [ { key: 'steps', label: '步数', unit: '步' }, { key: 'stepGoal', label: '步数目标', unit: '步' }, { key: 'distanceMeters', label: '距离', unit: 'km', transform: (v) => v / 1000, decimals: 2 }, { key: 'floorsAscended', label: '爬楼上行', unit: '层' }, { key: 'floorsDescended', label: '爬楼下行', unit: '层' }, { key: 'intensityMinutes', label: '强度分钟', unit: '分钟', hint: '中等及以上强度' }, { key: 'activeSeconds', label: '活动时长', unit: '小时', transform: SECONDS_TO_HOURS, decimals: 1 }, { key: 'sedentarySeconds', label: '久坐时长', unit: '小时', transform: SECONDS_TO_HOURS, decimals: 1 }, ], }, { title: '能量', fields: [ { key: 'caloriesBurned', label: '总消耗', unit: 'kcal' }, { key: 'activeCalories', label: '活动消耗', unit: 'kcal' }, { key: 'bmrCalories', label: '基础代谢', unit: 'kcal' }, ], }, { title: '心率', fields: [ { key: 'heartRate', label: '静息心率', unit: 'bpm' }, { key: 'heartRateMin', label: '最低心率', unit: 'bpm' }, { key: 'heartRateMax', label: '最高心率', unit: 'bpm' }, { key: 'heartRateVariability', label: '心率变异性', unit: 'ms', decimals: 1, hint: 'HRV,反映恢复情况' }, ], }, { title: '压力与身体电量', fields: [ { key: 'stress', label: '平均压力' }, { key: 'stressMax', label: '最高压力' }, { key: 'bodyBatteryHigh', label: '身体电量最高' }, { key: 'bodyBatteryLow', label: '身体电量最低' }, { key: 'bodyBatteryCharged', label: '当日充能' }, { key: 'bodyBatteryDrained', label: '当日消耗' }, ], }, { title: '睡眠', fields: [ { key: 'sleepDuration', label: '总时长', unit: '小时', decimals: 1 }, { key: 'sleepQuality', label: '睡眠评分', unit: '/100' }, { key: 'sleepDeep', label: '深睡', unit: '分钟', transform: SECONDS_TO_MINUTES }, { key: 'sleepLight', label: '浅睡', unit: '分钟', transform: SECONDS_TO_MINUTES }, { key: 'sleepRem', label: 'REM', unit: '分钟', transform: SECONDS_TO_MINUTES }, { key: 'sleepAwake', label: '夜间清醒', unit: '分钟', transform: SECONDS_TO_MINUTES }, { key: 'sleepSpo2Avg', label: '睡眠血氧', unit: '%', decimals: 1 }, { key: 'sleepRespirationAvg', label: '睡眠呼吸', unit: '次/分', decimals: 1 }, { key: 'sleepStressAvg', label: '睡眠压力', decimals: 1 }, ], }, { title: '血氧与呼吸', fields: [ { key: 'spo2Avg', label: '平均血氧', unit: '%', decimals: 1 }, { key: 'spo2Min', label: '最低血氧', unit: '%' }, { key: 'respirationAvg', label: '平均呼吸', unit: '次/分', decimals: 1 }, { key: 'respirationMin', label: '最低呼吸', unit: '次/分', decimals: 1 }, { key: 'respirationMax', label: '最高呼吸', unit: '次/分', decimals: 1 }, ], }, { title: '训练', fields: [ { key: 'trainingReadiness', label: '训练准备度', unit: '/100' }, { key: 'vo2max', label: 'VO2max', decimals: 1 }, { key: 'enduranceScore', label: '耐力分' }, ], }, ]; const ACTIVITY_LABEL: Record = { running: '跑步', cycling: '骑行', walking: '步行', hiking: '徒步', swimming: '游泳', table_tennis: '乒乓球', strength_training: '力量训练', indoor_cycling: '室内骑行', treadmill_running: '跑步机', }; function valueOf(day: HealthDay, key: Field['key']): number | null { if (key === 'sleepDeep') return day.sleep?.deepSeconds ?? null; if (key === 'sleepLight') return day.sleep?.lightSeconds ?? null; if (key === 'sleepRem') return day.sleep?.remSeconds ?? null; if (key === 'sleepAwake') return day.sleep?.awakeSeconds ?? null; const v = (day as any)[key]; return typeof v === 'number' ? v : null; } const iso = (d: Date) => d.toISOString().slice(0, 10); function Daily() { const [date, setDate] = useState(() => iso(new Date())); const [day, setDay] = useState(null); const [activities, setActivities] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [onlyRecorded, setOnlyRecorded] = useState(true); const load = useCallback(async (target: string) => { setLoading(true); setError(''); try { const [summary, acts] = await Promise.all([ apiClient.getHealthSummary(target, target), apiClient.getActivities(target, target), ]); setDay(summary[0] ?? null); setActivities(acts); } catch (err: any) { setError(errorMessage(err, '加载失败')); } finally { setLoading(false); } }, []); useEffect(() => { load(date); }, [date, load]); const shift = (delta: number) => { const d = new Date(date); d.setDate(d.getDate() + delta); if (d > new Date()) return; setDate(iso(d)); }; const recorded = useMemo(() => { if (!day) return 0; return GROUPS.reduce( (n, g) => n + g.fields.filter((f) => valueOf(day, f.key) != null).length, 0 ); }, [day]); const totalFields = GROUPS.reduce((n, g) => n + g.fields.length, 0); const isToday = date === iso(new Date()); const fmt = (f: Field, raw: number) => { const v = f.transform ? f.transform(raw) : raw; const decimals = f.decimals ?? 0; return v.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals, }); }; return (

每日数据

{day ? `已记录 ${recorded} / ${totalFields} 项指标` : '该日无数据'}

e.target.value && setDate(e.target.value)} />
{error &&
{error}
} {loading && } {!loading && !error && !day && (

{date} 没有数据。可能当天未佩戴设备,或尚未同步到这一天。

)} {!loading && !error && day && ( <> {GROUPS.map((g) => { const fields = onlyRecorded ? g.fields.filter((f) => valueOf(day, f.key) != null) : g.fields; if (fields.length === 0) return null; return (

{g.title} {fields.length} 项

{fields.map((f) => { const raw = valueOf(day, f.key); return (
{f.label} {f.hint && {f.hint}}
{raw == null ? ( 未记录 ) : ( <> {fmt(f, raw)} {f.unit && {f.unit}} )}
); })}
); })}

运动记录 {activities.length} 条

{activities.length === 0 ? (

当天没有运动记录。

) : (
{activities.map((a) => ( ))}
开始 类型 时长 距离 消耗 平均心率 最高心率
{a.start_time?.slice(11, 16)} {ACTIVITY_LABEL[a.activity_type] ?? a.activity_type?.replace(/_/g, ' ')} {a.duration != null ? `${Math.round(a.duration / 60)} 分` : '—'} {a.distance ? `${(a.distance / 1000).toFixed(2)} km` : '—'} {a.calories != null ? Math.round(a.calories) : '—'} {a.heart_rate_average ?? '—'} {a.heart_rate_max ?? '—'}
)}
)}
); } export default Daily;