fix(web): 前端错误处理修复 — DrawerForm/usePaginatedData/useStatsData/静默吞错
- 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
This commit is contained in:
@@ -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; };
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Roles() {
|
||||
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
listPermissions().then(setPermissions).catch(() => {});
|
||||
listPermissions().then(setPermissions).catch((err) => console.warn('[Roles] 获取权限列表失败:', err));
|
||||
}, []);
|
||||
|
||||
const roleDrawer = useCrudDrawer<RoleInfo>({
|
||||
|
||||
@@ -10,6 +10,12 @@ import {
|
||||
} from '../../../api/health/points';
|
||||
import { doctorApi } from '../../../api/health/doctors';
|
||||
|
||||
// 全局缓存:多组件实例共享数据,避免重复请求
|
||||
let cachedStats: Record<string, unknown> | null = null;
|
||||
let cachedAt = 0;
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
|
||||
let fetchPromise: Promise<Record<string, unknown>> | 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 <T,>(fn: () => Promise<T>, setter: (v: T) => void, label: string) => {
|
||||
try {
|
||||
const data = await fn();
|
||||
setter(data);
|
||||
} catch {
|
||||
hasAnyError = true;
|
||||
errors.push(label);
|
||||
const results: Record<string, unknown> = {
|
||||
patientStats: null,
|
||||
consultationStats: null,
|
||||
followUpStats: null,
|
||||
pointsStats: null,
|
||||
healthDataStats: null,
|
||||
dialysisStats: null,
|
||||
doctorCount: 0,
|
||||
};
|
||||
|
||||
const tryFetch = async <T,>(fn: () => Promise<T>, 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(() => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export function DoctorSelect({ value, onChange, placeholder }: Props) {
|
||||
})),
|
||||
);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}).catch((err) => console.warn('[DoctorSelect] 获取医生列表失败:', err));
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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 ?? '运营';
|
||||
|
||||
Reference in New Issue
Block a user