refactor(mp): 架构重构 — usePageData 统一数据加载 + Store 解耦 + 大页面拆分

新增 usePageData hook(useDidShow 节流 + usePullDownRefresh + loadingRef 防重入 + enabled 条件守卫),
44/58 页面迁移接入,消灭 4 种数据加载模式并存。

- 新增 hooks/usePageData.ts — 统一页面数据加载生命周期
- 新增 stores/index.ts — resetAllStores() 解耦 auth↔health store 依赖
- 新增 pages/index/useHomeData.ts — 首页数据 hook(424→282 行)
- 新增 pages/health/useHealthData.ts — 健康页数据 hook(422→254 行)
- 44 个页面迁移到 usePageData(9 患者端 + 15 医生端 + 20 子包)
- auth store logout 不再直接导入 health store

构建通过,测试 74/75(1 个预存失败)。
This commit is contained in:
iven
2026-05-15 01:13:01 +08:00
parent 0f58af245d
commit 1fd2c7a533
52 changed files with 791 additions and 664 deletions

View File

@@ -1,26 +1,14 @@
import { useState, useEffect, useRef } from 'react';
import { useState } from 'react';
import { View, Text, Input } from '@tarojs/components';
import Taro, { usePullDownRefresh } from '@tarojs/taro';
import { useHealthStore } from '../../stores/health';
import Taro from '@tarojs/taro';
import { useAuthStore } from '../../stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health';
import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis';
import { findThreshold, inputVitalSign, type HealthThreshold } from '../../services/health';
import Loading from '../../components/Loading';
import GuestGuard from '../../components/GuestGuard';
import { useHealthData, VITAL_TABS, type VitalType } from './useHealthData';
import './index.scss';
type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight';
const VITAL_TABS: { key: VitalType; label: string }[] = [
{ key: 'blood_pressure', label: '血压' },
{ key: 'heart_rate', label: '心率' },
{ key: 'blood_sugar', label: '血糖' },
{ key: 'weight', label: '体重' },
];
/** 根据阈值列表构建参考范围文案 */
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;
@@ -36,20 +24,14 @@ function buildRefRange(t: HealthThreshold[]): Record<VitalType, string> {
};
}
interface TrendPoint {
date: string;
value: number;
}
export default function Health() {
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 user = useAuthStore((s) => s.user);
const currentPatient = useAuthStore((s) => s.currentPatient);
const modeClass = useElderClass();
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
const {
user, todaySummary, loading, activeTab, trendData, trendLoading,
aiSuggestions, thresholds, handleTabChange, loadTrend, refreshToday,
} = useHealthData();
const [systolic, setSystolic] = useState('');
const [diastolic, setDiastolic] = useState('');
const [heartRateVal, setHeartRateVal] = useState('');
@@ -57,67 +39,11 @@ export default function Health() {
const [sugarPeriod, setSugarPeriod] = useState<'fasting' | 'postprandial'>('fasting');
const [weightVal, setWeightVal] = useState('');
const [saving, setSaving] = useState(false);
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
const [trendLoading, setTrendLoading] = useState(false);
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
const loadingRef = useRef(false);
useThrottledDidShow(() => {
if (!user || loadingRef.current) return;
// 批量发起请求,避免串行 setState 级联重渲染
loadingRef.current = true;
Promise.allSettled([
refreshToday(),
loadTrend(activeTab),
loadAiSuggestions(),
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
]).finally(() => { loadingRef.current = false; });
}, 5000);
usePullDownRefresh(() => {
if (!user) return;
Promise.all([refreshToday(true), loadTrend(activeTab), loadAiSuggestions()]).finally(() => {
Taro.stopPullDownRefresh();
});
});
if (!user) {
return <GuestGuard title='请先登录' desc='登录后即可记录和查看健康数据' />;
}
const loadAiSuggestions = async () => {
try {
const items = await listPendingSuggestions();
setAiSuggestions(items.slice(0, 3));
} catch {
setAiSuggestions([]);
}
};
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 {
setTrendData([]);
} finally {
setTrendLoading(false);
}
};
const handleTabChange = (tab: VitalType) => {
setActiveTab(tab);
loadTrend(tab);
};
const getWarnStatus = (type: VitalType): string | null => {
if (type === 'blood_pressure') {
const sys = parseFloat(systolic);
@@ -224,12 +150,10 @@ export default function Health() {
return (
<View className={`health-page ${modeClass}`}>
{/* 页头 */}
<View className='health-header'>
<Text className='health-title'></Text>
</View>
{/* AI 建议卡片 */}
{aiSuggestions.length > 0 && (
<View className='ai-suggestion-card' onClick={() => {
const first = aiSuggestions[0];
@@ -260,7 +184,6 @@ export default function Health() {
</View>
)}
{/* 类型 Tab */}
<View className='vital-tabs'>
{VITAL_TABS.map((tab) => {
const hasData = tab.key === 'blood_pressure' ? !!todaySummary?.blood_pressure
@@ -280,7 +203,6 @@ export default function Health() {
})}
</View>
{/* 录入区 */}
<View className='input-section'>
{activeTab === 'blood_pressure' && (
<View className='input-group'>
@@ -365,7 +287,6 @@ export default function Health() {
</View>
</View>
{/* 趋势图 */}
<View className='trend-section'>
<Text className='section-title'> 7 </Text>
{trendLoading ? (
@@ -377,7 +298,6 @@ export default function Health() {
) : (
<View className='trend-chart'>
<View className='trend-bars'>
{/* 阈值标线 */}
{getThresholdValue(activeTab, thresholds) && (() => {
const tv = getThresholdValue(activeTab, thresholds)!;
const pct = Math.min(95, (tv / maxTrendValue) * 100);
@@ -407,9 +327,6 @@ export default function Health() {
)}
</View>
{/* BLE 设备同步功能暂缓开放 */}
{/* 健康资讯入口 */}
<View
className='article-entry'
onClick={() => Taro.navigateTo({ url: '/pages/article/index' })}

View File

@@ -0,0 +1,96 @@
import { useState, useRef } from 'react';
import { useHealthStore } from '@/stores/health';
import { useAuthStore } from '@/stores/auth';
import { usePageData } from '@/hooks/usePageData';
import { getTrend, getHealthThresholds, DEFAULT_THRESHOLDS, type HealthThreshold } from '@/services/health';
import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis';
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 useHealthData() {
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 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 {
setTrendData([]);
} finally {
setTrendLoading(false);
}
};
const loadAiSuggestions = async () => {
try {
const items = await listPendingSuggestions();
setAiSuggestions(items.slice(0, 3));
} catch {
setAiSuggestions([]);
}
};
const fetchData = async () => {
await Promise.allSettled([
refreshToday(),
loadTrend(activeTab),
loadAiSuggestions(),
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
]);
};
usePageData(fetchData, {
throttleMs: 5000,
enablePullDown: true,
enabled: !!user,
});
const handleTabChange = (tab: VitalType) => {
setActiveTab(tab);
loadTrend(tab);
};
return {
user,
todaySummary,
loading,
activeTab,
trendData,
trendLoading,
aiSuggestions,
thresholds,
handleTabChange,
loadTrend,
refreshToday,
fetchTrend,
};
}