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,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}指标,建议关注`;
|
||||
}
|
||||
|
||||
149
apps/miniprogram/src/pages/index/useHomeData.ts
Normal file
149
apps/miniprogram/src/pages/index/useHomeData.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user