import React, { useState, useCallback, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useApiMutation } from '../../hooks/useApiMutation'; import { validateResponse } from '../../utils/validate'; import { teacherListSchema } from '../../api/schemas'; 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 { RefreshButton } from '../../components/RefreshButton'; import EditableCell from '../../components/EditableCell'; import PermissionButton from '../../components/PermissionButton'; import { usePermission } from '../../hooks/usePermission'; import { QueryErrorState, QueryEmpty } from '../../components/QueryState'; import { useVisibleRefetch } from '../../hooks/usePageVisible'; import { useDirtyGuard } from '../../hooks/useDirtyGuard'; interface TeacherRow { id: number; username: string; name: string; 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 [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 profileFormGuard = useDirtyGuard(form); const [saving, setSaving] = useState(false); const { data: fetchResult = { list: [], total: 0 }, isLoading, isFetching, isError, refetch, } = useQuery({ queryKey: ['rbac', 'teachers', page, pageSize, search], queryFn: async () => { return validateResponse( teacherListSchema, await api.get('/rbac/teachers', { params: { search: search || undefined, page, pageSize }, }), ); }, }); const data = fetchResult.list; const total = fetchResult.total; const loading = isLoading || isFetching; // RouteKeeper 保活页面切回时刷新教师列表 useVisibleRefetch(['rbac', 'teachers']); const saveProfileMutation = useApiMutation( async ({ id, values, }: { id: number; values: { subjects: string[]; joinedAt?: string; qualifications?: string }; }) => api.put(`/rbac/teachers/${id}/profile`, values), { invalidate: [['rbac', 'teachers']] }, ); const saveProfileCellMutation = useApiMutation( async ({ record, field, value }: { record: TeacherRow; field: string; value: unknown }) => api.put(`/rbac/teachers/${record.id}/profile`, { [field]: value }), { invalidate: [['rbac', 'teachers']] }, ); const handleSaveProfile = async () => { const values = await form.validateFields(); if (!profileModal) return; setSaving(true); try { await saveProfileMutation.mutateAsync({ id: profileModal.id, values: { subjects: values.subjects || [], joinedAt: values.joinedAt?.format('YYYY-MM-DD'), qualifications: values.qualifications, }, }); message.success('已更新'); setProfileModal(null); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const saveProfileCell = useCallback( async (record: TeacherRow, field: string, value: unknown) => { try { await saveProfileCellMutation.mutateAsync({ record, field, value }); message.success('已保存'); } catch { // 错误提示由 useApiMutation 统一处理 } }, [saveProfileCellMutation], ); 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: 'lastLoginAt', key: 'login', width: 160, render: (v: string) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-'), }, { title: '操作', key: 'actions', fixed: 'right' as const, 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 || '', }); profileFormGuard.snapshot(); }} > 档案 ), }, ], [saveProfileCell, form, profileFormGuard], ); return (

教师管理

{ setSearch(v); setPage(1); }} style={{ width: 220 }} /> void refetch()} /> {isError ? ( void refetch()} /> ) : ( }} scroll={{ x: 1300 }} pagination={{ current: page, pageSize, total, showSizeChanger: true, pageSizeOptions: [20, 50, 100], onChange: (nextPage, nextPageSize) => { 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, }} /> )} profileFormGuard.confirmClose(() => setProfileModal(null))} okText="保存" confirmLoading={saving} >