import { CardStepTitle, StatusIcon, UploadWrongLocalHeaderError } from "@/components"; import ExamGradeController from "@/services/apis/ExamGradeController"; import ExamPlanController from "@/services/apis/ExamPlanController"; import ExamStudentController from "@/services/apis/ExamStudentController"; import SysOrgController from "@/services/apis/SysOrgController"; import { CertificateType, DataImportMode, EducationStage, Gender } from "@/services/enums"; import { BulbOutlined, InboxOutlined } from "@ant-design/icons"; import { ActionType, PageContainer, ProCard, ProColumns, ProForm, ProFormInstance, ProFormSelect, ProTable } from "@ant-design/pro-components"; import { useModel, useParams } from "@umijs/max"; import { useRequest } from "ahooks"; import { Alert, App, Button, Space, Table, Tooltip, Tour, Typography, Upload, theme } from "antd"; import type { RcFile, UploadFile, UploadProps } from "antd/lib/upload/interface"; import lodash from 'lodash'; import { useRef, useState } from "react"; import ExamStudentImportEditModal from "./components/ExamStudentImportEditModal"; type ClassTotalItem = { classNumber: number; total: number; success: number; error: number; }; type ImportFormData = { examGradeId: number; sysOrgBranchId?: number; }; /** 监测学生批量导入 */ const OrgExamStudentImport: React.FC = () => { const reqParams = useParams() as unknown as { examPlanId: number }; const formRef = useRef>(); const actionRef = useRef(); const [tourOpen, setTourOpen] = useState(false); const currentRef = useRef(); const [editOpen, setEditOpen] = useState(false); const [grade, setGrade] = useState(); const { token } = theme.useToken(); const { message, modal } = App.useApp(); const { getDictValueEnum } = useModel('useDict'); const { initialState } = useModel('@@initialState'); const { currentUser } = initialState ?? {}; const [fileList, setFileList] = useState([]); const [uploading, setUploading] = useState(false); const [data, setData] = useState(); const [classTotal, setClassTotal] = useState(); // const [dataImportMode, setDataImportMode] = useState(DataImportMode.CLEAR_APPEND); const tourGradeRef = useRef(null); const tourDownloadRef = useRef(null); const tourChooseFileRef = useRef(null); const tourUploadRef = useRef(null); const tourStuCountRef = useRef(null); const tourStuDetailRef = useRef(null); const tourSubmitRef = useRef(null); const { data: baseData, loading } = useRequest(async () => { const res1 = await ExamGradeController.getListByExamPlanId({ examplanid: reqParams.examPlanId }); const res2 = await SysOrgController.getOrgBranchByOrgId({ orgid: currentUser?.sysOrgId ?? 0 }); const res3 = await ExamPlanController.getById({ id: reqParams.examPlanId }); return { examGrades: res1 ?? [], branches: res2 ?? [], hasNceeCourseComb: res3?.educationStage === EducationStage.SENIOR_HIGH_SCHOOL_STAGE, }; }); const uploadProps: UploadProps = { onRemove: () => { setFileList([]); }, beforeUpload: (file) => { setFileList([file]); return false; }, fileList, multiple: false, accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel', }; const handleBack = () => history.back(); // 计算统计数据 const handleClassTotal = (rows: API.UploadExamStudentOutput[]) => { let cts: ClassTotalItem[] = []; lodash.forEach(lodash.countBy(rows, 'classNumber'), function (v, k) { const classNumber = parseInt(k); const success = rows.filter(t => t.classNumber === classNumber && t.isSuccess === true)?.length ?? 0; cts.push({ classNumber, total: v, success, error: v - success, }); }); setClassTotal(cts); } // 上传 const handleUpload = async () => { if (!grade) { message.error('请先选择年级'); return; } const formData = new FormData(); formData.append('examPlanId', `${reqParams.examPlanId}`); formData.append('examGradeId', `${grade?.id}`); fileList.forEach((file) => { formData.append('file', file as RcFile); }); setUploading(true); try { const res = await ExamStudentController.upload(formData); setData(res); handleClassTotal(res?.rows ?? []); } catch { } finally { setUploading(false); } } // 保存编辑 const handleEdit = (v: API.UploadExamStudentOutput) => { const i = data?.rows?.findIndex(t => t.rowNumber === v.rowNumber) ?? -1; if (i !== -1) { const d = { ...data }; if (d.rows) { d.rows[i] = v; handleClassTotal(d.rows ?? []); setData(d); } } } // 删除行 const handleDelete = (rowNumber: number) => { modal.confirm({ title: '警告', content: '确定立即删除吗', okText: '确定', cancelText: '取消', centered: true, onOk: async () => { const i = data?.rows?.findIndex(t => t.rowNumber === rowNumber) ?? -1; if (i !== -1) { const d = { ...data }; if (d.rows) { d.rows.splice(i, 1); setData(d); handleClassTotal(d.rows ?? []); message.success('已删除'); return; } } message.error('删除失败'); }, }); } // 确认导入 const handleImport = async () => { const sysOrgId = currentUser?.sysOrgId; if (!sysOrgId) { message.error('获取机构ID错误'); return; } try { const importFormValues = await formRef.current?.validateFields(); if (!importFormValues) { return; } const { examGradeId, ...restValues } = importFormValues; const grade = baseData?.examGrades.find(t => t.id === examGradeId); if (!grade) { return; } const items = [...(data?.rows ?? [])]; if (items.some(t => t.isSuccess === false)) { message.error('还有校验未通过的数据不能导入'); return; } modal.confirm({ title: '导入提示', content: '批量导入会覆盖当前选择年级的所有学生信息,确认立即导入吗?', okText: '确定', cancelText: '取消', centered: true, onOk: async () => { try { await ExamStudentController.importAction({ examPlanId: reqParams.examPlanId, examGradeId, gradeId: grade?.gradeId, ...restValues, items: items.map(t => ({ examPlanId: reqParams.examPlanId, examGradeId, gradeId: grade?.gradeId, classNumber: t.classNumber, nceeCourseCombId: t.nceeCourseCombId, certificateType: t.certificateType ? JSON.parse(`${t.certificateType}`) : CertificateType.NONE, idNumber: t.idNumber?.toUpperCase(), name: t.name, gender: t.gender ? JSON.parse(`${t.gender}`) : Gender.UNKNOWN, examNumber: t.examNumber, studentNumber: t.studentNumber, roomNumber: t.roomNumber, seatNumber: t.seatNumber, remark: t.remark, })), dataImportMode: DataImportMode.CLEAR_APPEND, }); message.success('导入成功'); handleBack(); } catch { } } }); } catch { message.error('年级或校区未选择'); window.scrollTo({ top: 0, behavior: 'smooth' }); } } const columns: ProColumns[] = [ { title: '验证', valueType: 'option', width: 48, align: 'center', render: (_, r) => { if (r.isSuccess) { return (); } return ( ); }, }, { title: '行号', valueType: 'option', render: (v, _, index) => { return {index + 1}; }, width: 48, align: 'center', }, { title: '班级', dataIndex: 'classNumber', width: 48, align: 'center', }, { title: '姓名', dataIndex: 'name', align: 'center', width: 120, // className: 'minw-80', }, // { // title: '证件类型', // dataIndex: 'certificateTypeName', // width: 112, // align: 'center', // }, { title: '证件类型', dataIndex: 'certificateType', valueEnum: getDictValueEnum('certificate_type'), width: 120, align: 'center', }, { title: '证件号码', dataIndex: 'idNumber', width: 160, align: 'center', }, { title: '性别', dataIndex: 'gender', valueEnum: getDictValueEnum('gender'), width: 48, align: 'center', hideInSearch: true, }, { title: '自编监测号', dataIndex: 'examNumber', width: 112, align: 'center', }, ...(baseData?.hasNceeCourseComb ? [{ title: '选科组合', dataIndex: 'nceeCourseCombName', width: 80, align: 'center', hideInSearch: true, } as ProColumns] : []), { title: '校验结果', dataIndex: 'errorMessage', ellipsis: true, hideInSearch: true, width: 280, render: (_, r) => { if (r.isSuccess) { return '通过'; } return `${r.errorMessage?.join('、')}有误`; }, }, { title: '备注', dataIndex: 'remark', ellipsis: true, hideInSearch: true, width: 96, }, { title: '考场号', dataIndex: 'roomNumber', width: 64, align: 'center', }, { title: '座位号', dataIndex: 'seatNumber', width: 64, align: 'center', }, { title: '操作', valueType: 'option', fixed: 'right', width: 112, align: 'center', render: (_, r) => { return ( <> ); }, }, ]; return ( } onClick={() => { window.scrollTo({ top: 0 }); setTourOpen(true); }} >查看操作指引 } loading={loading} > 第一步:选择年级} ref={tourGradeRef} > layout="inline" submitter={false} formRef={formRef} scrollToFirstError={true} > a.grade.gradeNumber - b.grade.gradeNumber)?.map(t => ({ label: `${t.grade.fullName}(${t.gradeBeginName})`, value: t.id }))} style={{ width: 240 }} required rules={[{ required: true, message: '年级必须选择' }]} onChange={(e: number) => { const g = baseData?.examGrades.find(t => t.id === e); setGrade(g); }} /> {grade && grade.isRequiredSelfExamNumber && } {baseData?.branches && baseData.branches.length > 0 && ({ label: t.name, value: t.id }))} style={{ width: 240 }} required rules={[{ required: true, message: '校区必须选择' }]} /> } 第二步:下载模板} extra={ } style={{ marginTop: token.margin }} > 批量上传文件填写说明及注意事项: 模板格式: 模板文件中第一行为填写说明,第二行为标题行(从A至H列分别为班级、姓名、证件类型、证件号码、性别、自编监测号、选科组合、备注)。 填报数据必须在第一个工作表,并不能删除填写说明和标题行,不能删除和添加表格列! A. 班级必填): 填写班级数字序号, 如幼儿园中一班填1、中二班填2、大一班填1,小学三年级1班填1。 B. 姓名必填): 学生真实姓名。 C. 证件类型必填): 有居民身份证时,必须选择居民身份证,特殊情况可选择其他证件类型。 D. 证件号码: 有证件类型时必须填写,居民身份证为18位系统会进行严格的验证。 E. 性别: 证件类型选择为居民身份证时会自动处理。 F. 自编监测号: 一般情况不需要填写,有统一要求时才需要填写。 G. 选科组合仅高中阶段已选科学生需要填写, 学前、小学和初中不需要填写。 H. 备注: 有特殊情况自行填写。 I. 考场号: 有自编监测号时,学校有需要自行填写。 J. 座位号: 有自编监测号时,学校有需要自行填写。 第三步:上传文件 {!grade && (请先选择年级!)} } style={{ marginTop: token.margin }} ref={tourChooseFileRef} >

点击或拖入文件到此处

{data?.structureCorrect === false && }
第四步:确认数据} > {/*

根据表格验证提示修改数据无误后提交导入

*/}
1.人数统计 {v}, }, { title: '验证失败', dataIndex: 'error', width: 120, align: 'center', render: (v) => v > 0 ? {v} : null, }, {}, ]} dataSource={classTotal} summary={(rows) => { const total = rows.map(t => t.total).reduce((p, n) => p + n, 0); const success = rows.map(t => t.success).reduce((p, n) => p + n, 0); const error = rows.map(t => t.error).reduce((p, n) => p + n, 0); return ( 合计 {total} {success} {error || ''} ); }} />
actionRef={actionRef} cardProps={false} columns={columns} size="small" rowKey="rowNumber" bordered options={false} sticky={{ offsetHeader: 56 }} dataSource={[...data?.rows ?? []]} virtual scroll={{ y: Math.min(document.body.clientHeight - 56 - 24, 800) }} pagination={false} rowClassName={(r) => { if (!r.isSuccess) { return 'yb-row-error'; } return ''; }} toolbar={{ title: 2.学生明细, subTitle: '有校验错误的数据,点击对应行操作中的修改进行修改。', actions: [ // setDataImportMode(e.target.value)} // />, 重要提示:导入前会删除所选校区(若有)年级已填报学生!, ], }} search={false} />
tourGradeRef.current, }, { title: '下载模板', description: '下载批量导入监测学生模板文件,请按模板填写说明及注意事项填写文件。', target: () => tourDownloadRef.current, }, { title: '选择文件', description: '拖动文件至此区域,或点击该区域选择文件上传。', target: () => tourChooseFileRef.current, }, { title: '上传文件', description: '选择好批量导入文件后,点击此按钮上传文件,后台解析后返回校验结果和数据。', target: () => tourUploadRef.current, }, { title: '人数统计', description: '上传文件后会显示各班级人数、验证通过和失败的数量。', target: () => tourStuCountRef.current, }, { title: '学生明细', description: '所有上传的学生明细,请根据校验结果修改数据。', target: () => tourStuDetailRef.current, }, { title: '确认导入', description: '检查修改确认批量导入学生信息无误后确认导入。', target: () => tourSubmitRef.current, }, ]} open={tourOpen} onClose={() => setTourOpen(false)} /> {editOpen && currentRef.current && setEditOpen(false)} /> } {/* */} ); } export default OrgExamStudentImport;