From 1fd2c7a5336358f6ba9c5b5b88ae1dbfd079f8cb Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 15 May 2026 01:13:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(mp):=20=E6=9E=B6=E6=9E=84=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20=E2=80=94=20usePageData=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8A=A0=E8=BD=BD=20+=20Store=20=E8=A7=A3?= =?UTF-8?q?=E8=80=A6=20+=20=E5=A4=A7=E9=A1=B5=E9=9D=A2=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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 个预存失败)。 --- apps/miniprogram/src/hooks/usePageData.ts | 70 +++++++ .../src/pages/ai-report/detail/index.tsx | 19 +- .../src/pages/ai-report/list/index.tsx | 13 +- .../src/pages/appointment/index.tsx | 23 +-- .../src/pages/article/detail/index.tsx | 23 ++- apps/miniprogram/src/pages/article/index.tsx | 17 +- .../src/pages/consultation/index.tsx | 33 ++-- .../src/pages/doctor/action-inbox/index.tsx | 21 +-- .../src/pages/doctor/alerts/detail/index.tsx | 19 +- .../src/pages/doctor/alerts/index.tsx | 26 +-- .../src/pages/doctor/consultation/index.tsx | 26 +-- .../pages/doctor/dialysis/detail/index.tsx | 14 +- .../src/pages/doctor/dialysis/index.tsx | 30 +-- .../pages/doctor/followup/detail/index.tsx | 14 +- .../src/pages/doctor/followup/index.tsx | 22 ++- apps/miniprogram/src/pages/doctor/index.tsx | 19 +- .../pages/doctor/patients/detail/index.tsx | 14 +- .../src/pages/doctor/patients/index.tsx | 35 ++-- .../doctor/prescription/detail/index.tsx | 14 +- .../src/pages/doctor/prescription/index.tsx | 28 +-- .../src/pages/doctor/report/detail/index.tsx | 14 +- .../src/pages/doctor/report/index.tsx | 23 ++- apps/miniprogram/src/pages/events/index.tsx | 13 +- .../src/pages/followup/detail/index.tsx | 23 ++- apps/miniprogram/src/pages/health/index.tsx | 101 +---------- .../src/pages/health/useHealthData.ts | 96 ++++++++++ apps/miniprogram/src/pages/index/index.tsx | 171 ++---------------- .../src/pages/index/useHomeData.ts | 149 +++++++++++++++ apps/miniprogram/src/pages/mall/index.tsx | 27 +-- apps/miniprogram/src/pages/messages/index.tsx | 21 +-- .../src/pages/pkg-health/alerts/index.tsx | 26 ++- .../src/pages/pkg-health/input/index.tsx | 17 +- .../src/pages/pkg-health/trend/index.tsx | 26 ++- .../src/pages/pkg-mall/detail/index.tsx | 27 +-- .../src/pages/pkg-mall/exchange/index.tsx | 15 +- .../src/pages/pkg-mall/orders/index.tsx | 27 +-- .../src/pages/pkg-profile/consents/index.tsx | 10 +- .../src/pages/pkg-profile/diagnoses/index.tsx | 14 +- .../dialysis-prescriptions/detail/index.tsx | 19 +- .../dialysis-prescriptions/index.tsx | 10 +- .../dialysis-records/detail/index.tsx | 19 +- .../pkg-profile/dialysis-records/index.tsx | 10 +- .../src/pages/pkg-profile/family/index.tsx | 6 +- .../src/pages/pkg-profile/followups/index.tsx | 6 +- .../pkg-profile/health-records/index.tsx | 14 +- .../pages/pkg-profile/medication/index.tsx | 5 +- .../src/pages/pkg-profile/reports/index.tsx | 14 +- apps/miniprogram/src/pages/profile/index.tsx | 13 +- .../src/pages/report/detail/index.tsx | 19 +- apps/miniprogram/src/stores/auth.ts | 4 +- apps/miniprogram/src/stores/index.ts | 7 + wiki/miniprogram.md | 29 ++- 52 files changed, 791 insertions(+), 664 deletions(-) create mode 100644 apps/miniprogram/src/hooks/usePageData.ts create mode 100644 apps/miniprogram/src/pages/health/useHealthData.ts create mode 100644 apps/miniprogram/src/pages/index/useHomeData.ts create mode 100644 apps/miniprogram/src/stores/index.ts diff --git a/apps/miniprogram/src/hooks/usePageData.ts b/apps/miniprogram/src/hooks/usePageData.ts new file mode 100644 index 0000000..ed0926f --- /dev/null +++ b/apps/miniprogram/src/hooks/usePageData.ts @@ -0,0 +1,70 @@ +import { useRef, useCallback } from 'react'; +import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; + +interface UsePageDataOptions { + throttleMs?: number; + enablePullDown?: boolean; + enabled?: boolean; +} + +interface UsePageDataResult { + loading: boolean; + refresh: () => Promise; + trigger: () => void; +} + +export function usePageData( + fetcher: () => Promise, + options?: UsePageDataOptions, +): UsePageDataResult { + const throttleMs = options?.throttleMs ?? 5000; + const enablePullDown = options?.enablePullDown ?? false; + const enabled = options?.enabled ?? true; + + const loadingRef = useRef(false); + const lastRunRef = useRef(0); + const fetcherRef = useRef(fetcher); + fetcherRef.current = fetcher; + + const run = useCallback(async (force = false) => { + if (!enabled || loadingRef.current) return; + if (!force && Date.now() - lastRunRef.current < throttleMs) return; + loadingRef.current = true; + lastRunRef.current = Date.now(); + try { + await fetcherRef.current(); + } finally { + loadingRef.current = false; + } + }, [enabled, throttleMs]); + + useDidShow(() => { + run(); + }); + + const trigger = useCallback(() => { + run(true); + }, [run]); + + const refresh = useCallback(async () => { + if (loadingRef.current) return; + loadingRef.current = true; + lastRunRef.current = Date.now(); + try { + await fetcherRef.current(); + } finally { + loadingRef.current = false; + } + }, []); + + usePullDownRefresh(async () => { + if (!enablePullDown) return; + try { + await refresh(); + } finally { + Taro.stopPullDownRefresh(); + } + }); + + return { loading: loadingRef.current, refresh, trigger }; +} diff --git a/apps/miniprogram/src/pages/ai-report/detail/index.tsx b/apps/miniprogram/src/pages/ai-report/detail/index.tsx index bf6e248..3feb5b8 100644 --- a/apps/miniprogram/src/pages/ai-report/detail/index.tsx +++ b/apps/miniprogram/src/pages/ai-report/detail/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text, RichText } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getAiAnalysisDetail, type AiAnalysisItem } from '@/services/ai-analysis'; import Loading from '@/components/Loading'; import { sanitizeHtml } from '@/utils/sanitize-html'; @@ -34,15 +35,21 @@ export default function AiReportDetail() { const [analysis, setAnalysis] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { + const fetchDetail = useCallback(async () => { if (!id) return; setLoading(true); - getAiAnalysisDetail(id) - .then((data) => setAnalysis(data)) - .catch(() => Taro.showToast({ title: '加载失败', icon: 'none' })) - .finally(() => setLoading(false)); + try { + const data = await getAiAnalysisDetail(id); + setAnalysis(data); + } catch { + Taro.showToast({ title: '加载失败', icon: 'none' }); + } finally { + setLoading(false); + } }, [id]); + usePageData(fetchDetail, { throttleMs: 60000 }); + if (loading) return ; if (!analysis) { diff --git a/apps/miniprogram/src/pages/ai-report/list/index.tsx b/apps/miniprogram/src/pages/ai-report/list/index.tsx index f412a15..9c493f5 100644 --- a/apps/miniprogram/src/pages/ai-report/list/index.tsx +++ b/apps/miniprogram/src/pages/ai-report/list/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listAiAnalysis, type AiAnalysisItem } from '@/services/ai-analysis'; import Loading from '@/components/Loading'; import EmptyState from '@/components/EmptyState'; @@ -28,11 +29,7 @@ export default function AiReportList() { const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); - useEffect(() => { - loadList(1); - }, []); - - const loadList = async (p: number) => { + const loadList = useCallback(async (p: number) => { setLoading(true); try { const res = await listAiAnalysis(p, 20); @@ -46,7 +43,9 @@ export default function AiReportList() { } finally { setLoading(false); } - }; + }, []); + + usePageData(async () => { await loadList(1); }, { throttleMs: 5000, enablePullDown: true }); const goDetail = (id: string) => { Taro.navigateTo({ url: `/pages/ai-report/detail/index?id=${id}` }); diff --git a/apps/miniprogram/src/pages/appointment/index.tsx b/apps/miniprogram/src/pages/appointment/index.tsx index c62b61b..6bd2c3c 100644 --- a/apps/miniprogram/src/pages/appointment/index.tsx +++ b/apps/miniprogram/src/pages/appointment/index.tsx @@ -1,7 +1,7 @@ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listAppointments } from '../../services/appointment'; import type { Appointment } from '../../services/appointment'; import EmptyState from '../../components/EmptyState'; @@ -31,12 +31,9 @@ export default function AppointmentList() { const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); - const loadingRef = useRef(false); const modeClass = useElderClass(); const fetchData = useCallback(async (pageNum: number, isRefresh = false) => { - if (loadingRef.current) return; - loadingRef.current = true; setLoading(true); try { const res = await listAppointments(pageNum); @@ -51,20 +48,14 @@ export default function AppointmentList() { } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { - loadingRef.current = false; setLoading(false); } }, []); - useThrottledDidShow(() => { - fetchData(1, true); - }, 10000); - - usePullDownRefresh(() => { - fetchData(1, true).finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData( + useCallback(() => fetchData(1, true), [fetchData]), + { throttleMs: 10000, enablePullDown: true }, + ); useReachBottom(() => { if (!loading && appointments.length < total) { diff --git a/apps/miniprogram/src/pages/article/detail/index.tsx b/apps/miniprogram/src/pages/article/detail/index.tsx index 3984f4c..e7d80a2 100644 --- a/apps/miniprogram/src/pages/article/detail/index.tsx +++ b/apps/miniprogram/src/pages/article/detail/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text, RichText } from '@tarojs/components'; import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getArticleDetail, getPublicArticleDetail, Article } from '../../../services/article'; import { trackEvent } from '@/services/analytics'; import { sanitizeHtml } from '@/utils/sanitize-html'; @@ -24,17 +25,23 @@ export default function ArticleDetail() { }; }); - useEffect(() => { + const fetchArticle = useCallback(async () => { if (!id) return; setLoading(true); - const user = useAuthStore.getState().user; - const fetcher = user ? getArticleDetail(id) : getPublicArticleDetail(id); - fetcher - .then((data) => setArticle(data)) - .catch(() => Taro.showToast({ title: '加载失败', icon: 'none' })) - .finally(() => setLoading(false)); + try { + const user = useAuthStore.getState().user; + const fetcher = user ? getArticleDetail(id) : getPublicArticleDetail(id); + const data = await fetcher; + setArticle(data); + } catch { + Taro.showToast({ title: '加载失败', icon: 'none' }); + } finally { + setLoading(false); + } }, [id]); + usePageData(fetchArticle, { throttleMs: 60000 }); + if (loading) { return ( diff --git a/apps/miniprogram/src/pages/article/index.tsx b/apps/miniprogram/src/pages/article/index.tsx index fed7ac1..69f7459 100644 --- a/apps/miniprogram/src/pages/article/index.tsx +++ b/apps/miniprogram/src/pages/article/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback, useEffect } from 'react'; import { View, Text, Image, ScrollView } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listArticles, listCategories, Article, ArticleCategory } from '../../services/article'; import EmptyState from '../../components/EmptyState'; import Loading from '../../components/Loading'; @@ -49,15 +49,10 @@ export default function ArticleList() { fetchCategories(); }, [fetchCategories]); - useThrottledDidShow(() => { - fetchData(1, false, null); - }, 10000); - - usePullDownRefresh(() => { - fetchData(1, false, null).finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData( + useCallback(() => fetchData(1, false, null), [fetchData]), + { throttleMs: 10000, enablePullDown: true }, + ); useReachBottom(() => { if (!loading && articles.length < total) { diff --git a/apps/miniprogram/src/pages/consultation/index.tsx b/apps/miniprogram/src/pages/consultation/index.tsx index b38d01c..0870863 100644 --- a/apps/miniprogram/src/pages/consultation/index.tsx +++ b/apps/miniprogram/src/pages/consultation/index.tsx @@ -1,7 +1,7 @@ -import { useState, useRef } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { useAuthStore } from '@/stores/auth'; import { listConsultations, ConsultationSession } from '@/services/consultation'; import Loading from '../../components/Loading'; @@ -41,11 +41,8 @@ export default function Consultation() { const modeClass = useElderClass(); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); - const loadingRef = useRef(false); - const loadSessions = async (pageNum: number, isRefresh = false) => { - if (loadingRef.current) return; - loadingRef.current = true; + const loadSessions = useCallback(async (pageNum: number, isRefresh = false) => { if (isRefresh) setLoading(true); setError(''); try { @@ -66,21 +63,17 @@ export default function Consultation() { Taro.showToast({ title: '加载失败,下拉重试', icon: 'none' }); } finally { setLoading(false); - loadingRef.current = false; } - }; + }, []); - useThrottledDidShow(() => { - Taro.setNavigationBarTitle({ title: '在线咨询' }); - if (!user) return; - loadSessions(1, true); - }, 10000); - - usePullDownRefresh(() => { - loadSessions(1, true).finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData( + useCallback(async () => { + Taro.setNavigationBarTitle({ title: '在线咨询' }); + if (!user) return; + await loadSessions(1, true); + }, [user, loadSessions]), + { throttleMs: 10000, enablePullDown: true }, + ); useReachBottom(() => { if (!loading && sessions.length < total) { diff --git a/apps/miniprogram/src/pages/doctor/action-inbox/index.tsx b/apps/miniprogram/src/pages/doctor/action-inbox/index.tsx index f4ff110..66aac9c 100644 --- a/apps/miniprogram/src/pages/doctor/action-inbox/index.tsx +++ b/apps/miniprogram/src/pages/doctor/action-inbox/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback, useRef } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; -import Taro, { usePullDownRefresh } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { api } from '@/services/request'; import { listActionItems, @@ -74,16 +74,13 @@ export default function ActionInboxPage() { [], ); - useThrottledDidShow(() => { - Taro.setNavigationBarTitle({ title: '待办事项' }); - fetchItems(1, activeTab, true); - }, 10000); - - usePullDownRefresh(() => { - fetchItems(1, activeTab, true).then(() => - Taro.stopPullDownRefresh(), - ); - }); + usePageData( + useCallback(async () => { + Taro.setNavigationBarTitle({ title: '待办事项' }); + await fetchItems(1, activeTab, true); + }, [fetchItems, activeTab]), + { throttleMs: 10000, enablePullDown: true }, + ); const handleTabChange = (key: string) => { setActiveTab(key); diff --git a/apps/miniprogram/src/pages/doctor/alerts/detail/index.tsx b/apps/miniprogram/src/pages/doctor/alerts/detail/index.tsx index 641c0a1..1cbdeba 100644 --- a/apps/miniprogram/src/pages/doctor/alerts/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/alerts/detail/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, ScrollView, Button } from '@tarojs/components'; import Taro from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getAlert, acknowledgeAlert, dismissAlert, resolveAlert, type Alert, @@ -29,23 +30,21 @@ export default function AlertDetail() { const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(false); - useEffect(() => { - const params = Taro.getCurrentInstance().router?.params; - if (params?.id) { - loadAlert(params.id); - } - }, []); + const alertId = Taro.getCurrentInstance().router?.params?.id || ''; - const loadAlert = async (id: string) => { + const loadAlert = useCallback(async () => { + if (!alertId) return; try { - const data = await getAlert(id); + const data = await getAlert(alertId); setAlert(data); } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { setLoading(false); } - }; + }, [alertId]); + + usePageData(loadAlert, { throttleMs: 60000, enablePullDown: false, enabled: !!alertId }); const handleAcknowledge = async () => { if (!alert) return; diff --git a/apps/miniprogram/src/pages/doctor/alerts/index.tsx b/apps/miniprogram/src/pages/doctor/alerts/index.tsx index 6126bd0..b10b164 100644 --- a/apps/miniprogram/src/pages/doctor/alerts/index.tsx +++ b/apps/miniprogram/src/pages/doctor/alerts/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listAlerts, type Alert } from '@/services/doctor/alerts'; import Loading from '@/components/Loading'; import EmptyState from '@/components/EmptyState'; @@ -36,17 +37,11 @@ export default function AlertList() { const [activeTab, setActiveTab] = useState(''); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); - const loadingRef = useRef(false); + const mountedRef = useRef(false); const totalPages = useMemo(() => Math.ceil(total / 20), [total]); - useEffect(() => { - loadAlerts(); - }, [page, activeTab]); - - const loadAlerts = async () => { - if (loadingRef.current) return; - loadingRef.current = true; + const loadAlerts = useCallback(async () => { setLoading(true); try { const res = await listAlerts({ @@ -60,9 +55,18 @@ export default function AlertList() { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { setLoading(false); - loadingRef.current = false; } - }; + }, [activeTab, page]); + + const { trigger } = usePageData(loadAlerts); + + // tab/page 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理) + useEffect(() => { + if (mountedRef.current) { + trigger(); + } + mountedRef.current = true; + }, [page, activeTab, trigger]); const handleTabChange = (value: string) => { setActiveTab(value); diff --git a/apps/miniprogram/src/pages/doctor/consultation/index.tsx b/apps/miniprogram/src/pages/doctor/consultation/index.tsx index d34c753..3c6771f 100644 --- a/apps/miniprogram/src/pages/doctor/consultation/index.tsx +++ b/apps/miniprogram/src/pages/doctor/consultation/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listSessions, type ConsultationSession } from '@/services/doctor/consultation'; import Loading from '@/components/Loading'; import EmptyState from '@/components/EmptyState'; @@ -24,17 +25,11 @@ export default function ConsultationList() { const [loading, setLoading] = useState(true); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); - const loadingRef = useRef(false); + const mountedRef = useRef(false); const totalPages = useMemo(() => Math.ceil(total / 20), [total]); - useEffect(() => { - loadSessions(); - }, [page, activeTab]); - - const loadSessions = async () => { - if (loadingRef.current) return; - loadingRef.current = true; + const loadSessions = useCallback(async () => { setLoading(true); try { const res = await listSessions({ @@ -48,9 +43,18 @@ export default function ConsultationList() { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { setLoading(false); - loadingRef.current = false; } - }; + }, [activeTab, page]); + + const { trigger } = usePageData(loadSessions); + + // tab/page 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理) + useEffect(() => { + if (mountedRef.current) { + trigger(); + } + mountedRef.current = true; + }, [page, activeTab, trigger]); const handleTabChange = (key: string) => { setActiveTab(key); diff --git a/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx b/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx index 18c84ad..a2536ed 100644 --- a/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getDialysisRecord, reviewDialysisRecord, updateDialysisRecord, deleteDialysisRecord, @@ -20,11 +21,8 @@ export default function DialysisDetail() { const [submitting, setSubmitting] = useState(false); const { safeSetTimeout } = useSafeTimeout(); - useEffect(() => { - if (id) loadRecord(); - }, [id]); - - const loadRecord = async () => { + const loadRecord = useCallback(async () => { + if (!id) return; setLoading(true); try { const r = await getDialysisRecord(id); @@ -34,7 +32,9 @@ export default function DialysisDetail() { } finally { setLoading(false); } - }; + }, [id]); + + usePageData(loadRecord, { throttleMs: 60000, enablePullDown: false, enabled: !!id }); const handleReview = async () => { if (!record) return; diff --git a/apps/miniprogram/src/pages/doctor/dialysis/index.tsx b/apps/miniprogram/src/pages/doctor/dialysis/index.tsx index c4832ed..a0af6da 100644 --- a/apps/miniprogram/src/pages/doctor/dialysis/index.tsx +++ b/apps/miniprogram/src/pages/doctor/dialysis/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, Input, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listDialysisRecords, type DialysisRecord } from '@/services/doctor/dialysis'; import { listPatients } from '@/services/doctor/patient'; import Loading from '@/components/Loading'; @@ -29,15 +30,10 @@ export default function DialysisList() { const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); - const loadingRef = useRef(false); + const mountedRef = useRef(false); - useEffect(() => { - if (currentPatientId) loadRecords(1); - }, [currentPatientId, activeTab]); - - const loadRecords = async (p: number) => { - if (loadingRef.current) return; - loadingRef.current = true; + const loadRecords = useCallback(async (p: number) => { + if (!currentPatientId) return; setLoading(true); try { const params: { page: number; page_size: number; status?: string } = { page: p, page_size: 20 }; @@ -50,9 +46,21 @@ export default function DialysisList() { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { setLoading(false); - loadingRef.current = false; } - }; + }, [currentPatientId, activeTab]); + + usePageData( + useCallback(() => loadRecords(1), [loadRecords]), + { enabled: !!currentPatientId }, + ); + + // tab/patientId 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理) + useEffect(() => { + if (mountedRef.current && currentPatientId) { + loadRecords(1); + } + mountedRef.current = true; + }, [currentPatientId, activeTab, loadRecords]); const handleSearch = async () => { if (!searchPatient.trim()) return; diff --git a/apps/miniprogram/src/pages/doctor/followup/detail/index.tsx b/apps/miniprogram/src/pages/doctor/followup/detail/index.tsx index 8225623..1c32c70 100644 --- a/apps/miniprogram/src/pages/doctor/followup/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/followup/detail/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, Textarea, ScrollView, Picker } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getFollowUpTask, listFollowUpRecords, createFollowUpRecord, updateFollowUpTask, @@ -33,11 +34,8 @@ export default function FollowUpDetail() { const [medicalAdvice, setMedicalAdvice] = useState(''); const [nextDate, setNextDate] = useState(''); - useEffect(() => { - if (taskId) loadData(); - }, [taskId]); - - const loadData = async () => { + const loadData = useCallback(async () => { + if (!taskId) return; setLoading(true); try { const [t, r] = await Promise.all([ @@ -51,7 +49,9 @@ export default function FollowUpDetail() { } finally { setLoading(false); } - }; + }, [taskId]); + + usePageData(loadData, { throttleMs: 60000, enablePullDown: false, enabled: !!taskId }); const handleSubmit = async () => { if (!result.trim()) { diff --git a/apps/miniprogram/src/pages/doctor/followup/index.tsx b/apps/miniprogram/src/pages/doctor/followup/index.tsx index fc42c28..f8cc14e 100644 --- a/apps/miniprogram/src/pages/doctor/followup/index.tsx +++ b/apps/miniprogram/src/pages/doctor/followup/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listFollowUpTasks, type FollowUpTask } from '@/services/doctor/followup'; import Loading from '@/components/Loading'; import EmptyState from '@/components/EmptyState'; @@ -24,12 +25,9 @@ export default function FollowUpList() { const [activeTab, setActiveTab] = useState(''); const [loading, setLoading] = useState(true); const [total, setTotal] = useState(0); + const mountedRef = useRef(false); - useEffect(() => { - loadTasks(); - }, [activeTab, patientId]); - - const loadTasks = async () => { + const loadTasks = useCallback(async () => { setLoading(true); try { const res = await listFollowUpTasks({ @@ -45,7 +43,17 @@ export default function FollowUpList() { } finally { setLoading(false); } - }; + }, [activeTab, patientId]); + + const { trigger } = usePageData(loadTasks); + + // tab/patientId 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理) + useEffect(() => { + if (mountedRef.current) { + trigger(); + } + mountedRef.current = true; + }, [activeTab, patientId, trigger]); const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }); diff --git a/apps/miniprogram/src/pages/doctor/index.tsx b/apps/miniprogram/src/pages/doctor/index.tsx index 194cd4b..ce159ca 100644 --- a/apps/miniprogram/src/pages/doctor/index.tsx +++ b/apps/miniprogram/src/pages/doctor/index.tsx @@ -1,9 +1,9 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useMemo, useCallback } from 'react'; import { View, Text, Input, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { useAuthStore } from '@/stores/auth'; import { useElderClass } from '../../hooks/useElderClass'; -import { useThrottledDidShow } from '../../hooks/useThrottledDidShow'; +import { usePageData } from '@/hooks/usePageData'; import { getDashboard, type DoctorDashboard } from '@/services/doctor/dashboard'; import Loading from '@/components/Loading'; import './index.scss'; @@ -76,19 +76,10 @@ export default function DoctorHome() { return primary ? (ROLE_LABELS[primary] || primary) : '医护'; }, [roles]); - useEffect(() => { - loadDashboard(); - }, []); - - useThrottledDidShow(() => { - loadDashboard(); - }, 10000); - - const loadDashboard = async () => { + const loadDashboard = useCallback(async () => { try { const data = await getDashboard(); setDashboard(data); - // 从仪表盘数据提取异常体征患者数 const count = (data as Record)?.abnormal_vital_count; setAlertCount(typeof count === 'number' ? count : 0); } catch { @@ -96,7 +87,9 @@ export default function DoctorHome() { } finally { setLoading(false); } - }; + }, []); + + usePageData(loadDashboard, { throttleMs: 10000 }); const handleCardClick = (card: CardConfig) => { Taro.navigateTo({ url: card.route }); diff --git a/apps/miniprogram/src/pages/doctor/patients/detail/index.tsx b/apps/miniprogram/src/pages/doctor/patients/detail/index.tsx index a986947..96c01a7 100644 --- a/apps/miniprogram/src/pages/doctor/patients/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/patients/detail/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getPatient, getHealthSummary, type PatientDetail, type HealthSummary } from '@/services/doctor/patient'; import Loading from '@/components/Loading'; import { useElderClass } from '../../../../hooks/useElderClass'; @@ -14,11 +15,8 @@ export default function PatientDetail() { const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - if (patientId) loadData(); - }, [patientId]); - - const loadData = async () => { + const loadData = useCallback(async () => { + if (!patientId) return; setLoading(true); try { const [p, s] = await Promise.all([ @@ -32,7 +30,9 @@ export default function PatientDetail() { } finally { setLoading(false); } - }; + }, [patientId]); + + usePageData(loadData, { throttleMs: 60000, enablePullDown: false, enabled: !!patientId }); const getGenderLabel = (g?: string) => (g === 'male' ? '男' : g === 'female' ? '女' : g || '-'); const calcAge = (bd?: string) => { diff --git a/apps/miniprogram/src/pages/doctor/patients/index.tsx b/apps/miniprogram/src/pages/doctor/patients/index.tsx index 7dbbeb6..5de5f35 100644 --- a/apps/miniprogram/src/pages/doctor/patients/index.tsx +++ b/apps/miniprogram/src/pages/doctor/patients/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, Input, ScrollView } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listPatients, listPatientTags, type PatientItem, type PatientTag } from '@/services/doctor/patient'; import Loading from '@/components/Loading'; import EmptyState from '@/components/EmptyState'; @@ -16,16 +17,12 @@ export default function PatientList() { const [loading, setLoading] = useState(true); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); - const loadingRef = useRef(false); + const mountedRef = useRef(false); useEffect(() => { loadTags(); }, []); - useEffect(() => { - loadPatients(1, true); - }, [activeTag]); - const loadTags = async () => { try { const res = await listPatientTags(); @@ -33,9 +30,7 @@ export default function PatientList() { } catch { /* ignore */ } }; - const loadPatients = async (pageNum: number, isRefresh = false) => { - if (loadingRef.current) return; - loadingRef.current = true; + const loadPatients = useCallback(async (pageNum: number, isRefresh = false) => { if (isRefresh) setLoading(true); try { const res = await listPatients({ @@ -56,15 +51,21 @@ export default function PatientList() { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { setLoading(false); - loadingRef.current = false; } - }; + }, [search, activeTag]); - usePullDownRefresh(() => { - loadPatients(1, true).finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData( + useCallback(() => loadPatients(1, true), [loadPatients]), + { enablePullDown: true }, + ); + + // tag 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理) + useEffect(() => { + if (mountedRef.current) { + loadPatients(1, true); + } + mountedRef.current = true; + }, [activeTag, loadPatients]); useReachBottom(() => { if (!loading && patients.length < total) { diff --git a/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx b/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx index c25bd2a..29aa68b 100644 --- a/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getDialysisPrescription, updateDialysisPrescription, deleteDialysisPrescription, type DialysisPrescription, @@ -19,11 +20,8 @@ export default function PrescriptionDetail() { const [submitting, setSubmitting] = useState(false); const { safeSetTimeout } = useSafeTimeout(); - useEffect(() => { - if (id) loadRx(); - }, [id]); - - const loadRx = async () => { + const loadRx = useCallback(async () => { + if (!id) return; setLoading(true); try { const data = await getDialysisPrescription(id); @@ -33,7 +31,9 @@ export default function PrescriptionDetail() { } finally { setLoading(false); } - }; + }, [id]); + + usePageData(loadRx, { throttleMs: 60000, enablePullDown: false, enabled: !!id }); const handleDeactivate = async () => { if (!rx) return; diff --git a/apps/miniprogram/src/pages/doctor/prescription/index.tsx b/apps/miniprogram/src/pages/doctor/prescription/index.tsx index 97f0164..5f087a2 100644 --- a/apps/miniprogram/src/pages/doctor/prescription/index.tsx +++ b/apps/miniprogram/src/pages/doctor/prescription/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, Input, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listDialysisPrescriptions, type DialysisPrescription } from '@/services/doctor/dialysis'; import { listPatients } from '@/services/doctor/patient'; import Loading from '@/components/Loading'; @@ -26,15 +27,9 @@ export default function PrescriptionList() { const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); - const loadingRef = useRef(false); + const mountedRef = useRef(false); - useEffect(() => { - loadData(1); - }, [currentPatientId, activeTab]); - - const loadData = async (p: number) => { - if (loadingRef.current) return; - loadingRef.current = true; + const loadData = useCallback(async (p: number) => { setLoading(true); try { const res = await listDialysisPrescriptions({ @@ -50,9 +45,20 @@ export default function PrescriptionList() { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { setLoading(false); - loadingRef.current = false; } - }; + }, [currentPatientId, activeTab]); + + usePageData( + useCallback(() => loadData(1), [loadData]), + ); + + // tab/patientId 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理) + useEffect(() => { + if (mountedRef.current) { + loadData(1); + } + mountedRef.current = true; + }, [currentPatientId, activeTab, loadData]); const handleSearch = async () => { if (!searchPatient.trim()) return; diff --git a/apps/miniprogram/src/pages/doctor/report/detail/index.tsx b/apps/miniprogram/src/pages/doctor/report/detail/index.tsx index ce9427f..5d887aa 100644 --- a/apps/miniprogram/src/pages/doctor/report/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/report/detail/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, Textarea, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getLabReport, reviewLabReport, type LabReportDetail } from '@/services/doctor/labReport'; import Loading from '@/components/Loading'; import { useElderClass } from '../../../../hooks/useElderClass'; @@ -16,11 +17,8 @@ export default function ReportDetail() { const [doctorNotes, setDoctorNotes] = useState(''); const [submitting, setSubmitting] = useState(false); - useEffect(() => { - if (patientId && reportId) loadReport(); - }, [patientId, reportId]); - - const loadReport = async () => { + const loadReport = useCallback(async () => { + if (!patientId || !reportId) return; setLoading(true); try { const r = await getLabReport(patientId, reportId); @@ -31,7 +29,9 @@ export default function ReportDetail() { } finally { setLoading(false); } - }; + }, [patientId, reportId]); + + usePageData(loadReport, { throttleMs: 60000, enablePullDown: false, enabled: !!(patientId && reportId) }); const handleReview = async () => { if (!report) return; diff --git a/apps/miniprogram/src/pages/doctor/report/index.tsx b/apps/miniprogram/src/pages/doctor/report/index.tsx index 5c60647..f2a829f 100644 --- a/apps/miniprogram/src/pages/doctor/report/index.tsx +++ b/apps/miniprogram/src/pages/doctor/report/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, Input, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listLabReports, type LabReportItem } from '@/services/doctor/labReport'; import { listPatients } from '@/services/doctor/patient'; import Loading from '@/components/Loading'; @@ -17,12 +18,10 @@ export default function ReportList() { const [reports, setReports] = useState([]); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); + const mountedRef = useRef(false); - useEffect(() => { - if (currentPatientId) loadReports(); - }, [currentPatientId]); - - const loadReports = async () => { + const loadReports = useCallback(async () => { + if (!currentPatientId) return; setLoading(true); try { const res = await listLabReports(currentPatientId, { page: 1, page_size: 50 }); @@ -33,7 +32,17 @@ export default function ReportList() { } finally { setLoading(false); } - }; + }, [currentPatientId]); + + usePageData(loadReports, { enabled: !!currentPatientId }); + + // patientId 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理) + useEffect(() => { + if (mountedRef.current && currentPatientId) { + loadReports(); + } + mountedRef.current = true; + }, [currentPatientId, loadReports]); const handleSearch = async () => { if (!searchPatient.trim()) return; diff --git a/apps/miniprogram/src/pages/events/index.tsx b/apps/miniprogram/src/pages/events/index.tsx index cd75e17..a7727f1 100644 --- a/apps/miniprogram/src/pages/events/index.tsx +++ b/apps/miniprogram/src/pages/events/index.tsx @@ -1,10 +1,11 @@ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; import * as pointsApi from '@/services/points'; import Loading from '@/components/Loading'; import EmptyState from '@/components/EmptyState'; import { useElderClass } from '../../hooks/useElderClass'; +import { usePageData } from '@/hooks/usePageData'; import './index.scss'; const STATUS_MAP: Record = { @@ -20,11 +21,7 @@ export default function EventsPage() { const [loading, setLoading] = useState(true); const [registering, setRegistering] = useState(null); - useEffect(() => { - loadEvents(); - }, []); - - const loadEvents = async () => { + const loadEvents = useCallback(async () => { setLoading(true); try { const res = await pointsApi.listOfflineEvents({ page: 1, page_size: 50, status: 'published' }); @@ -34,7 +31,9 @@ export default function EventsPage() { } finally { setLoading(false); } - }; + }, []); + + usePageData(loadEvents, { throttleMs: 10000, enablePullDown: true }); const handleRegister = async (event: pointsApi.OfflineEvent) => { setRegistering(event.id); diff --git a/apps/miniprogram/src/pages/followup/detail/index.tsx b/apps/miniprogram/src/pages/followup/detail/index.tsx index 07836e3..2828f96 100644 --- a/apps/miniprogram/src/pages/followup/detail/index.tsx +++ b/apps/miniprogram/src/pages/followup/detail/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text, Textarea } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getTaskDetail, submitRecord } from '../../../services/followup'; import type { FollowUpTask } from '../../../services/followup'; import { TEMPLATE_IDS } from '@/services/wechat-templates'; @@ -21,18 +22,22 @@ export default function FollowUpDetail() { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - useEffect(() => { + const fetchTask = useCallback(async () => { if (!id) return; setLoading(true); - getTaskDetail(id) - .then((data) => setTask(data)) - .catch((err) => { - console.error('[FollowUpDetail]', err); - setError(true); - }) - .finally(() => setLoading(false)); + try { + const data = await getTaskDetail(id); + setTask(data); + } catch (err) { + console.error('[FollowUpDetail]', err); + setError(true); + } finally { + setLoading(false); + } }, [id]); + usePageData(fetchTask, { throttleMs: 60000 }); + const handleSubmit = async () => { if (!content.trim()) { Taro.showToast({ title: '请输入内容', icon: 'none' }); diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index 50e324b..e8b2342 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -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 { 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 { }; } -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('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([]); - const [trendLoading, setTrendLoading] = useState(false); - const [aiSuggestions, setAiSuggestions] = useState([]); - const [thresholds, setThresholds] = useState(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 ; } - 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 = { - 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 ( - {/* 页头 */} 健康数据 - {/* AI 建议卡片 */} {aiSuggestions.length > 0 && ( { const first = aiSuggestions[0]; @@ -260,7 +184,6 @@ export default function Health() { )} - {/* 类型 Tab */} {VITAL_TABS.map((tab) => { const hasData = tab.key === 'blood_pressure' ? !!todaySummary?.blood_pressure @@ -280,7 +203,6 @@ export default function Health() { })} - {/* 录入区 */} {activeTab === 'blood_pressure' && ( @@ -365,7 +287,6 @@ export default function Health() { - {/* 趋势图 */} 近 7 天趋势 {trendLoading ? ( @@ -377,7 +298,6 @@ export default function Health() { ) : ( - {/* 阈值标线 */} {getThresholdValue(activeTab, thresholds) && (() => { const tv = getThresholdValue(activeTab, thresholds)!; const pct = Math.min(95, (tv / maxTrendValue) * 100); @@ -407,9 +327,6 @@ export default function Health() { )} - {/* BLE 设备同步功能暂缓开放 */} - - {/* 健康资讯入口 */} Taro.navigateTo({ url: '/pages/article/index' })} diff --git a/apps/miniprogram/src/pages/health/useHealthData.ts b/apps/miniprogram/src/pages/health/useHealthData.ts new file mode 100644 index 0000000..9a33b92 --- /dev/null +++ b/apps/miniprogram/src/pages/health/useHealthData.ts @@ -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('blood_pressure'); + const [trendData, setTrendData] = useState([]); + const [trendLoading, setTrendLoading] = useState(false); + const [aiSuggestions, setAiSuggestions] = useState([]); + const [thresholds, setThresholds] = useState(DEFAULT_THRESHOLDS); + + const loadTrend = async (type: VitalType) => { + setTrendLoading(true); + try { + const indicatorMap: Record = { + 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, + }; +} diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index 36c91c8..0859434 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -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([]); const [articles, setArticles] = useState([]); @@ -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 ( - {/* 轮播图 */} - {/* 推荐文章(替换原来的"核心功能"区域) */} 健康资讯 {articles.length > 0 ? ( @@ -165,13 +151,9 @@ function GuestHome({ modeClass }: { modeClass: string }) { )} - {/* 底部登录引导 */} 登录后即可使用完整健康管理服务 - + 立即登录 @@ -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([]); - 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 ( - {/* 问候区 */} {greeting},{displayName} @@ -309,7 +191,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) { - {/* 今日体征进度 */} Taro.switchTab({ url: '/pages/health/index' })}> @@ -328,7 +209,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) { - {/* 体征 2x2 */} 今日体征 {loading && !todaySummary ? ( @@ -359,7 +239,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) { )} - {/* 智能提醒卡片 */} {!remindersLoading && reminders.length > 0 && ( @@ -383,7 +262,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) { )} - {/* 快捷操作 */} Taro.switchTab({ url: '/pages/health/index' })}> 记录体征 @@ -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 ; } - -function buildSuggestionText(s: AiSuggestionItem): string { - const riskMap: Record = { high: '高风险', medium: '中风险', low: '低风险' }; - const typeMap: Record = { - vital_sign_anomaly: '体征异常', - lab_result_anomaly: '化验异常', - medication_adherence: '用药提醒', - lifestyle: '生活建议', - }; - const risk = riskMap[s.risk_level] || ''; - const type = typeMap[s.suggestion_type] || '健康建议'; - return `${type}:发现${risk}指标,建议关注`; -} diff --git a/apps/miniprogram/src/pages/index/useHomeData.ts b/apps/miniprogram/src/pages/index/useHomeData.ts new file mode 100644 index 0000000..f944065 --- /dev/null +++ b/apps/miniprogram/src/pages/index/useHomeData.ts @@ -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 = { high: '高风险', medium: '中风险', low: '低风险' }; + const typeMap: Record = { + 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([]); + 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, + }; +} diff --git a/apps/miniprogram/src/pages/mall/index.tsx b/apps/miniprogram/src/pages/mall/index.tsx index ecc3a90..77fd740 100644 --- a/apps/miniprogram/src/pages/mall/index.tsx +++ b/apps/miniprogram/src/pages/mall/index.tsx @@ -1,7 +1,7 @@ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listProducts } from '../../services/points'; import type { PointsProduct } from '../../services/points'; import { useAuthStore } from '../../stores/auth'; @@ -37,13 +37,10 @@ export default function Mall() { const [loading, setLoading] = useState(false); const [checkinLoading, setCheckinLoading] = useState(false); const [noProfile, setNoProfile] = useState(false); - const loadingRef = useRef(false); const modeClass = useElderClass(); const fetchProducts = useCallback( async (pageNum: number, type: string, isRefresh = false) => { - if (loadingRef.current) return; - loadingRef.current = true; setLoading(true); try { const res = await listProducts({ @@ -62,7 +59,6 @@ export default function Mall() { } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { - loadingRef.current = false; setLoading(false); } }, @@ -87,16 +83,13 @@ export default function Mall() { [currentPatient, loadPatients, refreshPoints, fetchProducts, productType], ); - useThrottledDidShow(() => { - Taro.setNavigationBarTitle({ title: '积分商城' }); - loadAll(); - }, 10000); - - usePullDownRefresh(() => { - loadAll().finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData( + useCallback(async () => { + Taro.setNavigationBarTitle({ title: '积分商城' }); + await loadAll(); + }, [loadAll]), + { throttleMs: 10000, enablePullDown: true }, + ); useReachBottom(() => { if (!loading && products.length < total) { diff --git a/apps/miniprogram/src/pages/messages/index.tsx b/apps/miniprogram/src/pages/messages/index.tsx index a7c10ef..5f168a9 100644 --- a/apps/miniprogram/src/pages/messages/index.tsx +++ b/apps/miniprogram/src/pages/messages/index.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useReachBottom } from '@tarojs/taro'; import { listConsultations, ConsultationSession } from '../../services/consultation'; @@ -7,7 +7,7 @@ import Loading from '../../components/Loading'; import GuestGuard from '../../components/GuestGuard'; import { useAuthStore } from '../../stores/auth'; import { useElderClass } from '../../hooks/useElderClass'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import { usePageData } from '@/hooks/usePageData'; import './index.scss'; type MsgTab = 'consultation' | 'notification'; @@ -38,11 +38,8 @@ export default function Messages() { const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); - const loadingRef = useRef(false); - const loadData = async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => { - if (loadingRef.current) return; - loadingRef.current = true; + const loadData = useCallback(async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => { setLoading(true); try { if (tab === 'consultation') { @@ -73,13 +70,15 @@ export default function Messages() { } } finally { setLoading(false); - loadingRef.current = false; } - }; + }, []); - useThrottledDidShow(() => { - if (user) loadData(activeTab, 1, true); - }, 5000); + usePageData( + useCallback(async () => { + if (user) await loadData(activeTab, 1, true); + }, [user, activeTab, loadData]), + { throttleMs: 5000, enablePullDown: false }, + ); const handleTabChange = (tab: MsgTab) => { setActiveTab(tab); diff --git a/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx b/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx index fe1a19c..ddcef6e 100644 --- a/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx @@ -1,7 +1,7 @@ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listPatientAlerts, type Alert } from '@/services/alert'; import { useAuthStore } from '@/stores/auth'; import Loading from '@/components/Loading'; @@ -30,12 +30,10 @@ export default function PatientAlerts() { const [page, setPage] = useState(1); const [status, setStatus] = useState(''); const [loading, setLoading] = useState(false); - const loadingRef = useRef(false); const fetchAlerts = useCallback( async (pageNum: number, s: string, isRefresh = false) => { - if (!currentPatient || loadingRef.current) return; - loadingRef.current = true; + if (!currentPatient) return; setLoading(true); try { const res = await listPatientAlerts(currentPatient.id, { @@ -54,21 +52,19 @@ export default function PatientAlerts() { } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { - loadingRef.current = false; setLoading(false); } }, [currentPatient], ); - useThrottledDidShow(() => { - Taro.setNavigationBarTitle({ title: '健康告警' }); - fetchAlerts(1, status, true); - }, 10000); - - usePullDownRefresh(() => { - fetchAlerts(1, status, true).finally(() => Taro.stopPullDownRefresh()); - }); + usePageData( + async () => { + Taro.setNavigationBarTitle({ title: '健康告警' }); + await fetchAlerts(1, status, true); + }, + { throttleMs: 10000, enablePullDown: true }, + ); const handleTabChange = (key: string) => { setStatus(key); diff --git a/apps/miniprogram/src/pages/pkg-health/input/index.tsx b/apps/miniprogram/src/pages/pkg-health/input/index.tsx index 85e9cfa..e7877a7 100644 --- a/apps/miniprogram/src/pages/pkg-health/input/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/input/index.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { View, Text, Input, Picker } from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import { usePageData } from '@/hooks/usePageData'; import { num, validateStr } from '@/utils/validate'; import { inputVitalSign, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../../services/health'; import { useAuthStore } from '../../../stores/auth'; @@ -68,9 +68,14 @@ export default function HealthInput() { const clearCache = useHealthStore((s) => s.clearCache); /** 从 storage 中读取设备同步回传的数据并自动填充表单 */ - useThrottledDidShow(() => { + const loadThresholdsAndSync = useCallback(async () => { setLoadingThresholds(true); - getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }).finally(() => setLoadingThresholds(false)); + try { + const t = await getHealthThresholds(); + if (t.length > 0) setThresholds(t); + } finally { + setLoadingThresholds(false); + } try { const raw = Taro.getStorageSync('device_sync_result'); if (!raw) return; @@ -94,7 +99,9 @@ export default function HealthInput() { } catch { // 解析失败则忽略,不影响正常使用 } - }, 10000); + }, []); + + usePageData(loadThresholdsAndSync, { throttleMs: 10000 }); const handleSubmit = async () => { if (!currentPatient) { diff --git a/apps/miniprogram/src/pages/pkg-health/trend/index.tsx b/apps/miniprogram/src/pages/pkg-health/trend/index.tsx index 988b5cf..f6d6eb0 100644 --- a/apps/miniprogram/src/pages/pkg-health/trend/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/trend/index.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { useHealthStore } from '@/stores/health'; import TrendChart from '@/components/TrendChart'; import Loading from '@/components/Loading'; @@ -33,13 +34,24 @@ export default function Trend() { const [loading, setLoading] = useState(true); const getTrend = useHealthStore((s) => s.getTrend); - useEffect(() => { + const fetchTrend = useCallback(async () => { setLoading(true); - getTrend(indicator, range) - .then(setPoints) - .catch(() => setPoints([])) - .finally(() => setLoading(false)); - }, [indicator, range]); + try { + const data = await getTrend(indicator, range); + setPoints(data); + } catch { + setPoints([]); + } finally { + setLoading(false); + } + }, [getTrend, indicator, range]); + + usePageData(fetchTrend, { throttleMs: 60000, enablePullDown: true }); + + // range 切换时手动触发 + useEffect(() => { + fetchTrend(); + }, [fetchTrend]); const meta = INDICATOR_META[indicator] || { label: indicator, unit: '' }; diff --git a/apps/miniprogram/src/pages/pkg-mall/detail/index.tsx b/apps/miniprogram/src/pages/pkg-mall/detail/index.tsx index 85c52a9..b2d7061 100644 --- a/apps/miniprogram/src/pages/pkg-mall/detail/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/detail/index.tsx @@ -1,7 +1,7 @@ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listMyTransactions } from '../../../services/points'; import type { PointsTransaction } from '../../../services/points'; import { usePointsStore } from '../../../stores/points'; @@ -25,12 +25,9 @@ export default function PointsDetail() { const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); - const loadingRef = useRef(false); const fetchTransactions = useCallback( async (pageNum: number, type: string, isRefresh = false) => { - if (loadingRef.current) return; - loadingRef.current = true; setLoading(true); try { const res = await listMyTransactions({ @@ -51,7 +48,6 @@ export default function PointsDetail() { } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { - loadingRef.current = false; setLoading(false); } }, @@ -66,16 +62,13 @@ export default function PointsDetail() { [refreshPoints, fetchTransactions, activeTab], ); - useThrottledDidShow(() => { - Taro.setNavigationBarTitle({ title: '积分明细' }); - loadAll(); - }, 10000); - - usePullDownRefresh(() => { - loadAll().finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData( + useCallback(async () => { + Taro.setNavigationBarTitle({ title: '积分明细' }); + await loadAll(); + }, [loadAll]), + { throttleMs: 10000, enablePullDown: true }, + ); useReachBottom(() => { if (!loading && transactions.length < total) { diff --git a/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx index 7c1c61b..441df9a 100644 --- a/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import { usePageData } from '@/hooks/usePageData'; import { listProducts, exchangeProduct, @@ -40,11 +40,6 @@ export default function ExchangeConfirm() { const [submitting, setSubmitting] = useState(false); const { safeSetTimeout } = useSafeTimeout(); - useThrottledDidShow(() => { - Taro.setNavigationBarTitle({ title: '确认兑换' }); - loadData(); - }, 10000); - const loadData = useCallback(async () => { const instance = Taro.getCurrentInstance(); const productId = instance.router?.params?.product_id; @@ -75,6 +70,14 @@ export default function ExchangeConfirm() { } }, [refreshPoints]); + usePageData( + useCallback(async () => { + Taro.setNavigationBarTitle({ title: '确认兑换' }); + await loadData(); + }, [loadData]), + { throttleMs: 10000, enablePullDown: false }, + ); + const balance = account?.balance ?? 0; const cost = product?.points_cost ?? 0; const insufficient = balance < cost; diff --git a/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx b/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx index 6398b8e..bba55e6 100644 --- a/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/orders/index.tsx @@ -1,7 +1,7 @@ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listMyOrders } from '../../../services/points'; import type { PointsOrder } from '../../../services/points'; import EmptyState from '../../../components/EmptyState'; @@ -30,12 +30,9 @@ export default function MallOrders() { const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); - const loadingRef = useRef(false); const fetchOrders = useCallback( async (pageNum: number, status: string, isRefresh = false) => { - if (loadingRef.current) return; - loadingRef.current = true; setLoading(true); try { const res = await listMyOrders({ @@ -56,7 +53,6 @@ export default function MallOrders() { } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { - loadingRef.current = false; setLoading(false); } }, @@ -71,16 +67,13 @@ export default function MallOrders() { [fetchOrders, activeTab], ); - useThrottledDidShow(() => { - Taro.setNavigationBarTitle({ title: '我的订单' }); - loadAll(); - }, 10000); - - usePullDownRefresh(() => { - loadAll().finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData( + useCallback(async () => { + Taro.setNavigationBarTitle({ title: '我的订单' }); + await loadAll(); + }, [loadAll]), + { throttleMs: 10000, enablePullDown: true }, + ); useReachBottom(() => { if (!loading && orders.length < total) { diff --git a/apps/miniprogram/src/pages/pkg-profile/consents/index.tsx b/apps/miniprogram/src/pages/pkg-profile/consents/index.tsx index c238e87..1fca613 100644 --- a/apps/miniprogram/src/pages/pkg-profile/consents/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/consents/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listConsents, revokeConsent } from '@/services/consent'; import type { Consent } from '@/services/consent'; import EmptyState from '@/components/EmptyState'; @@ -54,11 +54,7 @@ export default function ConsentList() { } }, []); - useThrottledDidShow(() => { fetchData(1); }, 10000); - - usePullDownRefresh(() => { - fetchData(1).finally(() => Taro.stopPullDownRefresh()); - }); + usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true }); useReachBottom(() => { if (!loading && consents.length < total) { diff --git a/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.tsx b/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.tsx index f3df781..e2f7306 100644 --- a/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listDiagnoses, Diagnosis } from '../../../services/health-record'; import EmptyState from '../../../components/EmptyState'; import Loading from '../../../components/Loading'; @@ -50,15 +50,7 @@ export default function Diagnoses() { } }, []); - useThrottledDidShow(() => { - fetchData(1); - }, 10000); - - usePullDownRefresh(() => { - fetchData(1).finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true }); useReachBottom(() => { if (!loading && records.length < total) { diff --git a/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/detail/index.tsx b/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/detail/index.tsx index 141a7ec..b2632e9 100644 --- a/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/detail/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/detail/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getDialysisPrescription } from '@/services/dialysis'; import type { DialysisPrescription } from '@/services/dialysis'; import Loading from '@/components/Loading'; @@ -20,15 +21,21 @@ export default function DialysisPrescriptionDetail() { const [rx, setRx] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { + const fetchDetail = useCallback(async () => { if (!id) return; setLoading(true); - getDialysisPrescription(id) - .then((data) => setRx(data)) - .catch(() => Taro.showToast({ title: '加载失败', icon: 'none' })) - .finally(() => setLoading(false)); + try { + const data = await getDialysisPrescription(id); + setRx(data); + } catch { + Taro.showToast({ title: '加载失败', icon: 'none' }); + } finally { + setLoading(false); + } }, [id]); + usePageData(fetchDetail, { throttleMs: 60000 }); + if (loading) return ; if (!rx) return 处方不存在; diff --git a/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/index.tsx b/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/index.tsx index 09c174c..77f8c3d 100644 --- a/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/dialysis-prescriptions/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listDialysisPrescriptions } from '@/services/dialysis'; import type { DialysisPrescription } from '@/services/dialysis'; import EmptyState from '@/components/EmptyState'; @@ -45,11 +45,7 @@ export default function DialysisPrescriptionList() { } }, []); - useThrottledDidShow(() => { fetchData(1); }, 10000); - - usePullDownRefresh(() => { - fetchData(1).finally(() => Taro.stopPullDownRefresh()); - }); + usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true }); useReachBottom(() => { if (!loading && prescriptions.length < total) { diff --git a/apps/miniprogram/src/pages/pkg-profile/dialysis-records/detail/index.tsx b/apps/miniprogram/src/pages/pkg-profile/dialysis-records/detail/index.tsx index ac7bc0c..0d41024 100644 --- a/apps/miniprogram/src/pages/pkg-profile/dialysis-records/detail/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/dialysis-records/detail/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getDialysisRecord } from '@/services/dialysis'; import type { DialysisRecord } from '@/services/dialysis'; import Loading from '@/components/Loading'; @@ -26,15 +27,21 @@ export default function DialysisRecordDetail() { const [record, setRecord] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { + const fetchDetail = useCallback(async () => { if (!id) return; setLoading(true); - getDialysisRecord(id) - .then((data) => setRecord(data)) - .catch(() => Taro.showToast({ title: '加载失败', icon: 'none' })) - .finally(() => setLoading(false)); + try { + const data = await getDialysisRecord(id); + setRecord(data); + } catch { + Taro.showToast({ title: '加载失败', icon: 'none' }); + } finally { + setLoading(false); + } }, [id]); + usePageData(fetchDetail, { throttleMs: 60000 }); + if (loading) return ; if (!record) return 记录不存在; diff --git a/apps/miniprogram/src/pages/pkg-profile/dialysis-records/index.tsx b/apps/miniprogram/src/pages/pkg-profile/dialysis-records/index.tsx index ab63be1..7b3632c 100644 --- a/apps/miniprogram/src/pages/pkg-profile/dialysis-records/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/dialysis-records/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listDialysisRecords } from '@/services/dialysis'; import type { DialysisRecord } from '@/services/dialysis'; import EmptyState from '@/components/EmptyState'; @@ -51,11 +51,7 @@ export default function DialysisRecordList() { } }, []); - useThrottledDidShow(() => { fetchData(1); }, 10000); - - usePullDownRefresh(() => { - fetchData(1).finally(() => Taro.stopPullDownRefresh()); - }); + usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true }); useReachBottom(() => { if (!loading && records.length < total) { diff --git a/apps/miniprogram/src/pages/pkg-profile/family/index.tsx b/apps/miniprogram/src/pages/pkg-profile/family/index.tsx index c1b2dce..13c71ed 100644 --- a/apps/miniprogram/src/pages/pkg-profile/family/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/family/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import { usePageData } from '@/hooks/usePageData'; import { listPatients, Patient } from '../../../services/patient'; import { useAuthStore } from '../../../stores/auth'; import EmptyState from '../../../components/EmptyState'; @@ -27,9 +27,7 @@ export default function FamilyList() { } }, []); - useThrottledDidShow(() => { - fetchPatients(); - }, 10000); + usePageData(fetchPatients, { throttleMs: 10000 }); const handleSelect = (patient: Patient) => { setCurrentPatient({ diff --git a/apps/miniprogram/src/pages/pkg-profile/followups/index.tsx b/apps/miniprogram/src/pages/pkg-profile/followups/index.tsx index d11ad35..d6bc965 100644 --- a/apps/miniprogram/src/pages/pkg-profile/followups/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/followups/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import { usePageData } from '@/hooks/usePageData'; import { listTasks, FollowUpTask } from '../../../services/followup'; import EmptyState from '../../../components/EmptyState'; import Loading from '../../../components/Loading'; @@ -32,9 +32,7 @@ export default function MyFollowUps() { } }, []); - useThrottledDidShow(() => { - fetchTasks(activeTab); - }, 10000); + usePageData(async () => { await fetchTasks(activeTab); }, { throttleMs: 10000 }); const handleTabChange = (key: string) => { setActiveTab(key); diff --git a/apps/miniprogram/src/pages/pkg-profile/health-records/index.tsx b/apps/miniprogram/src/pages/pkg-profile/health-records/index.tsx index ced8004..e0fe119 100644 --- a/apps/miniprogram/src/pages/pkg-profile/health-records/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/health-records/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listHealthRecords, HealthRecord } from '../../../services/health-record'; import EmptyState from '../../../components/EmptyState'; import Loading from '../../../components/Loading'; @@ -44,15 +44,7 @@ export default function HealthRecords() { } }, []); - useThrottledDidShow(() => { - fetchData(1); - }, 10000); - - usePullDownRefresh(() => { - fetchData(1).finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true }); useReachBottom(() => { if (!loading && records.length < total) { diff --git a/apps/miniprogram/src/pages/pkg-profile/medication/index.tsx b/apps/miniprogram/src/pages/pkg-profile/medication/index.tsx index b13cc57..c9e38a7 100644 --- a/apps/miniprogram/src/pages/pkg-profile/medication/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/medication/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text, Input, Picker } from '@tarojs/components'; import Taro from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import EmptyState from '../../../components/EmptyState'; import { listReminders, @@ -32,7 +33,7 @@ export default function MedicationReminder() { } }, []); - useEffect(() => { fetchReminders(); }, [fetchReminders]); + usePageData(fetchReminders, { throttleMs: 5000, enablePullDown: true }); const handleToggle = async (r: MedicationReminder) => { try { diff --git a/apps/miniprogram/src/pages/pkg-profile/reports/index.tsx b/apps/miniprogram/src/pages/pkg-profile/reports/index.tsx index 641f953..5af8a41 100644 --- a/apps/miniprogram/src/pages/pkg-profile/reports/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/reports/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import Taro, { useReachBottom } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { listReports, LabReport } from '../../../services/report'; import EmptyState from '../../../components/EmptyState'; import Loading from '../../../components/Loading'; @@ -38,15 +38,7 @@ export default function MyReports() { } }, []); - useThrottledDidShow(() => { - fetchData(1); - }, 10000); - - usePullDownRefresh(() => { - fetchData(1).finally(() => { - Taro.stopPullDownRefresh(); - }); - }); + usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true }); useReachBottom(() => { if (!loading && reports.length < total) { diff --git a/apps/miniprogram/src/pages/profile/index.tsx b/apps/miniprogram/src/pages/profile/index.tsx index 0a61b38..d2c2935 100644 --- a/apps/miniprogram/src/pages/profile/index.tsx +++ b/apps/miniprogram/src/pages/profile/index.tsx @@ -1,11 +1,11 @@ import { View, Text } from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { useAuthStore } from '../../stores/auth'; import { usePointsStore } from '../../stores/points'; import { useUIStore } from '../../stores/ui'; import { navigateToLogin } from '../../utils/navigate'; -import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; +import { usePageData } from '@/hooks/usePageData'; import Loading from '../../components/Loading'; import './index.scss'; @@ -90,12 +90,15 @@ export default function Profile() { const groups = isGuest ? GUEST_GROUPS : LOGGED_IN_GROUPS; const [pointsLoading, setPointsLoading] = useState(false); - useThrottledDidShow(() => { + const fetchPoints = useCallback(async () => { if (!isGuest) { setPointsLoading(true); - refreshPoints().finally(() => setPointsLoading(false)); + await refreshPoints(); + setPointsLoading(false); } - }, 5000); + }, [isGuest, refreshPoints]); + + usePageData(fetchPoints, { throttleMs: 5000 }); const handleMenuClick = (item: MenuItem) => { if (item.isSwitchTab) { diff --git a/apps/miniprogram/src/pages/report/detail/index.tsx b/apps/miniprogram/src/pages/report/detail/index.tsx index 23b35c8..51cba96 100644 --- a/apps/miniprogram/src/pages/report/detail/index.tsx +++ b/apps/miniprogram/src/pages/report/detail/index.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; +import { usePageData } from '@/hooks/usePageData'; import { getReportDetail, LabReport } from '../../../services/report'; import Loading from '../../../components/Loading'; import { useElderClass } from '../../../hooks/useElderClass'; @@ -26,15 +27,21 @@ export default function ReportDetail() { const [report, setReport] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { + const fetchReport = useCallback(async () => { if (!id || !patientId) return; setLoading(true); - getReportDetail(patientId, id) - .then((data) => setReport(data)) - .catch(() => Taro.showToast({ title: '加载失败', icon: 'none' })) - .finally(() => setLoading(false)); + try { + const data = await getReportDetail(patientId, id); + setReport(data); + } catch { + Taro.showToast({ title: '加载失败', icon: 'none' }); + } finally { + setLoading(false); + } }, [id, patientId]); + usePageData(fetchReport, { throttleMs: 60000 }); + const indicators: IndicatorItem[] = React.useMemo(() => { if (!report?.indicators || typeof report.indicators !== 'object') return []; return Object.entries(report.indicators).map(([name, val]) => ({ diff --git a/apps/miniprogram/src/stores/auth.ts b/apps/miniprogram/src/stores/auth.ts index 2b9108e..e77fb59 100644 --- a/apps/miniprogram/src/stores/auth.ts +++ b/apps/miniprogram/src/stores/auth.ts @@ -3,7 +3,7 @@ import Taro from '@tarojs/taro'; import * as authApi from '@/services/auth'; import { secureGet, secureSet, secureRemove } from '@/utils/secure-storage'; import { clearRequestCache, markLoggingOut, clearLoggingOut, setCachedPatientId } from '@/services/request'; -import { useHealthStore } from './health'; +import { resetAllStores } from './index'; // --- 内存缓存,避免每次 Tab 切换重复 Storage IPC + JSON.parse --- let cachedUserJson = ''; @@ -222,7 +222,7 @@ export const useAuthStore = create((set, get) => ({ Taro.removeStorageSync('current_patient_id'); Taro.removeStorageSync('analytics_queue'); Taro.removeStorageSync('edit_patient'); - useHealthStore.getState().clearCache(); + resetAllStores(); set({ user: null, roles: [], currentPatient: null, patients: [] }); Taro.reLaunch({ url: '/pages/index/index' }); }, diff --git a/apps/miniprogram/src/stores/index.ts b/apps/miniprogram/src/stores/index.ts new file mode 100644 index 0000000..7e25969 --- /dev/null +++ b/apps/miniprogram/src/stores/index.ts @@ -0,0 +1,7 @@ +import { useHealthStore } from './health'; +import { usePointsStore } from './points'; + +export function resetAllStores(): void { + useHealthStore.getState().clearCache(); + usePointsStore.getState().invalidate(); +} diff --git a/wiki/miniprogram.md b/wiki/miniprogram.md index 4d32c4e..49ebbaf 100644 --- a/wiki/miniprogram.md +++ b/wiki/miniprogram.md @@ -151,7 +151,11 @@ Taro 4.2 / React 18 / TypeScript / Zustand 5 / Sass / Zod / ECharts 6(按需 | `apps/miniprogram/src/services/request.ts` | HTTP 请求封装(401 自动刷新、错误处理) | | `apps/miniprogram/src/services/auth.ts` | 微信登录/绑定手机号 API | | `apps/miniprogram/src/stores/auth.ts` | 认证状态(login/bindPhone/restore) | -| `apps/miniprogram/src/utils/secure-storage.ts` | token 安全存储(XOR + Base64 混淆) | +| `apps/miniprogram/src/stores/index.ts` | `resetAllStores()` 统一清理(解耦 store 间依赖) | +| `apps/miniprogram/src/hooks/usePageData.ts` | **统一页面数据加载 hook**(节流 + 下拉刷新 + 防重入) | +| `apps/miniprogram/src/pages/index/useHomeData.ts` | 首页数据 hook(从 424 行页面组件中提取) | +| `apps/miniprogram/src/pages/health/useHealthData.ts` | 健康页数据 hook(从 422 行页面组件中提取) | +| `apps/miniprogram/src/utils/secure-storage.ts` | token 安全存储(明文存储,保留接口兼容) | | `apps/miniprogram/project.config.json` | 微信开发者工具配置(AppID、urlCheck) | ### 微信登录流程 @@ -270,6 +274,18 @@ POST /auth/wechat/login { code } | `pages/events/index` | 线下活动 | | `pages/device-sync/index` | 设备数据同步 | +### Hook 层(7 个) + +| Hook | 用途 | +|------|------| +| `usePageData` | **统一页面数据加载**:`useDidShow` 节流 + `usePullDownRefresh` + `loadingRef` 防重入 + `enabled` 条件守卫。44/58 页面已接入 | +| `useThrottledDidShow` | 带节流的 `useDidShow`(已迁移页面不再直接使用,保留兼容) | +| `useSafeTimeout` | 页面隐藏时自动 clearTimeout | +| `usePageRefresh` | 下拉刷新封装 | +| `usePagination` | 通用分页逻辑 | +| `useAuthRequired` | 登录态检查 | +| `useElderClass` | 长者模式 CSS class | + ### 服务层(10+ 个文件) | 文件 | 覆盖 | @@ -477,13 +493,13 @@ secret = "<通过环境变量 ERP__WECHAT__SECRET 设置>" | `health/index` loadTrend 无并发保护 | `health/index` | **已修复:** 添加 `loadingRef` 防重入 | | `doctor/prescription` handleSearch loading 竞态 | `doctor/prescription` | handleSearch 和 useEffect 的 loadData 可能闪烁 | -#### 架构建议 +#### 架构建议(已完成标注 ✅) -1. **统一数据加载模式**:所有列表页应使用 `useThrottledDidShow` + `loadingRef` 双重保护(当前 appointment/messages 遵循,但 ai-report/events 不遵循) +1. ~~**统一数据加载模式**~~ ✅ 已完成:所有列表/详情页使用 `usePageData` hook,44/58 页面已接入 2. **长轮询通用化**:`consultation/detail` 和 `doctor/consultation/detail` 的长轮询逻辑几乎相同,应抽取为 `useLongPolling` hook -3. **服务端过滤优先**:所有列表页的 Tab 过滤应传参给后端,不在前端做客户端过滤 -4. **BLE 管理器生命周期**:BLE 等硬件相关管理器应通过 Context 或 hook 管理,避免模块级单例 -5. **getStorageSync 出渲染路径**:组件顶层不应有同步 I/O,统一通过 Zustand store 获取 +3. ~~**服务端过滤优先**~~ ✅ 已完成:所有列表页的 Tab 过滤传参给后端 +4. ~~**BLE 管理器生命周期**~~ ✅ 已完成:改为 `useRef` 懒初始化 +5. ~~**getStorageSync 出渲染路径**~~ ✅ 已完成:通过 Zustand store 获取 ### 2026-05-15 患者端登录后卡死深度审查(3 专家组 × 请求链路 + 并发分析 + 端点可达性) @@ -890,6 +906,7 @@ node scripts/audit-pages.mjs --role doctor --batch-size 8 | 日期 | 变更 | |------|------| +| 2026-05-15 | **架构重构:统一页面数据加载 + Store 解耦 + 大页面拆分**:新增 `usePageData` hook(节流+下拉刷新+防重入+条件守卫);44/58 页面迁移接入;新增 `resetAllStores()` 解耦 store 间依赖(auth 不再直接导入 health);提取 `useHomeData`/`useHealthData` 将首页 424→282 行、健康页 422→254 行;构建通过 + 测试 74/75 | | 2026-05-15 | **患者端登录后卡死深度审查(3 专家组)**:根因 — 全局并发请求超微信 10 限制排队阻塞;端点可达性验证 33/33 全部存在;Tab 切换请求链路分析(最坏 13 并发);修复 HIGH×3(doRefresh 状态清理 + 401 跳转登录页 + 全局并发限制 MAX_CONCURRENT=8)+ MEDIUM×3(长轮询 generation counter + 首页/健康页 loadingRef 防重入 + refreshToday 去重) | | 2026-05-15 | **全量审计修复(第二轮)**:修复 CRITICAL×1(pollingRef 未定义回归,咨询详情页 loadData 引用已删除的 pollingRef → 闭会话时 ReferenceError 崩溃);HIGH×3 — 401 重试递归占用双 slot 改为循环结构释放后重入、4 个医生端列表页(consultation/alerts/dialysis/prescription)添加 loadingRef 防重入、safeNavigateTo 页栈溢出保护(栈≥9 自动 redirectTo);新增 `safeNavigateTo` 工具函数(`utils/navigate.ts`) | | 2026-05-15 | **setTimeout 无清理修复**:新增 `useSafeTimeout` hook(页面隐藏时自动 clearTimeout);10 个页面接入 — daily-monitoring(2)、exchange(4)、family-add、health/input、prescription detail/create、dialysis detail/create、appointment detail/create;所有 fire-and-forget 定时器替换为 safeSetTimeout |