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:
iven
2026-05-21 22:40:42 +08:00
parent d70b027f20
commit a7b5548b35
8 changed files with 122 additions and 49 deletions

View File

@@ -80,6 +80,8 @@ export const appointmentApi = {
patient_id?: string; patient_id?: string;
doctor_id?: string; doctor_id?: string;
date?: string; date?: string;
search?: string;
appointment_type?: string;
}) => { }) => {
const { data } = await client.get<{ const { data } = await client.get<{
success: boolean; success: boolean;

View File

@@ -46,8 +46,16 @@ export function DrawerForm({
}, [open, initialValues, form]); }, [open, initialValues, form]);
const handleSubmit = async () => { const handleSubmit = async () => {
const values = await form.validateFields(); try {
await onSubmit(values); 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 = const gridStyle: React.CSSProperties =

View File

@@ -81,7 +81,8 @@ export function usePaginatedData<T, F = string>(
filtersRef.current ?? searchTextRef.current, filtersRef.current ?? searchTextRef.current,
); );
setState({ data: result.data, total: result.total, page: targetPage, loading: false }); setState({ data: result.data, total: result.total, page: targetPage, loading: false });
} catch { } catch (err) {
console.warn('[usePaginatedData] 加载数据失败:', err);
message.error('加载数据失败'); message.error('加载数据失败');
setState((s) => ({ ...s, loading: false })); setState((s) => ({ ...s, loading: false }));
} }
@@ -89,26 +90,22 @@ export function usePaginatedData<T, F = string>(
[pageSize], [pageSize],
); );
useEffect(() => { // 合并初始 fetch 和 filters 变化时的 fetch消除双重请求
if (shouldAutoFetch) {
refresh(1);
}
}, [shouldAutoFetch, refresh]);
// 筛选条件变化时自动刷新(解决 FollowUpTaskList 等组件直接调用 setFilters 不触发刷新的问题)
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
useEffect(() => { useEffect(() => {
if (isFirstRender.current) { if (isFirstRender.current) {
isFirstRender.current = false; isFirstRender.current = false;
if (shouldAutoFetch) {
refresh(1);
}
return; return;
} }
if (shouldAutoFetch) { if (shouldAutoFetch) {
refresh(1); refresh(1);
} }
// refresh 每次渲染都稳定不放入依赖数组filters 变化触发重新 fetch
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]); }, [shouldAutoFetch, filters]);
return { ...state, searchText, setSearchText, filters, setFilters, refresh }; return { ...state, searchText, setSearchText, filters, setFilters, refresh };
} }

View File

