feat: add teacher management page with profile editing

This commit is contained in:
2026-07-06 15:36:52 +08:00
parent 9e0d9ff39c
commit 9eec7c34be
2 changed files with 245 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ import BillsPage from './pages/Bills';
import RoomVisualPage from './pages/RoomVisual';
import OperationLogsPage from './pages/OperationLogs';
import UsersPage from './pages/Users';
import TeachersPage from './pages/Teachers';
import DepositsPage from './pages/Deposits';
import ClassroomsPage from './pages/Classrooms';
import ClassesPage from './pages/Classes';
@@ -173,6 +174,14 @@ const App: React.FC = () => {
</PermissionRoute>
}
/>
<Route
path="teachers"
element={
<PermissionRoute permission="user:view">
<TeachersPage />
</PermissionRoute>
}
/>
<Route
path="classrooms"
element={

View File

@@ -0,0 +1,236 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Table, Input, Button, Modal, Form, Select, DatePicker, Tag, Space, message } from 'antd';
import { EditOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import api from '../../api';
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: '老师',
class_teacher: '班主任',
dormitory_supervisor: '宿管',
institution_head: '机构负责人',
};
const ROLE_TYPE_LABELS: Record<string, string> = {
subject_teacher: '任课教师',
head_teacher: '班主任',
life_teacher: '生活老师',
academic_teacher: '教务老师',
};
const PAGE_SIZE = 20;
const TeachersPage: React.FC = () => {
const [data, setData] = useState<TeacherRow[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [profileModal, setProfileModal] = useState<TeacherRow | null>(null);
const [form] = Form.useForm<ProfileFormValues>();
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await api.get<TeacherListResponse>('/rbac/teachers', {
params: { search: search || undefined, page, pageSize: PAGE_SIZE },
});
setData(res.list);
setTotal(res.total);
} catch {
// silent
}
setLoading(false);
}, [page, search]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleSaveProfile = async () => {
const values = await form.validateFields();
if (!profileModal) return;
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);
}
};
const columns = [
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
{ 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']) => p?.subjects?.join('、') || '-',
},
{
title: '入职日期',
dataIndex: 'profile',
key: 'joinedAt',
width: 110,
render: (p: TeacherRow['profile']) => p?.joinedAt || '-',
},
{
title: '状态',
dataIndex: 'isActive',
key: 'status',
width: 80,
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: 80,
render: (_: unknown, r: TeacherRow) => (
<Button
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 || '',
});
}}
>
</Button>
),
},
];
return (
<div>
<h2 style={{ marginBottom: 16 }}></h2>
<Space style={{ marginBottom: 16 }}>
<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: 1200 }}
pagination={{
current: page,
pageSize: PAGE_SIZE,
total,
onChange: setPage,
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}
onOk={handleSaveProfile}
onCancel={() => setProfileModal(null)}
okText="保存"
>
<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;