294 lines
8.8 KiB
TypeScript
294 lines
8.8 KiB
TypeScript
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<string, string> = {
|
|
super_admin: '超级管理员',
|
|
teacher: '任课老师',
|
|
academic: '教务管理员',
|
|
accommodation_operations: '住宿运营管理员',
|
|
classroom_operations: '教室运营管理员',
|
|
system_admin: '系统管理员',
|
|
class_teacher: '班主任',
|
|
dormitory_supervisor: '宿管',
|
|
institution_head: '机构负责人',
|
|
};
|
|
|
|
const ROLE_TYPE_LABELS: Record<string, string> = {
|
|
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<TeacherRow[]>([]);
|
|
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<TeacherRow | null>(null);
|
|
const [form] = Form.useForm<ProfileFormValues>();
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await api.get<TeacherListResponse>('/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) => <Tag key={r.code}>{ROLE_LABELS[r.code] || r.name}</Tag>),
|
|
},
|
|
{
|
|
title: '任课班级',
|
|
dataIndex: 'classAssignments',
|
|
key: 'classes',
|
|
width: 200,
|
|
render: (ca: TeacherRow['classAssignments']) =>
|
|
ca?.length
|
|
? ca.map((a, i) => (
|
|
<Tag key={i}>
|
|
{a.className || '-'}
|
|
{a.subject ? ` (${a.subject})` : ''}
|
|
</Tag>
|
|
))
|
|
: '-',
|
|
},
|
|
{
|
|
title: '科目',
|
|
dataIndex: 'profile',
|
|
key: 'subjects',
|
|
width: 130,
|
|
render: (p: TeacherRow['profile'], r: TeacherRow) => (
|
|
<EditableCell
|
|
value={p?.subjects || []}
|
|
editor="tags"
|
|
options={(p?.subjects || []).map((value) => ({ value, label: value }))}
|
|
permission="teacher:edit"
|
|
onSave={(next) => saveProfileCell(r, 'subjects', next)}
|
|
>
|
|
{p?.subjects?.join('、') || '-'}
|
|
</EditableCell>
|
|
),
|
|
},
|
|
{
|
|
title: '入职日期',
|
|
dataIndex: 'profile',
|
|
key: 'joinedAt',
|
|
width: 110,
|
|
render: (p: TeacherRow['profile'], r: TeacherRow) => (
|
|
<EditableCell
|
|
value={p?.joinedAt}
|
|
editor="date"
|
|
permission="teacher:edit"
|
|
onSave={(next) => saveProfileCell(r, 'joinedAt', next)}
|
|
>
|
|
{p?.joinedAt || '-'}
|
|
</EditableCell>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'isActive',
|
|
key: 'status',
|
|
width: 90,
|
|
render: (v: boolean) => <Tag color={v ? 'green' : 'red'}>{v ? '在职' : '停用'}</Tag>,
|
|
},
|
|
{
|
|
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) => (
|
|
<PermissionButton
|
|
permission="teacher:edit"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => {
|
|
setProfileModal(r);
|
|
form.setFieldsValue({
|
|
subjects: r.profile?.subjects || [],
|
|
joinedAt: r.profile?.joinedAt ? dayjs(r.profile.joinedAt) : null,
|
|
qualifications: r.profile?.qualifications || '',
|
|
});
|
|
}}
|
|
>
|
|
档案
|
|
</PermissionButton>
|
|
),
|
|
},
|
|
],
|
|
[saveProfileCell, form],
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<h2 style={{ marginBottom: 16 }}>教师管理</h2>
|
|
<Space
|
|
style={{ marginBottom: 16 }}
|
|
wrap
|
|
className="responsive-toolbar responsive-toolbar--single"
|
|
>
|
|
<Input.Search
|
|
placeholder="搜索姓名/用户名"
|
|
allowClear
|
|
onSearch={(v) => {
|
|
setSearch(v);
|
|
setPage(1);
|
|
}}
|
|
style={{ width: 220 }}
|
|
/>
|
|
</Space>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={data}
|
|
rowKey="id"
|
|
loading={loading}
|
|
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 ? (
|
|
<Space wrap>
|
|
{r.classAssignments.map((a, i) => (
|
|
<Tag key={i} color="blue">
|
|
{ROLE_TYPE_LABELS[a.roleType] || a.roleType}: {a.className}
|
|
{a.subject ? ` — ${a.subject}` : ''}
|
|
</Tag>
|
|
))}
|
|
</Space>
|
|
) : null,
|
|
}}
|
|
/>
|
|
<Modal
|
|
title={`编辑档案 — ${profileModal?.name || ''}`}
|
|
open={!!profileModal && canEditTeachers}
|
|
onOk={canEditTeachers ? handleSaveProfile : undefined}
|
|
onCancel={() => setProfileModal(null)}
|
|
okText="保存"
|
|
confirmLoading={saving}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="subjects" label="任教学科">
|
|
<Select mode="tags" placeholder="输入学科后回车添加" />
|
|
</Form.Item>
|
|
<Form.Item name="joinedAt" label="入职日期">
|
|
<DatePicker style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="qualifications" label="资质/备注">
|
|
<Input.TextArea rows={3} placeholder="教师资格证、学历、备注等" />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TeachersPage;
|