feat(admin): 弹窗未保存内容保护与空状态引导扩展

- 新增 useDirtyGuard hook:关闭弹窗时若表单被修改,先确认再关闭
- 接入 17 个弹窗:账号/密码/档案、角色、教室、宿舍/床位/柜子、
  考勤机、机构、班级、教师、考试、排课、入住/退宿/换房、费用
  (子组件弹窗用 useEffect 在回填后快照,时序正确)
- 学生列表空状态带「添加学生」引导动作
- 排课创建成功提示「同步钉钉」,考试创建成功提示「去详情录成绩」

aislop scan: 5 引擎 0 issues
This commit is contained in:
2026-08-07 17:56:13 +08:00
parent bcbf3c8b2e
commit c10476d203
16 changed files with 273 additions and 49 deletions

View File

@@ -40,7 +40,9 @@ import { validateResponse } from '../../utils/validate';
import { classOptionsSchema, examsSchema } from '../../api/schemas';
import { getErrorMessage } from '../../utils/error';
import { QueryErrorState } from '../../components/QueryState';
import { NextStepHint } from '../../components/NextStepHint';
import { useVisibleRefetch } from '../../hooks/usePageVisible';
import { useDirtyGuard } from '../../hooks/useDirtyGuard';
const ExamsPage: React.FC = () => {
const { modal } = App.useApp();
@@ -48,6 +50,7 @@ const ExamsPage: React.FC = () => {
const { hasPermission } = usePermission();
const canPurgeExam = hasPermission('exam:purge');
const [form] = Form.useForm<ExamFormValues>();
const examFormGuard = useDirtyGuard(form);
const [batchLoading, setBatchLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
@@ -56,6 +59,8 @@ const ExamsPage: React.FC = () => {
const [classId, setClassId] = useState<number>();
const [showArchived, setShowArchived] = useState(false);
const [selectedExamIds, setSelectedExamIds] = useState<number[]>([]);
// 考试创建成功后的「下一步:去详情录成绩」引导
const [examCreatedId, setExamCreatedId] = useState<number | null>(null);
const [debouncedFilters] = useDebounceValue(
{ keyword, examType, classId, showArchived },
200,
@@ -152,6 +157,7 @@ const ExamsPage: React.FC = () => {
const openCreate = () => {
form.resetFields();
form.setFieldValue('examDate', dayjs());
examFormGuard.snapshot();
setModalOpen(true);
};
@@ -160,9 +166,10 @@ const ExamsPage: React.FC = () => {
const values = await form.validateFields();
setSaving(true);
const payload = { ...values, examDate: values.examDate.format('YYYY-MM-DD') };
await saveMutation.mutateAsync(payload);
const created = (await saveMutation.mutateAsync(payload)) as { id?: number };
message.success('考试已创建');
setModalOpen(false);
if (created?.id != null) setExamCreatedId(created.id);
} catch {
// 校验错误静默,接口错误由 useApiMutation 统一提示
} finally {
@@ -255,6 +262,20 @@ const ExamsPage: React.FC = () => {
return (
<div className="exam-page">
{examCreatedId !== null && (
<NextStepHint
title="考试已创建"
description="接下来可以在考试详情中添加学生名单、录入成绩。"
action={{
label: '去考试详情',
onClick: () => {
navigate(`/exams/${examCreatedId}`);
setExamCreatedId(null);
},
}}
onClose={() => setExamCreatedId(null)}
/>
)}
<div className="exam-toolbar">
<Space wrap>
<Input
@@ -461,7 +482,7 @@ const ExamsPage: React.FC = () => {
saving={saving}
form={form}
classes={classes}
onCancel={() => setModalOpen(false)}
onCancel={() => examFormGuard.confirmClose(() => setModalOpen(false))}
onSubmit={() => void submit()}
/>
</div>