import React from 'react'; import { Button, Card, Col, DatePicker, Descriptions, Form, Input, InputNumber, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { DownloadOutlined, PlusOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import { useUserStore } from '../../store/user/userStore'; import PermissionButton from '../../components/PermissionButton'; import { message } from '../../ui/app-message'; import { buildTeacherCandidateOptions, type TeacherCandidateUser } from './teacher-candidate'; export interface ClassStudent { id: number; studentId: number; studentName: string; studentNo: string; joinDate: string; leaveDate: string | null; status: string; } export interface ClassTeacher { id: number; userId: number; username: string; roleType: string; subject: string | null; } export 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; } export interface AttendanceSummary { total: number; present: number; late: number; absent: number; leave: number; presentRate: number; absentRate: number; lateRate: number; leaveRate: number; } export 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; } export interface StudentItem { id: number; name: string; studentNo?: string; } export const STATUS_MAP: Record = { enrolling: { color: 'blue', text: '招生中' }, active: { color: 'green', text: '在读' }, ended: { color: 'default', text: '结课' }, suspended: { color: 'orange', text: '停课' }, }; export const TYPE_MAP: Record = { culture: '文化课', professional: '专业课', bootcamp: '集训营', sprint: '冲刺营', }; export const ROLE_MAP: Record = { subject_teacher: '任课老师', head_teacher: '班主任', life_teacher: '生活老师', academic_teacher: '学服老师', }; export const WEEK_DAY_MAP: Record = { 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日', }; export const SCHEDULE_TYPE_MAP: Record = { INTERNAL: '内部排课', RENTAL: '租赁', }; export const ClassInfoTab: React.FC<{ detail: ClassDetail; teachers: ClassTeacher[]; editingInfo: boolean; editForm: ReturnType[0]; onSave: () => void; onEdit: () => void; onCancel: () => void; getTeacherName: (teacher: ClassTeacher) => string; }> = ({ detail, teachers, editingInfo, editForm, onSave, onEdit, onCancel, getTeacherName }) => { return (
{editingInfo ? (
({ value: k, label: v.text, }))} /> 保存
) : (
{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 || '-'} {(() => { const headTeacher = teachers.find( (teacher) => teacher.roleType === 'head_teacher', ); return headTeacher ? getTeacherName(headTeacher) : '-'; })()} {detail.notes || '-'} 编辑
)}
); }; export const ClassStudentsTab: React.FC<{ id?: string; detail?: ClassDetail | null; students: ClassStudent[]; allStudents: StudentItem[]; selectedStudentIds: number[]; modalOpen: boolean; onOpen: () => void; onAdd: () => void; onClose: () => void; onRemove: (studentId: number) => void; onSelect: (ids: number[]) => void; }> = ({ id, detail, students, allStudents, selectedStudentIds, modalOpen, onOpen, onAdd, onClose, onRemove, onSelect, }) => { 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' ? ( onRemove(r.studentId)}> 移除 ) : null, }, ]; return (
} type="primary" onClick={onOpen} style={{ marginBottom: 16, marginRight: 8 }} > 添加学员 } onClick={() => { const token = useUserStore.getState().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], }} /> onSubjectChange(e.target.value)} /> )}
); }; export const ClassScheduleTab: React.FC<{ schedules: ClassScheduleItem[]; scheduleDateRange: [dayjs.Dayjs | null, dayjs.Dayjs | null]; onRangeChange: (dates: [dayjs.Dayjs | null, dayjs.Dayjs | null]) => void; }> = ({ schedules, scheduleDateRange, onRangeChange }) => { 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 (
onRangeChange(dates as [dayjs.Dayjs | null, dayjs.Dayjs | null])} placeholder={['开始日期', '结束日期']} /> columns={scheduleColumns} dataSource={schedules} rowKey="id" scroll={{ x: 'max-content' }} pagination={{ defaultPageSize: 20, showSizeChanger: true, pageSizeOptions: [20, 50, 100], }} />
); }; export const ClassAttendanceTab: React.FC<{ attendanceSummary: AttendanceSummary | null; attendanceDateRange: [dayjs.Dayjs | null, dayjs.Dayjs | null]; onRangeChange: (dates: [dayjs.Dayjs | null, dayjs.Dayjs | null]) => void; }> = ({ attendanceSummary, attendanceDateRange, onRangeChange }) => { return (
onRangeChange(dates as [dayjs.Dayjs | null, dayjs.Dayjs | null])} placeholder={['开始日期', '结束日期']} /> {attendanceSummary && ( )}
); };