import React, { useEffect, useState, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Card, Tabs, Descriptions, Table, Button, Space, Select, Modal, Tag, Popconfirm, Form, Input, DatePicker, InputNumber, Row, Col, Statistic, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { ArrowLeftOutlined, PlusOutlined, DownloadOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; import { message } from '../../ui/app-message'; import { buildTeacherCandidateOptions, type TeacherCandidateUser } from './teacher-candidate'; // ---- Types ---- interface ClassStudent { id: number; studentId: number; studentName: string; studentNo: string; joinDate: string; leaveDate: string | null; status: string; } interface ClassTeacher { id: number; userId: number; username: string; roleType: string; subject: string | null; } interface ClassScheduleItem { id: number; classId: number; classroomId: number; classroomName: string; weekDay: number; startTime: string; endTime: string; attendanceAdvanceMinutes: number; startDate: string; endDate: string; subject: string; teacherId: number | null; scheduleType: string; status: string; notes: string | null; } interface AttendanceSummary { total: number; present: number; late: number; absent: number; leave: number; presentRate: number; absentRate: number; lateRate: number; leaveRate: number; } interface ClassDetail { id: number; name: string; code: string; classType: string; startDate: string | null; endDate: string | null; status: string; maxStudents: number; notes: string | null; studentCount: number; students?: ClassStudent[]; teachers?: ClassTeacher[]; createdAt: string; updatedAt: string; } interface StudentItem { id: number; name: string; studentNo?: string; } type UserItem = TeacherCandidateUser; // ---- Constants ---- const STATUS_MAP: Record = { enrolling: { color: 'blue', text: '招生中' }, active: { color: 'green', text: '在读' }, ended: { color: 'default', text: '结课' }, suspended: { color: 'orange', text: '停课' }, }; const TYPE_MAP: Record = { culture: '文化课', professional: '专业课', bootcamp: '集训营', sprint: '冲刺营', }; const ROLE_MAP: Record = { subject_teacher: '任课老师', head_teacher: '班主任', life_teacher: '生活老师', academic_teacher: '学服老师', }; const WEEK_DAY_MAP: Record = { 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日', }; const SCHEDULE_TYPE_MAP: Record = { INTERNAL: '内部排课', RENTAL: '租赁', }; // ---- Component ---- const ClassDetailPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [detail, setDetail] = useState(null); const [students, setStudents] = useState([]); const [teachers, setTeachers] = useState([]); const [loading, setLoading] = useState(false); const [editForm] = Form.useForm(); const [editingInfo, setEditingInfo] = useState(false); // Student modal state const [studentModalOpen, setStudentModalOpen] = useState(false); const [allStudents, setAllStudents] = useState([]); const [selectedStudentIds, setSelectedStudentIds] = useState([]); // Teacher modal state const [teacherModalOpen, setTeacherModalOpen] = useState(false); const [allUsers, setAllUsers] = useState([]); const [teacherRole, setTeacherRole] = useState('subject_teacher'); const [teacherSubject, setTeacherSubject] = useState(''); const [teacherUserId, setTeacherUserId] = useState(); // Schedule & attendance state const [schedules, setSchedules] = useState([]); const [scheduleDateRange, setScheduleDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]); const [attendanceSummary, setAttendanceSummary] = useState(null); const [attendanceDateRange, setAttendanceDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]); const fetchDetail = useCallback(async () => { setLoading(true); try { const res = await api.get(`/classes/${id}`) as ClassDetail; setDetail(res); setStudents(res.students || []); setTeachers(res.teachers || []); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '加载失败'); } finally { setLoading(false); } }, [id]); useEffect(() => { fetchDetail(); }, [fetchDetail]); const fetchSchedules = useCallback(async () => { if (!id) return; try { const params: Record = {}; if (scheduleDateRange?.[0]) params.startDate = scheduleDateRange[0].format('YYYY-MM-DD'); if (scheduleDateRange?.[1]) params.endDate = scheduleDateRange[1].format('YYYY-MM-DD'); const res = await api.get(`/classes/${id}/schedule`, { params }); setSchedules(res || []); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '加载课表失败'); } }, [id, scheduleDateRange]); useEffect(() => { fetchSchedules(); }, [fetchSchedules]); const fetchAttendanceSummary = useCallback(async () => { if (!id) return; try { const params: Record = {}; if (attendanceDateRange?.[0]) params.startDate = attendanceDateRange[0].format('YYYY-MM-DD'); if (attendanceDateRange?.[1]) params.endDate = attendanceDateRange[1].format('YYYY-MM-DD'); const res = await api.get(`/classes/${id}/attendance-summary`, { params }); setAttendanceSummary(res || null); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '加载出勤汇总失败'); } }, [id, attendanceDateRange]); useEffect(() => { fetchAttendanceSummary(); }, [fetchAttendanceSummary]); const handleSaveInfo = async () => { try { const values = await editForm.validateFields(); await api.put(`/classes/${id}`, { name: values.name, code: values.code, classType: values.classType, startDate: values.startDate?.format('YYYY-MM-DD'), endDate: values.endDate?.format('YYYY-MM-DD'), maxStudents: values.maxStudents, status: values.status, notes: values.notes, }); setEditingInfo(false); fetchDetail(); message.success('已更新'); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '更新失败'); } }; const handleRemoveStudent = async (studentId: number) => { try { await api.delete(`/classes/${id}/students/${studentId}`); fetchDetail(); message.success('已移除'); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '移除失败'); } }; const handleAddStudents = async () => { if (!selectedStudentIds.length) return; try { await api.post(`/classes/${id}/students`, { studentIds: selectedStudentIds }); setStudentModalOpen(false); setSelectedStudentIds([]); fetchDetail(); message.success('已添加'); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '添加失败'); } }; const handleAddTeacher = async () => { if (!teacherUserId) return; try { await api.post(`/classes/${id}/teachers`, { userId: teacherUserId, roleType: teacherRole, subject: teacherSubject || undefined, }); setTeacherModalOpen(false); fetchDetail(); message.success('已添加'); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '添加失败'); } }; const handleRemoveTeacher = async (userId: number) => { try { await api.delete(`/classes/${id}/teachers/${userId}`); fetchDetail(); message.success('已移除'); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '移除失败'); } }; const openStudentModal = async () => { try { const res = await api.get('/students', { params: { includeArchived: 'false' } }) as StudentItem[]; setAllStudents(res || []); setSelectedStudentIds([]); setStudentModalOpen(true); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '加载学员列表失败'); } }; const openTeacherModal = async () => { try { const res = await api.get('/rbac/users') as UserItem[]; setAllUsers(res || []); setTeacherUserId(undefined); setTeacherRole('subject_teacher'); setTeacherSubject(''); setTeacherModalOpen(true); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '加载用户列表失败'); } }; if (!detail) return null; const studentColumns: ColumnsType = [ { title: '姓名', dataIndex: 'studentName' }, { title: '学号', dataIndex: 'studentNo' }, { title: '加入日期', dataIndex: 'joinDate' }, { title: '离班日期', dataIndex: 'leaveDate', render: (v: string | null) => v || '-' }, { title: '状态', dataIndex: 'status', render: (v: string) => ( {v === 'active' ? '在读' : '已离班'} ), }, { title: '操作', render: (_: unknown, r: ClassStudent) => r.status === 'active' ? ( handleRemoveStudent(r.studentId)}> 移除 ) : null, }, ]; const teacherColumns: ColumnsType = [ { title: '姓名', dataIndex: 'username' }, { title: '角色', dataIndex: 'roleType', render: (v: string) => {ROLE_MAP[v] || v}, }, { title: '科目', dataIndex: 'subject', render: (v: string | null) => v || '-', }, { title: '操作', render: (_: unknown, r: ClassTeacher) => ( handleRemoveTeacher(r.userId)}> 移除 ), }, ]; const scheduleColumns: ColumnsType = [ { title: '教室', dataIndex: 'classroomName', render: (v: string | null) => v || '-' }, { title: '星期', dataIndex: 'weekDay', render: (v: number) => WEEK_DAY_MAP[v] || v }, { title: '时间', render: (_: unknown, r: ClassScheduleItem) => `${r.startTime} - ${r.endTime}` }, { title: '签到窗口', render: (_: unknown, r: ClassScheduleItem) => `课前 ${r.attendanceAdvanceMinutes ?? 30} 分钟至下课` }, { title: '日期范围', render: (_: unknown, r: ClassScheduleItem) => `${r.startDate} ~ ${r.endDate}` }, { title: '科目', dataIndex: 'subject' }, { title: '类型', dataIndex: 'scheduleType', render: (v: string) => SCHEDULE_TYPE_MAP[v] || v }, { title: '状态', dataIndex: 'status', render: (v: string) => {v === 'active' ? '启用' : v}, }, ]; return ( ) : (
{TYPE_MAP[detail.classType]} {detail.startDate ? dayjs(detail.startDate).format('YYYY-MM-DD') : '-'} {detail.endDate ? dayjs(detail.endDate).format('YYYY-MM-DD') : '-'} {detail.studentCount}/{detail.maxStudents || '-'} {teachers.find((t) => t.roleType === 'head_teacher')?.username || '-'} {detail.notes || '-'} { editForm.setFieldsValue({ name: detail.name, code: detail.code, classType: detail.classType, startDate: detail.startDate ? dayjs(detail.startDate) : undefined, endDate: detail.endDate ? dayjs(detail.endDate) : undefined, maxStudents: detail.maxStudents, status: detail.status, notes: detail.notes, }); setEditingInfo(true); }} > 编辑
)} ), }, { key: 'students', label: `花名册 (${students.filter((s) => s.status === 'active').length})`, children: (
} type="primary" onClick={openStudentModal} style={{ marginBottom: 16, marginRight: 8 }} > 添加学员 } onClick={() => { const token = localStorage.getItem('token'); fetch(`/api/classes/${id}/roster/export`, { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => { if (!res.ok) throw new Error('导出失败'); return res.blob(); }) .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `班级花名册-${detail?.name || id}.xlsx`; a.click(); URL.revokeObjectURL(url); message.success('花名册导出成功'); }) .catch(() => message.error('花名册导出失败')); }} > 导出花名册 columns={studentColumns} dataSource={students} rowKey="id" pagination={{ defaultPageSize: 20, showSizeChanger: true, pageSizeOptions: [20, 50, 100], }} /> setStudentModalOpen(false)} > setTeacherSubject(e.target.value)} /> )}
), }, { key: 'schedule', label: '课表', children: (
setScheduleDateRange(dates as [dayjs.Dayjs | null, dayjs.Dayjs | null])} placeholder={['开始日期', '结束日期']} /> columns={scheduleColumns} dataSource={schedules} rowKey="id" pagination={{ defaultPageSize: 20, showSizeChanger: true, pageSizeOptions: [20, 50, 100], }} />
), }, { key: 'attendance-summary', label: '出勤汇总', children: (
setAttendanceDateRange(dates as [dayjs.Dayjs | null, dayjs.Dayjs | null])} placeholder={['开始日期', '结束日期']} /> {attendanceSummary && ( )}
), }, ]} />
); }; export default ClassDetailPage;