feat: add teacher management page with profile editing
This commit is contained in:
@@ -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={
|
||||
|
||||
236
apps/admin/src/pages/Teachers/index.tsx
Normal file
236
apps/admin/src/pages/Teachers/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user