From 0053fed505e497aa1bec6fd754be79956be7a0c5 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 5 Jul 2026 19:10:48 +0800 Subject: [PATCH] feat: add Classes list page with CRUD modal --- apps/admin/src/App.tsx | 18 ++ apps/admin/src/layouts/MainLayout.tsx | 1 + apps/admin/src/pages/Classes/detail.tsx | 9 + apps/admin/src/pages/Classes/index.tsx | 267 ++++++++++++++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 apps/admin/src/pages/Classes/detail.tsx create mode 100644 apps/admin/src/pages/Classes/index.tsx diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index a4d4489..c5e0899 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -15,6 +15,8 @@ import OperationLogsPage from './pages/OperationLogs'; import UsersPage from './pages/Users'; import DepositsPage from './pages/Deposits'; import ClassroomsPage from './pages/Classrooms'; +import ClassesPage from './pages/Classes'; +import ClassDetailPage from './pages/Classes/detail'; import TenantsPage from './pages/Tenants'; import ClassroomRentalsPage from './pages/ClassroomRentals'; import ClassroomSchedulePage from './pages/ClassroomSchedule'; @@ -118,6 +120,22 @@ const App: React.FC = () => { } /> + + + + } + /> + + + + } + /> , label: '费用录入', permission: 'expense:view' }, { key: '/deposits', icon: , label: '押金管理', permission: 'deposit:view' }, { key: '/bills', icon: , label: '账单管理', permission: 'bill:view' }, + { key: '/classes', icon: , label: '班级管理', permission: 'class:view' }, { key: 'classroom-group', icon: , diff --git a/apps/admin/src/pages/Classes/detail.tsx b/apps/admin/src/pages/Classes/detail.tsx new file mode 100644 index 0000000..e7a12d5 --- /dev/null +++ b/apps/admin/src/pages/Classes/detail.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { Card } from 'antd'; + +// Placeholder — Task 1.5 will implement the full detail page. +const ClassDetailPage: React.FC = () => { + return 班级详情 — 待实现; +}; + +export default ClassDetailPage; diff --git a/apps/admin/src/pages/Classes/index.tsx b/apps/admin/src/pages/Classes/index.tsx new file mode 100644 index 0000000..8d67691 --- /dev/null +++ b/apps/admin/src/pages/Classes/index.tsx @@ -0,0 +1,267 @@ +import React, { useEffect, useState, useMemo, useCallback } from 'react'; +import { + Table, Button, Input, Select, Space, Tag, Modal, Form, InputNumber, + DatePicker, Popconfirm, message, Card, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { PlusOutlined, SearchOutlined, TeamOutlined } 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; + departmentId: number | null; + 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; + 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 fetchData = useCallback(async () => { + setLoading(true); + try { + const params: ClassQueryParams = {}; + if (filterStatus) params.status = filterStatus; + if (filterType) params.classType = filterType; + const res = await api.get('/classes', { params }); + setData(res as ClassItem[]); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }, [filterStatus, filterType]); + + 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, + startDate: record.startDate ? dayjs(record.startDate) : undefined, + endDate: record.endDate ? dayjs(record.endDate) : undefined, + }); + setModalOpen(true); + }; + + const handleSubmit = async () => { + 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(); + }; + + const handleDelete = async (id: number) => { + await api.delete(`/classes/${id}`); + message.success('已删除'); + fetchData(); + }; + + const columns: ColumnsType = [ + { + title: '班级名称', dataIndex: 'name', + 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: 90, + render: (v: string) => { + const cfg = STATUS_MAP[v] || { color: 'default', text: v }; + return {cfg.text}; + }, + }, + { + title: '操作', width: 200, + render: (_: unknown, r: ClassItem) => ( + + + handleEdit(r)}> + 编辑 + + 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} + pagination={{ pageSize: 20 }} + /> + + setModalOpen(false)} + width={600} + > +
+ + + + + + + + ({ value: k, label: v.text }))} /> + + + + +
+
+
+ ); +}; + +export default ClassesPage;