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;
doctor_id?: string;
date?: string;
search?: string;
appointment_type?: string;
}) => {
const { data } = await client.get<{
success: boolean;

View File

@@ -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 =

View File

@@ -81,7 +81,8 @@ export function usePaginatedData<T, F = string>(
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<T, F = string>(
[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 };
}

View File

@@ -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; };

View File

@@ -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>({

View File

@@ -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(() => {

View File

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

View File

@@ -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 ?? '运营';