Files
gongxue-base/apps/admin/src/pages/Teachers/index.tsx

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;