Files
hms/apps/miniprogram/src/pages/index/useHomeData.ts
iven d24aefe750 fix(mp): 安全修复 + 健康Tab重构为总览
Phase 0 安全修复:
- 移除 secure-storage-aes.ts 硬编码 'hms-default-key' fallback
- production 模式空密钥时拒绝加解密(返回空/不加密)
- dev 模式保留明文兼容(warn 日志提醒)
- .env/.env.h5 注入随机加密密钥
- secureGet 明文 fallback 按环境分级处理
- 新增 8 个测试覆盖空密钥 dev/production 行为

Phase 1 健康Tab重构:
- health/index.tsx 从体征录入页改为健康总览Dashboard
- 新增今日体征摘要卡片(2x2 网格 + 状态标签)
- 新增快捷入口(录入体征/趋势/报告/用药)
- 新增告警提示卡片(待处理告警数量)
- 体征录入移至 pkg-health/input/index(已有页面)
- useHealthData → useHealthOverview(新增 alertCount)

首页增强:
- useHomeData 新增告警计数查询(listPatientAlerts)
- 首页新增告警提示卡片入口
- "记录体征"按钮改为跳转录入页而非健康Tab
2026-05-22 11:48:57 +08:00

175 lines
6.6 KiB
TypeScript

import { useState, useMemo, useRef, useEffect } from 'react';
import { useHealthStore } from '@/stores/health';
import { useAuthStore } from '@/stores/auth';
import { usePageData } from '@/hooks/usePageData';
import { trackPageView } from '@/services/analytics';
import * as appointmentApi from '@/services/appointment';
import * as followupApi from '@/services/followup';
import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis';
import { notificationService } from '@/services/notification';
import { listPatientAlerts } from '@/services/alert';
export interface ReminderItem {
id: string;
text: string;
type: 'ai' | 'appointment' | 'followup';
tag: string;
}
function buildSuggestionText(s: AiSuggestionItem): string {
const riskMap: Record<string, string> = { high: '高风险', medium: '中风险', low: '低风险' };
const typeMap: Record<string, string> = {
vital_sign_anomaly: '体征异常',
lab_result_anomaly: '化验异常',
medication_adherence: '用药提醒',
lifestyle: '生活建议',
};
const risk = riskMap[s.risk_level] || '';
const type = typeMap[s.suggestion_type] || '健康建议';
return `${type}:发现${risk}指标,建议关注`;
}
export function useHomeData() {
const user = useAuthStore((s) => s.user);
const currentPatient = useAuthStore((s) => s.currentPatient);
const todaySummary = useHealthStore((s) => s.todaySummary);
const loading = useHealthStore((s) => s.loading);
const refreshToday = useHealthStore((s) => s.refreshToday);
const [reminders, setReminders] = useState<ReminderItem[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [remindersLoading, setRemindersLoading] = useState(false);
const [alertCount, setAlertCount] = useState(0);
const fetchData = async () => {
const patientId = useAuthStore.getState().currentPatient?.id;
if (!patientId) return;
refreshToday();
loadReminders(patientId);
loadUnread();
loadAlertCount(patientId);
trackPageView('home');
};
const { trigger, refresh } = usePageData(fetchData, {
throttleMs: 5000,
enablePullDown: true,
enabled: !!user,
});
// currentPatient 从 null 变为有值时重新触发加载
// 解决 loadPatients 异步完成前 useDidShow 已触发 fetchData 并因 patientId 为空提前返回的问题
const prevPatientRef = useRef<string | null>(null);
useEffect(() => {
const pid = currentPatient?.id ?? null;
if (pid && pid !== prevPatientRef.current) {
prevPatientRef.current = pid;
trigger();
}
}, [currentPatient?.id, trigger]);
const loadUnread = async () => {
try {
const res = await notificationService.getUnreadCount() as { data?: { count?: number } | number };
const d = res.data;
setUnreadCount(typeof d === 'object' && d ? (d.count ?? 0) : 0);
} catch (err) { console.warn('[home] 获取未读消息数失败:', err); }
};
const loadReminders = async (patientId: string) => {
setRemindersLoading(true);
try {
const items: ReminderItem[] = [];
const [apptRes, taskRes, suggestRes] = await Promise.allSettled([
appointmentApi.listAppointments(patientId, 1),
followupApi.listTasks(patientId, 'pending'),
listPendingSuggestions(),
]);
if (suggestRes.status === 'fulfilled') {
for (const s of suggestRes.value.slice(0, 1)) {
items.push({ id: s.id, text: buildSuggestionText(s), type: 'ai', tag: 'AI 建议' });
}
}
if (apptRes.status === 'fulfilled') {
for (const a of apptRes.value.data.slice(0, 1)) {
if (a.status === 'pending' || a.status === 'confirmed') {
items.push({
id: a.id,
text: `${a.appointment_date} ${a.start_time}${a.doctor_name || '医护'} ${a.department || '门诊'}`,
type: 'appointment',
tag: '预约',
});
}
}
}
if (taskRes.status === 'fulfilled') {
for (const t of taskRes.value.data.slice(0, 1)) {
items.push({
id: t.id,
text: `${t.follow_up_type} · 截止 ${t.planned_date}`,
type: 'followup',
tag: '随访',
});
}
}
setReminders(items.slice(0, 3));
} catch (err) {
console.warn('[home] 加载提醒列表失败:', err);
setReminders([]);
} finally {
setRemindersLoading(false);
}
};
const loadAlertCount = async (patientId: string) => {
try {
const res = await listPatientAlerts(patientId, { status: 'pending', page: 1, page_size: 1 });
setAlertCount(res.total ?? 0);
} catch {
setAlertCount(0);
}
};
const summary = todaySummary || {};
const indicators = [!!summary.blood_pressure, !!summary.heart_rate, !!summary.blood_sugar, !!summary.weight];
const completedCount = indicators.filter(Boolean).length;
const progressPercent = Math.round((completedCount / 4) * 100);
const indicatorCapsules = useMemo(() => [
{ label: '血压', done: !!summary.blood_pressure },
{ label: '心率', done: !!summary.heart_rate },
{ label: '血糖', done: !!summary.blood_sugar },
{ label: '体重', done: !!summary.weight },
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
const healthItems = useMemo(() => [
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'systolic_bp_morning' },
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' },
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar' },
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
const hour = new Date().getHours();
const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好';
const displayName = user?.display_name || currentPatient?.name || user?.username || (user?.phone ? `${user.phone.slice(-4)}` : '') || '用户';
return {
user,
currentPatient,
todaySummary: summary,
loading,
reminders,
unreadCount,
remindersLoading,
alertCount,
indicatorCapsules,
healthItems,
completedCount,
progressPercent,
greeting,
displayName,
trigger,
refresh,
};
}