diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index 166c7c2..dc41f43 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -3,7 +3,7 @@ import { View, Text, Input } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { useHealthStore } from '../../stores/health'; import { useAuthStore } from '../../stores/auth'; -import { inputVitalSign, getTrend } from '../../services/health'; +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 './index.scss'; @@ -17,12 +17,21 @@ const VITAL_TABS: { key: VitalType; label: string }[] = [ { key: 'weight', label: '体重' }, ]; -const REF_RANGES: Record = { - blood_pressure: { range: '收缩压 90-140 / 舒张压 60-90 mmHg', warn: '血压偏高,确认提交?' }, - heart_rate: { range: '60-100 bpm', warn: '心率异常,确认提交?' }, - blood_sugar: { range: '空腹 3.9-6.1 / 餐后 <7.8 mmol/L', warn: '血糖偏高,确认提交?' }, - weight: { range: '根据 BMI 18.5-24 计算', warn: '' }, -}; +/** 根据阈值列表构建参考范围文案 */ +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; @@ -43,11 +52,13 @@ export default function Health() { const [trendData, setTrendData] = useState([]); const [trendLoading, setTrendLoading] = useState(false); const [aiSuggestions, setAiSuggestions] = useState([]); + const [thresholds, setThresholds] = useState(DEFAULT_THRESHOLDS); useDidShow(() => { refreshToday(); loadTrend(activeTab); loadAiSuggestions(); + getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }); }); const loadAiSuggestions = async () => { @@ -86,18 +97,29 @@ export default function Health() { if (type === 'blood_pressure') { const sys = parseFloat(systolic); const dia = parseFloat(diastolic); - if (sys > 140 || dia > 90) return REF_RANGES.blood_pressure.warn; + 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); - if (val > 100 || val < 60) return REF_RANGES.heart_rate.warn; + 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' && val > 6.1) return REF_RANGES.blood_sugar.warn; - if (sugarPeriod === 'postprandial' && val > 7.8) return REF_RANGES.blood_sugar.warn; + 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) { @@ -245,7 +267,7 @@ export default function Health() { value={diastolic} onInput={(e) => setDiastolic(e.detail.value)} /> - {REF_RANGES.blood_pressure.range} + {refRanges.blood_pressure} )} @@ -259,7 +281,7 @@ export default function Health() { value={heartRateVal} onInput={(e) => setHeartRateVal(e.detail.value)} /> - {REF_RANGES.heart_rate.range} + {refRanges.heart_rate} )} @@ -287,7 +309,7 @@ export default function Health() { 餐后 2h - {REF_RANGES.blood_sugar.range} + {refRanges.blood_sugar} )} @@ -301,7 +323,7 @@ export default function Health() { value={weightVal} onInput={(e) => setWeightVal(e.detail.value)} /> - {REF_RANGES.weight.range} + {refRanges.weight} )} diff --git a/apps/miniprogram/src/pages/pkg-health/input/index.tsx b/apps/miniprogram/src/pages/pkg-health/input/index.tsx index 765c8f6..82b227d 100644 --- a/apps/miniprogram/src/pages/pkg-health/input/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/input/index.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { View, Text, Input, Picker } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { z } from 'zod'; -import { inputVitalSign } from '../../../services/health'; +import { inputVitalSign, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../../services/health'; import { useAuthStore } from '../../../stores/auth'; import { useHealthStore } from '@/stores/health'; import { usePointsStore } from '@/stores/points'; @@ -29,14 +29,30 @@ const vitalSignSchema = z.object({ note: z.string().max(200, '备注不能超过200字').optional(), }); -const WARN_THRESHOLDS: Record = { - blood_pressure: { max: 180, warning: '收缩压偏高,建议及时就医' }, - heart_rate: { max: 120, min: 50, warning: '心率异常,请注意休息' }, - blood_sugar_fasting: { max: 11.0, warning: '血糖偏高,建议就医检查' }, -}; +/** 根据动态阈值生成警告配置 */ +function getWarnForIndicator( + thresholds: HealthThreshold[], + indicator: string, +): { max?: number; min?: number; warning: string } | null { + const high = findThreshold(thresholds, indicator === 'blood_pressure' ? 'systolic_bp' : indicator, 'high'); + const low = findThreshold(thresholds, indicator === 'blood_pressure' ? 'systolic_bp' : indicator, 'low'); + if (!high && !low) return null; + const warningMap: Record = { + blood_pressure: '收缩压偏高,建议及时就医', + heart_rate: '心率异常,请注意休息', + blood_sugar_fasting: '血糖偏高,建议就医检查', + blood_sugar_postprandial: '血糖偏高,建议就医检查', + }; + return { + max: high?.threshold_value, + min: low?.threshold_value, + warning: warningMap[indicator] ?? '数值异常,请关注', + }; +} export default function HealthInput() { const [indicatorIdx, setIndicatorIdx] = useState(0); + const [thresholds, setThresholds] = useState(DEFAULT_THRESHOLDS); const [value, setValue] = useState(''); const [systolic, setSystolic] = useState(''); const [diastolic, setDiastolic] = useState(''); @@ -47,6 +63,7 @@ export default function HealthInput() { /** 从 storage 中读取设备同步回传的数据并自动填充表单 */ useDidShow(() => { + getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }); try { const raw = Taro.getStorageSync('device_sync_result'); if (!raw) return; @@ -102,7 +119,7 @@ export default function HealthInput() { return; } - const threshold = WARN_THRESHOLDS[currentIndicator]; + const threshold = getWarnForIndicator(thresholds, currentIndicator); if (threshold) { const val = input.value; if ((threshold.max && val > threshold.max) || (threshold.min && val < threshold.min)) { diff --git a/apps/miniprogram/src/services/health.ts b/apps/miniprogram/src/services/health.ts index 469c0ea..4d18c93 100644 --- a/apps/miniprogram/src/services/health.ts +++ b/apps/miniprogram/src/services/health.ts @@ -1,3 +1,4 @@ +import Taro from '@tarojs/taro'; import { api } from './request'; export interface VitalSignInput { @@ -36,6 +37,10 @@ export async function inputVitalSign(patientId: string, data: VitalSignInput) { if (data.extra?.systolic) body.systolic_bp_morning = data.extra.systolic; if (data.extra?.diastolic) body.diastolic_bp_morning = data.extra.diastolic; break; + case 'blood_pressure_evening': + if (data.extra?.systolic) body.systolic_bp_evening = data.extra.systolic; + if (data.extra?.diastolic) body.diastolic_bp_evening = data.extra.diastolic; + break; case 'heart_rate': body.heart_rate = Math.round(data.value); break; @@ -45,6 +50,12 @@ export async function inputVitalSign(patientId: string, data: VitalSignInput) { case 'blood_sugar': body.blood_sugar = data.value; break; + case 'body_temperature': + body.body_temperature = data.value; + break; + case 'spo2': + body.spo2 = Math.round(data.value); + break; case 'water_intake': body.water_intake_ml = Math.round(data.value); break; @@ -113,3 +124,62 @@ export async function listDailyMonitoring( params, ); } + +// ---- Health Thresholds (健康阈值) ---- + +export interface HealthThreshold { + id: string; + indicator: string; + direction: string; + threshold_value: number; + level: string; + department: string | null; + age_min: number | null; + age_max: number | null; + is_active: boolean; +} + +const THRESHOLD_CACHE_KEY = 'health_thresholds'; +const THRESHOLD_TTL = 24 * 60 * 60 * 1000; // 24h + +/** 从缓存或 API 获取健康阈值列表 */ +export async function getHealthThresholds(): Promise { + try { + const cached = Taro.getStorageSync(THRESHOLD_CACHE_KEY) as + | { data: HealthThreshold[]; ts: number } + | undefined; + if (cached && Date.now() - cached.ts < THRESHOLD_TTL) { + return cached.data; + } + } catch { /* cache miss */ } + + try { + const data = await api.get('/health/critical-value-thresholds/public'); + Taro.setStorageSync(THRESHOLD_CACHE_KEY, { data, ts: Date.now() }); + return data; + } catch { + return []; + } +} + +/** 查找匹配的阈值,缓存未命中时返回 undefined */ +export function findThreshold( + thresholds: HealthThreshold[], + indicator: string, + direction: string, + level = 'warning', +): HealthThreshold | undefined { + return thresholds.find( + (t) => t.indicator === indicator && t.direction === direction && t.level === level && t.is_active, + ); +} + +/** 内置默认阈值(API 不可用时的降级方案) */ +export const DEFAULT_THRESHOLDS: HealthThreshold[] = [ + { id: '_bp_sys_high', indicator: 'systolic_bp', direction: 'high', threshold_value: 140, level: 'warning', department: null, age_min: null, age_max: null, is_active: true }, + { id: '_bp_dia_high', indicator: 'diastolic_bp', direction: 'high', threshold_value: 90, level: 'warning', department: null, age_min: null, age_max: null, is_active: true }, + { id: '_hr_high', indicator: 'heart_rate', direction: 'high', threshold_value: 100, level: 'warning', department: null, age_min: null, age_max: null, is_active: true }, + { id: '_hr_low', indicator: 'heart_rate', direction: 'low', threshold_value: 60, level: 'warning', department: null, age_min: null, age_max: null, is_active: true }, + { id: '_bs_fasting_high', indicator: 'blood_sugar_fasting', direction: 'high', threshold_value: 6.1, level: 'warning', department: null, age_min: null, age_max: null, is_active: true }, + { id: '_bs_pp_high', indicator: 'blood_sugar_postprandial', direction: 'high', threshold_value: 7.8, level: 'warning', department: null, age_min: null, age_max: null, is_active: true }, +];