|
@@ -0,0 +1,774 @@
|
|
|
+import { toValueEnum } from "@/common/converter";
|
|
|
+import { downloadFileByBlob } from "@/common/net/download";
|
|
|
+import { CardStepTitle, FileLink, FileUpload, SuperTable, TabBadge } from "@/components";
|
|
|
+import ReportButton from "@/components/ReportButton";
|
|
|
+import ExamAbsentReplaceAuditController from "@/services/apis/ExamAbsentReplaceAuditController";
|
|
|
+import ExamAbsentReplaceController from "@/services/apis/ExamAbsentReplaceController";
|
|
|
+import ExamGradeController from "@/services/apis/ExamGradeController";
|
|
|
+import ExamOrgDataReportController from "@/services/apis/ExamOrgDataReportController";
|
|
|
+import SysOrgController from "@/services/apis/SysOrgController";
|
|
|
+import { AuditStatus, DataReportStatus, DataReportType, ExamStatus, ResourceFileType } from "@/services/enums";
|
|
|
+import { BulbOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined } from "@ant-design/icons";
|
|
|
+import { ActionType, PageContainer, ProCard, ProColumns, ProDescriptions, ProDescriptionsItemProps, ProTable } from "@ant-design/pro-components";
|
|
|
+import { history, useModel, useParams } from "@umijs/max";
|
|
|
+import { useRequest } from "ahooks";
|
|
|
+import { Alert, App, Badge, Button, Card, Drawer, Space, Tag, Tour, Typography, theme } from "antd";
|
|
|
+import { useCallback, useRef, useState } from "react";
|
|
|
+import ExamAbsentReplaceDetailDrawer from "./components/ExamAbsentReplaceDetailDrawer";
|
|
|
+import ExamAbsentReplaceEditModal from "./components/ExamAbsentReplaceEditModal";
|
|
|
+
|
|
|
+const OrgExamAbsentReplaceReport: React.FC = () => {
|
|
|
+ const reqParams = useParams() as unknown as { examPlanId: number };
|
|
|
+
|
|
|
+ const { token } = theme.useToken();
|
|
|
+ const { getDictValueEnum, getKeyDict, getDict } = useModel('useDict');
|
|
|
+ const examStatusDict = getKeyDict('exam_status');
|
|
|
+ const dataReportStatusDict = getKeyDict('data_report_status');
|
|
|
+ const dataReportTypeDict = getKeyDict('data_report_type');
|
|
|
+ const auditStatusDict = getKeyDict('audit_status');
|
|
|
+ // const { baseData } = useModel('useBaseData');
|
|
|
+
|
|
|
+ const { initialState } = useModel('@@initialState');
|
|
|
+ const { currentUser } = initialState ?? {};
|
|
|
+
|
|
|
+ const [detailOpen, setDetailOpen] = useState(false);
|
|
|
+ const [remarkOpen, setRemarkOpen] = useState(false);
|
|
|
+ const [editOpen, setEditOpen] = useState(false);
|
|
|
+ const actionRef = useRef<ActionType>();
|
|
|
+ const currentRef = useRef<Partial<API.ExamAbsentReplaceOutput>>();
|
|
|
+
|
|
|
+ const [activeKey, setActiveKey] = useState<React.Key>('2');
|
|
|
+ const [statusCount, seStatusCount] = useState<Record<number, number>>({});
|
|
|
+
|
|
|
+ const [tourOpen, setTourOpen] = useState(false);
|
|
|
+ const tourAddRef = useRef(null);
|
|
|
+ const tourDownloadRef = useRef(null);
|
|
|
+ const tourUploadRef = useRef(null);
|
|
|
+ const tourReportRef = useRef(null);
|
|
|
+
|
|
|
+ const { message, modal, notification } = App.useApp();
|
|
|
+
|
|
|
+ const { data: gradeBranchData } = useRequest(async () => {
|
|
|
+ const res1 = await ExamGradeController.getListByExamPlanId({ examplanid: reqParams.examPlanId });
|
|
|
+ const res2 = await SysOrgController.getOrgBranchByOrgId({ orgid: currentUser?.sysOrgId ?? 0 });
|
|
|
+ return {
|
|
|
+ examGrades: res1 ?? [],
|
|
|
+ branches: res2 ?? [],
|
|
|
+ hasBranch: (res2?.length ?? 0) > 0,
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 加载上报数据
|
|
|
+ const { data: reportData, run: loadReport } = useRequest(() => {
|
|
|
+ return ExamOrgDataReportController.getByTypeExamPlanId({ type: DataReportType.ABSENT_REPLACE, examplanid: reqParams.examPlanId });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 加载数量统计
|
|
|
+ const loadCount = useCallback(async (params: API.ExamAbsentReplacePageInput) => {
|
|
|
+ const m = await ExamAbsentReplaceController.queryStatusCount(params);
|
|
|
+ const tc = m?.reduce((a, b) => a + b.count, 0) ?? 0;
|
|
|
+
|
|
|
+ const d: Record<number, number> = { 0: tc };
|
|
|
+ m?.forEach((t) => { d[t.status] = t.count; });
|
|
|
+
|
|
|
+ seStatusCount(d);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 加载统计数据
|
|
|
+ const { data: studentCountData, run: loadStudentCount, loading } = useRequest(() => {
|
|
|
+ return ExamAbsentReplaceController.getOrgGradeClassStudentCount({ examplanid: reqParams.examPlanId });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 统计数据列定义
|
|
|
+ let studentCountColumns: ProColumns<any>[] = [
|
|
|
+ {
|
|
|
+ title: '年级',
|
|
|
+ dataIndex: ['Grade', 'name'],
|
|
|
+ width: 64,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'left',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '合计',
|
|
|
+ dataIndex: 'GradeTotal',
|
|
|
+ width: 64,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'left',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ studentCountData?.classNumberList?.forEach(t => {
|
|
|
+ studentCountColumns.push({
|
|
|
+ title: `${t}班`,
|
|
|
+ dataIndex: `${t}`,
|
|
|
+ width: 56,
|
|
|
+ align: 'center',
|
|
|
+ render: (v) => v ? v : null,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ studentCountColumns.push({});
|
|
|
+
|
|
|
+ // 删除
|
|
|
+ const handleDelete = useCallback((id: number) => {
|
|
|
+ modal.confirm({
|
|
|
+ title: '警告',
|
|
|
+ content: '确定立即删除吗',
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ centered: true,
|
|
|
+ onOk: async () => {
|
|
|
+ await ExamAbsentReplaceController.del({ id });
|
|
|
+ message.success('已删除');
|
|
|
+ actionRef.current?.reload();
|
|
|
+ loadStudentCount();
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 上报
|
|
|
+ const handleSubmit = useCallback(() => {
|
|
|
+ return new Promise<void>((resolve, reject) => {
|
|
|
+ if ((studentCountData?.total ?? 0) > 0 && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) === 0) {
|
|
|
+ message.error('未上传《缺测替补学生明细表》和《会议记录》打印盖章的扫描电子文件');
|
|
|
+ reject();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let content = `共 ${studentCountData?.total ?? 0} 个学生,上报后不能再修改,确定立即上报吗?`;
|
|
|
+ modal.confirm({
|
|
|
+ title: '警告',
|
|
|
+ content,
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ centered: true,
|
|
|
+ onOk: async () => {
|
|
|
+ try {
|
|
|
+ await ExamOrgDataReportController.submit({ examPlanId: reqParams.examPlanId, type: DataReportType.ABSENT_REPLACE })
|
|
|
+ message.success('已上报');
|
|
|
+ loadReport();
|
|
|
+ resolve();
|
|
|
+ }
|
|
|
+ catch (ex) {
|
|
|
+ const exm = ex as any;
|
|
|
+ notification.error({
|
|
|
+ message: '上报失败',
|
|
|
+ description: exm?.info?.errorMessage ? `${exm?.info?.errorMessage}!请仔细检查缺测替补学生明细信息和佐证材料` : JSON.stringify(ex),
|
|
|
+ })
|
|
|
+ reject();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onCancel: () => reject(),
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }, [studentCountData, reportData]);
|
|
|
+
|
|
|
+ // 删除佐证材料
|
|
|
+ const handleDeleteAttachment = useCallback((id: number, fileId: string) => {
|
|
|
+ modal.confirm({
|
|
|
+ title: '警告',
|
|
|
+ content: '确定立即删除吗',
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ centered: true,
|
|
|
+ onOk: async () => {
|
|
|
+ await ExamAbsentReplaceController.delAttachment({ sourceId: id, fileId });
|
|
|
+ message.success('已删除');
|
|
|
+ actionRef.current?.reload();
|
|
|
+ loadStudentCount();
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 删除上报佐证材料
|
|
|
+ const handleDeleteReportAttachment = useCallback(async (id: number, fileId: string) => {
|
|
|
+ modal.confirm({
|
|
|
+ title: '警告',
|
|
|
+ content: '确定立即删除吗',
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ centered: true,
|
|
|
+ onOk: async () => {
|
|
|
+ await ExamOrgDataReportController.delAttachment({ sourceId: id, fileId });
|
|
|
+ message.success('已删除');
|
|
|
+ loadReport();
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 下载打印文件
|
|
|
+ const handleDownloadPrintFile = useCallback(async () => {
|
|
|
+ const res = await ExamAbsentReplaceController.exportPrintTable({ examplanid: reqParams.examPlanId });
|
|
|
+ if (res) {
|
|
|
+ downloadFileByBlob(res.data, res.fileName);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ message.error('下载失败');
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 提交单个学生审核
|
|
|
+ const handleStudentSubmit = useCallback((id: number) => {
|
|
|
+ modal.confirm({
|
|
|
+ title: '警告',
|
|
|
+ content: '确定立即提交吗',
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ centered: true,
|
|
|
+ onOk: async () => {
|
|
|
+ await ExamAbsentReplaceAuditController.submit({ id });
|
|
|
+ message.success('已提交');
|
|
|
+ actionRef.current?.reload();
|
|
|
+ loadStudentCount();
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 是否可操作和上报
|
|
|
+ const reportable = (reportData?.examDataReport?.status === ExamStatus.ACTIVE && reportData?.isExpired === false &&
|
|
|
+ (reportData?.examOrgDataReport?.status === DataReportStatus.UNREPORT ||
|
|
|
+ reportData?.examOrgDataReport?.status === DataReportStatus.REJECTED));
|
|
|
+
|
|
|
+ // 工具栏按定义
|
|
|
+ let detailActions = [];
|
|
|
+ if (reportable) {
|
|
|
+ detailActions.push(
|
|
|
+ <Button
|
|
|
+ key="add"
|
|
|
+ ref={tourAddRef}
|
|
|
+ type="primary"
|
|
|
+ disabled={!reportable}
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ onClick={() => {
|
|
|
+ currentRef.current = { id: 0, isReplaced: true };
|
|
|
+ setEditOpen(true);
|
|
|
+ }}
|
|
|
+ >添加缺测替补学生</Button>
|
|
|
+ );
|
|
|
+ // detailActions.push(
|
|
|
+ // <Button
|
|
|
+ // key="import"
|
|
|
+ // disabled={!reportable}
|
|
|
+ // icon={<UploadOutlined />}
|
|
|
+ // onClick={() => history.push(`/exam-s/sp-stu/import/${reqParams.examPlanId}`)}
|
|
|
+ // >批量导入</Button>
|
|
|
+ // );
|
|
|
+ detailActions.push(
|
|
|
+ <Button
|
|
|
+ key="download"
|
|
|
+ ref={tourDownloadRef}
|
|
|
+ disabled={!reportable}
|
|
|
+ icon={<DownloadOutlined />}
|
|
|
+ onClick={handleDownloadPrintFile}
|
|
|
+ >下载打印表格文件</Button>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 明细表列定义
|
|
|
+ const detailColumns: ProColumns<API.ExamAbsentReplaceOutput>[] = [
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: 'status',
|
|
|
+ hideInSearch: true,
|
|
|
+ width: 80,
|
|
|
+ align: 'center',
|
|
|
+ valueEnum: getDictValueEnum('audit_status', true),
|
|
|
+ // render: (_, r) => {
|
|
|
+ // const s = auditStatusDict[r.status];
|
|
|
+ // return <Tag color={s.antStatus} style={{ marginRight: 0 }}>{s.name}</Tag>;
|
|
|
+ // },
|
|
|
+ render: (_, r) => {
|
|
|
+ const s = auditStatusDict[r.status];
|
|
|
+ return (
|
|
|
+ <Space direction="vertical" size="small" style={{ lineHeight: 1 }}>
|
|
|
+ <Tag color={s.antStatus} style={{ marginRight: 0 }}>{s.name}</Tag>
|
|
|
+ <Typography.Text
|
|
|
+ type={r.isReplaced ? 'warning' : 'danger'}
|
|
|
+ style={{ fontSize: token.fontSizeSM }}
|
|
|
+ >
|
|
|
+ {r.isReplaced ? '有替补' : '无替补'}
|
|
|
+ </Typography.Text>
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ...(gradeBranchData?.hasBranch ? [{
|
|
|
+ title: '校区',
|
|
|
+ dataIndex: 'sysOrgBranchId',
|
|
|
+ width: 96,
|
|
|
+ align: 'center',
|
|
|
+ hideInSearch: !gradeBranchData?.hasBranch,
|
|
|
+ valueEnum: toValueEnum(gradeBranchData?.branches ?? []),
|
|
|
+ } as ProColumns] : []),
|
|
|
+ {
|
|
|
+ title: '年级',
|
|
|
+ dataIndex: 'gradeId',
|
|
|
+ width: 80,
|
|
|
+ align: 'center',
|
|
|
+ valueEnum: toValueEnum(gradeBranchData?.examGrades?.map(t => ({ id: t.gradeId, name: `${t.grade.name}(${t.gradeBeginName})` })) ?? []),
|
|
|
+ render: (_, r) => {
|
|
|
+ return (
|
|
|
+ <Space size="small" direction="vertical" style={{ lineHeight: 1 }}>
|
|
|
+ {r.examGrade?.grade.name}
|
|
|
+ <Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM }}>({r.examGrade?.gradeBeginName})</Typography.Text>
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '班级',
|
|
|
+ dataIndex: 'classNumber',
|
|
|
+ width: 64,
|
|
|
+ align: 'center',
|
|
|
+ renderText: (v) => `${v}班`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '姓名',
|
|
|
+ dataIndex: 'absentName',
|
|
|
+ width: 112,
|
|
|
+ align: 'center',
|
|
|
+ render: (v, r) => <a onClick={() => { currentRef.current = r; setDetailOpen(true); }}>{v}</a>,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '监测号',
|
|
|
+ dataIndex: 'absentExamNumber',
|
|
|
+ width: 160,
|
|
|
+ align: 'center',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '缺测替补原因',
|
|
|
+ dataIndex: 'absentReason',
|
|
|
+ hideInSearch: true,
|
|
|
+ width: 240,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '缺测科目',
|
|
|
+ dataIndex: 'absentCourseList',
|
|
|
+ width: 240,
|
|
|
+ render: (_, r) => {
|
|
|
+ return r.absentCourseList?.map(t => t.name).join('、');
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '家长电话',
|
|
|
+ dataIndex: 'patriarchTel',
|
|
|
+ hideInSearch: true,
|
|
|
+ width: 128,
|
|
|
+ align: 'center',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '替补学生姓名',
|
|
|
+ dataIndex: 'replaceName',
|
|
|
+ width: 112,
|
|
|
+ align: 'center',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '替补学生监测号',
|
|
|
+ dataIndex: 'replaceExamNumber',
|
|
|
+ width: 160,
|
|
|
+ align: 'center',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '备注',
|
|
|
+ dataIndex: 'remark',
|
|
|
+ hideInSearch: true,
|
|
|
+ width: 120,
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ title: '佐证材料(每个学生最多上传3个)',
|
|
|
+ valueType: 'option',
|
|
|
+ hideInSearch: true,
|
|
|
+ width: 280 + token.paddingXS * 2,
|
|
|
+ // className: 'minw-280',
|
|
|
+ fixed: 'right',
|
|
|
+ render: (_, r) => {
|
|
|
+ let editable = reportable && [AuditStatus.UNSUBMIT, AuditStatus.REJECTED, AuditStatus.AUDIT].includes(r.status);
|
|
|
+ if (!editable) {
|
|
|
+ editable = r.status === AuditStatus.REJECTED && reportData?.examDataReport?.status === ExamStatus.ACTIVE;
|
|
|
+ }
|
|
|
+ const li = r.attachmentList?.map((t, i) => {
|
|
|
+ return (
|
|
|
+ <FileLink
|
|
|
+ key={i}
|
|
|
+ fileExtName={t.fileExtName}
|
|
|
+ fileName={t.fileName}
|
|
|
+ url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
|
|
|
+ thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
|
|
|
+ card
|
|
|
+ onDelete={editable ? async () => handleDeleteAttachment(r.id, t.fileId) : undefined}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ });
|
|
|
+ return (
|
|
|
+ <Space direction="vertical" style={{ width: 280 }}>
|
|
|
+ {li}
|
|
|
+ {editable && (li?.length ?? 0) < 3 &&
|
|
|
+ <FileUpload
|
|
|
+ addText="添加文件"
|
|
|
+ tipText="仅支持PDF或图片文件"
|
|
|
+ accept="image/jpeg,image/png,image/gif,application/pdf"
|
|
|
+ limitSize={20}
|
|
|
+ onUpload={async (file, onUploadProgress) => {
|
|
|
+ const fsp = file.name.split('.');
|
|
|
+ let extName = '';
|
|
|
+ if (fsp.length > 1) {
|
|
|
+ extName = fsp[fsp.length - 1].toLowerCase();
|
|
|
+ }
|
|
|
+ if (extName === '' || !['jpg', 'jpeg', 'png', 'gif', 'pdf'].includes(extName)) {
|
|
|
+ message.error('文件类型错误,请重新选择!')
|
|
|
+ return { success: false, errorType: 'fileTypeError', errorMessage: '文件类型错误,请选择扩展名为.jpg、.jpeg、.png或.pdf的文件' };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('type', `${ResourceFileType.EXAM_ABSENT_REPLACE}`);
|
|
|
+ formData.append('sourceId', `${r.id}`);
|
|
|
+ formData.append('fileName', `${file.name}`);
|
|
|
+ formData.append('file', file);
|
|
|
+ await ExamAbsentReplaceController.uploadAttachment(formData, {
|
|
|
+ onUploadProgress: (p: any) => {
|
|
|
+ const progress = parseFloat((p.loaded / p.total * 100).toFixed(1));
|
|
|
+ onUploadProgress?.(progress);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ actionRef.current?.reload();
|
|
|
+ return { success: true };
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return { success: false };
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ }
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '佐证材料',
|
|
|
+ hideInSearch: true,
|
|
|
+ hideInTable: true,
|
|
|
+ render: (_, r) => {
|
|
|
+ const li = r.attachmentList?.map((t, i) => {
|
|
|
+ return (
|
|
|
+ <FileLink
|
|
|
+ key={i}
|
|
|
+ fileExtName={t.fileExtName}
|
|
|
+ fileName={t.fileName}
|
|
|
+ url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
|
|
|
+ thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
|
|
|
+ card
|
|
|
+ />
|
|
|
+ );
|
|
|
+ });
|
|
|
+ return (
|
|
|
+ <Space direction="vertical" style={{ width: '100%' }}>
|
|
|
+ {li?.length === 0 ? '未上传' : li}
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ valueType: 'option',
|
|
|
+ width: 96,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'right',
|
|
|
+ render: (_, r) => {
|
|
|
+ if (reportable && [AuditStatus.UNSUBMIT, AuditStatus.REJECTED, AuditStatus.AUDIT].includes(r.status)) {
|
|
|
+ return (
|
|
|
+ <Space>
|
|
|
+ <a onClick={() => { currentRef.current = r; setEditOpen(true); }}>修改</a>
|
|
|
+ <a onClick={() => handleDelete(r.id)}>删除</a>
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (reportData?.examDataReport?.status === ExamStatus.ACTIVE && r.status === AuditStatus.REJECTED) {
|
|
|
+ return (
|
|
|
+ <Space>
|
|
|
+ <a onClick={() => { currentRef.current = r; setEditOpen(true); }}>修改</a>
|
|
|
+ <a onClick={() => handleDelete(r.id)}>删除</a>
|
|
|
+ <a onClick={() => handleStudentSubmit(r.id)}>提交</a>
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ },
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 呈现状态 tab
|
|
|
+ const renderTabItems = useCallback(() => {
|
|
|
+ let items: { key: string; label: React.ReactNode }[] = [];
|
|
|
+ items = items.concat(
|
|
|
+ getDict('audit_status')?.filter(t => t.value > 1)?.sort((a, b) => a.sort - b.sort)?.map((t) => ({
|
|
|
+ key: `${t.value}`,
|
|
|
+ label: (
|
|
|
+ <span>
|
|
|
+ {t.name}
|
|
|
+ <TabBadge color={t.antColor} count={statusCount[t.value] ?? 0} active={activeKey === `${t.value}`} />
|
|
|
+ </span>
|
|
|
+ ),
|
|
|
+ })),
|
|
|
+ );
|
|
|
+ items.push({
|
|
|
+ key: '0',
|
|
|
+ label: (<span>全部<TabBadge count={statusCount[0]} active={activeKey === 0} /></span>),
|
|
|
+ });
|
|
|
+ return items;
|
|
|
+ }, [activeKey, statusCount]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PageContainer
|
|
|
+ title={`${reportData?.examPlan?.fullName ?? ''} - 缺测替补学生上报`}
|
|
|
+ onBack={() => history.back()}
|
|
|
+ extra={reportable &&
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ icon={<BulbOutlined />}
|
|
|
+ onClick={() => {
|
|
|
+ window.scrollTo({ top: 0 });
|
|
|
+ setTourOpen(true);
|
|
|
+ }}
|
|
|
+ >查看操作指引</Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <ProCard
|
|
|
+ title={<CardStepTitle>基本情况</CardStepTitle>}
|
|
|
+ extra={reportData?.examOrgDataReport?.status &&
|
|
|
+ <Tag
|
|
|
+ style={{ marginRight: 0 }}
|
|
|
+ color={dataReportStatusDict[reportData.examOrgDataReport.status].antColor}
|
|
|
+ >
|
|
|
+ {dataReportStatusDict[reportData.examOrgDataReport.status].name}
|
|
|
+ </Tag>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <ProDescriptions>
|
|
|
+ <ProDescriptions.Item label="上报类型">
|
|
|
+ {reportData?.examDataReport?.type ? dataReportTypeDict[reportData?.examDataReport?.type].name : ''}
|
|
|
+ </ProDescriptions.Item>
|
|
|
+ <ProDescriptions.Item label="监测上报">
|
|
|
+ {reportData?.examDataReport?.status &&
|
|
|
+ <Badge
|
|
|
+ status={examStatusDict[reportData.examDataReport.status].antStatus as any}
|
|
|
+ text={examStatusDict[reportData.examDataReport.status].name}
|
|
|
+ />
|
|
|
+ }
|
|
|
+ </ProDescriptions.Item>
|
|
|
+ <ProDescriptions.Item label="截止时间">
|
|
|
+ <Typography.Text strong>
|
|
|
+ {reportData?.examDataReport?.endTime}
|
|
|
+ {reportData?.isExpired && reportData.examDataReport?.status === ExamStatus.ACTIVE &&
|
|
|
+ <Typography.Text type="danger">(已截止)</Typography.Text>
|
|
|
+ }
|
|
|
+ </Typography.Text>
|
|
|
+ </ProDescriptions.Item>
|
|
|
+ <ProDescriptions.Item label="上报人员">{reportData?.examOrgDataReport?.reportSysUser?.name}</ProDescriptions.Item>
|
|
|
+ <ProDescriptions.Item label="上报时间">{reportData?.examOrgDataReport?.reportTime}</ProDescriptions.Item>
|
|
|
+ <ProDescriptions.Item label="备注说明">{reportData?.examOrgDataReport?.remark}</ProDescriptions.Item>
|
|
|
+ <ProDescriptions.Item label="上报说明" span={3}>
|
|
|
+ <Typography.Text ellipsis>{reportData?.examDataReport?.remark || '无'}</Typography.Text>
|
|
|
+ {reportData?.examDataReport?.remark && <Button type="link" size="small" onClick={() => setRemarkOpen(true)}>详情</Button>}
|
|
|
+ </ProDescriptions.Item>
|
|
|
+ </ProDescriptions>
|
|
|
+ <Drawer open={remarkOpen} title="上报说明" width={720} maskClosable onClose={() => setRemarkOpen(false)}>
|
|
|
+ <pre>{reportData?.examDataReport?.remark || '无'}</pre>
|
|
|
+ </Drawer>
|
|
|
+ </ProCard>
|
|
|
+
|
|
|
+ <ProCard
|
|
|
+ style={{ marginTop: token.margin }}
|
|
|
+ title={<CardStepTitle>缺测替补学生上报</CardStepTitle>}
|
|
|
+ extra={reportable &&
|
|
|
+ <Space>
|
|
|
+ <span key="tooltip">离上报结束还有</span>
|
|
|
+ <ReportButton
|
|
|
+ key="time"
|
|
|
+ buttonRef={tourReportRef}
|
|
|
+ expireTime={reportData?.examDataReport?.endTime}
|
|
|
+ showIcon
|
|
|
+ onSubmit={handleSubmit}
|
|
|
+ onTimerFinish={() => loadReport()}
|
|
|
+ />
|
|
|
+ </Space>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <Alert
|
|
|
+ type="warning"
|
|
|
+ message={<Typography.Title level={5}>上报步骤:</Typography.Title>}
|
|
|
+ description={
|
|
|
+ <Typography>
|
|
|
+ <ol>
|
|
|
+ <li>在下方 <Typography.Text strong>缺测替补学生明细</Typography.Text> 中录入缺测替补学生信息,<Typography.Text type="danger">并上传佐证材料(必传)</Typography.Text>;</li>
|
|
|
+ <li>缺测替补学生信息录入完成后,点击 <Typography.Text strong>下载打印表格文件</Typography.Text> 下载文件打印签字盖章;</li>
|
|
|
+ <li>扫描已签字盖章的文件为电子文档(PDF或图片);</li>
|
|
|
+ <li>在 <Typography.Text strong> 缺测替补学生上报</Typography.Text> 的 <Typography.Text strong>上传《缺测替补学生明细表》打印、签字、盖章的扫描电子文件</Typography.Text> 中上传电子文档<Typography.Text type="danger">(必传)</Typography.Text>;</li>
|
|
|
+ <li>确认无误后点击 <Typography.Text strong> 缺测替补学生上报</Typography.Text> 右侧的 <Typography.Text strong>立即上报</Typography.Text> 按钮完成上传。</li>
|
|
|
+ </ol>
|
|
|
+ </Typography>
|
|
|
+ }
|
|
|
+ />
|
|
|
+
|
|
|
+ <Typography.Title level={5} style={{ marginTop: token.marginSM }} type="danger">上传《缺测替补学生明细表》打印、签字、盖章的扫描电子文件(最多上传6个文件):</Typography.Title>
|
|
|
+ <Card bordered ref={tourUploadRef}>
|
|
|
+ <Space direction="vertical" style={{ width: '100%' }}>
|
|
|
+ {reportData?.examOrgDataReport?.attachmentList?.map((t, i) => {
|
|
|
+ return (
|
|
|
+ <FileLink
|
|
|
+ key={i}
|
|
|
+ fileExtName={t.fileExtName}
|
|
|
+ fileName={t.fileName}
|
|
|
+ url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
|
|
|
+ thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
|
|
|
+ card
|
|
|
+ onDelete={reportable ? async () => handleDeleteReportAttachment(reportData.examOrgDataReportId ?? 0, t.fileId) : undefined}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ }) ?? null}
|
|
|
+ {reportable && reportData?.examOrgDataReport && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) < 6 &&
|
|
|
+ <FileUpload
|
|
|
+ addText="添加文件"
|
|
|
+ tipText="仅支持PDF或图片文件"
|
|
|
+ accept="image/jpeg,image/png,image/gif,application/pdf"
|
|
|
+ limitSize={20}
|
|
|
+ onUpload={async (file, onUploadProgress) => {
|
|
|
+ const fsp = file.name.split('.');
|
|
|
+ let extName = '';
|
|
|
+ if (fsp.length > 1) {
|
|
|
+ extName = fsp[fsp.length - 1].toLowerCase();
|
|
|
+ }
|
|
|
+ if (extName === '' || !['jpg', 'jpeg', 'png', 'gif', 'pdf'].includes(extName)) {
|
|
|
+ message.error('文件类型错误,请重新选择!')
|
|
|
+ return { success: false, errorType: 'fileTypeError', errorMessage: '文件类型错误,请选择扩展名为.jpg、.jpeg、.png或.pdf的文件' };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('type', `${ResourceFileType.EXAM_ORG_DATA_REPORT}`);
|
|
|
+ formData.append('dataReportType', `${DataReportType.ABSENT_REPLACE}`);
|
|
|
+ formData.append('examPlanId', `${reportData?.examPlan?.id ?? 0}`);
|
|
|
+ formData.append('sourceId', `${reportData?.examOrgDataReportId ?? 0}`);
|
|
|
+ formData.append('fileName', `${file.name}`);
|
|
|
+ formData.append('file', file);
|
|
|
+ await ExamOrgDataReportController.uploadAttachment(formData, {
|
|
|
+ onUploadProgress: (p: any) => {
|
|
|
+ const progress = parseFloat((p.loaded / p.total * 100).toFixed(1));
|
|
|
+ onUploadProgress?.(progress);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ loadReport();
|
|
|
+ return { success: true };
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return { success: false };
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ }
|
|
|
+ </Space>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Typography.Title level={5} style={{ marginTop: token.marginSM }}>缺测替补学生人数统计:</Typography.Title>
|
|
|
+ <ProTable<any>
|
|
|
+ style={{ marginTop: token.margin }}
|
|
|
+ search={false}
|
|
|
+ pagination={false}
|
|
|
+ size="small"
|
|
|
+ bordered
|
|
|
+ options={{ setting: false, density: false, reloadIcon: <ReloadOutlined onClick={loadStudentCount} /> }}
|
|
|
+ rowKey="GradeId"
|
|
|
+ loading={loading}
|
|
|
+ columns={studentCountColumns}
|
|
|
+ columnEmptyText=""
|
|
|
+ dataSource={studentCountData?.items ?? []}
|
|
|
+ toolBarRender={false}
|
|
|
+ />
|
|
|
+ </ProCard>
|
|
|
+
|
|
|
+ <SuperTable<API.ExamAbsentReplaceOutput>
|
|
|
+ headerTitle={<CardStepTitle>缺测替补学生明细</CardStepTitle>}
|
|
|
+ style={{ marginTop: token.margin }}
|
|
|
+ actionRef={actionRef}
|
|
|
+ scroll={{ x: '100%' }}
|
|
|
+ rowKey="id"
|
|
|
+ columns={detailColumns}
|
|
|
+ // search={{ showHiddenNum: false }}
|
|
|
+ search={false}
|
|
|
+ options={{ setting: false, fullScreen: false }}
|
|
|
+ toolbar={{
|
|
|
+ menu: {
|
|
|
+ type: 'tab',
|
|
|
+ activeKey: activeKey,
|
|
|
+ items: renderTabItems(),
|
|
|
+ onChange: (key) => {
|
|
|
+ setActiveKey(key as React.Key);
|
|
|
+ actionRef.current?.reload();
|
|
|
+ },
|
|
|
+ },
|
|
|
+ actions: detailActions,
|
|
|
+ }}
|
|
|
+ request={async (params, sort) => {
|
|
|
+ return SuperTable.requestPageAgent({ params, sort }, async (p) => {
|
|
|
+ const qparams = { ...p, examPlanId: reqParams.examPlanId };
|
|
|
+ await loadCount(qparams);
|
|
|
+ const res = await ExamAbsentReplaceController.queryPageList({
|
|
|
+ ...qparams,
|
|
|
+ status: activeKey !== '0' ? parseInt(activeKey as string) : undefined,
|
|
|
+ });
|
|
|
+ return res;
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ />
|
|
|
+
|
|
|
+ {editOpen && currentRef.current && gradeBranchData &&
|
|
|
+ <ExamAbsentReplaceEditModal
|
|
|
+ examPlanId={reqParams.examPlanId}
|
|
|
+ data={currentRef.current}
|
|
|
+ gradeBranch={gradeBranchData}
|
|
|
+ onFinish={() => {
|
|
|
+ actionRef.current?.reload();
|
|
|
+ loadStudentCount();
|
|
|
+ }}
|
|
|
+ onClose={() => setEditOpen(false)}
|
|
|
+ />
|
|
|
+ }
|
|
|
+
|
|
|
+ {detailOpen && currentRef.current &&
|
|
|
+ <ExamAbsentReplaceDetailDrawer
|
|
|
+ data={currentRef.current}
|
|
|
+ columns={detailColumns as ProDescriptionsItemProps<Partial<API.ExamAbsentReplaceOutput>>[]}
|
|
|
+ onClose={() => setDetailOpen(false)}
|
|
|
+ />
|
|
|
+ }
|
|
|
+
|
|
|
+ <Tour
|
|
|
+ steps={[
|
|
|
+ {
|
|
|
+ title: '添加缺测替补学生信息',
|
|
|
+ description: '添加完缺测替补学生信息后,在列表中上传相应的佐证材料(一个学生最多3个图片或PDF)',
|
|
|
+ target: () => tourAddRef.current,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '下载打印表格文件',
|
|
|
+ description: '打印表格按年级分页,按文件要求签字和盖章',
|
|
|
+ target: () => tourDownloadRef.current,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '上传签字盖章文件',
|
|
|
+ description: '打印文件签字盖章后,扫描成电子文件(PDF或图片)上传,推荐PDF文件,最多上传6个文件',
|
|
|
+ target: () => tourUploadRef.current,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '上报缺测替补学生信息',
|
|
|
+ description: '确认无误后立即上报',
|
|
|
+ target: () => tourReportRef.current,
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ open={tourOpen}
|
|
|
+ onClose={() => setTourOpen(false)}
|
|
|
+ />
|
|
|
+ </PageContainer>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default OrgExamAbsentReplaceReport;
|