index.tsx 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. import { toValueEnum } from "@/common/converter";
  2. import { downloadFileByBlob } from "@/common/net/download";
  3. import { CardStepTitle, FileLink, FileUpload, SuperTable, TabBadge } from "@/components";
  4. import ReportButton from "@/components/ReportButton";
  5. import ExamGradeController from "@/services/apis/ExamGradeController";
  6. import ExamOrgDataReportController from "@/services/apis/ExamOrgDataReportController";
  7. import ExamSpecialStudentController from "@/services/apis/ExamSpecialStudentController";
  8. import SysOrgController from "@/services/apis/SysOrgController";
  9. import { AuditStatus, DataReportStatus, DataReportType, ExamStatus, ResourceFileType } from "@/services/enums";
  10. import { BulbOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UploadOutlined } from "@ant-design/icons";
  11. import { ActionType, PageContainer, ProCard, ProColumns, ProDescriptions, ProDescriptionsItemProps, ProTable } from "@ant-design/pro-components";
  12. import { history, useModel, useParams } from "@umijs/max";
  13. import { useRequest } from "ahooks";
  14. import { Alert, App, Badge, Button, Card, Drawer, Space, Tag, Tour, Typography, theme } from "antd";
  15. import { useCallback, useRef, useState } from "react";
  16. import ExamSpecialStudentDetailDrawer from "./components/ExamSpecialStudentDetailDrawer";
  17. import ExamSpecialStudentEditModal from "./components/ExamSpecialStudentEditModal";
  18. const OrgExamSpecialStudentReport: React.FC = () => {
  19. const reqParams = useParams() as unknown as { examPlanId: number };
  20. const { token } = theme.useToken();
  21. const { getDictValueEnum, getKeyDict, getDict } = useModel('useDict');
  22. const examStatusDict = getKeyDict('exam_status');
  23. const dataReportStatusDict = getKeyDict('data_report_status');
  24. const dataReportTypeDict = getKeyDict('data_report_type');
  25. const auditStatusDict = getKeyDict('audit_status');
  26. // const { baseData } = useModel('useBaseData');
  27. const { initialState } = useModel('@@initialState');
  28. const { currentUser } = initialState ?? {};
  29. const [detailOpen, setDetailOpen] = useState(false);
  30. const [remarkOpen, setRemarkOpen] = useState(false);
  31. const [editOpen, setEditOpen] = useState(false);
  32. const actionRef = useRef<ActionType>();
  33. const currentRef = useRef<Partial<API.ExamSpecialStudentOutput>>();
  34. const [activeKey, setActiveKey] = useState<React.Key>('2');
  35. const [statusCount, seStatusCount] = useState<Record<number, number>>({});
  36. const [tourOpen, setTourOpen] = useState(false);
  37. const tourAddRef = useRef(null);
  38. const tourDownloadRef = useRef(null);
  39. const tourUploadRef = useRef(null);
  40. const tourReportRef = useRef(null);
  41. const { message, modal, notification } = App.useApp();
  42. const { data: gradeBranchData } = useRequest(async () => {
  43. const res1 = await ExamGradeController.getListByExamPlanId({ examplanid: reqParams.examPlanId });
  44. const res2 = await SysOrgController.getOrgBranchByOrgId({ orgid: currentUser?.sysOrgId ?? 0 });
  45. return {
  46. examGrades: res1 ?? [],
  47. branches: res2 ?? [],
  48. hasBranch: (res2?.length ?? 0) > 0,
  49. };
  50. });
  51. // 加载上报数据
  52. const { data: reportData, run: loadReport } = useRequest(() => {
  53. return ExamOrgDataReportController.getByTypeExamPlanId({ type: DataReportType.SP_STUDENT, examplanid: reqParams.examPlanId });
  54. });
  55. // 加载数量统计
  56. const loadCount = useCallback(async (params: API.ExamSpecialStudentPageInput) => {
  57. const m = await ExamSpecialStudentController.queryStatusCount(params);
  58. const tc = m?.reduce((a, b) => a + b.count, 0) ?? 0;
  59. const d: Record<number, number> = { 0: tc };
  60. m?.forEach((t) => { d[t.status] = t.count; });
  61. seStatusCount(d);
  62. }, []);
  63. // 加载统计数据
  64. const { data: studentCountData, run: loadStudentCount, loading } = useRequest(() => {
  65. return ExamSpecialStudentController.getOrgGradeClassStudentCount({ examplanid: reqParams.examPlanId });
  66. });
  67. // 统计数据列定义
  68. let studentCountColumns: ProColumns<any>[] = [
  69. {
  70. title: '年级',
  71. dataIndex: ['Grade', 'name'],
  72. width: 64,
  73. align: 'center',
  74. fixed: 'left',
  75. },
  76. {
  77. title: '合计',
  78. dataIndex: 'GradeTotal',
  79. width: 64,
  80. align: 'center',
  81. fixed: 'left',
  82. },
  83. ];
  84. studentCountData?.classNumberList?.forEach(t => {
  85. studentCountColumns.push({
  86. title: `${t}班`,
  87. dataIndex: `${t}`,
  88. width: 56,
  89. align: 'center',
  90. render: (v) => v ? v : null,
  91. });
  92. });
  93. studentCountColumns.push({});
  94. // 删除
  95. const handleDelete = useCallback((id: number) => {
  96. modal.confirm({
  97. title: '警告',
  98. content: '确定立即删除吗',
  99. okText: '确定',
  100. cancelText: '取消',
  101. centered: true,
  102. onOk: async () => {
  103. await ExamSpecialStudentController.del({ id });
  104. message.success('已删除');
  105. actionRef.current?.reload();
  106. loadStudentCount();
  107. },
  108. });
  109. }, []);
  110. // 上报
  111. const handleSubmit = useCallback(() => {
  112. return new Promise<void>((resolve, reject) => {
  113. if ((studentCountData?.total ?? 0) > 0 && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) === 0) {
  114. message.error('未上传《特殊学生明细表》和《会议记录》打印盖章的扫描电子文件');
  115. reject();
  116. return;
  117. }
  118. let content = `共 ${studentCountData?.total ?? 0} 个学生,上报后不能再修改,确定立即上报吗?`;
  119. modal.confirm({
  120. title: '警告',
  121. content,
  122. okText: '确定',
  123. cancelText: '取消',
  124. centered: true,
  125. onOk: async () => {
  126. try {
  127. await ExamOrgDataReportController.submit({ examPlanId: reqParams.examPlanId, type: DataReportType.SP_STUDENT })
  128. message.success('已上报');
  129. loadReport();
  130. resolve();
  131. }
  132. catch (ex) {
  133. const exm = ex as any;
  134. notification.error({
  135. message: '上报失败',
  136. description: exm?.info?.errorMessage ? `${exm?.info?.errorMessage}!请仔细检查特殊学生明细信息和佐证材料` : JSON.stringify(ex),
  137. })
  138. reject();
  139. }
  140. },
  141. onCancel: () => reject(),
  142. });
  143. });
  144. }, [studentCountData, reportData]);
  145. // 删除佐证材料
  146. const handleDeleteAttachment = useCallback((id: number, fileId: string) => {
  147. modal.confirm({
  148. title: '警告',
  149. content: '确定立即删除吗',
  150. okText: '确定',
  151. cancelText: '取消',
  152. centered: true,
  153. onOk: async () => {
  154. await ExamSpecialStudentController.delAttachment({ sourceId: id, fileId });
  155. message.success('已删除');
  156. actionRef.current?.reload();
  157. loadStudentCount();
  158. },
  159. });
  160. }, []);
  161. // 删除上报佐证材料
  162. const handleDeleteReportAttachment = useCallback(async (id: number, fileId: string) => {
  163. modal.confirm({
  164. title: '警告',
  165. content: '确定立即删除吗',
  166. okText: '确定',
  167. cancelText: '取消',
  168. centered: true,
  169. onOk: async () => {
  170. await ExamOrgDataReportController.delAttachment({ sourceId: id, fileId });
  171. message.success('已删除');
  172. loadReport();
  173. },
  174. });
  175. }, []);
  176. // 下载打印文件
  177. const handleDownloadPrintFile = useCallback(async () => {
  178. const res = await ExamSpecialStudentController.exportPrintTable({ examplanid: reqParams.examPlanId });
  179. if (res) {
  180. downloadFileByBlob(res.data, res.fileName);
  181. }
  182. }, []);
  183. // // 提交单个学生审核
  184. // const handleStudentSubmit = useCallback((id: number) => {
  185. // modal.confirm({
  186. // title: '警告',
  187. // content: '确定立即提交吗',
  188. // okText: '确定',
  189. // cancelText: '取消',
  190. // centered: true,
  191. // onOk: async () => {
  192. // await ExamSpecialStudentAuditController.submit({ id });
  193. // message.success('已提交');
  194. // actionRef.current?.reload();
  195. // loadStudentCount();
  196. // },
  197. // });
  198. // }, [])
  199. // 是否可操作和上报
  200. const reportable = (reportData?.examDataReport?.status === ExamStatus.ACTIVE && reportData?.isExpired === false &&
  201. (reportData?.examOrgDataReport?.status === DataReportStatus.UNREPORT ||
  202. reportData?.examOrgDataReport?.status === DataReportStatus.REJECTED));
  203. // 工具栏按定义
  204. let detailActions = [];
  205. if (reportable) {
  206. detailActions.push(
  207. <Button
  208. key="add"
  209. ref={tourAddRef}
  210. type="primary"
  211. disabled={!reportable}
  212. icon={<PlusOutlined />}
  213. onClick={() => {
  214. currentRef.current = { id: 0 };
  215. setEditOpen(true);
  216. }}
  217. >添加特殊学生</Button>
  218. );
  219. detailActions.push(
  220. <Button
  221. key="import"
  222. disabled={!reportable}
  223. icon={<UploadOutlined />}
  224. onClick={() => history.push(`/exam-s/sp-stu/import/${reqParams.examPlanId}`)}
  225. >批量导入</Button>
  226. );
  227. detailActions.push(
  228. <Button
  229. key="download"
  230. ref={tourDownloadRef}
  231. disabled={!reportable}
  232. icon={<DownloadOutlined />}
  233. onClick={handleDownloadPrintFile}
  234. >下载打印表格文件</Button>
  235. );
  236. }
  237. // 明细表列定义
  238. const detailColumns: ProColumns<API.ExamSpecialStudentOutput>[] = [
  239. {
  240. title: '状态',
  241. dataIndex: 'status',
  242. hideInSearch: true,
  243. width: 80,
  244. align: 'center',
  245. valueEnum: getDictValueEnum('audit_status', true),
  246. render: (_, r) => {
  247. // const s = auditStatusDict[r.status];
  248. // return <Tag color={s.antStatus} style={{ marginRight: 0 }}>{s.name}</Tag>;
  249. const s = auditStatusDict[r.status];
  250. const ta = <Tag color={s.antStatus} style={{ marginRight: 0 }}>{s.name}</Tag>;
  251. if (r.isPreIdentified) {
  252. return (
  253. <Space direction="vertical" size="small" style={{ lineHeight: 1 }}>
  254. {ta}
  255. <Typography.Text type="warning" style={{ fontSize: token.fontSizeSM }}>往期已认定</Typography.Text>
  256. </Space>
  257. );
  258. }
  259. if (r.preTotalScore !== null) {
  260. return (
  261. <Space direction="vertical" size="small" style={{ lineHeight: 1 }}>
  262. {ta}
  263. <Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
  264. 前期{r.preTotalCourse}科得分
  265. <Typography.Text type="warning" strong style={{ fontSize: token.fontSizeSM }}>{r.preTotalScore}</Typography.Text>
  266. </Typography.Text>
  267. </Space>
  268. );
  269. }
  270. return ta;
  271. },
  272. },
  273. // {
  274. // title: '校区',
  275. // dataIndex: ['sysOrgBranch', 'name'],
  276. // width: 88,
  277. // align: 'center',
  278. // },
  279. ...(gradeBranchData?.hasBranch ? [{
  280. title: '校区',
  281. dataIndex: 'sysOrgBranchId',
  282. width: 96,
  283. align: 'center',
  284. hideInSearch: !gradeBranchData?.hasBranch,
  285. valueEnum: toValueEnum(gradeBranchData?.branches ?? []),
  286. } as ProColumns] : []),
  287. // {
  288. // title: '年级',
  289. // dataIndex: 'gradeId',
  290. // width: 72,
  291. // align: 'center',
  292. // render: (_, r) => `${r.examGrade?.grade.name}`,
  293. // },
  294. {
  295. title: '年级',
  296. dataIndex: 'gradeId',
  297. width: 80,
  298. align: 'center',
  299. valueEnum: toValueEnum(gradeBranchData?.examGrades?.map(t => ({ id: t.gradeId, name: `${t.grade.name}(${t.gradeBeginName})` })) ?? []),
  300. render: (_, r) => {
  301. return (
  302. <Space size="small" direction="vertical" style={{ lineHeight: 1 }}>
  303. {r.examGrade?.grade.name}
  304. <Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM }}>({r.examGrade?.gradeBeginName})</Typography.Text>
  305. </Space>
  306. );
  307. },
  308. },
  309. {
  310. title: '班级',
  311. dataIndex: 'classNumber',
  312. width: 64,
  313. align: 'center',
  314. renderText: (v) => `${v}班`,
  315. },
  316. {
  317. title: '姓名',
  318. dataIndex: 'name',
  319. width: 112,
  320. align: 'center',
  321. render: (v, r) => <a onClick={() => { currentRef.current = r; setDetailOpen(true); }}>{v}</a>,
  322. },
  323. {
  324. title: '证件类型',
  325. dataIndex: 'certificateType',
  326. valueEnum: getDictValueEnum('certificate_type'),
  327. width: 112,
  328. align: 'center',
  329. },
  330. {
  331. title: '证件号码',
  332. dataIndex: 'idNumber',
  333. width: 160,
  334. align: 'center',
  335. },
  336. // {
  337. // title: '学籍号码',
  338. // width: 168,
  339. // align: 'center',
  340. // },
  341. {
  342. title: '特殊原因',
  343. dataIndex: 'applyReason',
  344. hideInSearch: true,
  345. className: 'minw-120',
  346. },
  347. {
  348. title: '家长电话',
  349. dataIndex: 'patriarchTel',
  350. hideInSearch: true,
  351. width: 128,
  352. align: 'center',
  353. },
  354. {
  355. title: '备注',
  356. dataIndex: 'remark',
  357. hideInSearch: true,
  358. // className: 'minw-64',
  359. width: 120,
  360. },
  361. {
  362. title: '佐证材料(每个学生最多上传3个)',
  363. valueType: 'option',
  364. hideInSearch: true,
  365. width: 280,
  366. fixed: 'right',
  367. render: (_, r) => {
  368. let editable = reportable && [AuditStatus.UNSUBMIT, AuditStatus.REJECTED, AuditStatus.AUDIT].includes(r.status);
  369. if (!editable) {
  370. editable = r.status === AuditStatus.REJECTED && reportData?.examDataReport?.status === ExamStatus.ACTIVE;
  371. }
  372. const li = r.attachmentList?.map((t, i) => {
  373. return (
  374. <FileLink
  375. key={i}
  376. fileExtName={t.fileExtName}
  377. fileName={t.fileName}
  378. url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
  379. thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
  380. card
  381. onDelete={editable ? async () => handleDeleteAttachment(r.id, t.fileId) : undefined}
  382. />
  383. );
  384. });
  385. return (
  386. <Space direction="vertical" style={{ width: 280 }}>
  387. {li}
  388. {editable && (li?.length ?? 0) < 3 &&
  389. <FileUpload
  390. addText="添加文件"
  391. tipText="仅支持PDF或图片文件"
  392. accept="image/jpeg,image/png,image/gif,application/pdf"
  393. limitSize={20}
  394. onUpload={async (file, onUploadProgress) => {
  395. const fsp = file.name.split('.');
  396. let extName = '';
  397. if (fsp.length > 1) {
  398. extName = fsp[fsp.length - 1].toLowerCase();
  399. }
  400. if (extName === '' || !['jpg', 'jpeg', 'png', 'gif', 'pdf'].includes(extName)) {
  401. message.error('文件类型错误,请重新选择!')
  402. return { success: false, errorType: 'fileTypeError', errorMessage: '文件类型错误,请选择扩展名为.jpg、.jpeg、.png或.pdf的文件' };
  403. }
  404. try {
  405. const formData = new FormData();
  406. formData.append('type', `${ResourceFileType.EXAM_SPECIAL_STUDENT}`);
  407. formData.append('sourceId', `${r.id}`);
  408. formData.append('fileName', `${file.name}`);
  409. formData.append('file', file);
  410. await ExamSpecialStudentController.uploadAttachment(formData, {
  411. onUploadProgress: (p: any) => {
  412. const progress = parseFloat((p.loaded / p.total * 100).toFixed(1));
  413. onUploadProgress?.(progress);
  414. }
  415. });
  416. actionRef.current?.reload();
  417. return { success: true };
  418. }
  419. catch {
  420. return { success: false };
  421. }
  422. }}
  423. />
  424. }
  425. </Space>
  426. );
  427. },
  428. },
  429. {
  430. title: '佐证材料',
  431. // valueType: 'option',
  432. hideInSearch: true,
  433. hideInTable: true,
  434. width: 280,
  435. fixed: 'right',
  436. render: (_, r) => {
  437. const li = r.attachmentList?.map((t, i) => {
  438. return (
  439. <FileLink
  440. key={i}
  441. fileExtName={t.fileExtName}
  442. fileName={t.fileName}
  443. url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
  444. thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
  445. card
  446. />
  447. );
  448. });
  449. return (
  450. <Space direction="vertical" style={{ width: '100%' }}>
  451. {li?.length === 0 ? '未上传' : li}
  452. </Space>
  453. );
  454. },
  455. },
  456. {
  457. title: '操作',
  458. valueType: 'option',
  459. width: 120,
  460. align: 'center',
  461. fixed: 'right',
  462. // hideInTable: !reportable,
  463. render: (_, r) => {
  464. if (reportable && [AuditStatus.UNSUBMIT, AuditStatus.REJECTED, AuditStatus.AUDIT].includes(r.status)) {
  465. return (
  466. <Space>
  467. <a onClick={() => { currentRef.current = r; setEditOpen(true); }}>修改</a>
  468. <a onClick={() => handleDelete(r.id)}>删除</a>
  469. </Space>
  470. );
  471. }
  472. // if (reportData?.examDataReport?.status === ExamStatus.ACTIVE && r.status === AuditStatus.REJECTED) {
  473. // return (
  474. // <Space>
  475. // <a onClick={() => { currentRef.current = r; setEditOpen(true); }}>修改</a>
  476. // <a onClick={() => handleDelete(r.id)}>删除</a>
  477. // <a onClick={() => handleStudentSubmit(r.id)}>提交</a>
  478. // </Space>
  479. // );
  480. // }
  481. return null;
  482. },
  483. }
  484. ];
  485. // 呈现状态 tab
  486. const renderTabItems = useCallback(() => {
  487. let items: { key: string; label: React.ReactNode }[] = [];
  488. items = items.concat(
  489. getDict('audit_status')?.filter(t => t.value > 1)?.sort((a, b) => a.sort - b.sort)?.map((t) => ({
  490. key: `${t.value}`,
  491. label: (
  492. <span>
  493. {t.name}
  494. <TabBadge color={t.antColor} count={statusCount[t.value] ?? 0} active={activeKey === `${t.value}`} />
  495. </span>
  496. ),
  497. })),
  498. );
  499. items.push({
  500. key: '0',
  501. label: (<span>全部<TabBadge count={statusCount[0]} active={activeKey === 0} /></span>),
  502. });
  503. return items;
  504. }, [activeKey, statusCount]);
  505. return (
  506. <PageContainer
  507. title={`${reportData?.examPlan?.fullName ?? ''} - 特殊学生上报`}
  508. onBack={() => history.back()}
  509. extra={reportable &&
  510. <Button
  511. type="link"
  512. icon={<BulbOutlined />}
  513. onClick={() => {
  514. window.scrollTo({ top: 0 });
  515. setTourOpen(true);
  516. }}
  517. >查看操作指引</Button>
  518. }
  519. >
  520. <ProCard
  521. title={<CardStepTitle>基本情况</CardStepTitle>}
  522. extra={reportData?.examOrgDataReport?.status &&
  523. <Tag
  524. style={{ marginRight: 0 }}
  525. color={dataReportStatusDict[reportData.examOrgDataReport.status].antColor}
  526. >
  527. {dataReportStatusDict[reportData.examOrgDataReport.status].name}
  528. </Tag>
  529. }
  530. >
  531. <ProDescriptions>
  532. <ProDescriptions.Item label="上报类型">
  533. {reportData?.examDataReport?.type ? dataReportTypeDict[reportData?.examDataReport?.type].name : ''}
  534. </ProDescriptions.Item>
  535. <ProDescriptions.Item label="监测上报">
  536. {reportData?.examDataReport?.status &&
  537. <Badge
  538. status={examStatusDict[reportData.examDataReport.status].antStatus as any}
  539. text={examStatusDict[reportData.examDataReport.status].name}
  540. />
  541. }
  542. </ProDescriptions.Item>
  543. <ProDescriptions.Item label="截止时间">
  544. <Typography.Text strong>
  545. {reportData?.examDataReport?.endTime}
  546. {reportData?.isExpired && reportData.examDataReport?.status === ExamStatus.ACTIVE &&
  547. <Typography.Text type="danger">(已截止)</Typography.Text>
  548. }
  549. </Typography.Text>
  550. </ProDescriptions.Item>
  551. <ProDescriptions.Item label="上报人员">{reportData?.examOrgDataReport?.reportSysUser?.name}</ProDescriptions.Item>
  552. <ProDescriptions.Item label="上报时间">{reportData?.examOrgDataReport?.reportTime}</ProDescriptions.Item>
  553. <ProDescriptions.Item label="备注说明">{reportData?.examOrgDataReport?.remark}</ProDescriptions.Item>
  554. <ProDescriptions.Item label="上报说明" span={3}>
  555. <Typography.Text ellipsis>{reportData?.examDataReport?.remark || '无'}</Typography.Text>
  556. {reportData?.examDataReport?.remark && <Button type="link" size="small" onClick={() => setRemarkOpen(true)}>详情</Button>}
  557. </ProDescriptions.Item>
  558. </ProDescriptions>
  559. <Drawer open={remarkOpen} title="上报说明" width={720} maskClosable onClose={() => setRemarkOpen(false)}>
  560. <pre>{reportData?.examDataReport?.remark || '无'}</pre>
  561. </Drawer>
  562. </ProCard>
  563. <ProCard
  564. style={{ marginTop: token.margin }}
  565. title={<CardStepTitle>特殊学生上报</CardStepTitle>}
  566. extra={reportable &&
  567. <Space>
  568. <span key="tooltip">离上报结束还有</span>
  569. <ReportButton
  570. key="time"
  571. buttonRef={tourReportRef}
  572. expireTime={reportData?.examDataReport?.endTime}
  573. showIcon
  574. onSubmit={handleSubmit}
  575. onTimerFinish={() => loadReport()}
  576. />
  577. </Space>
  578. }
  579. >
  580. <Alert
  581. type="warning"
  582. message={<Typography.Title level={5}>上报步骤:</Typography.Title>}
  583. description={
  584. <Typography>
  585. <ol>
  586. <li><Typography.Text type="danger" strong>只需要上报新增特殊学生,往期已认定的特殊学生不能重复上报;</Typography.Text></li>
  587. <li>在下方 <Typography.Text strong>特殊学生明细</Typography.Text> 中录入特殊学生信息,并上传佐证材料<Typography.Text type="danger">(必传)</Typography.Text>;</li>
  588. <li>特殊学生信息录入完成后,点击 <Typography.Text strong>下载打印表格文件</Typography.Text> 下载文件打印签字盖章;</li>
  589. <li>扫描已签字盖章的文件为电子文档(PDF或图片);</li>
  590. <li>在 <Typography.Text strong> 特殊学生上报</Typography.Text> 的 <Typography.Text strong>上传《特殊学生明细表》和《会议记录》打印盖章的扫描电子文件</Typography.Text> 中上传电子文档<Typography.Text type="danger">(必传)</Typography.Text>;</li>
  591. <li>确认无误后点击 <Typography.Text strong> 特殊学生上报</Typography.Text> 右侧的 <Typography.Text strong>立即上报</Typography.Text> 按钮完成上传。</li>
  592. </ol>
  593. </Typography>
  594. }
  595. />
  596. <Typography.Title level={5} style={{ marginTop: token.marginSM }} type="danger">上传《特殊学生明细表》和《会议记录》打印盖章的扫描电子文件(最多上传6个文件):</Typography.Title>
  597. <Card bordered ref={tourUploadRef}>
  598. <Space direction="vertical" style={{ width: '100%' }}>
  599. {reportData?.examOrgDataReport?.attachmentList?.map((t, i) => {
  600. return (
  601. <FileLink
  602. key={i}
  603. fileExtName={t.fileExtName}
  604. fileName={t.fileName}
  605. url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
  606. thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
  607. card
  608. onDelete={reportable ? async () => handleDeleteReportAttachment(reportData.examOrgDataReportId ?? 0, t.fileId) : undefined}
  609. />
  610. );
  611. }) ?? null}
  612. {reportable && reportData?.examOrgDataReport && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) < 6 &&
  613. <FileUpload
  614. addText="添加文件"
  615. tipText="仅支持PDF或图片文件"
  616. accept="image/jpeg,image/png,image/gif,application/pdf"
  617. limitSize={20}
  618. onUpload={async (file, onUploadProgress) => {
  619. const fsp = file.name.split('.');
  620. let extName = '';
  621. if (fsp.length > 1) {
  622. extName = fsp[fsp.length - 1].toLowerCase();
  623. }
  624. if (extName === '' || !['jpg', 'jpeg', 'png', 'gif', 'pdf'].includes(extName)) {
  625. message.error('文件类型错误,请重新选择!')
  626. return { success: false, errorType: 'fileTypeError', errorMessage: '文件类型错误,请选择扩展名为.jpg、.jpeg、.png或.pdf的文件' };
  627. }
  628. try {
  629. const formData = new FormData();
  630. formData.append('type', `${ResourceFileType.EXAM_ORG_DATA_REPORT}`);
  631. formData.append('dataReportType', `${DataReportType.SP_STUDENT}`);
  632. formData.append('examPlanId', `${reportData?.examPlan?.id ?? 0}`);
  633. formData.append('sourceId', `${reportData?.examOrgDataReportId ?? 0}`);
  634. formData.append('fileName', `${file.name}`);
  635. formData.append('file', file);
  636. await ExamOrgDataReportController.uploadAttachment(formData, {
  637. onUploadProgress: (p: any) => {
  638. const progress = parseFloat((p.loaded / p.total * 100).toFixed(1));
  639. onUploadProgress?.(progress);
  640. }
  641. });
  642. loadReport();
  643. return { success: true };
  644. }
  645. catch {
  646. return { success: false };
  647. }
  648. }}
  649. />
  650. }
  651. </Space>
  652. </Card>
  653. <Typography.Title level={5} style={{ marginTop: token.marginSM }}>特殊学生人数统计:</Typography.Title>
  654. <ProTable<any>
  655. style={{ marginTop: token.margin }}
  656. search={false}
  657. pagination={false}
  658. size="small"
  659. bordered
  660. options={{ setting: false, density: false, reloadIcon: <ReloadOutlined onClick={loadStudentCount} /> }}
  661. rowKey="GradeId"
  662. loading={loading}
  663. columns={studentCountColumns}
  664. columnEmptyText=""
  665. dataSource={studentCountData?.items ?? []}
  666. toolBarRender={false}
  667. />
  668. </ProCard>
  669. <SuperTable<API.ExamSpecialStudentOutput>
  670. headerTitle={<CardStepTitle>特殊学生明细</CardStepTitle>}
  671. style={{ marginTop: token.margin }}
  672. actionRef={actionRef}
  673. scroll={{ x: 'max-content' }}
  674. rowKey="id"
  675. columns={detailColumns}
  676. // search={{ showHiddenNum: false }}
  677. search={false}
  678. options={{ setting: false, fullScreen: false }}
  679. toolbar={{
  680. menu: {
  681. type: 'tab',
  682. activeKey: activeKey,
  683. items: renderTabItems(),
  684. onChange: (key) => {
  685. setActiveKey(key as React.Key);
  686. actionRef.current?.reload();
  687. },
  688. },
  689. actions: detailActions,
  690. }}
  691. request={async (params, sort) => {
  692. return SuperTable.requestPageAgent({ params, sort }, async (p) => {
  693. const qparams = { ...p, examPlanId: reqParams.examPlanId };
  694. await loadCount(qparams);
  695. const res = await ExamSpecialStudentController.queryPageList({
  696. ...qparams,
  697. status: activeKey !== '0' ? parseInt(activeKey as string) : undefined,
  698. });
  699. return res;
  700. });
  701. }}
  702. // rowSelection={reportable ? {} : false}
  703. />
  704. {editOpen && currentRef.current && gradeBranchData &&
  705. <ExamSpecialStudentEditModal
  706. examPlanId={reqParams.examPlanId}
  707. data={currentRef.current}
  708. gradeBranch={gradeBranchData}
  709. onFinish={() => {
  710. actionRef.current?.reload();
  711. loadStudentCount();
  712. }}
  713. onClose={() => setEditOpen(false)}
  714. />
  715. }
  716. {detailOpen && currentRef.current &&
  717. <ExamSpecialStudentDetailDrawer
  718. data={currentRef.current}
  719. columns={detailColumns as ProDescriptionsItemProps<Partial<API.ExamSpecialStudentOutput>>[]}
  720. onClose={() => setDetailOpen(false)}
  721. />
  722. }
  723. <Tour
  724. steps={[
  725. {
  726. title: '添加特殊学生信息',
  727. description: '添加完特殊学生信息后,在列表中上传相应的佐证材料(一个学生最多3个图片或PDF)',
  728. target: () => tourAddRef.current,
  729. },
  730. {
  731. title: '下载打印表格文件',
  732. description: '打印表格按年级分页,按文件要求签字和盖章',
  733. target: () => tourDownloadRef.current,
  734. },
  735. {
  736. title: '上传签字盖章文件',
  737. description: '打印文件签字盖章后,扫描成电子文件(PDF或图片)上传,推荐PDF文件,最多上传6个文件',
  738. target: () => tourUploadRef.current,
  739. },
  740. {
  741. title: '上报特殊学生信息',
  742. description: '确认无误后立即上报',
  743. target: () => tourReportRef.current,
  744. },
  745. ]}
  746. open={tourOpen}
  747. onClose={() => setTourOpen(false)}
  748. />
  749. </PageContainer>
  750. );
  751. }
  752. export default OrgExamSpecialStudentReport;