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
113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { useHealthStore } from '@/stores/health';
|
|
import { useAuthStore } from '@/stores/auth';
|
|
import { usePageData } from '@/hooks/usePageData';
|
|
import { getHealthThresholds, DEFAULT_THRESHOLDS, type HealthThreshold } from '@/services/health';
|
|
import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis';
|
|
import { listPatientAlerts } from '@/services/alert';
|
|
|
|
export type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight';
|
|
|
|
export const VITAL_TABS: { key: VitalType; label: string }[] = [
|
|
{ key: 'blood_pressure', label: '血压' },
|
|
{ key: 'heart_rate', label: '心率' },
|
|
{ key: 'blood_sugar', label: '血糖' },
|
|
{ key: 'weight', label: '体重' },
|
|
];
|
|
|
|
export interface TrendPoint {
|
|
date: string;
|
|
value: number;
|
|
}
|
|
|
|
export function useHealthOverview() {
|
|
const user = useAuthStore((s) => s.user);
|
|
const todaySummary = useHealthStore((s) => s.todaySummary);
|
|
const loading = useHealthStore((s) => s.loading);
|
|
const refreshToday = useHealthStore((s) => s.refreshToday);
|
|
const fetchTrend = useHealthStore((s) => s.getTrend);
|
|
|
|
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
|
|
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
|
|
const [trendLoading, setTrendLoading] = useState(false);
|
|
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
|
|
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
|
|
const [alertCount, setAlertCount] = useState(0);
|
|
|
|
const loadTrend = async (type: VitalType) => {
|
|
setTrendLoading(true);
|
|
try {
|
|
const indicatorMap: Record<VitalType, string> = {
|
|
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 (err) {
|
|
console.warn('[health] 加载趋势数据失败:', err);
|
|
setTrendData([]);
|
|
} finally {
|
|
setTrendLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadAiSuggestions = async () => {
|
|
try {
|
|
const items = await listPendingSuggestions();
|
|
setAiSuggestions(items.slice(0, 3));
|
|
} catch {
|
|
setAiSuggestions([]);
|
|
}
|
|
};
|
|
|
|
const loadAlertCount = async () => {
|
|
const patientId = useAuthStore.getState().currentPatient?.id;
|
|
if (!patientId) return;
|
|
try {
|
|
const res = await listPatientAlerts(patientId, { status: 'pending', page: 1, page_size: 1 });
|
|
setAlertCount(res.total ?? 0);
|
|
} catch {
|
|
setAlertCount(0);
|
|
}
|
|
};
|
|
|
|
const fetchData = async () => {
|
|
const results = await Promise.allSettled([
|
|
refreshToday(),
|
|
loadTrend(activeTab),
|
|
loadAiSuggestions(),
|
|
loadAlertCount(),
|
|
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
|
|
]);
|
|
return results;
|
|
};
|
|
|
|
usePageData(fetchData, {
|
|
throttleMs: 5000,
|
|
enablePullDown: true,
|
|
enabled: !!user,
|
|
});
|
|
|
|
const handleTabChange = (tab: VitalType) => {
|
|
setActiveTab(tab);
|
|
loadTrend(tab);
|
|
};
|
|
|
|
return {
|
|
user,
|
|
todaySummary,
|
|
loading,
|
|
error: false,
|
|
activeTab,
|
|
trendData,
|
|
trendLoading,
|
|
aiSuggestions,
|
|
thresholds,
|
|
alertCount,
|
|
handleTabChange,
|
|
fetchData,
|
|
};
|
|
}
|