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