@@ -221,7 +221,7 @@ export default function Home() {
if (role === 'doctor' || role === 'nurse') { if (role === 'doctor' || role === 'nurse') {
pointsApi.getPersonalStats() pointsApi.getPersonalStats()
.then((data) => { if (!cancelled) setPersonalStats(data); }) .then((data) => { if (!cancelled) setPersonalStats(data); })
.catch(() => {}) .catch((err) => console.warn('[Home] 获取个人积分统计失败:', err))
.finally(() => { if (!cancelled) setPersonalLoading(false); }); .finally(() => { if (!cancelled) setPersonalLoading(false); });
} else { } else {
setPersonalLoading(false); setPersonalLoading(false);
@@ -229,13 +229,13 @@ export default function Home() {
listPendingTasks(1, 5) listPendingTasks(1, 5)
.then((result) => { if (!cancelled) setPendingTasks(result.data); }) .then((result) => { if (!cancelled) setPendingTasks(result.data); })
.catch(() => {}); .catch((err) => console.warn('[Home] 获取待办任务失败:', err));
listAuditLogs({ page: 1, page_size: 5 }) listAuditLogs({ page: 1, page_size: 5 })
.then((result) => { .then((result) => {
if (!cancelled) setRecentActivities(result.data.filter((a) => a.action !== 'login_failed')); if (!cancelled) setRecentActivities(result.data.filter((a) => a.action !== 'login_failed'));
}) })
.catch(() => {}) .catch((err) => console.warn('[Home] 获取审计日志失败:', err))
.finally(() => { if (!cancelled) setActivitiesLoading(false); }); .finally(() => { if (!cancelled) setActivitiesLoading(false); });
return () => { cancelled = true; }; return () => { cancelled = true; };

View File

@@ -43,7 +43,7 @@ export default function Roles() {
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]); const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
listPermissions().then(setPermissions).catch(() => {}); listPermissions().then(setPermissions).catch((err) => console.warn('[Roles] 获取权限列表失败:', err));
}, []); }, []);
const roleDrawer = useCrudDrawer<RoleInfo>({ const roleDrawer = useCrudDrawer<RoleInfo>({

View File

@@ -10,6 +10,12 @@ import {
} from '../../../api/health/points'; } from '../../../api/health/points';
import { doctorApi } from '../../../api/health/doctors'; 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 { export interface StatsData {
patientStats: PatientStatistics | null; patientStats: PatientStatistics | null;
consultationStats: ConsultationStatistics | null; consultationStats: ConsultationStatistics | null;
@@ -36,41 +42,101 @@ export function useStatsData(): StatsData {
const [doctorCount, setDoctorCount] = useState(0); const [doctorCount, setDoctorCount] = useState(0);
const fetchAllStats = useCallback(async () => { 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); setLoading(true);
setError(null); 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) => { const results: Record<string, unknown> = {
try { patientStats: null,
const data = await fn(); consultationStats: null,
setter(data); followUpStats: null,
} catch { pointsStats: null,
hasAnyError = true; healthDataStats: null,
errors.push(label); 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([ if (hasAnyError && errors.length === 7) {
tryFetch(() => pointsApi.getPatientStats({ silent: true }), setPatientStats, '患者'), setError('加载统计数据失败');
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) { return results;
setError('加载统计数据失败'); })();
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(() => { useEffect(() => {

View File

@@ -25,7 +25,7 @@ export function DoctorSelect({ value, onChange, placeholder }: Props) {
})), })),
); );
} }
}).catch(() => {}); }).catch((err) => console.warn('[DoctorSelect] 获取医生列表失败:', err));
return () => { cancelled = true; }; return () => { cancelled = true; };
}, []); }, []);

View File

@@ -32,19 +32,19 @@ export default function OperatorWorkbench() {
useEffect(() => { useEffect(() => {
actionInboxApi.stats() actionInboxApi.stats()
.then((s) => setStats(s ?? null)) .then((s) => setStats(s ?? null))
.catch(() => {}); .catch((err) => console.warn('[OperatorWorkbench] 获取行动收件箱统计失败:', err));
actionInboxApi.list({ status: 'pending', page: 1, page_size: 5 }) actionInboxApi.list({ status: 'pending', page: 1, page_size: 5 })
.then((r) => setActionItems(r.data)) .then((r) => setActionItems(r.data))
.catch(() => {}); .catch((err) => console.warn('[OperatorWorkbench] 获取行动列表失败:', err));
dashboardApi.getPointsRecentActivity() dashboardApi.getPointsRecentActivity()
.then((d) => setPointsActivity(d ?? [])) .then((d) => setPointsActivity(d ?? []))
.catch(() => {}); .catch((err) => console.warn('[OperatorWorkbench] 获取积分活动失败:', err));
dashboardApi.getArticleStats() dashboardApi.getArticleStats()
.then((d) => setArticleStats(d ?? null)) .then((d) => setArticleStats(d ?? null))
.catch(() => {}); .catch((err) => console.warn('[OperatorWorkbench] 获取文章统计失败:', err));
}, []); }, []);
const firstName = user?.display_name ?? user?.username ?? '运营'; const firstName = user?.display_name ?? user?.username ?? '运营';