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,46 +1,36 @@
import { View, Text, Swiper, SwiperItem, Image } from '@tarojs/components';
import { useState, useMemo, useRef } from 'react';
import Taro, { usePullDownRefresh, useDidShow, useDidHide } from '@tarojs/taro';
import { useState } from 'react';
import Taro, { useDidShow, useDidHide } from '@tarojs/taro';
import { useAuthStore } from '../../stores/auth';
import { useUIStore } from '../../stores/ui';
import { navigateToLogin } from '../../utils/navigate';
import { useHealthStore } from '../../stores/health';
import ProgressRing from '../../components/ProgressRing';
import Loading from '../../components/Loading';
import { usePageData } from '@/hooks/usePageData';
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
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 { api } from '@/services/request';
import type { Article } from '@/services/article';
import ProgressRing from '../../components/ProgressRing';
import Loading from '../../components/Loading';
import { useHomeData, type ReminderItem } from './useHomeData';
import './index.scss';
interface ReminderItem {
id: string;
text: string;
type: 'ai' | 'appointment' | 'followup';
tag: string;
}
interface PublicBanner {
id: string;
title?: string;
subtitle?: string;
desc?: string;
image_url?: string;
link_type?: string;
link_target?: string;
}
// ─── 访客首页 ───
const FALLBACK_SLIDES = [
{ id: 'slide-1', title: '专业血透中心', desc: '三甲级医护团队全程守护', image_url: '' },
{ id: 'slide-2', title: '智慧健康管理', desc: 'AI 驱动个性化健康方案', image_url: '' },
{ id: 'slide-3', title: '温馨就医环境', desc: '舒适安全的治疗体验', image_url: '' },
];
// ─── 访客首页 ───
function GuestHome({ modeClass }: { modeClass: string }) {
const [banners, setBanners] = useState<PublicBanner[]>([]);
const [articles, setArticles] = useState<Article[]>([]);
@@ -49,10 +39,6 @@ function GuestHome({ modeClass }: { modeClass: string }) {
useDidShow(() => { setSwiperAutoplay(true); });
useDidHide(() => { setSwiperAutoplay(false); });
useThrottledDidShow(() => {
loadPublicData();
}, 10_000);
const loadPublicData = async () => {
let tenantId = Taro.getStorageSync('tenant_id');
if (!tenantId) {
@@ -92,11 +78,12 @@ function GuestHome({ modeClass }: { modeClass: string }) {
}
};
usePageData(loadPublicData, { throttleMs: 10_000, enablePullDown: true, enabled: true });
const slides = banners.length > 0 ? banners : FALLBACK_SLIDES;
return (
<View className={`guest-page ${modeClass}`}>
{/* 轮播图 */}
<Swiper
className='guest-swiper'
indicatorDots
@@ -124,7 +111,6 @@ function GuestHome({ modeClass }: { modeClass: string }) {
))}
</Swiper>
{/* 推荐文章(替换原来的"核心功能"区域) */}
<View className='guest-section'>
<Text className='guest-section-title'></Text>
{articles.length > 0 ? (
@@ -165,13 +151,9 @@ function GuestHome({ modeClass }: { modeClass: string }) {
)}
</View>
{/* 底部登录引导 */}
<View className='guest-login-prompt'>
<Text className='guest-login-text'>使</Text>
<View
className='guest-login-btn'
onClick={navigateToLogin}
>
<View className='guest-login-btn' onClick={navigateToLogin}>
<Text className='guest-login-btn-text'></Text>
</View>
</View>
@@ -182,110 +164,11 @@ function GuestHome({ modeClass }: { modeClass: string }) {
// ─── 登录后首页 ───
function HomeDashboard({ modeClass }: { modeClass: string }) {
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 remindersLoadingRef = useRef(false);
const { trigger: triggerHomeRefresh } = useThrottledDidShow(() => {
refreshToday();
loadReminders();
loadUnread();
trackPageView('home');
}, 5000);
usePullDownRefresh(() => {
Promise.all([refreshToday(true), loadReminders(), loadUnread()]).finally(() => {
Taro.stopPullDownRefresh();
});
});
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 {
// ignore
}
};
const loadReminders = async () => {
const patientId = useAuthStore.getState().currentPatient?.id;
if (!patientId || remindersLoadingRef.current) return;
remindersLoadingRef.current = true;
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.data.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 {
setReminders([]);
} finally {
remindersLoadingRef.current = false;
setRemindersLoading(false);
}
};
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)}` : '') || '用户';
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 {
healthItems, indicatorCapsules, completedCount, progressPercent,
loading, todaySummary, reminders, remindersLoading, unreadCount,
greeting, displayName,
} = useHomeData();
const getStatusTag = (status?: string) => {
if (status === 'high' || status === 'low') return { label: status === 'high' ? '偏高' : '偏低', cls: 'tag-warn' };
@@ -295,7 +178,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
return (
<View className={`home-page ${modeClass}`}>
{/* 问候区 */}
<View className='greeting-section'>
<View className='greeting-left'>
<Text className='greeting-text'>{greeting}{displayName}</Text>
@@ -309,7 +191,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
</View>
</View>
{/* 今日体征进度 */}
<View className='checkin-card' onClick={() => Taro.switchTab({ url: '/pages/health/index' })}>
<View className='checkin-left'>
<ProgressRing percent={progressPercent} />
@@ -328,7 +209,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
</View>
</View>
{/* 体征 2x2 */}
<View className='vitals-section'>
<Text className='section-title'></Text>
{loading && !todaySummary ? (
@@ -359,7 +239,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
)}
</View>
{/* 智能提醒卡片 */}
{!remindersLoading && reminders.length > 0 && (
<View className='reminder-card'>
<View className='reminder-header'>
@@ -383,7 +262,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
</View>
)}
{/* 快捷操作 */}
<View className='action-section'>
<View className='action-btn action-primary' onClick={() => Taro.switchTab({ url: '/pages/health/index' })}>
<Text className='action-btn-text'></Text>
@@ -396,7 +274,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
);
}
// ─── 首页入口:根据登录状态切换 ───
// ─── 首页入口 ───
export default function Index() {
const user = useAuthStore((s) => s.user);
@@ -408,16 +286,3 @@ export default function Index() {
}
return <HomeDashboard modeClass={modeClass} />;
}
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}指标,建议关注`;
}

View File

@@ -0,0 +1,149 @@
import { useState, useMemo, useRef } 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';
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 fetchData = async () => {
const patientId = useAuthStore.getState().currentPatient?.id;
if (!patientId) return;
refreshToday();
loadReminders(patientId);
loadUnread();
trackPageView('home');
};
const { trigger, refresh } = usePageData(fetchData, {
throttleMs: 5000,
enablePullDown: true,
enabled: !!user,
});
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 { /* ignore */ }
};
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.data.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 {
setReminders([]);
} finally {
setRemindersLoading(false);
}
};
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,
indicatorCapsules,
healthItems,
completedCount,
progressPercent,
greeting,
displayName,
trigger,
refresh,
};
}