From a7b5548b358eca254f3a2281914226d4ca79f8ca Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 21 May 2026 22:40:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=89=8D=E7=AB=AF=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E4=BF=AE=E5=A4=8D=20=E2=80=94=20Dra?= =?UTF-8?q?werForm/usePaginatedData/useStatsData/=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E5=90=9E=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DrawerForm: validateFields 添加 try-catch 防止 unhandled rejection - usePaginatedData: 合并双重 useEffect 消除重复请求 - useStatsData: 模块级缓存+Promise 去重,避免 6 组件实例×7 API=42 请求 - appointments API: 补传 patientSearch/appointmentType 参数 - Home/Roles/DoctorSelect/OperatorWorkbench: .catch(() => {}) → console.warn --- apps/web/src/api/health/appointments.ts | 2 + apps/web/src/components/DrawerForm.tsx | 12 +- apps/web/src/hooks/usePaginatedData.ts | 19 ++- apps/web/src/pages/Home.tsx | 6 +- apps/web/src/pages/Roles.tsx | 2 +- .../StatisticsDashboard/useStatsData.ts | 120 ++++++++++++++---- .../pages/health/components/DoctorSelect.tsx | 2 +- .../workbench/OperatorWorkbench.tsx | 8 +- 8 files changed, 122 insertions(+), 49 deletions(-) diff --git a/apps/web/src/api/health/appointments.ts b/apps/web/src/api/health/appointments.ts index 7df9d5a..5488412 100644 --- a/apps/web/src/api/health/appointments.ts +++ b/apps/web/src/api/health/appointments.ts @@ -80,6 +80,8 @@ export const appointmentApi = { patient_id?: string; doctor_id?: string; date?: string; + search?: string; + appointment_type?: string; }) => { const { data } = await client.get<{ success: boolean; diff --git a/apps/web/src/components/DrawerForm.tsx b/apps/web/src/components/DrawerForm.tsx index cb51e01..d07db5f 100644 --- a/apps/web/src/components/DrawerForm.tsx +++ b/apps/web/src/components/DrawerForm.tsx @@ -46,8 +46,16 @@ export function DrawerForm({ }, [open, initialValues, form]); const handleSubmit = async () => { - const values = await form.validateFields(); - await onSubmit(values); + try { + const values = await form.validateFields(); + await onSubmit(values); + } catch (error: unknown) { + // validateFields 失败时 error 包含 errorFields(预期行为,不记录) + // 其他类型的错误才记录 + if (error && typeof error === 'object' && !('errorFields' in error)) { + console.error('[DrawerForm] submit error:', error); + } + } }; const gridStyle: React.CSSProperties = diff --git a/apps/web/src/hooks/usePaginatedData.ts b/apps/web/src/hooks/usePaginatedData.ts index 9c47ca7..276220c 100644 --- a/apps/web/src/hooks/usePaginatedData.ts +++ b/apps/web/src/hooks/usePaginatedData.ts @@ -81,7 +81,8 @@ export function usePaginatedData( filtersRef.current ?? searchTextRef.current, ); setState({ data: result.data, total: result.total, page: targetPage, loading: false }); - } catch { + } catch (err) { + console.warn('[usePaginatedData] 加载数据失败:', err); message.error('加载数据失败'); setState((s) => ({ ...s, loading: false })); } @@ -89,26 +90,22 @@ export function usePaginatedData( [pageSize], ); - useEffect(() => { - if (shouldAutoFetch) { - - refresh(1); - } - }, [shouldAutoFetch, refresh]); - - // 筛选条件变化时自动刷新(解决 FollowUpTaskList 等组件直接调用 setFilters 不触发刷新的问题) + // 合并初始 fetch 和 filters 变化时的 fetch,消除双重请求 const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; + if (shouldAutoFetch) { + refresh(1); + } return; } if (shouldAutoFetch) { - refresh(1); } + // refresh 每次渲染都稳定,不放入依赖数组;filters 变化触发重新 fetch // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filters]); + }, [shouldAutoFetch, filters]); return { ...state, searchText, setSearchText, filters, setFilters, refresh }; } diff --git a/apps/web/src/pages/Home.tsx b/apps/web/src/pages/Home.tsx index db243d0..5d9602b 100644 --- a/apps/web/src/pages/Home.tsx +++ b/apps/web/src/pages/Home.tsx @@ -221,7 +221,7 @@ export default function Home() { if (role === 'doctor' || role === 'nurse') { pointsApi.getPersonalStats() .then((data) => { if (!cancelled) setPersonalStats(data); }) - .catch(() => {}) + .catch((err) => console.warn('[Home] 获取个人积分统计失败:', err)) .finally(() => { if (!cancelled) setPersonalLoading(false); }); } else { setPersonalLoading(false); @@ -229,13 +229,13 @@ export default function Home() { listPendingTasks(1, 5) .then((result) => { if (!cancelled) setPendingTasks(result.data); }) - .catch(() => {}); + .catch((err) => console.warn('[Home] 获取待办任务失败:', err)); listAuditLogs({ page: 1, page_size: 5 }) .then((result) => { if (!cancelled) setRecentActivities(result.data.filter((a) => a.action !== 'login_failed')); }) - .catch(() => {}) + .catch((err) => console.warn('[Home] 获取审计日志失败:', err)) .finally(() => { if (!cancelled) setActivitiesLoading(false); }); return () => { cancelled = true; }; diff --git a/apps/web/src/pages/Roles.tsx b/apps/web/src/pages/Roles.tsx index e1fa79b..0a7cf0e 100644 --- a/apps/web/src/pages/Roles.tsx +++ b/apps/web/src/pages/Roles.tsx @@ -43,7 +43,7 @@ export default function Roles() { const [selectedPermIds, setSelectedPermIds] = useState([]); useEffect(() => { - listPermissions().then(setPermissions).catch(() => {}); + listPermissions().then(setPermissions).catch((err) => console.warn('[Roles] 获取权限列表失败:', err)); }, []); const roleDrawer = useCrudDrawer({ diff --git a/apps/web/src/pages/health/StatisticsDashboard/useStatsData.ts b/apps/web/src/pages/health/StatisticsDashboard/useStatsData.ts index f566649..7c8163d 100644 --- a/apps/web/src/pages/health/StatisticsDashboard/useStatsData.ts +++ b/apps/web/src/pages/health/StatisticsDashboard/useStatsData.ts @@ -10,6 +10,12 @@ import { } from '../../../api/health/points'; import { doctorApi } from '../../../api/health/doctors'; +// 全局缓存:多组件实例共享数据,避免重复请求 +let cachedStats: Record | null = null; +let cachedAt = 0; +const CACHE_TTL = 5 * 60 * 1000; // 5 分钟 +let fetchPromise: Promise> | null = null; + export interface StatsData { patientStats: PatientStatistics | null; consultationStats: ConsultationStatistics | null; @@ -36,41 +42,101 @@ export function useStatsData(): StatsData { const [doctorCount, setDoctorCount] = useState(0); const fetchAllStats = useCallback(async () => { + // 缓存未过期,直接使用 + if (cachedStats && Date.now() - cachedAt < CACHE_TTL) { + const c = cachedStats; + setPatientStats(c.patientStats as PatientStatistics | null); + setConsultationStats(c.consultationStats as ConsultationStatistics | null); + setFollowUpStats(c.followUpStats as FollowUpStatistics | null); + setPointsStats(c.pointsStats as PointsStatistics | null); + setHealthDataStats(c.healthDataStats as HealthDataStats | null); + setDialysisStats(c.dialysisStats as DialysisStatistics | null); + setDoctorCount(c.doctorCount as number); + setLoading(false); + return; + } + + // 已有正在进行的请求,等待它完成 + if (fetchPromise) { + const c = await fetchPromise; + setPatientStats(c.patientStats as PatientStatistics | null); + setConsultationStats(c.consultationStats as ConsultationStatistics | null); + setFollowUpStats(c.followUpStats as FollowUpStatistics | null); + setPointsStats(c.pointsStats as PointsStatistics | null); + setHealthDataStats(c.healthDataStats as HealthDataStats | null); + setDialysisStats(c.dialysisStats as DialysisStatistics | null); + setDoctorCount(c.doctorCount as number); + setLoading(false); + return; + } + setLoading(true); setError(null); - let hasAnyError = false; - const errors: string[] = []; + // 创建新请求 + fetchPromise = (async () => { + let hasAnyError = false; + const errors: string[] = []; - const tryFetch = async (fn: () => Promise, setter: (v: T) => void, label: string) => { - try { - const data = await fn(); - setter(data); - } catch { - hasAnyError = true; - errors.push(label); + const results: Record = { + patientStats: null, + consultationStats: null, + followUpStats: null, + pointsStats: null, + healthDataStats: null, + dialysisStats: null, + doctorCount: 0, + }; + + const tryFetch = async (fn: () => Promise, key: string, label: string) => { + try { + const data = await fn(); + results[key] = data; + } catch { + hasAnyError = true; + errors.push(label); + } + }; + + await Promise.all([ + tryFetch(() => pointsApi.getPatientStats({ silent: true }), 'patientStats', '患者'), + tryFetch(() => pointsApi.getConsultationStats({ silent: true }), 'consultationStats', '咨询'), + tryFetch(() => pointsApi.getFollowUpStats({ silent: true }), 'followUpStats', '随访'), + tryFetch(() => pointsApi.getStatistics({ silent: true }), 'pointsStats', '积分'), + tryFetch(() => pointsApi.getHealthDataStats({ silent: true }), 'healthDataStats', '健康数据'), + tryFetch(() => pointsApi.getDialysisStats({ silent: true }), 'dialysisStats', '透析'), + tryFetch( + async () => { const r = await doctorApi.list({ page: 1, page_size: 1 }); return r.total; }, + 'doctorCount', + '医护', + ), + ]); + + if (!hasAnyError || errors.length < 7) { + cachedStats = results; + cachedAt = Date.now(); } - }; - await Promise.all([ - tryFetch(() => pointsApi.getPatientStats({ silent: true }), setPatientStats, '患者'), - tryFetch(() => pointsApi.getConsultationStats({ silent: true }), setConsultationStats, '咨询'), - tryFetch(() => pointsApi.getFollowUpStats({ silent: true }), setFollowUpStats, '随访'), - tryFetch(() => pointsApi.getStatistics({ silent: true }), setPointsStats, '积分'), - tryFetch(() => pointsApi.getHealthDataStats({ silent: true }), setHealthDataStats, '健康数据'), - tryFetch(() => pointsApi.getDialysisStats({ silent: true }), setDialysisStats, '透析'), - tryFetch( - async () => { const r = await doctorApi.list({ page: 1, page_size: 1 }); return r.total; }, - setDoctorCount, - '医护', - ), - ]); + if (hasAnyError && errors.length === 7) { + setError('加载统计数据失败'); + } - if (hasAnyError && errors.length === 7) { - setError('加载统计数据失败'); + return results; + })(); + + try { + const c = await fetchPromise; + setPatientStats(c.patientStats as PatientStatistics | null); + setConsultationStats(c.consultationStats as ConsultationStatistics | null); + setFollowUpStats(c.followUpStats as FollowUpStatistics | null); + setPointsStats(c.pointsStats as PointsStatistics | null); + setHealthDataStats(c.healthDataStats as HealthDataStats | null); + setDialysisStats(c.dialysisStats as DialysisStatistics | null); + setDoctorCount(c.doctorCount as number); + } finally { + fetchPromise = null; + setLoading(false); } - - setLoading(false); }, []); useEffect(() => { diff --git a/apps/web/src/pages/health/components/DoctorSelect.tsx b/apps/web/src/pages/health/components/DoctorSelect.tsx index 18bc8b5..6a4e8b3 100644 --- a/apps/web/src/pages/health/components/DoctorSelect.tsx +++ b/apps/web/src/pages/health/components/DoctorSelect.tsx @@ -25,7 +25,7 @@ export function DoctorSelect({ value, onChange, placeholder }: Props) { })), ); } - }).catch(() => {}); + }).catch((err) => console.warn('[DoctorSelect] 获取医生列表失败:', err)); return () => { cancelled = true; }; }, []); diff --git a/apps/web/src/pages/health/components/workbench/OperatorWorkbench.tsx b/apps/web/src/pages/health/components/workbench/OperatorWorkbench.tsx index 21b9177..34cc317 100644 --- a/apps/web/src/pages/health/components/workbench/OperatorWorkbench.tsx +++ b/apps/web/src/pages/health/components/workbench/OperatorWorkbench.tsx @@ -32,19 +32,19 @@ export default function OperatorWorkbench() { useEffect(() => { actionInboxApi.stats() .then((s) => setStats(s ?? null)) - .catch(() => {}); + .catch((err) => console.warn('[OperatorWorkbench] 获取行动收件箱统计失败:', err)); actionInboxApi.list({ status: 'pending', page: 1, page_size: 5 }) .then((r) => setActionItems(r.data)) - .catch(() => {}); + .catch((err) => console.warn('[OperatorWorkbench] 获取行动列表失败:', err)); dashboardApi.getPointsRecentActivity() .then((d) => setPointsActivity(d ?? [])) - .catch(() => {}); + .catch((err) => console.warn('[OperatorWorkbench] 获取积分活动失败:', err)); dashboardApi.getArticleStats() .then((d) => setArticleStats(d ?? null)) - .catch(() => {}); + .catch((err) => console.warn('[OperatorWorkbench] 获取文章统计失败:', err)); }, []); const firstName = user?.display_name ?? user?.username ?? '运营';