feat: 学生档案与报告生成
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import React, { useState } from 'react';
|
||||
import { App, Button, DatePicker, Form, Input, Modal, Select, Table, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import api from '../../api';
|
||||
import { message } from '../../ui/app-message';
|
||||
import { useApiMutation } from '../../hooks/useApiMutation';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
import PermissionButton from '../PermissionButton';
|
||||
import { EditableArchiveCell } from './EditableArchiveCell';
|
||||
import { CLASS_TYPE_OPTIONS, COURSE_CATEGORY_OPTIONS, ENROLLMENT_STATUS_MAP, formatEnrollmentDisplayName, getClassTypeLabel, getCourseCategoryLabel, getEnrollmentStatus } from './shared';
|
||||
import type { EnrollmentRecord, TabProps } from './shared';
|
||||
|
||||
const ENROLLMENT_FIELDS = {
|
||||
courseCategory: 'courseCategory',
|
||||
classType: 'classType',
|
||||
className: 'className',
|
||||
headTeacher: 'headTeacher',
|
||||
subjectTeacher: 'subjectTeacher',
|
||||
startDate: 'startDate',
|
||||
endDate: 'endDate',
|
||||
status: 'status',
|
||||
} as const;
|
||||
|
||||
export const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
|
||||
data,
|
||||
studentId,
|
||||
}) => {
|
||||
const { modal } = App.useApp();
|
||||
const { hasPermission } = usePermission();
|
||||
const canPurgeArchive = hasPermission('archive:purge');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const addEnrollmentMutation = useApiMutation(
|
||||
async (payload: Record<string, unknown>) =>
|
||||
api.post(`/archive/${studentId}/enrollments`, payload),
|
||||
{ invalidate: [['archive', studentId]] },
|
||||
);
|
||||
const saveEnrollmentCellMutation = useApiMutation(
|
||||
async ({ id, field, value }: { id: number; field: string; value: unknown }) =>
|
||||
api.put(`/archive/enrollments/${id}`, { [field]: value }),
|
||||
{ invalidate: [['archive', studentId]] },
|
||||
);
|
||||
const purgeEnrollmentMutation = useApiMutation(
|
||||
async (id: number) => api.delete(`/archive/enrollments/${id}/permanent`),
|
||||
{ invalidate: [['archive', studentId]] },
|
||||
);
|
||||
|
||||
const handleAdd = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
await addEnrollmentMutation.mutateAsync({
|
||||
...values,
|
||||
startDate: values.startDate?.format('YYYY-MM-DD'),
|
||||
endDate: values.endDate?.format('YYYY-MM-DD'),
|
||||
});
|
||||
message.success('报读记录已添加');
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveCell = async (record: EnrollmentRecord, field: string, value: unknown) => {
|
||||
try {
|
||||
await saveEnrollmentCellMutation.mutateAsync({ id: record.id, field, value });
|
||||
message.success('报读记录已保存');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurge = (record: EnrollmentRecord) => {
|
||||
modal.confirm({
|
||||
title: `永久删除报读记录(${formatEnrollmentDisplayName(record)})?`,
|
||||
content: '删除后不可恢复,被考试成绩引用时将无法删除。确定继续?',
|
||||
okText: '永久删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await purgeEnrollmentMutation.mutateAsync(record.id);
|
||||
message.success('已永久删除(不可恢复)');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnsType<EnrollmentRecord> = [
|
||||
{
|
||||
title: '课程类别',
|
||||
dataIndex: 'courseCategory',
|
||||
render: (v: string, r) => (
|
||||
<EditableArchiveCell value={v} field={ENROLLMENT_FIELDS.courseCategory} record={r} editor="select" options={COURSE_CATEGORY_OPTIONS} onSave={saveCell}>
|
||||
{getCourseCategoryLabel(v)}
|
||||
</EditableArchiveCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '班型',
|
||||
dataIndex: 'classType',
|
||||
render: (v: string, r) => (
|
||||
<EditableArchiveCell value={v} field={ENROLLMENT_FIELDS.classType} record={r} editor="select" options={CLASS_TYPE_OPTIONS} onSave={saveCell}>
|
||||
{getClassTypeLabel(v)}
|
||||
</EditableArchiveCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '班级名称',
|
||||
dataIndex: 'className',
|
||||
render: (v: string, r) => (
|
||||
<EditableArchiveCell value={v} field={ENROLLMENT_FIELDS.className} record={r} onSave={saveCell}>
|
||||
{v || '-'}
|
||||
</EditableArchiveCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '班主任',
|
||||
dataIndex: 'headTeacher',
|
||||
render: (v: string, r) => (
|
||||
<EditableArchiveCell value={v} field={ENROLLMENT_FIELDS.headTeacher} record={r} onSave={saveCell}>
|
||||
{v || '-'}
|
||||
</EditableArchiveCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任课教师',
|
||||
dataIndex: 'subjectTeacher',
|
||||
render: (v: string, r) => (
|
||||
<EditableArchiveCell value={v} field={ENROLLMENT_FIELDS.subjectTeacher} record={r} onSave={saveCell}>
|
||||
{v || '-'}
|
||||
</EditableArchiveCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '开始日期',
|
||||
dataIndex: 'startDate',
|
||||
render: (v: string, r) => (
|
||||
<EditableArchiveCell value={v} field={ENROLLMENT_FIELDS.startDate} record={r} editor="date" onSave={saveCell}>
|
||||
{v || '-'}
|
||||
</EditableArchiveCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结束日期',
|
||||
dataIndex: 'endDate',
|
||||
render: (v: string, r) => (
|
||||
<EditableArchiveCell value={v} field={ENROLLMENT_FIELDS.endDate} record={r} editor="date" onSave={saveCell}>
|
||||
{v || '-'}
|
||||
</EditableArchiveCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (v: string, r) => {
|
||||
const status = getEnrollmentStatus(v);
|
||||
return (
|
||||
<EditableArchiveCell
|
||||
value={v}
|
||||
field={ENROLLMENT_FIELDS.status}
|
||||
record={r}
|
||||
editor="select"
|
||||
options={Object.entries(ENROLLMENT_STATUS_MAP).map(([value, item]) => ({
|
||||
value,
|
||||
label: item.text,
|
||||
}))}
|
||||
onSave={saveCell}
|
||||
>
|
||||
<Tag color={status.color}>{status.text}</Tag>
|
||||
</EditableArchiveCell>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: unknown, r: EnrollmentRecord) =>
|
||||
r.status === 'archived' && canPurgeArchive ? (
|
||||
<Button size="small" danger type="link" onClick={() => handlePurge(r)}>
|
||||
删除
|
||||
</Button>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PermissionButton
|
||||
permission="student:edit"
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setModalOpen(true);
|
||||
}}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
添加报读记录
|
||||
</PermissionButton>
|
||||
<Table<EnrollmentRecord>
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')}
|
||||
pagination={{
|
||||
defaultPageSize: 15,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [15, 30, 50],
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
title="添加报读记录"
|
||||
open={modalOpen && hasPermission('student:edit')}
|
||||
onOk={hasPermission('student:edit') ? handleAdd : undefined}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
confirmLoading={saving}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="courseCategory"
|
||||
label="课程类别"
|
||||
rules={[{ required: true, message: '请选择课程类别' }]}
|
||||
>
|
||||
<Select options={COURSE_CATEGORY_OPTIONS} placeholder="请选择" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="classType"
|
||||
label="班型"
|
||||
rules={[{ required: true, message: '请选择班型' }]}
|
||||
>
|
||||
<Select options={CLASS_TYPE_OPTIONS} placeholder="请选择" />
|
||||
</Form.Item>
|
||||
<Form.Item name="className" label="班级名称">
|
||||
<Input placeholder="如:2024届冲刺班" />
|
||||
</Form.Item>
|
||||
<Form.Item name="headTeacher" label="班主任">
|
||||
<Input placeholder="班主任姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="subjectTeacher" label="任课教师">
|
||||
<Input placeholder="任课教师姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="startDate" label="开始日期">
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="endDate" label="结束日期">
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user