import { useState, useEffect } from 'react'; import { View, Text, Input } from '@tarojs/components'; import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; import { useHealthStore } from '../../stores/health'; import { useAuthStore } from '../../stores/auth'; import { useElderClass } from '../../hooks/useElderClass'; import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health'; import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis'; import Loading from '../../components/Loading'; import GuestGuard from '../../components/GuestGuard'; import './index.scss'; type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight'; const VITAL_TABS: { key: VitalType; label: string }[] = [ { key: 'blood_pressure', label: '血压' }, { key: 'heart_rate', label: '心率' }, { key: 'blood_sugar', label: '血糖' }, { key: 'weight', label: '体重' }, ]; /** 根据阈值列表构建参考范围文案 */ function buildRefRange(t: HealthThreshold[]): Record { const bpSys = findThreshold(t, 'systolic_bp', 'high')?.threshold_value ?? 140; const bpDia = findThreshold(t, 'diastolic_bp', 'high')?.threshold_value ?? 90; const hrHigh = findThreshold(t, 'heart_rate', 'high')?.threshold_value ?? 100; const hrLow = findThreshold(t, 'heart_rate', 'low')?.threshold_value ?? 60; const bsFasting = findThreshold(t, 'blood_sugar_fasting', 'high')?.threshold_value ?? 6.1; const bsPp = findThreshold(t, 'blood_sugar_postprandial', 'high')?.threshold_value ?? 7.8; return { blood_pressure: `收缩压 90-${bpSys} / 舒张压 60-${bpDia} mmHg`, heart_rate: `${hrLow}-${hrHigh} bpm`, blood_sugar: `空腹 3.9-${bsFasting} / 餐后 <${bsPp} mmol/L`, weight: '根据 BMI 18.5-24 计算', }; } interface TrendPoint { date: string; value: number; } export default function Health() { const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore(); const { user, currentPatient } = useAuthStore(); const modeClass = useElderClass(); const [activeTab, setActiveTab] = useState('blood_pressure'); const [systolic, setSystolic] = useState(''); const [diastolic, setDiastolic] = useState(''); const [heartRateVal, setHeartRateVal] = useState(''); const [sugarVal, setSugarVal] = useState(''); const [sugarPeriod, setSugarPeriod] = useState<'fasting' | 'postprandial'>('fasting'); const [weightVal, setWeightVal] = useState(''); const [saving, setSaving] = useState(false); const [trendData, setTrendData] = useState([]); const [trendLoading, setTrendLoading] = useState(false); const [aiSuggestions, setAiSuggestions] = useState([]); const [thresholds, setThresholds] = useState(DEFAULT_THRESHOLDS); useDidShow(() => { if (!user) return; refreshToday(); loadTrend(activeTab); loadAiSuggestions(); getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }); }); usePullDownRefresh(() => { if (!user) return; Promise.all([refreshToday(true), loadTrend(activeTab), loadAiSuggestions()]).finally(() => { Taro.stopPullDownRefresh(); }); }); if (!user) { return ; } const loadAiSuggestions = async () => { try { const items = await listPendingSuggestions(); setAiSuggestions(items.slice(0, 3)); } catch { setAiSuggestions([]); } }; const loadTrend = async (type: VitalType) => { setTrendLoading(true); try { const indicatorMap: Record = { blood_pressure: 'systolic_bp_morning', heart_rate: 'heart_rate', blood_sugar: 'blood_sugar', weight: 'weight', }; const points = await fetchTrend(indicatorMap[type], '7d'); setTrendData(points); } catch { setTrendData([]); } finally { setTrendLoading(false); } }; const handleTabChange = (tab: VitalType) => { setActiveTab(tab); loadTrend(tab); }; const getWarnStatus = (type: VitalType): string | null => { if (type === 'blood_pressure') { const sys = parseFloat(systolic); const dia = parseFloat(diastolic); const sysMax = findThreshold(thresholds, 'systolic_bp', 'high')?.threshold_value ?? 140; const diaMax = findThreshold(thresholds, 'diastolic_bp', 'high')?.threshold_value ?? 90; if (sys > sysMax || dia > diaMax) return '血压偏高,确认提交?'; } else if (type === 'heart_rate') { const val = parseFloat(heartRateVal); const hrHigh = findThreshold(thresholds, 'heart_rate', 'high')?.threshold_value ?? 100; const hrLow = findThreshold(thresholds, 'heart_rate', 'low')?.threshold_value ?? 60; if (val > hrHigh || val < hrLow) return '心率异常,确认提交?'; } else if (type === 'blood_sugar') { const val = parseFloat(sugarVal); if (sugarPeriod === 'fasting') { const bsMax = findThreshold(thresholds, 'blood_sugar_fasting', 'high')?.threshold_value ?? 6.1; if (val > bsMax) return '血糖偏高,确认提交?'; } else { const bsMax = findThreshold(thresholds, 'blood_sugar_postprandial', 'high')?.threshold_value ?? 7.8; if (val > bsMax) return '血糖偏高,确认提交?'; } } return null; }; const refRanges = buildRefRange(thresholds); const handleSave = async () => { const patientId = currentPatient?.id; if (!patientId) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; } const warnMsg = getWarnStatus(activeTab); if (warnMsg) { const { confirm } = await Taro.showModal({ title: '异常提示', content: warnMsg, confirmText: '确认提交', cancelText: '再看看', }); if (!confirm) return; } setSaving(true); try { switch (activeTab) { case 'blood_pressure': { const sys = parseFloat(systolic); const dia = parseFloat(diastolic); if (!sys || !dia) { Taro.showToast({ title: '请填写完整', icon: 'none' }); return; } await inputVitalSign(patientId, { indicator_type: 'blood_pressure', value: sys, extra: { systolic: sys, diastolic: dia }, }); setSystolic(''); setDiastolic(''); break; } case 'heart_rate': { const val = parseFloat(heartRateVal); if (!val) { Taro.showToast({ title: '请填写心率', icon: 'none' }); return; } await inputVitalSign(patientId, { indicator_type: 'heart_rate', value: val }); setHeartRateVal(''); break; } case 'blood_sugar': { const val = parseFloat(sugarVal); if (!val) { Taro.showToast({ title: '请填写血糖值', icon: 'none' }); return; } const bsType = sugarPeriod === 'fasting' ? 'blood_sugar_fasting' : 'blood_sugar_postprandial'; await inputVitalSign(patientId, { indicator_type: bsType, value: val }); setSugarVal(''); break; } case 'weight': { const val = parseFloat(weightVal); if (!val) { Taro.showToast({ title: '请填写体重', icon: 'none' }); return; } await inputVitalSign(patientId, { indicator_type: 'weight', value: val }); setWeightVal(''); break; } } Taro.showToast({ title: '保存成功', icon: 'success' }); refreshToday(true); loadTrend(activeTab); } catch { Taro.showToast({ title: '保存失败', icon: 'none' }); } finally { setSaving(false); } }; const maxTrendValue = Math.max(...trendData.map((d) => d.value), 1); const getThresholdValue = (type: VitalType, th: HealthThreshold[]): number | null => { if (type === 'blood_pressure') return findThreshold(th, 'systolic_bp', 'high')?.threshold_value ?? 140; if (type === 'heart_rate') return findThreshold(th, 'heart_rate', 'high')?.threshold_value ?? 100; if (type === 'blood_sugar') return findThreshold(th, 'blood_sugar_fasting', 'high')?.threshold_value ?? 6.1; return null; }; const dayLabels = ['日', '一', '二', '三', '四', '五', '六']; return ( {/* 页头 */} 健康数据 {/* AI 建议卡片 */} {aiSuggestions.length > 0 && ( { const first = aiSuggestions[0]; if (first?.suggestion_type === 'appointment') { Taro.navigateTo({ url: `/pages/appointment/create/index` }); } else if (first?.suggestion_type === 'followup') { Taro.navigateTo({ url: '/pages/pkg-profile/followups/index' }); } else { Taro.navigateTo({ url: '/pages/health/index' }); } }}> AI 健康建议 {aiSuggestions.length} 条待查看 {aiSuggestions.map((s) => { const riskCls = s.risk_level === 'high' ? 'ai-risk-high' : s.risk_level === 'medium' ? 'ai-risk-medium' : 'ai-risk-low'; const typeLabel = s.suggestion_type === 'followup' ? '随访' : s.suggestion_type === 'appointment' ? '预约' : '预警'; const params = s.params as Record | null; const reason = (params?.reason as string) || (params?.message as string) || typeLabel; return ( {reason.slice(0, 40)} ); })} )} {/* 类型 Tab */} {VITAL_TABS.map((tab) => { const hasData = tab.key === 'blood_pressure' ? !!todaySummary?.blood_pressure : tab.key === 'heart_rate' ? !!todaySummary?.heart_rate : tab.key === 'blood_sugar' ? !!todaySummary?.blood_sugar : !!todaySummary?.weight; return ( handleTabChange(tab.key)} > {tab.label} {!hasData && } ); })} {/* 录入区 */} {activeTab === 'blood_pressure' && ( 收缩压(高压) setSystolic(e.detail.value)} /> 舒张压(低压) setDiastolic(e.detail.value)} /> {refRanges.blood_pressure} )} {activeTab === 'heart_rate' && ( 心率 setHeartRateVal(e.detail.value)} /> {refRanges.heart_rate} )} {activeTab === 'blood_sugar' && ( 血糖值 setSugarVal(e.detail.value)} /> setSugarPeriod('fasting')} > 空腹 setSugarPeriod('postprandial')} > 餐后 2h {refRanges.blood_sugar} )} {activeTab === 'weight' && ( 体重 (kg) setWeightVal(e.detail.value)} /> {refRanges.weight} )} {saving ? '保存中...' : '保存'} {/* 趋势图 */} 近 7 天趋势 {trendLoading ? ( ) : trendData.length === 0 ? ( 暂无趋势数据 ) : ( {/* 阈值标线 */} {getThresholdValue(activeTab, thresholds) && (() => { const tv = getThresholdValue(activeTab, thresholds)!; const pct = Math.min(95, (tv / maxTrendValue) * 100); return ( {tv} ); })()} {trendData.map((point, i) => { const heightPct = Math.max(8, (point.value / maxTrendValue) * 100); const tv = getThresholdValue(activeTab, thresholds); const isAbnormal = tv ? point.value >= tv : false; const dayOfWeek = new Date(point.date).getDay(); return ( {dayLabels[dayOfWeek]} ); })} )} {/* BLE 设备同步功能暂缓开放 */} {/* 健康资讯入口 */} Taro.navigateTo({ url: '/pages/article/index' })} > 最新健康资讯 › ); }