import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { Table, Button, Input, Select, Space, Tag, Modal, Form, InputNumber, DatePicker, Popconfirm, message, Card, Switch, Empty, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined, SearchOutlined, TeamOutlined, InboxOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; // ---- Types ---- interface ClassItem { id: number; name: string; code: string; classType: string; startDate: string | null; endDate: string | null; status: string; headTeacherId: number | null; lifeTeacherId: number | null; academicTeacherId: number | null; maxStudents: number; notes: string | null; studentCount: number; isArchived: boolean; createdAt: string; updatedAt: string; } interface ClassFormValues { name: string; code: string; classType: string; startDate?: dayjs.Dayjs; endDate?: dayjs.Dayjs; maxStudents?: number; status?: string; notes?: string; } interface ClassQueryParams { status?: string; classType?: string; } // ---- 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: '冲刺营', }; // ---- Component ---- const ClassesPage: React.FC = () => { const navigate = useNavigate(); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [searchText, setSearchText] = useState(''); const [filterStatus, setFilterStatus] = useState(); const [filterType, setFilterType] = useState(); const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const [showArchived, setShowArchived] = useState(false); const handleArchive = async (id: number, archive: boolean) => { try { await api.put(`/classes/${id}/${archive ? 'archive' : 'restore'}`); message.success(archive ? '已归档' : '已恢复'); fetchData(); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '操作失败'); } }; const fetchData = useCallback(async () => { setLoading(true); try { const params: Record = {}; if (filterStatus) params.status = filterStatus; if (filterType) params.classType = filterType; params.isArchived = showArchived; const res = await api.get('/classes', { params } as Record); setData(res); } catch (e: any) { message.error(e?.message || '加载失败,请稍后重试'); } } finally { setLoading(false); } }, [filterStatus, filterType, showArchived]); useEffect(() => { fetchData(); }, [fetchData]); const filtered = useMemo(() => { if (!searchText) return data; const q = searchText.toLowerCase(); return data.filter( (c) => c.name?.toLowerCase().includes(q) || c.code?.toLowerCase().includes(q), ); }, [data, searchText]); const handleCreate = () => { setEditing(null); form.resetFields(); setModalOpen(true); }; const handleEdit = (record: ClassItem) => { setEditing(record); form.setFieldsValue({ ...record, notes: record.notes ?? undefined, startDate: record.startDate ? dayjs(record.startDate) : undefined, endDate: record.endDate ? dayjs(record.endDate) : undefined, }); setModalOpen(true); }; const handleSubmit = async () => { setSaving(true); try { const values = await form.validateFields(); const payload = { ...values, startDate: values.startDate?.format('YYYY-MM-DD'), endDate: values.endDate?.format('YYYY-MM-DD'), }; if (editing) { await api.put(`/classes/${editing.id}`, payload); message.success('更新成功'); } else { await api.post('/classes', payload); message.success('创建成功'); } setModalOpen(false); fetchData(); } catch (e: any) { message.error(e?.message || '操作失败'); } finally { setSaving(false); } }; const handleDelete = async (id: number) => { try { await api.delete(`/classes/${id}`); message.success('已删除'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); } }; const columns: ColumnsType = useMemo(() => [ { title: '班级名称', dataIndex: 'name', width: 120, sorter: (a, b) => a.name.localeCompare(b.name), }, { title: '编码', dataIndex: 'code', width: 140 }, { title: '班型', dataIndex: 'classType', width: 100, render: (v: string) => {TYPE_MAP[v] || v}, }, { title: '开班日期', dataIndex: 'startDate', width: 110, render: (v: string | null) => v || '-', }, { title: '学员', width: 100, render: (_: unknown, r: ClassItem) => `${r.studentCount || 0}/${r.maxStudents || '-'}`, }, { title: '状态', dataIndex: 'status', width: 100, render: (v: string) => { const cfg = STATUS_MAP[v] || { color: 'default', text: v }; return {cfg.text}; }, }, { title: '操作', width: 280, render: (_: unknown, r: ClassItem) => ( handleEdit(r)}> 编辑 {r.isArchived ? ( handleArchive(r.id, false)}> 恢复 ) : ( handleArchive(r.id, true)}> 归档 )} handleDelete(r.id)}> 删除 ), }, ], []); return ( } value={searchText} onChange={(e) => setSearchText(e.target.value)} style={{ width: 200 }} /> ({ value: k, label: v.text }))} /> } onClick={handleCreate}> 创建班级 归档 columns={columns} dataSource={filtered} rowKey="id" loading={loading} locale={{ emptyText: }} pagination={{ pageSize: 20 }} scroll={{ x: 1100 }} /> setModalOpen(false)} confirmLoading={saving} width={600} >
({ value: k, label: v.text }))} />
); }; export default ClassesPage;