fix(admin): 补全 React hooks 依赖并修复页面稳定性

- 各页面 useCallback/useMemo 依赖补全(modal/mutation/setter 等),避免闭包过期
- ECharts 使用 optionRef、MainLayout 缓存菜单转换函数、main.tsx 增加挂载点校验
- 学生同步结果改用 recordsCount 展示
This commit is contained in:
2026-08-05 21:14:08 +08:00
parent ab4765cee5
commit 1a1e90c72f
21 changed files with 439 additions and 341 deletions

View File

@@ -93,10 +93,10 @@ const StudentsPage: React.FC = () => {
const [saving, setSaving] = useState(false);
const [jinshujuOpen, setJinshujuOpen] = useState(false);
const openDrawer = (studentId: number) => {
const openDrawer = useCallback((studentId: number) => {
setDrawerStudentId(studentId);
setDrawerOpen(true);
};
}, []);
const logCreateRef = React.useRef(hasPermission('log:create'));
const sensitiveModalRef = React.useRef<ReturnType<typeof modal.confirm> | null>(null);
@@ -112,38 +112,41 @@ const StudentsPage: React.FC = () => {
};
}, []);
const handleViewSensitive = (studentId: number, field: string, value: string) => {
if (!logCreateRef.current) return;
sensitiveModalRef.current = modal.confirm({
title: '查看敏感信息',
content: `您即将查看 "${field}" 的完整信息。此操作将被记录。`,
okText: '确认查看',
cancelText: '取消',
onOk: async () => {
if (!logCreateRef.current) return;
try {
await api.post('/operation-logs/audit', {
module: '学生管理',
action: '查看敏感信息',
targetId: studentId,
targetType: 'student',
detail: `查看${field}`,
});
modal.info({
title: field,
content: value,
okText: '关闭',
});
} catch (e) {
console.error('审计日志记录失败', e);
message.error('审计日志记录失败,请稍后重试');
}
},
afterClose: () => {
sensitiveModalRef.current = null;
},
});
};
const handleViewSensitive = useCallback(
(studentId: number, field: string, value: string) => {
if (!logCreateRef.current) return;
sensitiveModalRef.current = modal.confirm({
title: '查看敏感信息',
content: `您即将查看 "${field}" 的完整信息。此操作将被记录。`,
okText: '确认查看',
cancelText: '取消',
onOk: async () => {
if (!logCreateRef.current) return;
try {
await api.post('/operation-logs/audit', {
module: '学生管理',
action: '查看敏感信息',
targetId: studentId,
targetType: 'student',
detail: `查看${field}`,
});
modal.info({
title: field,
content: value,
okText: '关闭',
});
} catch (e) {
console.error('审计日志记录失败', e);
message.error('审计日志记录失败,请稍后重试');
}
},
afterClose: () => {
sensitiveModalRef.current = null;
},
});
},
[modal],
);
const {
data = [],
@@ -329,41 +332,50 @@ const StudentsPage: React.FC = () => {
}
};
const handleArchive = async (id: number) => {
try {
await archiveMutation.mutateAsync(id);
message.success('已归档');
} catch {
// 错误提示由 useApiMutation 统一处理
}
};
const handleArchive = useCallback(
async (id: number) => {
try {
await archiveMutation.mutateAsync(id);
message.success('已归档');
} catch {
// 错误提示由 useApiMutation 统一处理
}
},
[archiveMutation],
);
const handleRestore = async (id: number) => {
try {
await restoreMutation.mutateAsync(id);
message.success('已恢复');
} catch {
// 错误提示由 useApiMutation 统一处理
}
};
const handleRestore = useCallback(
async (id: number) => {
try {
await restoreMutation.mutateAsync(id);
message.success('已恢复');
} catch {
// 错误提示由 useApiMutation 统一处理
}
},
[restoreMutation],
);
const handlePurge = (id: number, name: string) => {
modal.confirm({
title: `永久删除学生「${name}」?`,
content: '删除后不可恢复,该学生及其关联数据将无法找回。确定继续?',
okText: '永久删除',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: async () => {
try {
await purgeMutation.mutateAsync(id);
message.success('已永久删除(不可恢复)');
} catch {
// 错误提示由 useApiMutation 统一处理
}
},
});
};
const handlePurge = useCallback(
(id: number, name: string) => {
modal.confirm({
title: `永久删除学生「${name}」?`,
content: '删除后不可恢复,该学生及其关联数据将无法找回。确定继续?',
okText: '永久删除',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: async () => {
try {
await purgeMutation.mutateAsync(id);
message.success('已永久删除(不可恢复)');
} catch {
// 错误提示由 useApiMutation 统一处理
}
},
});
},
[modal, purgeMutation],
);
const handleBatchDelete = async () => {
if (batchLoading) return;
@@ -450,7 +462,9 @@ const StudentsPage: React.FC = () => {
if (log?.status === 'partial') {
message.warning(log.errorMessage || '钉钉同步完成,但有数据需要人工处理');
} else {
message.success(log?.errorMessage || `钉钉同步完成,共处理 ${res.synced}`);
message.success(
log?.errorMessage || `钉钉同步完成,共处理 ${log?.recordsCount ?? res.synced}`,
);
}
void queryClient.invalidateQueries({ queryKey: ['students'] });
} catch (e: unknown) {
@@ -506,6 +520,10 @@ const StudentsPage: React.FC = () => {
saveCell,
handleViewSensitive,
form,
openDrawer,
handleArchive,
handleRestore,
handlePurge,
],
);