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:
@@ -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' })}
|
||||
|
||||
96
apps/miniprogram/src/pages/health/useHealthData.ts
Normal file
96
apps/miniprogram/src/pages/health/useHealthData.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user