From 20a4dfad84ebbdc3423c5973766474cdc4b72591 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 5 Jul 2026 19:16:02 +0800 Subject: [PATCH] feat: add Class detail page with student roster and teacher tabs --- apps/admin/src/pages/Classes/detail.tsx | 504 +++++++++++++++++++++++- 1 file changed, 500 insertions(+), 4 deletions(-) diff --git a/apps/admin/src/pages/Classes/detail.tsx b/apps/admin/src/pages/Classes/detail.tsx index e7a12d5..1c5d8ab 100644 --- a/apps/admin/src/pages/Classes/detail.tsx +++ b/apps/admin/src/pages/Classes/detail.tsx @@ -1,9 +1,505 @@ -import React from 'react'; -import { Card } from 'antd'; +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, message, Form, Input, DatePicker, InputNumber, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { ArrowLeftOutlined, PlusOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; + +// ---- Types ---- + +interface ClassStudent { + id: number; + studentId: number; + studentName: string; + studentNo: string; + joinDate: string; + status: string; +} + +interface ClassTeacher { + id: number; + userId: number; + username: string; + roleType: string; + subject: string | null; +} + +interface ClassDetail { + id: number; + name: string; + code: string; + departmentId: number | null; + 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; +} + +interface UserItem { + id: number; + username: 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: '冲刺营', +}; + +const ROLE_MAP: Record = { + subject_teacher: '任课老师', + head_teacher: '班主任', + life_teacher: '生活老师', + academic_teacher: '学服老师', +}; + +// ---- Component ---- -// Placeholder — Task 1.5 will implement the full detail page. const ClassDetailPage: React.FC = () => { - return 班级详情 — 待实现; + 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(); + + 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 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('/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: 'status', + render: (v: string) => ( + + {v === 'active' ? '在读' : '已离班'} + + ), + }, + { + title: '操作', + render: (_: unknown, r: ClassStudent) => ( + handleRemoveStudent(r.studentId)}> + + + ), + }, + ]; + + 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)}> + + + ), + }, + ]; + + return ( + + + + + + ) : ( +
+ + + {TYPE_MAP[detail.classType]} + + + {detail.startDate || '-'} + + + {detail.endDate || '-'} + + + {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: ( +
+ + + columns={studentColumns} + dataSource={students} + rowKey="id" + pagination={{ pageSize: 20 }} + /> + setStudentModalOpen(false)} + > + ({ + value: u.id, + label: u.username, + }))} + filterOption={(input, option) => + (option?.label as string)?.toLowerCase().includes(input.toLowerCase()) + } + /> + setTeacherSubject(e.target.value)} + /> + )} + + +
+ ), + }, + ]} + /> +
+ ); }; export default ClassDetailPage;