新增考试归档与恢复功能
Some checks failed
CI / check (pull_request) Failing after 2m15s

This commit is contained in:
2026-07-24 16:18:35 +08:00
parent 4046e29e33
commit 3573351f59
7 changed files with 280 additions and 29 deletions

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Card, Col, Empty, Form, Input, Progress, Row, Select, Space, Tag } from 'antd';
import { CalendarOutlined, PlusOutlined, SearchOutlined, TeamOutlined } from '@ant-design/icons';
import { Button, Card, Col, Empty, Form, Input, Popconfirm, Progress, Row, Select, Space, Switch, Tag } from 'antd';
import { CalendarOutlined, InboxOutlined, PlusOutlined, SearchOutlined, TeamOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router-dom';
import api from '../../api';
@@ -21,6 +21,7 @@ const ExamsPage: React.FC = () => {
const [keyword, setKeyword] = useState('');
const [examType, setExamType] = useState<string>();
const [classId, setClassId] = useState<number>();
const [showArchived, setShowArchived] = useState(false);
const loadClasses = useCallback(async () => {
const result = await api.get<ClassOption[]>('/classes');
@@ -34,6 +35,7 @@ const ExamsPage: React.FC = () => {
if (keyword.trim()) params.set('keyword', keyword.trim());
if (examType) params.set('examType', examType);
if (classId) params.set('classId', String(classId));
params.set('isArchived', String(showArchived));
const result = await api.get<ExamItem[]>(`/exams?${params.toString()}`);
setData(result ?? []);
} catch (error) {
@@ -41,7 +43,7 @@ const ExamsPage: React.FC = () => {
} finally {
setLoading(false);
}
}, [classId, examType, keyword]);
}, [classId, examType, keyword, showArchived]);
useEffect(() => {
void loadClasses().catch((error: { message?: string }) => message.error(error?.message || '加载班级失败'));
@@ -80,6 +82,16 @@ const ExamsPage: React.FC = () => {
}
};
const changeArchiveStatus = async (exam: ExamItem, archive: boolean) => {
try {
await api.put(`/exams/${exam.id}/${archive ? 'archive' : 'restore'}`);
message.success(archive ? '考试已归档' : '考试已恢复');
await loadExams();
} catch (error) {
message.error((error as { message?: string })?.message || '操作失败');
}
};
return (
<div className="exam-page">
<div className="exam-toolbar">
@@ -89,6 +101,11 @@ const ExamsPage: React.FC = () => {
<Select value={classId} onChange={setClassId} options={classOptions} placeholder="考试班级" allowClear showSearch optionFilterProp="label" style={{ width: 180 }} />
</Space>
<Space wrap>
<span className="exam-archive-toggle">
<InboxOutlined />
<Switch size="small" checked={showArchived} onChange={setShowArchived} />
</span>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}></Button>
</Space>
</div>
@@ -105,9 +122,27 @@ const ExamsPage: React.FC = () => {
className="exam-card"
loading={loading}
title={<Space><Tag color="blue">{exam.examType}</Tag><span>{exam.examName}</span></Space>}
extra={<Tag color="green"></Tag>}
extra={<Tag color={exam.status === 'archived' ? 'default' : 'green'}>{exam.status === 'archived' ? '已归档' : '成绩录入'}</Tag>}
actions={[
<span key="detail" onClick={() => navigate(`/exams/${exam.id}`)}></span>,
exam.status === 'archived' ? (
<Popconfirm
key="restore"
title="确认恢复该考试?"
onConfirm={() => changeArchiveStatus(exam, false)}
>
<span></span>
</Popconfirm>
) : (
<Popconfirm
key="archive"
title="确认归档该考试?"
description={`当前已录入 ${exam.enteredScores}/${exam.totalStudents} 人,归档后成绩将变为只读。`}
onConfirm={() => changeArchiveStatus(exam, true)}
>
<span></span>
</Popconfirm>
),
]}
>
<div className="exam-meta"><span></span><strong>{exam.subject}</strong></div>