import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { Table, Input, Modal, Form, Select, DatePicker, Tag, Space } from 'antd'; import { EditOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import { message } from '../../ui/app-message'; import EditableCell from '../../components/EditableCell'; import PermissionButton from '../../components/PermissionButton'; import { usePermission } from '../../hooks/usePermission'; interface TeacherRow { id: number; username: string; name: string; isActive: boolean; profile: { subjects?: string[]; joinedAt?: string; qualifications?: string } | null; lastLoginAt: string; roles: { code: string; name: string }[]; classAssignments: { roleType: string; subject: string; className: string | null }[]; } interface TeacherListResponse { list: TeacherRow[]; total: number; } interface ProfileFormValues { subjects: string[]; joinedAt: dayjs.Dayjs | null; qualifications: string; } const ROLE_LABELS: Record = { super_admin: '超级管理员', teacher: '任课老师', academic: '教务管理员', accommodation_operations: '住宿运营管理员', classroom_operations: '教室运营管理员', system_admin: '系统管理员', class_teacher: '班主任', dormitory_supervisor: '宿管', institution_head: '机构负责人', }; const ROLE_TYPE_LABELS: Record = { subject_teacher: '任课教师', head_teacher: '班主任', life_teacher: '生活老师', academic_teacher: '教务老师', }; const DEFAULT_PAGE_SIZE = 20; const TeachersPage: React.FC = () => { const { hasPermission } = usePermission(); const canEditTeachers = hasPermission('teacher:edit'); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [search, setSearch] = useState(''); const [profileModal, setProfileModal] = useState(null); const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const fetchData = useCallback(async () => { setLoading(true); try { const res = await api.get('/rbac/teachers', { params: { search: search || undefined, page, pageSize }, }); setData(res.list); setTotal(res.total); } catch { // silent } setLoading(false); }, [page, pageSize, search]); useEffect(() => { fetchData(); }, [fetchData]); const handleSaveProfile = async () => { const values = await form.validateFields(); if (!profileModal) return; setSaving(true); try { await api.put(`/rbac/teachers/${profileModal.id}/profile`, { subjects: values.subjects || [], joinedAt: values.joinedAt?.format('YYYY-MM-DD'), qualifications: values.qualifications, }); message.success('已更新'); setProfileModal(null); fetchData(); } catch (e: unknown) { let msg = '更新失败'; if (e !== null && typeof e === 'object' && 'message' in e) { msg = String(e.message); } message.error(msg); } finally { setSaving(false); } }; const saveProfileCell = useCallback( async (record: TeacherRow, field: string, value: unknown) => { await api.put(`/rbac/teachers/${record.id}/profile`, { [field]: value }); message.success('已保存'); await fetchData(); }, [fetchData], ); const columns = useMemo( () => [ { title: '姓名', dataIndex: 'name', key: 'name', width: 120 }, { title: '用户名', dataIndex: 'username', key: 'username', width: 130 }, { title: '角色', dataIndex: 'roles', key: 'roles', width: 220, render: (roles: TeacherRow['roles']) => roles.map((r) => {ROLE_LABELS[r.code] || r.name}), }, { title: '任课班级', dataIndex: 'classAssignments', key: 'classes', width: 200, render: (ca: TeacherRow['classAssignments']) => ca?.length ? ca.map((a, i) => ( {a.className || '-'} {a.subject ? ` (${a.subject})` : ''} )) : '-', }, { title: '科目', dataIndex: 'profile', key: 'subjects', width: 130, render: (p: TeacherRow['profile'], r: TeacherRow) => ( ({ value, label: value }))} permission="teacher:edit" onSave={(next) => saveProfileCell(r, 'subjects', next)} > {p?.subjects?.join('、') || '-'} ), }, { title: '入职日期', dataIndex: 'profile', key: 'joinedAt', width: 110, render: (p: TeacherRow['profile'], r: TeacherRow) => ( saveProfileCell(r, 'joinedAt', next)} > {p?.joinedAt || '-'} ), }, { title: '状态', dataIndex: 'isActive', key: 'status', width: 90, render: (v: boolean) => {v ? '在职' : '停用'}, }, { title: '最后登录', dataIndex: 'lastLoginAt', key: 'login', width: 160, render: (v: string) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-'), }, { title: '操作', key: 'actions', width: 100, render: (_: unknown, r: TeacherRow) => ( } onClick={() => { setProfileModal(r); form.setFieldsValue({ subjects: r.profile?.subjects || [], joinedAt: r.profile?.joinedAt ? dayjs(r.profile.joinedAt) : null, qualifications: r.profile?.qualifications || '', }); }} > 档案 ), }, ], [saveProfileCell, form], ); return (

教师管理

{ setSearch(v); setPage(1); }} style={{ width: 220 }} /> { setPage(nextPage); setPageSize(nextPageSize); }, showTotal: (t) => `共 ${t} 人`, }} expandable={{ rowExpandable: (r) => (r.classAssignments || []).length > 0, expandedRowRender: (r) => r.classAssignments?.length ? ( {r.classAssignments.map((a, i) => ( {ROLE_TYPE_LABELS[a.roleType] || a.roleType}: {a.className} {a.subject ? ` — ${a.subject}` : ''} ))} ) : null, }} /> setProfileModal(null)} okText="保存" confirmLoading={saving} >