diff --git a/apps/admin/src/pages/Exams/index.tsx b/apps/admin/src/pages/Exams/index.tsx index e487235..13e3151 100644 --- a/apps/admin/src/pages/Exams/index.tsx +++ b/apps/admin/src/pages/Exams/index.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Button, Card, Col, Empty, Form, Input, Popconfirm, Progress, Row, Select, Space, Switch, Tag } from 'antd'; +import { Button, Card, Checkbox, 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'; import { message } from '../../ui/app-message'; import ExamFormModal from './ExamFormModal'; +import { selectAllExamIds, toggleExamSelection } from './selection'; import type { ClassOption, ExamFormValues, ExamItem } from './types'; import { EXAM_TYPE_OPTIONS } from './types'; import './style.css'; @@ -16,12 +17,14 @@ const ExamsPage: React.FC = () => { const [data, setData] = useState([]); const [classes, setClasses] = useState([]); const [loading, setLoading] = useState(false); + const [batchLoading, setBatchLoading] = useState(false); const [saving, setSaving] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [keyword, setKeyword] = useState(''); const [examType, setExamType] = useState(); const [classId, setClassId] = useState(); const [showArchived, setShowArchived] = useState(false); + const [selectedExamIds, setSelectedExamIds] = useState([]); const loadClasses = useCallback(async () => { const result = await api.get('/classes'); @@ -29,6 +32,7 @@ const ExamsPage: React.FC = () => { }, []); const loadExams = useCallback(async () => { + setSelectedExamIds([]); setLoading(true); try { const params = new URLSearchParams(); @@ -92,6 +96,46 @@ const ExamsPage: React.FC = () => { } }; + const allCurrentSelected = data.length > 0 && selectedExamIds.length === data.length; + const partiallySelected = selectedExamIds.length > 0 && !allCurrentSelected; + + const toggleSelectAll = (checked: boolean) => { + setSelectedExamIds(selectAllExamIds(data.map((exam) => exam.id), checked)); + }; + + const changeArchiveView = (checked: boolean) => { + setSelectedExamIds([]); + setShowArchived(checked); + }; + + const batchChangeArchiveStatus = async (archive: boolean) => { + if (selectedExamIds.length === 0 || batchLoading) return; + setBatchLoading(true); + try { + if (archive) { + const result = await api.put<{ archived: number; skipped: number }>('/exams/batch-archive', { + ids: selectedExamIds, + }); + message.success( + `已归档 ${result.archived} 场考试${result.skipped ? `,跳过 ${result.skipped} 场` : ''}`, + ); + } else { + const result = await api.put<{ restored: number; skipped: number }>('/exams/batch-restore', { + ids: selectedExamIds, + }); + message.success( + `已恢复 ${result.restored} 场考试${result.skipped ? `,跳过 ${result.skipped} 场` : ''}`, + ); + } + setSelectedExamIds([]); + await loadExams(); + } catch (error) { + message.error((error as { message?: string })?.message || '批量操作失败'); + } finally { + setBatchLoading(false); + } + }; + return (
@@ -101,12 +145,40 @@ const ExamsPage: React.FC = () => {