index.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. import { CardStepTitle, StatusIcon, UploadWrongLocalHeaderError } from "@/components";
  2. import ExamGradeController from "@/services/apis/ExamGradeController";
  3. import ExamPlanController from "@/services/apis/ExamPlanController";
  4. import ExamStudentController from "@/services/apis/ExamStudentController";
  5. import SysOrgController from "@/services/apis/SysOrgController";
  6. import { CertificateType, DataImportMode, EducationStage, Gender } from "@/services/enums";
  7. import { BulbOutlined, InboxOutlined } from "@ant-design/icons";
  8. import { ActionType, PageContainer, ProCard, ProColumns, ProForm, ProFormInstance, ProFormSelect, ProTable } from "@ant-design/pro-components";
  9. import { useModel, useParams } from "@umijs/max";
  10. import { useRequest } from "ahooks";
  11. import { Alert, App, Button, Space, Table, Tooltip, Tour, Typography, Upload, theme } from "antd";
  12. import type { RcFile, UploadFile, UploadProps } from "antd/lib/upload/interface";
  13. import lodash from 'lodash';
  14. import { useRef, useState } from "react";
  15. import ExamStudentImportEditModal from "./components/ExamStudentImportEditModal";
  16. type ClassTotalItem = {
  17. classNumber: number;
  18. total: number;
  19. success: number;
  20. error: number;
  21. };
  22. type ImportFormData = {
  23. examGradeId: number;
  24. sysOrgBranchId?: number;
  25. };
  26. /** 监测学生批量导入 */
  27. const OrgExamStudentImport: React.FC = () => {
  28. const reqParams = useParams() as unknown as { examPlanId: number };
  29. const formRef = useRef<ProFormInstance<ImportFormData>>();
  30. const actionRef = useRef<ActionType>();
  31. const [tourOpen, setTourOpen] = useState(false);
  32. const currentRef = useRef<API.UploadExamStudentOutput>();
  33. const [editOpen, setEditOpen] = useState(false);
  34. const [grade, setGrade] = useState<API.ExamGradeOutput>();
  35. const { token } = theme.useToken();
  36. const { message, modal } = App.useApp();
  37. const { getDictValueEnum } = useModel('useDict');
  38. const { initialState } = useModel('@@initialState');
  39. const { currentUser } = initialState ?? {};
  40. const [fileList, setFileList] = useState<UploadFile[]>([]);
  41. const [uploading, setUploading] = useState(false);
  42. const [data, setData] = useState<API.UploadExamDataOutput_UploadExamStudentOutput>();
  43. const [classTotal, setClassTotal] = useState<ClassTotalItem[]>();
  44. // const [dataImportMode, setDataImportMode] = useState(DataImportMode.CLEAR_APPEND);
  45. const tourGradeRef = useRef(null);
  46. const tourDownloadRef = useRef(null);
  47. const tourChooseFileRef = useRef(null);
  48. const tourUploadRef = useRef(null);
  49. const tourStuCountRef = useRef(null);
  50. const tourStuDetailRef = useRef(null);
  51. const tourSubmitRef = useRef(null);
  52. const { data: baseData, loading } = useRequest(async () => {
  53. const res1 = await ExamGradeController.getListByExamPlanId({ examplanid: reqParams.examPlanId });
  54. const res2 = await SysOrgController.getOrgBranchByOrgId({ orgid: currentUser?.sysOrgId ?? 0 });
  55. const res3 = await ExamPlanController.getById({ id: reqParams.examPlanId });
  56. return {
  57. examGrades: res1 ?? [],
  58. branches: res2 ?? [],
  59. hasNceeCourseComb: res3?.educationStage === EducationStage.SENIOR_HIGH_SCHOOL_STAGE,
  60. };
  61. });
  62. const uploadProps: UploadProps = {
  63. onRemove: () => {
  64. setFileList([]);
  65. },
  66. beforeUpload: (file) => {
  67. setFileList([file]);
  68. return false;
  69. },
  70. fileList,
  71. multiple: false,
  72. accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel',
  73. };
  74. const handleBack = () => history.back();
  75. // 计算统计数据
  76. const handleClassTotal = (rows: API.UploadExamStudentOutput[]) => {
  77. let cts: ClassTotalItem[] = [];
  78. lodash.forEach(lodash.countBy(rows, 'classNumber'), function (v, k) {
  79. const classNumber = parseInt(k);
  80. const success = rows.filter(t => t.classNumber === classNumber && t.isSuccess === true)?.length ?? 0;
  81. cts.push({
  82. classNumber,
  83. total: v,
  84. success,
  85. error: v - success,
  86. });
  87. });
  88. setClassTotal(cts);
  89. }
  90. // 上传
  91. const handleUpload = async () => {
  92. if (!grade) {
  93. message.error('请先选择年级');
  94. return;
  95. }
  96. const formData = new FormData();
  97. formData.append('examPlanId', `${reqParams.examPlanId}`);
  98. formData.append('examGradeId', `${grade?.id}`);
  99. fileList.forEach((file) => {
  100. formData.append('file', file as RcFile);
  101. });
  102. setUploading(true);
  103. try {
  104. const res = await ExamStudentController.upload(formData);
  105. setData(res);
  106. handleClassTotal(res?.rows ?? []);
  107. }
  108. catch { }
  109. finally {
  110. setUploading(false);
  111. }
  112. }
  113. // 保存编辑
  114. const handleEdit = (v: API.UploadExamStudentOutput) => {
  115. const i = data?.rows?.findIndex(t => t.rowNumber === v.rowNumber) ?? -1;
  116. if (i !== -1) {
  117. const d = { ...data };
  118. if (d.rows) {
  119. d.rows[i] = v;
  120. handleClassTotal(d.rows ?? []);
  121. setData(d);
  122. }
  123. }
  124. }
  125. // 删除行
  126. const handleDelete = (rowNumber: number) => {
  127. modal.confirm({
  128. title: '警告',
  129. content: '确定立即删除吗',
  130. okText: '确定',
  131. cancelText: '取消',
  132. centered: true,
  133. onOk: async () => {
  134. const i = data?.rows?.findIndex(t => t.rowNumber === rowNumber) ?? -1;
  135. if (i !== -1) {
  136. const d = { ...data };
  137. if (d.rows) {
  138. d.rows.splice(i, 1);
  139. setData(d);
  140. handleClassTotal(d.rows ?? []);
  141. message.success('已删除');
  142. return;
  143. }
  144. }
  145. message.error('删除失败');
  146. },
  147. });
  148. }
  149. // 确认导入
  150. const handleImport = async () => {
  151. const sysOrgId = currentUser?.sysOrgId;
  152. if (!sysOrgId) {
  153. message.error('获取机构ID错误');
  154. return;
  155. }
  156. try {
  157. const importFormValues = await formRef.current?.validateFields();
  158. if (!importFormValues) {
  159. return;
  160. }
  161. const { examGradeId, ...restValues } = importFormValues;
  162. const grade = baseData?.examGrades.find(t => t.id === examGradeId);
  163. if (!grade) {
  164. return;
  165. }
  166. const items = [...(data?.rows ?? [])];
  167. if (items.some(t => t.isSuccess === false)) {
  168. message.error('还有校验未通过的数据不能导入');
  169. return;
  170. }
  171. modal.confirm({
  172. title: '导入提示',
  173. content: '批量导入会覆盖当前选择年级的所有学生信息,确认立即导入吗?',
  174. okText: '确定',
  175. cancelText: '取消',
  176. centered: true,
  177. onOk: async () => {
  178. try {
  179. await ExamStudentController.importAction({
  180. examPlanId: reqParams.examPlanId,
  181. examGradeId,
  182. gradeId: grade?.gradeId,
  183. ...restValues,
  184. items: items.map(t => ({
  185. examPlanId: reqParams.examPlanId,
  186. examGradeId,
  187. gradeId: grade?.gradeId,
  188. classNumber: t.classNumber,
  189. nceeCourseCombId: t.nceeCourseCombId,
  190. certificateType: t.certificateType ? JSON.parse(`${t.certificateType}`) : CertificateType.NONE,
  191. idNumber: t.idNumber?.toUpperCase(),
  192. name: t.name,
  193. gender: t.gender ? JSON.parse(`${t.gender}`) : Gender.UNKNOWN,
  194. examNumber: t.examNumber,
  195. studentNumber: t.studentNumber,
  196. roomNumber: t.roomNumber,
  197. seatNumber: t.seatNumber,
  198. remark: t.remark,
  199. })),
  200. dataImportMode: DataImportMode.CLEAR_APPEND,
  201. });
  202. message.success('导入成功');
  203. handleBack();
  204. }
  205. catch { }
  206. }
  207. });
  208. }
  209. catch {
  210. message.error('年级或校区未选择');
  211. window.scrollTo({ top: 0, behavior: 'smooth' });
  212. }
  213. }
  214. const columns: ProColumns<API.UploadExamStudentOutput>[] = [
  215. {
  216. title: '验证',
  217. valueType: 'option',
  218. width: 48,
  219. align: 'center',
  220. render: (_, r) => {
  221. if (r.isSuccess) {
  222. return (<StatusIcon status="success" filled />);
  223. }
  224. return (
  225. <Tooltip title={r.errorMessage}>
  226. <StatusIcon status="error" filled />
  227. </Tooltip>
  228. );
  229. },
  230. },
  231. {
  232. title: '行号',
  233. valueType: 'option',
  234. render: (v, _, index) => {
  235. return <Typography.Text type="secondary">{index + 1}</Typography.Text>;
  236. },
  237. width: 48,
  238. align: 'center',
  239. },
  240. {
  241. title: '班级',
  242. dataIndex: 'classNumber',
  243. width: 48,
  244. align: 'center',
  245. },
  246. {
  247. title: '姓名',
  248. dataIndex: 'name',
  249. align: 'center',
  250. width: 120,
  251. // className: 'minw-80',
  252. },
  253. // {
  254. // title: '证件类型',
  255. // dataIndex: 'certificateTypeName',
  256. // width: 112,
  257. // align: 'center',
  258. // },
  259. {
  260. title: '证件类型',
  261. dataIndex: 'certificateType',
  262. valueEnum: getDictValueEnum('certificate_type'),
  263. width: 120,
  264. align: 'center',
  265. },
  266. {
  267. title: '证件号码',
  268. dataIndex: 'idNumber',
  269. width: 160,
  270. align: 'center',
  271. },
  272. {
  273. title: '性别',
  274. dataIndex: 'gender',
  275. valueEnum: getDictValueEnum('gender'),
  276. width: 48,
  277. align: 'center',
  278. hideInSearch: true,
  279. },
  280. {
  281. title: '自编监测号',
  282. dataIndex: 'examNumber',
  283. width: 112,
  284. align: 'center',
  285. },
  286. ...(baseData?.hasNceeCourseComb ? [{
  287. title: '选科组合',
  288. dataIndex: 'nceeCourseCombName',
  289. width: 80,
  290. align: 'center',
  291. hideInSearch: true,
  292. } as ProColumns] : []),
  293. {
  294. title: '校验结果',
  295. dataIndex: 'errorMessage',
  296. ellipsis: true,
  297. hideInSearch: true,
  298. width: 280,
  299. render: (_, r) => {
  300. if (r.isSuccess) {
  301. return '通过';
  302. }
  303. return `${r.errorMessage?.join('、')}有误`;
  304. },
  305. },
  306. {
  307. title: '备注',
  308. dataIndex: 'remark',
  309. ellipsis: true,
  310. hideInSearch: true,
  311. width: 96,
  312. },
  313. {
  314. title: '考场号',
  315. dataIndex: 'roomNumber',
  316. width: 64,
  317. align: 'center',
  318. },
  319. {
  320. title: '座位号',
  321. dataIndex: 'seatNumber',
  322. width: 64,
  323. align: 'center',
  324. },
  325. {
  326. title: '操作',
  327. valueType: 'option',
  328. fixed: 'right',
  329. width: 112,
  330. align: 'center',
  331. render: (_, r) => {
  332. return (
  333. <>
  334. <Button type="link" size="small" onClick={() => {
  335. currentRef.current = { ...r };
  336. setEditOpen(true);
  337. }}>修改</Button>
  338. <Button type="link" size="small" onClick={() => handleDelete(r.rowNumber)}>删除</Button>
  339. </>
  340. );
  341. },
  342. },
  343. ];
  344. return (
  345. <PageContainer
  346. title="监测学生批量导入"
  347. onBack={handleBack}
  348. extra={
  349. <Button
  350. type="link"
  351. icon={<BulbOutlined />}
  352. onClick={() => {
  353. window.scrollTo({ top: 0 });
  354. setTourOpen(true);
  355. }}
  356. >查看操作指引</Button>
  357. }
  358. loading={loading}
  359. >
  360. <ProCard
  361. title={<CardStepTitle>第一步:选择年级</CardStepTitle>}
  362. ref={tourGradeRef}
  363. >
  364. <Alert type="warning" showIcon message="每个年级所有班级学生填写在一个批量导入文件中,每个年级分开导入,有几个年级导入几次!" closable style={{ marginBottom: token.margin, color: token.colorError }} />
  365. <ProForm<ImportFormData>
  366. layout="inline"
  367. submitter={false}
  368. formRef={formRef}
  369. scrollToFirstError={true}
  370. >
  371. <ProFormSelect
  372. label="年级"
  373. name="examGradeId"
  374. options={baseData?.examGrades?.sort((a, b) => a.grade.gradeNumber - b.grade.gradeNumber)?.map(t => ({ label: `${t.grade.fullName}(${t.gradeBeginName})`, value: t.id }))}
  375. style={{ width: 240 }}
  376. required
  377. rules={[{ required: true, message: '年级必须选择' }]}
  378. onChange={(e: number) => {
  379. const g = baseData?.examGrades.find(t => t.id === e);
  380. setGrade(g);
  381. }}
  382. />
  383. {grade && grade.isRequiredSelfExamNumber &&
  384. <Alert
  385. type="warning"
  386. message={`该年级必须自编监测号(长度为${grade.selfExamNumberLength}位)`}
  387. showIcon
  388. style={{
  389. marginRight: token.marginLG,
  390. }}
  391. />
  392. }
  393. {baseData?.branches && baseData.branches.length > 0 &&
  394. <ProFormSelect
  395. label="校区"
  396. name="sysOrgBranchId"
  397. options={baseData.branches.map(t => ({ label: t.name, value: t.id }))}
  398. style={{ width: 240 }}
  399. required
  400. rules={[{ required: true, message: '校区必须选择' }]}
  401. />
  402. }
  403. </ProForm>
  404. </ProCard>
  405. <ProCard
  406. title={<CardStepTitle>第二步:下载模板</CardStepTitle>}
  407. extra={
  408. <Button
  409. type="primary"
  410. ref={tourDownloadRef}
  411. onClick={() => {
  412. window.open('/doc-templates/学生信息上报-填报模板.xlsx');
  413. }}
  414. >下载模板</Button>
  415. }
  416. style={{ marginTop: token.margin }}
  417. >
  418. <Typography.Paragraph>
  419. <Typography.Text type="warning" strong>批量上传文件填写说明及注意事项:</Typography.Text>
  420. </Typography.Paragraph>
  421. <Typography.Paragraph>
  422. <Typography.Text strong>模板格式:</Typography.Text>
  423. 模板文件中第一行为填写说明,第二行为标题行(从A至H列分别为班级、姓名、证件类型、证件号码、性别、自编监测号、选科组合、备注)。
  424. <Typography.Text type="danger">填报数据必须在第一个工作表,并不能删除填写说明和标题行,不能删除和添加表格列!</Typography.Text>
  425. </Typography.Paragraph>
  426. <Typography.Paragraph>
  427. <Typography.Text strong>A. 班级</Typography.Text>
  428. (<Typography.Text type="danger">必填</Typography.Text>):
  429. 填写班级数字序号,
  430. <Typography.Text mark>如幼儿园中一班填1、中二班填2、大一班填1,小学三年级1班填1。</Typography.Text>
  431. </Typography.Paragraph>
  432. <Typography.Paragraph>
  433. <Typography.Text strong>B. 姓名</Typography.Text>
  434. (<Typography.Text type="danger">必填</Typography.Text>):
  435. 学生真实姓名。
  436. </Typography.Paragraph>
  437. <Typography.Paragraph>
  438. <Typography.Text strong>C. 证件类型</Typography.Text>
  439. (<Typography.Text type="danger">必填</Typography.Text>):
  440. 有居民身份证时,必须选择居民身份证,特殊情况可选择其他证件类型。
  441. </Typography.Paragraph>
  442. <Typography.Paragraph>
  443. <Typography.Text strong>D. 证件号码</Typography.Text>:
  444. 有证件类型时必须填写,居民身份证为18位系统会进行严格的验证。
  445. </Typography.Paragraph>
  446. <Typography.Paragraph>
  447. <Typography.Text strong>E. 性别</Typography.Text>:
  448. 证件类型选择为居民身份证时会自动处理。
  449. </Typography.Paragraph>
  450. <Typography.Paragraph>
  451. <Typography.Text strong>F. 自编监测号</Typography.Text>:
  452. 一般情况不需要填写,有统一要求时才需要填写。
  453. </Typography.Paragraph>
  454. <Typography.Paragraph>
  455. <Typography.Text strong>G. 选科组合</Typography.Text>:
  456. <Typography.Text mark>仅高中阶段已选科学生需要填写</Typography.Text>,
  457. 学前、小学和初中不需要填写。
  458. </Typography.Paragraph>
  459. <Typography.Paragraph>
  460. <Typography.Text strong>H. 备注</Typography.Text>:
  461. 有特殊情况自行填写。
  462. </Typography.Paragraph>
  463. <Typography.Paragraph>
  464. <Typography.Text strong>I. 考场号</Typography.Text>:
  465. 有自编监测号时,学校有需要自行填写。
  466. </Typography.Paragraph>
  467. <Typography.Paragraph>
  468. <Typography.Text strong>J. 座位号</Typography.Text>:
  469. 有自编监测号时,学校有需要自行填写。
  470. </Typography.Paragraph>
  471. </ProCard>
  472. <ProCard
  473. title={
  474. <CardStepTitle>
  475. 第三步:上传文件
  476. {!grade && <Typography.Text type="danger">(请先选择年级!)</Typography.Text>}
  477. </CardStepTitle>}
  478. style={{ marginTop: token.margin }}
  479. ref={tourChooseFileRef}
  480. >
  481. <Space direction="vertical" style={{ width: '100%' }}>
  482. <Upload.Dragger {...uploadProps} disabled={!grade}>
  483. <p className="ant-upload-drag-icon"><InboxOutlined /></p>
  484. <p className="ant-upload-text">点击或拖入文件到此处</p>
  485. </Upload.Dragger>
  486. <Space>
  487. <Button
  488. type="primary"
  489. disabled={fileList.length === 0}
  490. loading={uploading}
  491. onClick={handleUpload}
  492. ref={tourUploadRef}
  493. >{uploading ? '上传中' : '立即上传'}</Button>
  494. <UploadWrongLocalHeaderError />
  495. </Space>
  496. {data?.structureCorrect === false &&
  497. <Alert
  498. type="error"
  499. showIcon
  500. message={data?.errorMessage?.join('')}
  501. />
  502. }
  503. </Space>
  504. </ProCard>
  505. <ProCard
  506. style={{ marginTop: token.margin }}
  507. title={<CardStepTitle>第四步:确认数据</CardStepTitle>}
  508. >
  509. {/* <p className="ant-upload-text">根据表格验证提示修改数据无误后提交导入</p> */}
  510. <div ref={tourStuCountRef}>
  511. <Typography.Title level={5}>1.人数统计</Typography.Title>
  512. <Table
  513. pagination={false}
  514. scroll={{ x: 'max-content' }}
  515. size="small"
  516. bordered
  517. rowKey="classNumber"
  518. columns={[
  519. { title: '班级', dataIndex: 'classNumber', width: 80, align: 'center', },
  520. { title: '学生人数', dataIndex: 'total', width: 80, align: 'center' },
  521. {
  522. title: '验证通过',
  523. dataIndex: 'success',
  524. width: 120,
  525. align: 'center',
  526. render: (v) => <Typography.Text type="success">{v}</Typography.Text>,
  527. },
  528. {
  529. title: '验证失败',
  530. dataIndex: 'error',
  531. width: 120,
  532. align: 'center',
  533. render: (v) => v > 0 ? <Typography.Text type="danger">{v}</Typography.Text> : null,
  534. },
  535. {},
  536. ]}
  537. dataSource={classTotal}
  538. summary={(rows) => {
  539. const total = rows.map(t => t.total).reduce((p, n) => p + n, 0);
  540. const success = rows.map(t => t.success).reduce((p, n) => p + n, 0);
  541. const error = rows.map(t => t.error).reduce((p, n) => p + n, 0);
  542. return (
  543. <Table.Summary.Row style={{ fontStyle: 'italic', fontWeight: 'bold' }}>
  544. <Table.Summary.Cell index={0} align="center">合计</Table.Summary.Cell>
  545. <Table.Summary.Cell index={1} align="center">{total}</Table.Summary.Cell>
  546. <Table.Summary.Cell index={2} align="center">
  547. <Typography.Text type="success">{success}</Typography.Text>
  548. </Table.Summary.Cell>
  549. <Table.Summary.Cell index={3} align="center">
  550. <Typography.Text type="danger">{error || ''}</Typography.Text>
  551. </Table.Summary.Cell>
  552. <Table.Summary.Cell index={4} align="center"></Table.Summary.Cell>
  553. </Table.Summary.Row>
  554. );
  555. }}
  556. />
  557. </div>
  558. <div ref={tourStuDetailRef}>
  559. <ProTable<API.UploadExamStudentOutput>
  560. actionRef={actionRef}
  561. cardProps={false}
  562. columns={columns}
  563. size="small"
  564. rowKey="rowNumber"
  565. bordered
  566. options={false}
  567. sticky={{ offsetHeader: 56 }}
  568. dataSource={[...data?.rows ?? []]}
  569. virtual
  570. scroll={{ y: Math.min(document.body.clientHeight - 56 - 24, 800) }}
  571. pagination={false}
  572. rowClassName={(r) => {
  573. if (!r.isSuccess) {
  574. return 'yb-row-error';
  575. }
  576. return '';
  577. }}
  578. toolbar={{
  579. title: <Typography.Title level={5} style={{ marginBottom: 0 }}>2.学生明细</Typography.Title>,
  580. subTitle: '有校验错误的数据,点击对应行操作中的修改进行修改。',
  581. actions: [
  582. // <Radio.Group
  583. // key="importType"
  584. // options={getDictOptions('data_import_mode')}
  585. // value={dataImportMode}
  586. // onChange={e => setDataImportMode(e.target.value)}
  587. // />,
  588. <Typography.Text key="tooltip" type="warning">重要提示:导入前会删除所选校区(若有)年级已填报学生!</Typography.Text>,
  589. <Button
  590. key="import"
  591. type="primary"
  592. disabled={!data?.structureCorrect || uploading}
  593. onClick={handleImport}
  594. ref={tourSubmitRef}
  595. >确认导入</Button>
  596. ],
  597. }}
  598. search={false}
  599. />
  600. </div>
  601. </ProCard>
  602. <Tour
  603. steps={[
  604. {
  605. title: '选择年级',
  606. description: '导入前必须选择年级,如果有多个校区的学校必须选择校区。',
  607. target: () => tourGradeRef.current,
  608. },
  609. {
  610. title: '下载模板',
  611. description: '下载批量导入监测学生模板文件,请按模板填写说明及注意事项填写文件。',
  612. target: () => tourDownloadRef.current,
  613. },
  614. {
  615. title: '选择文件',
  616. description: '拖动文件至此区域,或点击该区域选择文件上传。',
  617. target: () => tourChooseFileRef.current,
  618. },
  619. {
  620. title: '上传文件',
  621. description: '选择好批量导入文件后,点击此按钮上传文件,后台解析后返回校验结果和数据。',
  622. target: () => tourUploadRef.current,
  623. },
  624. {
  625. title: '人数统计',
  626. description: '上传文件后会显示各班级人数、验证通过和失败的数量。',
  627. target: () => tourStuCountRef.current,
  628. },
  629. {
  630. title: '学生明细',
  631. description: '所有上传的学生明细,请根据校验结果修改数据。',
  632. target: () => tourStuDetailRef.current,
  633. },
  634. {
  635. title: '确认导入',
  636. description: '检查修改确认批量导入学生信息无误后确认导入。',
  637. target: () => tourSubmitRef.current,
  638. },
  639. ]}
  640. open={tourOpen}
  641. onClose={() => setTourOpen(false)}
  642. />
  643. {editOpen && currentRef.current &&
  644. <ExamStudentImportEditModal
  645. isRequiredSelfExamNumber={grade?.isRequiredSelfExamNumber}
  646. selfExamNumberLength={grade?.selfExamNumberLength}
  647. data={currentRef.current}
  648. hasNceeCourseComb={baseData?.hasNceeCourseComb}
  649. onFinish={handleEdit}
  650. onClose={() => setEditOpen(false)}
  651. />
  652. }
  653. {/* <FloatButton.BackTop visibilityHeight={100} /> */}
  654. </PageContainer>
  655. );
  656. }
  657. export default OrgExamStudentImport;