T40 UI 审计修复(60 页面全覆盖): - 新增 $acc-d/$wrn-d 渐变中间色变量,修复首页轮播渐变硬编码 - 替换 8 处裸 white 为 $white 设计变量(5 个 SCSS 文件) - 修复 7 处触摸目标 40/44px → 48px(健康/消息/咨询/预约/首页) - 3 页面新增 Loading 状态(体征录入/个人中心/就诊人添加) - statusTag 移除硬编码布局值,改用 SCSS mixin 控制 - 医生端 14 页面架构 Hook 层补充(useThrottledDidShow 替换 useEffect) - 移除 action-inbox 未使用 import 安全 P0 修复: - JWT 中间件加固:token 类型校验 + 过期预检 + 类型别名简化 - 速率限制增强:滑动窗口 + 暴力破解防护 - analytics handler 错误处理完善 文档: - T40 审计报告(24 PASS / 36 PASS_WITH_ISSUES / 0 NEEDS_WORK) - 5 份 DevTools/性能审计讨论记录 - wiki 症状导航 + 小程序章节更新
187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
import Taro from '@tarojs/taro';
|
||
import { api } from './request';
|
||
|
||
export interface VitalSignInput {
|
||
indicator_type: string;
|
||
value: number;
|
||
measured_at?: string;
|
||
note?: string;
|
||
extra?: Record<string, number>;
|
||
}
|
||
|
||
export interface TodaySummary {
|
||
blood_pressure?: { systolic: number; diastolic: number; status: string; reference_range?: string };
|
||
heart_rate?: { value: number; status: string; reference_range?: string };
|
||
blood_sugar?: { value: number; status: string; reference_range?: string };
|
||
weight?: { value: number; status: string; reference_range?: string };
|
||
}
|
||
|
||
export async function getTodaySummary(patientId?: string) {
|
||
const params: Record<string, string> = {};
|
||
if (patientId) params.patient_id = patientId;
|
||
return api.get<TodaySummary>('/health/vital-signs/today', params);
|
||
}
|
||
|
||
/**
|
||
* 提交生命体征数据。
|
||
* 小程序使用简单的 indicator_type + value 模型,
|
||
* 后端 CreateVitalSignsReq 期望结构化字段(systolic_bp_morning 等)。
|
||
* 此函数负责将指示器类型映射到后端结构化格式。
|
||
*/
|
||
export async function inputVitalSign(patientId: string, data: VitalSignInput) {
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const body: Record<string, unknown> = { record_date: today };
|
||
|
||
switch (data.indicator_type) {
|
||
case 'blood_pressure':
|
||
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;
|
||
case 'weight':
|
||
body.weight = data.value;
|
||
break;
|
||
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;
|
||
case 'urine_output':
|
||
body.urine_output_ml = Math.round(data.value);
|
||
break;
|
||
default:
|
||
console.warn(`[inputVitalSign] 未知的 indicator_type: ${data.indicator_type}`);
|
||
break;
|
||
}
|
||
|
||
if (data.note) body.notes = data.note;
|
||
return api.post(`/health/patients/${patientId}/vital-signs`, body);
|
||
}
|
||
|
||
export async function getTrend(indicator: string, range: string) {
|
||
return api.get<{ indicator: string; data_points: { date: string; value: number }[] }>(
|
||
'/health/vital-signs/trend',
|
||
{ indicator, range },
|
||
);
|
||
}
|
||
|
||
// ---- Daily Monitoring (日常监测) ----
|
||
|
||
export interface DailyMonitoring {
|
||
id: string;
|
||
patient_id: string;
|
||
record_date: string;
|
||
morning_bp_systolic: number | null;
|
||
morning_bp_diastolic: number | null;
|
||
evening_bp_systolic: number | null;
|
||
evening_bp_diastolic: number | null;
|
||
weight: number | null;
|
||
blood_sugar: number | null;
|
||
fluid_intake: number | null;
|
||
urine_output: number | null;
|
||
notes: string | null;
|
||
created_at: string;
|
||
updated_at: string;
|
||
version: number;
|
||
}
|
||
|
||
export interface CreateDailyMonitoringReq {
|
||
patient_id: string;
|
||
record_date: string;
|
||
morning_bp_systolic?: number;
|
||
morning_bp_diastolic?: number;
|
||
evening_bp_systolic?: number;
|
||
evening_bp_diastolic?: number;
|
||
weight?: number;
|
||
blood_sugar?: number;
|
||
fluid_intake?: number;
|
||
urine_output?: number;
|
||
notes?: string;
|
||
}
|
||
|
||
export async function createDailyMonitoring(data: CreateDailyMonitoringReq) {
|
||
return api.post<DailyMonitoring>('/health/daily-monitoring', data);
|
||
}
|
||
|
||
export async function listDailyMonitoring(
|
||
patientId: string,
|
||
params?: { page?: number; page_size?: number },
|
||
) {
|
||
return api.get<{ data: DailyMonitoring[]; total: number }>(
|
||
`/health/patients/${patientId}/daily-monitoring`,
|
||
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 },
|
||
];
|