forked from wangziqi/gongxue-base
Server: - Add DingTalk attendance import service with SSE progress streaming - Add IntegrationConfig entity & module for multi-tenant DingTalk setup - Add ExpenseType entity & ExpenseTypesModule - Add SeedModule for DB initialization - Add UserDingMapping entity for DingTalk user linkage - Attendance service: import flow with dedup & student auto-mapping - Rooms service: time-range overlap queries - Sync controller/service: DingTalk integration wiring - Permission guard: refactor to pure re-export - Campus scope middleware: tenant-aware filtering Admin UI: - Attendance page: import UI with progress & result summary - All pages: tableStyle/tablePagination standardization - Login page: responsive styling - Sensitive data: useViewSensitive hook for masked viewing - Vite config: path aliases, build optimization - Test infra: vitest config, test utilities Docs: PRD DingTalk batch 1 & 2 design docs
189 lines
4.9 KiB
TypeScript
189 lines
4.9 KiB
TypeScript
import React, { useEffect, useState, useMemo } from 'react';
|
|
import { Card, Tabs, Table, Tag, Empty, Spin, message } from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import api from '../../api';
|
|
|
|
interface AssignedClass {
|
|
classId: number;
|
|
className: string;
|
|
classCode: string;
|
|
roleType: string;
|
|
subject: string;
|
|
}
|
|
|
|
interface ScheduleItem {
|
|
id: number;
|
|
classId: number;
|
|
weekDay: number;
|
|
startTime: string;
|
|
endTime: string;
|
|
subject: string;
|
|
scheduleType: string;
|
|
}
|
|
|
|
interface StudentItem {
|
|
studentId: number;
|
|
studentName: string;
|
|
studentNo: string;
|
|
className: string;
|
|
classId: number;
|
|
joinDate: string;
|
|
}
|
|
|
|
interface WorkspaceData {
|
|
assignedClasses: AssignedClass[];
|
|
todaySchedules: ScheduleItem[];
|
|
myStudents: StudentItem[];
|
|
}
|
|
|
|
const ROLE_LABELS: Record<string, string> = {
|
|
head_teacher: '班主任',
|
|
life_teacher: '生活老师',
|
|
academic_teacher: '教务老师',
|
|
subject_teacher: '任课教师',
|
|
};
|
|
|
|
const WEEKDAY_LABELS: Record<string, string> = {
|
|
'1': '周一',
|
|
'2': '周二',
|
|
'3': '周三',
|
|
'4': '周四',
|
|
'5': '周五',
|
|
'6': '周六',
|
|
'7': '周日',
|
|
};
|
|
|
|
const TeacherWorkspacePage: React.FC = () => {
|
|
const [data, setData] = useState<WorkspaceData | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = (await api.get('/rbac/teacher-workspace')) as WorkspaceData;
|
|
setData(res);
|
|
} catch (e) {
|
|
console.error(e);
|
|
message.error('数据加载失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, []);
|
|
|
|
const classColumns: ColumnsType<AssignedClass> = useMemo(() => [
|
|
{
|
|
title: '班级名称',
|
|
dataIndex: 'className',
|
|
render: (v: string, r: AssignedClass) => `${v} (${r.classCode})`,
|
|
},
|
|
{
|
|
title: '角色',
|
|
dataIndex: 'roleType',
|
|
render: (v: string) => <Tag>{ROLE_LABELS[v] || v}</Tag>,
|
|
},
|
|
{
|
|
title: '科目',
|
|
dataIndex: 'subject',
|
|
render: (v: string | null) => v || '-',
|
|
},
|
|
], []);
|
|
|
|
const scheduleColumns: ColumnsType<ScheduleItem> = useMemo(() => [
|
|
{
|
|
title: '时间',
|
|
key: 'time',
|
|
render: (_: unknown, r: ScheduleItem) => `${r.startTime} - ${r.endTime}`,
|
|
},
|
|
{
|
|
title: '星期',
|
|
dataIndex: 'weekDay',
|
|
render: (v: number) => <Tag>{WEEKDAY_LABELS[String(v)] || v}</Tag>,
|
|
},
|
|
{
|
|
title: '科目',
|
|
dataIndex: 'subject',
|
|
},
|
|
{
|
|
title: '类型',
|
|
dataIndex: 'scheduleType',
|
|
render: (v: string) => (
|
|
<Tag color={v === 'INTERNAL' ? 'blue' : 'orange'}>
|
|
{v === 'INTERNAL' ? '内部课程' : '租赁'}
|
|
</Tag>
|
|
),
|
|
},
|
|
], []);
|
|
|
|
const studentColumns: ColumnsType<StudentItem> = useMemo(() => [
|
|
{ title: '姓名', dataIndex: 'studentName' },
|
|
{ title: '学号', dataIndex: 'studentNo', render: (v: string) => v || '-' },
|
|
{ title: '班级', dataIndex: 'className' },
|
|
{ title: '加入日期', dataIndex: 'joinDate', render: (v: string) => v || '-' },
|
|
], []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ textAlign: 'center', padding: 80 }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card title="教师工作台">
|
|
<Tabs
|
|
defaultActiveKey="classes"
|
|
items={[
|
|
{
|
|
key: 'classes',
|
|
label: `我的班级 (${data?.assignedClasses.length || 0})`,
|
|
children: data?.assignedClasses.length ? (
|
|
<Table<AssignedClass>
|
|
columns={classColumns}
|
|
dataSource={data.assignedClasses}
|
|
rowKey="classId"
|
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 个班级` }}
|
|
/>
|
|
) : (
|
|
<Empty description="暂无分配的班级" />
|
|
),
|
|
},
|
|
{
|
|
key: 'schedule',
|
|
label: `今日课程 (${data?.todaySchedules.length || 0})`,
|
|
children: data?.todaySchedules.length ? (
|
|
<Table<ScheduleItem>
|
|
columns={scheduleColumns}
|
|
dataSource={data.todaySchedules}
|
|
rowKey="id"
|
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 节` }}
|
|
/>
|
|
) : (
|
|
<Empty description="今日无排课" />
|
|
),
|
|
},
|
|
{
|
|
key: 'students',
|
|
label: `我的学生 (${data?.myStudents.length || 0})`,
|
|
children: data?.myStudents.length ? (
|
|
<Table<StudentItem>
|
|
columns={studentColumns}
|
|
dataSource={data.myStudents}
|
|
rowKey="studentId"
|
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 人` }}
|
|
/>
|
|
) : (
|
|
<Empty description="暂无学生" />
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default TeacherWorkspacePage;
|