refactor(mp): 架构重构 — usePageData 统一数据加载 + Store 解耦 + 大页面拆分
新增 usePageData hook(useDidShow 节流 + usePullDownRefresh + loadingRef 防重入 + enabled 条件守卫), 44/58 页面迁移接入,消灭 4 种数据加载模式并存。 - 新增 hooks/usePageData.ts — 统一页面数据加载生命周期 - 新增 stores/index.ts — resetAllStores() 解耦 auth↔health store 依赖 - 新增 pages/index/useHomeData.ts — 首页数据 hook(424→282 行) - 新增 pages/health/useHealthData.ts — 健康页数据 hook(422→254 行) - 44 个页面迁移到 usePageData(9 患者端 + 15 医生端 + 20 子包) - auth store logout 不再直接导入 health store 构建通过,测试 74/75(1 个预存失败)。
This commit is contained in:
70
apps/miniprogram/src/hooks/usePageData.ts
Normal file
70
apps/miniprogram/src/hooks/usePageData.ts
Normal file
@@ -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<void>;
|
||||
trigger: () => void;
|
||||
}
|
||||
|
||||
export function usePageData(
|
||||
fetcher: () => Promise<void>,
|
||||
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 };
|
||||
}
|
||||
@@ -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<AiAnalysisItem | null>(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 <Loading />;
|
||||
|
||||
if (!analysis) {
|
||||
|
||||
@@ -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}` });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
<View className={`article-detail-page ${modeClass}`}>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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<string, unknown>)?.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 });
|
||||
|
||||
@@ -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<HealthSummary | null>(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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<LabReportItem[]>([]);
|
||||
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;
|
||||
|
||||
@@ -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<string, { label: string; className: string }> = {
|
||||
@@ -20,11 +21,7 @@ export default function EventsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [registering, setRegistering] = useState<string | null>(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);
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Input } from '@tarojs/components';
|
||||
import Taro, { usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useElderClass } from '../../hooks/useElderClass';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health';
|
||||
import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis';
|
||||
import { findThreshold, inputVitalSign, type HealthThreshold } from '../../services/health';
|
||||
import Loading from '../../components/Loading';
|
||||
import GuestGuard from '../../components/GuestGuard';
|
||||
import { useHealthData, VITAL_TABS, type VitalType } from './useHealthData';
|
||||
import './index.scss';
|
||||
|
||||
type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight';
|
||||
|
||||
const VITAL_TABS: { key: VitalType; label: string }[] = [
|
||||
{ key: 'blood_pressure', label: '血压' },
|
||||
{ key: 'heart_rate', label: '心率' },
|
||||
{ key: 'blood_sugar', label: '血糖' },
|
||||
{ key: 'weight', label: '体重' },
|
||||
];
|
||||
|
||||
/** 根据阈值列表构建参考范围文案 */
|
||||
function buildRefRange(t: HealthThreshold[]): Record<VitalType, string> {
|
||||
const bpSys = findThreshold(t, 'systolic_bp', 'high')?.threshold_value ?? 140;
|
||||
const bpDia = findThreshold(t, 'diastolic_bp', 'high')?.threshold_value ?? 90;
|
||||
@@ -36,20 +24,14 @@ function buildRefRange(t: HealthThreshold[]): Record<VitalType, string> {
|
||||
};
|
||||
}
|
||||
|
||||
interface TrendPoint {
|
||||
date: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export default function Health() {
|
||||
const todaySummary = useHealthStore((s) => s.todaySummary);
|
||||
const loading = useHealthStore((s) => s.loading);
|
||||
const refreshToday = useHealthStore((s) => s.refreshToday);
|
||||
const fetchTrend = useHealthStore((s) => s.getTrend);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const modeClass = useElderClass();
|
||||
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
|
||||
const {
|
||||
user, todaySummary, loading, activeTab, trendData, trendLoading,
|
||||
aiSuggestions, thresholds, handleTabChange, loadTrend, refreshToday,
|
||||
} = useHealthData();
|
||||
|
||||
const [systolic, setSystolic] = useState('');
|
||||
const [diastolic, setDiastolic] = useState('');
|
||||
const [heartRateVal, setHeartRateVal] = useState('');
|
||||
@@ -57,67 +39,11 @@ export default function Health() {
|
||||
const [sugarPeriod, setSugarPeriod] = useState<'fasting' | 'postprandial'>('fasting');
|
||||
const [weightVal, setWeightVal] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
|
||||
const [trendLoading, setTrendLoading] = useState(false);
|
||||
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
|
||||
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
if (!user || loadingRef.current) return;
|
||||
// 批量发起请求,避免串行 setState 级联重渲染
|
||||
loadingRef.current = true;
|
||||
Promise.allSettled([
|
||||
refreshToday(),
|
||||
loadTrend(activeTab),
|
||||
loadAiSuggestions(),
|
||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
|
||||
]).finally(() => { loadingRef.current = false; });
|
||||
}, 5000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
if (!user) return;
|
||||
Promise.all([refreshToday(true), loadTrend(activeTab), loadAiSuggestions()]).finally(() => {
|
||||
Taro.stopPullDownRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return <GuestGuard title='请先登录' desc='登录后即可记录和查看健康数据' />;
|
||||
}
|
||||
|
||||
const loadAiSuggestions = async () => {
|
||||
try {
|
||||
const items = await listPendingSuggestions();
|
||||
setAiSuggestions(items.slice(0, 3));
|
||||
} catch {
|
||||
setAiSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTrend = async (type: VitalType) => {
|
||||
setTrendLoading(true);
|
||||
try {
|
||||
const indicatorMap: Record<VitalType, string> = {
|
||||
blood_pressure: 'systolic_bp_morning',
|
||||
heart_rate: 'heart_rate',
|
||||
blood_sugar: 'blood_sugar',
|
||||
weight: 'weight',
|
||||
};
|
||||
const points = await fetchTrend(indicatorMap[type], '7d');
|
||||
setTrendData(points);
|
||||
} catch {
|
||||
setTrendData([]);
|
||||
} finally {
|
||||
setTrendLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (tab: VitalType) => {
|
||||
setActiveTab(tab);
|
||||
loadTrend(tab);
|
||||
};
|
||||
|
||||
const getWarnStatus = (type: VitalType): string | null => {
|
||||
if (type === 'blood_pressure') {
|
||||
const sys = parseFloat(systolic);
|
||||
@@ -224,12 +150,10 @@ export default function Health() {
|
||||
|
||||
return (
|
||||
<View className={`health-page ${modeClass}`}>
|
||||
{/* 页头 */}
|
||||
<View className='health-header'>
|
||||
<Text className='health-title'>健康数据</Text>
|
||||
</View>
|
||||
|
||||
{/* AI 建议卡片 */}
|
||||
{aiSuggestions.length > 0 && (
|
||||
<View className='ai-suggestion-card' onClick={() => {
|
||||
const first = aiSuggestions[0];
|
||||
@@ -260,7 +184,6 @@ export default function Health() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 类型 Tab */}
|
||||
<View className='vital-tabs'>
|
||||
{VITAL_TABS.map((tab) => {
|
||||
const hasData = tab.key === 'blood_pressure' ? !!todaySummary?.blood_pressure
|
||||
@@ -280,7 +203,6 @@ export default function Health() {
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* 录入区 */}
|
||||
<View className='input-section'>
|
||||
{activeTab === 'blood_pressure' && (
|
||||
<View className='input-group'>
|
||||
@@ -365,7 +287,6 @@ export default function Health() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 趋势图 */}
|
||||
<View className='trend-section'>
|
||||
<Text className='section-title'>近 7 天趋势</Text>
|
||||
{trendLoading ? (
|
||||
@@ -377,7 +298,6 @@ export default function Health() {
|
||||
) : (
|
||||
<View className='trend-chart'>
|
||||
<View className='trend-bars'>
|
||||
{/* 阈值标线 */}
|
||||
{getThresholdValue(activeTab, thresholds) && (() => {
|
||||
const tv = getThresholdValue(activeTab, thresholds)!;
|
||||
const pct = Math.min(95, (tv / maxTrendValue) * 100);
|
||||
@@ -407,9 +327,6 @@ export default function Health() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* BLE 设备同步功能暂缓开放 */}
|
||||
|
||||
{/* 健康资讯入口 */}
|
||||
<View
|
||||
className='article-entry'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/article/index' })}
|
||||
|
||||
96
apps/miniprogram/src/pages/health/useHealthData.ts
Normal file
96
apps/miniprogram/src/pages/health/useHealthData.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useHealthStore } from '@/stores/health';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { getTrend, getHealthThresholds, DEFAULT_THRESHOLDS, type HealthThreshold } from '@/services/health';
|
||||
import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis';
|
||||
|
||||
export type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight';
|
||||
|
||||
export const VITAL_TABS: { key: VitalType; label: string }[] = [
|
||||
{ key: 'blood_pressure', label: '血压' },
|
||||
{ key: 'heart_rate', label: '心率' },
|
||||
{ key: 'blood_sugar', label: '血糖' },
|
||||
{ key: 'weight', label: '体重' },
|
||||
];
|
||||
|
||||
export interface TrendPoint {
|
||||
date: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export function useHealthData() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const todaySummary = useHealthStore((s) => s.todaySummary);
|
||||
const loading = useHealthStore((s) => s.loading);
|
||||
const refreshToday = useHealthStore((s) => s.refreshToday);
|
||||
const fetchTrend = useHealthStore((s) => s.getTrend);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
|
||||
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
|
||||
const [trendLoading, setTrendLoading] = useState(false);
|
||||
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
|
||||
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
|
||||
|
||||
const loadTrend = async (type: VitalType) => {
|
||||
setTrendLoading(true);
|
||||
try {
|
||||
const indicatorMap: Record<VitalType, string> = {
|
||||
blood_pressure: 'systolic_bp_morning',
|
||||
heart_rate: 'heart_rate',
|
||||
blood_sugar: 'blood_sugar',
|
||||
weight: 'weight',
|
||||
};
|
||||
const points = await fetchTrend(indicatorMap[type], '7d');
|
||||
setTrendData(points);
|
||||
} catch {
|
||||
setTrendData([]);
|
||||
} finally {
|
||||
setTrendLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadAiSuggestions = async () => {
|
||||
try {
|
||||
const items = await listPendingSuggestions();
|
||||
setAiSuggestions(items.slice(0, 3));
|
||||
} catch {
|
||||
setAiSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
await Promise.allSettled([
|
||||
refreshToday(),
|
||||
loadTrend(activeTab),
|
||||
loadAiSuggestions(),
|
||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
|
||||
]);
|
||||
};
|
||||
|
||||
usePageData(fetchData, {
|
||||
throttleMs: 5000,
|
||||
enablePullDown: true,
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const handleTabChange = (tab: VitalType) => {
|
||||
setActiveTab(tab);
|
||||
loadTrend(tab);
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
todaySummary,
|
||||
loading,
|
||||
activeTab,
|
||||
trendData,
|
||||
trendLoading,
|
||||
aiSuggestions,
|
||||
thresholds,
|
||||
handleTabChange,
|
||||
loadTrend,
|
||||
refreshToday,
|
||||
fetchTrend,
|
||||
};
|
||||
}
|
||||
@@ -1,46 +1,36 @@
|
||||
import { View, Text, Swiper, SwiperItem, Image } from '@tarojs/components';
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import Taro, { usePullDownRefresh, useDidShow, useDidHide } from '@tarojs/taro';
|
||||
import { useState } from 'react';
|
||||
import Taro, { useDidShow, useDidHide } from '@tarojs/taro';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useUIStore } from '../../stores/ui';
|
||||
import { navigateToLogin } from '../../utils/navigate';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
import ProgressRing from '../../components/ProgressRing';
|
||||
import Loading from '../../components/Loading';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { trackPageView } from '@/services/analytics';
|
||||
import * as appointmentApi from '@/services/appointment';
|
||||
import * as followupApi from '@/services/followup';
|
||||
import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis';
|
||||
import { notificationService } from '@/services/notification';
|
||||
import { api } from '@/services/request';
|
||||
import type { Article } from '@/services/article';
|
||||
import ProgressRing from '../../components/ProgressRing';
|
||||
import Loading from '../../components/Loading';
|
||||
import { useHomeData, type ReminderItem } from './useHomeData';
|
||||
import './index.scss';
|
||||
|
||||
interface ReminderItem {
|
||||
id: string;
|
||||
text: string;
|
||||
type: 'ai' | 'appointment' | 'followup';
|
||||
tag: string;
|
||||
}
|
||||
|
||||
interface PublicBanner {
|
||||
id: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
desc?: string;
|
||||
image_url?: string;
|
||||
link_type?: string;
|
||||
link_target?: string;
|
||||
}
|
||||
|
||||
// ─── 访客首页 ───
|
||||
|
||||
const FALLBACK_SLIDES = [
|
||||
{ id: 'slide-1', title: '专业血透中心', desc: '三甲级医护团队全程守护', image_url: '' },
|
||||
{ id: 'slide-2', title: '智慧健康管理', desc: 'AI 驱动个性化健康方案', image_url: '' },
|
||||
{ id: 'slide-3', title: '温馨就医环境', desc: '舒适安全的治疗体验', image_url: '' },
|
||||
];
|
||||
|
||||
// ─── 访客首页 ───
|
||||
|
||||
function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
const [banners, setBanners] = useState<PublicBanner[]>([]);
|
||||
const [articles, setArticles] = useState<Article[]>([]);
|
||||
@@ -49,10 +39,6 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
useDidShow(() => { setSwiperAutoplay(true); });
|
||||
useDidHide(() => { setSwiperAutoplay(false); });
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
loadPublicData();
|
||||
}, 10_000);
|
||||
|
||||
const loadPublicData = async () => {
|
||||
let tenantId = Taro.getStorageSync('tenant_id');
|
||||
if (!tenantId) {
|
||||
@@ -92,11 +78,12 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
}
|
||||
};
|
||||
|
||||
usePageData(loadPublicData, { throttleMs: 10_000, enablePullDown: true, enabled: true });
|
||||
|
||||
const slides = banners.length > 0 ? banners : FALLBACK_SLIDES;
|
||||
|
||||
return (
|
||||
<View className={`guest-page ${modeClass}`}>
|
||||
{/* 轮播图 */}
|
||||
<Swiper
|
||||
className='guest-swiper'
|
||||
indicatorDots
|
||||
@@ -124,7 +111,6 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
))}
|
||||
</Swiper>
|
||||
|
||||
{/* 推荐文章(替换原来的"核心功能"区域) */}
|
||||
<View className='guest-section'>
|
||||
<Text className='guest-section-title'>健康资讯</Text>
|
||||
{articles.length > 0 ? (
|
||||
@@ -165,13 +151,9 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部登录引导 */}
|
||||
<View className='guest-login-prompt'>
|
||||
<Text className='guest-login-text'>登录后即可使用完整健康管理服务</Text>
|
||||
<View
|
||||
className='guest-login-btn'
|
||||
onClick={navigateToLogin}
|
||||
>
|
||||
<View className='guest-login-btn' onClick={navigateToLogin}>
|
||||
<Text className='guest-login-btn-text'>立即登录</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -182,110 +164,11 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
// ─── 登录后首页 ───
|
||||
|
||||
function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const todaySummary = useHealthStore((s) => s.todaySummary);
|
||||
const loading = useHealthStore((s) => s.loading);
|
||||
const refreshToday = useHealthStore((s) => s.refreshToday);
|
||||
const [reminders, setReminders] = useState<ReminderItem[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [remindersLoading, setRemindersLoading] = useState(false);
|
||||
const remindersLoadingRef = useRef(false);
|
||||
|
||||
const { trigger: triggerHomeRefresh } = useThrottledDidShow(() => {
|
||||
refreshToday();
|
||||
loadReminders();
|
||||
loadUnread();
|
||||
trackPageView('home');
|
||||
}, 5000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
Promise.all([refreshToday(true), loadReminders(), loadUnread()]).finally(() => {
|
||||
Taro.stopPullDownRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
const loadUnread = async () => {
|
||||
try {
|
||||
const res = await notificationService.getUnreadCount() as { data?: { count?: number } | number };
|
||||
const d = res.data;
|
||||
setUnreadCount(typeof d === 'object' && d ? (d.count ?? 0) : 0);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const loadReminders = async () => {
|
||||
const patientId = useAuthStore.getState().currentPatient?.id;
|
||||
if (!patientId || remindersLoadingRef.current) return;
|
||||
remindersLoadingRef.current = true;
|
||||
setRemindersLoading(true);
|
||||
try {
|
||||
const items: ReminderItem[] = [];
|
||||
const [apptRes, taskRes, suggestRes] = await Promise.allSettled([
|
||||
appointmentApi.listAppointments(patientId, 1),
|
||||
followupApi.listTasks(patientId, 'pending'),
|
||||
listPendingSuggestions(),
|
||||
]);
|
||||
|
||||
if (suggestRes.status === 'fulfilled') {
|
||||
for (const s of suggestRes.value.data.slice(0, 1)) {
|
||||
items.push({ id: s.id, text: buildSuggestionText(s), type: 'ai', tag: 'AI 建议' });
|
||||
}
|
||||
}
|
||||
if (apptRes.status === 'fulfilled') {
|
||||
for (const a of apptRes.value.data.slice(0, 1)) {
|
||||
if (a.status === 'pending' || a.status === 'confirmed') {
|
||||
items.push({
|
||||
id: a.id,
|
||||
text: `${a.appointment_date} ${a.start_time} — ${a.doctor_name || '医护'} ${a.department || '门诊'}`,
|
||||
type: 'appointment',
|
||||
tag: '预约',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (taskRes.status === 'fulfilled') {
|
||||
for (const t of taskRes.value.data.slice(0, 1)) {
|
||||
items.push({
|
||||
id: t.id,
|
||||
text: `${t.follow_up_type} · 截止 ${t.planned_date}`,
|
||||
type: 'followup',
|
||||
tag: '随访',
|
||||
});
|
||||
}
|
||||
}
|
||||
setReminders(items.slice(0, 3));
|
||||
} catch {
|
||||
setReminders([]);
|
||||
} finally {
|
||||
remindersLoadingRef.current = false;
|
||||
setRemindersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hour = new Date().getHours();
|
||||
const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好';
|
||||
const displayName = user?.display_name || currentPatient?.name || user?.username || (user?.phone ? `${user.phone.slice(-4)}` : '') || '用户';
|
||||
|
||||
const summary = todaySummary || {};
|
||||
const indicators = [!!summary.blood_pressure, !!summary.heart_rate, !!summary.blood_sugar, !!summary.weight];
|
||||
const completedCount = indicators.filter(Boolean).length;
|
||||
const progressPercent = Math.round((completedCount / 4) * 100);
|
||||
|
||||
const indicatorCapsules = useMemo(() => [
|
||||
{ label: '血压', done: !!summary.blood_pressure },
|
||||
{ label: '心率', done: !!summary.heart_rate },
|
||||
{ label: '血糖', done: !!summary.blood_sugar },
|
||||
{ label: '体重', done: !!summary.weight },
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
|
||||
const healthItems = useMemo(() => [
|
||||
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'systolic_bp_morning' },
|
||||
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' },
|
||||
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar' },
|
||||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
const {
|
||||
healthItems, indicatorCapsules, completedCount, progressPercent,
|
||||
loading, todaySummary, reminders, remindersLoading, unreadCount,
|
||||
greeting, displayName,
|
||||
} = useHomeData();
|
||||
|
||||
const getStatusTag = (status?: string) => {
|
||||
if (status === 'high' || status === 'low') return { label: status === 'high' ? '偏高' : '偏低', cls: 'tag-warn' };
|
||||
@@ -295,7 +178,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
|
||||
return (
|
||||
<View className={`home-page ${modeClass}`}>
|
||||
{/* 问候区 */}
|
||||
<View className='greeting-section'>
|
||||
<View className='greeting-left'>
|
||||
<Text className='greeting-text'>{greeting},{displayName}</Text>
|
||||
@@ -309,7 +191,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 今日体征进度 */}
|
||||
<View className='checkin-card' onClick={() => Taro.switchTab({ url: '/pages/health/index' })}>
|
||||
<View className='checkin-left'>
|
||||
<ProgressRing percent={progressPercent} />
|
||||
@@ -328,7 +209,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 体征 2x2 */}
|
||||
<View className='vitals-section'>
|
||||
<Text className='section-title'>今日体征</Text>
|
||||
{loading && !todaySummary ? (
|
||||
@@ -359,7 +239,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 智能提醒卡片 */}
|
||||
{!remindersLoading && reminders.length > 0 && (
|
||||
<View className='reminder-card'>
|
||||
<View className='reminder-header'>
|
||||
@@ -383,7 +262,6 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 快捷操作 */}
|
||||
<View className='action-section'>
|
||||
<View className='action-btn action-primary' onClick={() => Taro.switchTab({ url: '/pages/health/index' })}>
|
||||
<Text className='action-btn-text'>记录体征</Text>
|
||||
@@ -396,7 +274,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 首页入口:根据登录状态切换 ───
|
||||
// ─── 首页入口 ───
|
||||
|
||||
export default function Index() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
@@ -408,16 +286,3 @@ export default function Index() {
|
||||
}
|
||||
return <HomeDashboard modeClass={modeClass} />;
|
||||
}
|
||||
|
||||
function buildSuggestionText(s: AiSuggestionItem): string {
|
||||
const riskMap: Record<string, string> = { high: '高风险', medium: '中风险', low: '低风险' };
|
||||
const typeMap: Record<string, string> = {
|
||||
vital_sign_anomaly: '体征异常',
|
||||
lab_result_anomaly: '化验异常',
|
||||
medication_adherence: '用药提醒',
|
||||
lifestyle: '生活建议',
|
||||
};
|
||||
const risk = riskMap[s.risk_level] || '';
|
||||
const type = typeMap[s.suggestion_type] || '健康建议';
|
||||
return `${type}:发现${risk}指标,建议关注`;
|
||||
}
|
||||
|
||||
149
apps/miniprogram/src/pages/index/useHomeData.ts
Normal file
149
apps/miniprogram/src/pages/index/useHomeData.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import { useHealthStore } from '@/stores/health';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { usePageData } from '@/hooks/usePageData';
|
||||
import { trackPageView } from '@/services/analytics';
|
||||
import * as appointmentApi from '@/services/appointment';
|
||||
import * as followupApi from '@/services/followup';
|
||||
import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis';
|
||||
import { notificationService } from '@/services/notification';
|
||||
|
||||
export interface ReminderItem {
|
||||
id: string;
|
||||
text: string;
|
||||
type: 'ai' | 'appointment' | 'followup';
|
||||
tag: string;
|
||||
}
|
||||
|
||||
function buildSuggestionText(s: AiSuggestionItem): string {
|
||||
const riskMap: Record<string, string> = { high: '高风险', medium: '中风险', low: '低风险' };
|
||||
const typeMap: Record<string, string> = {
|
||||
vital_sign_anomaly: '体征异常',
|
||||
lab_result_anomaly: '化验异常',
|
||||
medication_adherence: '用药提醒',
|
||||
lifestyle: '生活建议',
|
||||
};
|
||||
const risk = riskMap[s.risk_level] || '';
|
||||
const type = typeMap[s.suggestion_type] || '健康建议';
|
||||
return `${type}:发现${risk}指标,建议关注`;
|
||||
}
|
||||
|
||||
export function useHomeData() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const todaySummary = useHealthStore((s) => s.todaySummary);
|
||||
const loading = useHealthStore((s) => s.loading);
|
||||
const refreshToday = useHealthStore((s) => s.refreshToday);
|
||||
const [reminders, setReminders] = useState<ReminderItem[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [remindersLoading, setRemindersLoading] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
const patientId = useAuthStore.getState().currentPatient?.id;
|
||||
if (!patientId) return;
|
||||
refreshToday();
|
||||
loadReminders(patientId);
|
||||
loadUnread();
|
||||
trackPageView('home');
|
||||
};
|
||||
|
||||
const { trigger, refresh } = usePageData(fetchData, {
|
||||
throttleMs: 5000,
|
||||
enablePullDown: true,
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const loadUnread = async () => {
|
||||
try {
|
||||
const res = await notificationService.getUnreadCount() as { data?: { count?: number } | number };
|
||||
const d = res.data;
|
||||
setUnreadCount(typeof d === 'object' && d ? (d.count ?? 0) : 0);
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const loadReminders = async (patientId: string) => {
|
||||
setRemindersLoading(true);
|
||||
try {
|
||||
const items: ReminderItem[] = [];
|
||||
const [apptRes, taskRes, suggestRes] = await Promise.allSettled([
|
||||
appointmentApi.listAppointments(patientId, 1),
|
||||
followupApi.listTasks(patientId, 'pending'),
|
||||
listPendingSuggestions(),
|
||||
]);
|
||||
|
||||
if (suggestRes.status === 'fulfilled') {
|
||||
for (const s of suggestRes.value.data.slice(0, 1)) {
|
||||
items.push({ id: s.id, text: buildSuggestionText(s), type: 'ai', tag: 'AI 建议' });
|
||||
}
|
||||
}
|
||||
if (apptRes.status === 'fulfilled') {
|
||||
for (const a of apptRes.value.data.slice(0, 1)) {
|
||||
if (a.status === 'pending' || a.status === 'confirmed') {
|
||||
items.push({
|
||||
id: a.id,
|
||||
text: `${a.appointment_date} ${a.start_time} — ${a.doctor_name || '医护'} ${a.department || '门诊'}`,
|
||||
type: 'appointment',
|
||||
tag: '预约',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (taskRes.status === 'fulfilled') {
|
||||
for (const t of taskRes.value.data.slice(0, 1)) {
|
||||
items.push({
|
||||
id: t.id,
|
||||
text: `${t.follow_up_type} · 截止 ${t.planned_date}`,
|
||||
type: 'followup',
|
||||
tag: '随访',
|
||||
});
|
||||
}
|
||||
}
|
||||
setReminders(items.slice(0, 3));
|
||||
} catch {
|
||||
setReminders([]);
|
||||
} finally {
|
||||
setRemindersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const summary = todaySummary || {};
|
||||
const indicators = [!!summary.blood_pressure, !!summary.heart_rate, !!summary.blood_sugar, !!summary.weight];
|
||||
const completedCount = indicators.filter(Boolean).length;
|
||||
const progressPercent = Math.round((completedCount / 4) * 100);
|
||||
|
||||
const indicatorCapsules = useMemo(() => [
|
||||
{ label: '血压', done: !!summary.blood_pressure },
|
||||
{ label: '心率', done: !!summary.heart_rate },
|
||||
{ label: '血糖', done: !!summary.blood_sugar },
|
||||
{ label: '体重', done: !!summary.weight },
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
|
||||
const healthItems = useMemo(() => [
|
||||
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'systolic_bp_morning' },
|
||||
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' },
|
||||
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar' },
|
||||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
|
||||
const hour = new Date().getHours();
|
||||
const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好';
|
||||
const displayName = user?.display_name || currentPatient?.name || user?.username || (user?.phone ? `${user.phone.slice(-4)}` : '') || '用户';
|
||||
|
||||
return {
|
||||
user,
|
||||
currentPatient,
|
||||
todaySummary: summary,
|
||||
loading,
|
||||
reminders,
|
||||
unreadCount,
|
||||
remindersLoading,
|
||||
indicatorCapsules,
|
||||
healthItems,
|
||||
completedCount,
|
||||
progressPercent,
|
||||
greeting,
|
||||
displayName,
|
||||
trigger,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: '' };
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<DialysisPrescription | null>(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 <View className={`detail-page ${modeClass}`}><Loading /></View>;
|
||||
if (!rx) return <View className={`detail-page ${modeClass}`}><View className='empty-state'><Text className='empty-text'>处方不存在</Text></View></View>;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<DialysisRecord | null>(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 <View className={`detail-page ${modeClass}`}><Loading /></View>;
|
||||
if (!record) return <View className={`detail-page ${modeClass}`}><View className='empty-state'><Text className='empty-text'>记录不存在</Text></View></View>;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<LabReport | null>(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]) => ({
|
||||
|
||||
@@ -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<AuthState>((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' });
|
||||
},
|
||||
|
||||
7
apps/miniprogram/src/stores/index.ts
Normal file
7
apps/miniprogram/src/stores/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useHealthStore } from './health';
|
||||
import { usePointsStore } from './points';
|
||||
|
||||
export function resetAllStores(): void {
|
||||
useHealthStore.getState().clearCache();
|
||||
usePointsStore.getState().invalidate();
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user