import { useEffect, useState } from 'react'; import { Link, f7 } from 'framework7-react'; import { apiClient, errorMessage, FitnessAge, HealthDay } from '../services/api'; import { METRICS, metricHref } from '../lib/metrics'; import MetricCard from '../components/charts/MetricCard'; import Skeleton from '../components/Skeleton'; import Screen from '../components/Screen'; import AiPanel from '../components/AiPanel'; import { FEATURES } from '../features'; import { daysAgo, today as todayIso } from '../lib/day'; import './Health.css'; import './Settings.css'; const WINDOW_DAYS = 30; /* Sections name metric ids; the labels, units and accessors come from the registry so 今日 / 健康 / 趋势 cannot disagree about what a metric is. */ const SECTIONS: Array<{ title: string; items: string[] }> = [ { title: '身体指标', items: ['heartRate', 'heartRateVariability', 'respirationAvg', 'spo2Avg'] }, { title: '恢复', items: ['bodyBatteryHigh', 'stress', 'trainingReadiness', 'enduranceScore'] }, { title: '睡眠', items: ['sleepDuration', 'sleepQuality', 'deepShare', 'remShare'] }, { title: '活动', items: ['steps', 'intensityMinutes', 'floorsAscended', 'distance'] }, { title: '能量', items: ['caloriesBurned', 'activeCalories', 'bmrCalories', 'sedentary'] }, { title: '身体成分', items: ['weight', 'bodyFat', 'hydration', 'hillScore'] }, ]; /* Screens that are not a single metric, so they get their own entries. */ const LINKS: Array<[string, string, string]> = [ ['/body/', '身体成分与血压', '体重、体脂、肌肉量、血压记录'], ['/race/', '成绩预测', '5 公里到全马的预测完赛时间'], ['/challenges/', '挑战赛', '徽章挑战与好友挑战'], ]; function BodyAge({ data }: { data: FitnessAge | null }) { if (!data) return null; const go = () => f7.views.current.router.navigate('/body-age/'); if (data.value == null) { return (

身体年龄

); } const delta = data.delta ?? 0; return (

身体年龄

); } function HealthPage() { const [days, setDays] = useState([]); const [bodyAge, setBodyAge] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { const load = async () => { try { setDays( await apiClient.getHealthSummary( daysAgo(WINDOW_DAYS - 1), todayIso() ) ); } catch (err: any) { setError(errorMessage(err, '加载失败')); } finally { setLoading(false); } }; load(); // Body age is a separate, optional read: an empty profile must not stop // the rest of the page from rendering. apiClient.getFitnessAge().then(setBodyAge).catch(() => setBodyAge(null)); }, []); if (loading) { return ( ); } if (error) { return (
{error}
); } if (days.length === 0) { return (

还没有任何健康数据。

去同步 Garmin 数据
); } // The most recent day that actually recorded a given metric — showing "—" // because today's sleep has not synced yet would hide data that exists. const latest = (pick: (d: HealthDay) => number | null) => { for (let i = days.length - 1; i >= 0; i--) { const v = pick(days[i]); if (v != null) return { value: v, date: days[i].date }; } return { value: null as number | null, date: null as string | null }; }; return ( {FEATURES.ai && } {SECTIONS.map((section) => (

{section.title}

{section.items.map((id) => { const def = METRICS[id]; if (!def) return null; const { value, date } = latest(def.pick); const stale = date != null && date !== days[days.length - 1].date; return ( f7.views.current.router.navigate(metricHref(id))} /> ); })}
))}

更多

{LINKS.map(([href, label, sub]) => ( {label} {sub} ))}

参考区间为一般人群的定位范围,非诊断标准。如有健康疑问请咨询专业医师。

); } export default HealthPage;