refactor(miniprogram): 体征阈值改用动态 API — 替代硬编码参考范围
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- health.ts 新增 getHealthThresholds/findThreshold/DEFAULT_THRESHOLDS
- 24h storage 缓存 + 降级到内置默认值
- health/index.tsx: REF_RANGES → buildRefRange(thresholds)
- pkg-health/input: WARN_THRESHOLDS → getWarnForIndicator(thresholds)
This commit is contained in:
iven
2026-05-02 11:40:54 +08:00
parent e8ee441ae1
commit 2cc0f5af25
3 changed files with 131 additions and 22 deletions

View File

@@ -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<VitalType, { range: string; warn: string }> = {
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<VitalType, string> {
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<TrendPoint[]>([]);
const [trendLoading, setTrendLoading] = useState(false);
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
const [thresholds, setThresholds] = useState<HealthThreshold[]>(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)}
/>
<Text className='input-ref'>{REF_RANGES.blood_pressure.range}</Text>
<Text className='input-ref'>{refRanges.blood_pressure}</Text>
</View>
)}
@@ -259,7 +281,7 @@ export default function Health() {
value={heartRateVal}
onInput={(e) => setHeartRateVal(e.detail.value)}
/>
<Text className='input-ref'>{REF_RANGES.heart_rate.range}</Text>
<Text className='input-ref'>{refRanges.heart_rate}</Text>
</View>
)}
@@ -287,7 +309,7 @@ export default function Health() {
<Text className='period-btn-text'> 2h</Text>
</View>
</View>
<Text className='input-ref'>{REF_RANGES.blood_sugar.range}</Text>
<Text className='input-ref'>{refRanges.blood_sugar}</Text>
</View>
)}
@@ -301,7 +323,7 @@ export default function Health() {
value={weightVal}
onInput={(e) => setWeightVal(e.detail.value)}
/>
<Text className='input-ref'>{REF_RANGES.weight.range}</Text>
<Text className='input-ref'>{refRanges.weight}</Text>
</View>
)}

View File

@@ -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<string, { max?: number; min?: number; warning: string }> = {
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<string, string> = {
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<HealthThreshold[]>(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)) {

View File

@@ -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<HealthThreshold[]> {
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<HealthThreshold[]>('/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 },
];