ScanPaper.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. <template>
  2. <div class="scan-paper">
  3. <div class="part-box part-box-pad part-box-flex scan-head">
  4. <div>
  5. <h2>课程(代码):{{ task.courseName }}({{ task.courseCode }})</h2>
  6. </div>
  7. <div>
  8. <el-button :disabled="!hasSelectedData" type="primary" @click="toBind"
  9. >重新绑定</el-button
  10. >
  11. <el-button :disabled="!canClear" type="danger" @click="clearStage"
  12. >清空</el-button
  13. >
  14. <el-button
  15. :disabled="!canSave"
  16. :loading="saving"
  17. type="primary"
  18. @click="toSave"
  19. >保存</el-button
  20. >
  21. <el-button
  22. type="primary"
  23. :loading="scanStatus === 'SCAN'"
  24. :disabled="!canScan"
  25. @click="toScan"
  26. >
  27. {{ statusDesc[scanStatus] }}
  28. </el-button>
  29. </div>
  30. </div>
  31. <div class="scan-body">
  32. <div class="scan-result">
  33. <div class="mb-4 tab-btns scan-result-head">
  34. <el-button
  35. size="medium"
  36. :type="curTab === 'normal' ? 'primary' : 'default'"
  37. @click="switchTab('normal')"
  38. >正常 <span>[{{ normalCount }}]</span></el-button
  39. >
  40. <el-button
  41. size="medium"
  42. :type="curTab === 'error' ? 'danger' : 'default'"
  43. @click="switchTab('error')"
  44. >异常 <span>[{{ errorCount }}]</span></el-button
  45. >
  46. </div>
  47. <div class="scan-result-body">
  48. <scan-result-table
  49. v-if="isNormalTab"
  50. ref="scanResultTableRef"
  51. :table-data.sync="scanStageList"
  52. tab="normal"
  53. @row-click="rowClickHandle"
  54. @select-change="selectChange"
  55. @delete-paper="deletePaperHandle"
  56. ></scan-result-table>
  57. <scan-result-table
  58. v-else
  59. ref="scanResultTableRef"
  60. :table-data.sync="errorStageList"
  61. tab="error"
  62. @row-click="rowClickHandle"
  63. @select-change="selectChange"
  64. @delete-paper="deletePaperHandle"
  65. ></scan-result-table>
  66. </div>
  67. <div v-if="isNormalTab" class="scan-result-foot">
  68. <span>共</span>
  69. <span class="color-primary mlr-1">{{ studentCount }}</span>
  70. <span>人,</span>
  71. <span class="color-primary mr-1">{{ paperCount }}</span>
  72. <span>张图片</span>
  73. </div>
  74. </div>
  75. <div class="scan-content">
  76. <image-contain
  77. v-if="curPaper && curPaper.url"
  78. ref="ImageContain"
  79. :image="curPaper"
  80. :show-rotate="false"
  81. @on-prev="toPrevPaper"
  82. @on-next="toNextPaper"
  83. ></image-contain>
  84. </div>
  85. </div>
  86. <!-- ManualBindDialog -->
  87. <manual-bind-dialog
  88. ref="ManualBindDialog"
  89. :datas="selectList"
  90. :task="task"
  91. @confirm="bindConfirm"
  92. ></manual-bind-dialog>
  93. <!-- SelectBatchNoDialog -->
  94. <select-batch-no-dialog
  95. ref="SelectBatchNoDialog"
  96. @confirm="saveScanData"
  97. ></select-batch-no-dialog>
  98. </div>
  99. </template>
  100. <script>
  101. import { mapState } from "vuex";
  102. import {
  103. getPreUploadFiles,
  104. clearDir,
  105. deleteFiles,
  106. decodeImageCode,
  107. getDirScanFile,
  108. batchSaveImages,
  109. } from "../../../plugins/imageOcr";
  110. import db from "../../../plugins/db";
  111. import { evokeScanner } from "../../../plugins/scanner";
  112. import ImageContain from "@/components/ImageContain.vue";
  113. import ScanResultTable from "../components/ScanResultTable.vue";
  114. import ManualBindDialog from "../components/ManualBindDialog.vue";
  115. import SelectBatchNoDialog from "../components/SelectBatchNoDialog.vue";
  116. import timeMixins from "@/mixins/setTimeMixins";
  117. import { getStudentInfo } from "../api";
  118. import log4js from "@/plugins/logger";
  119. import { randomCode, calcSum } from "@/plugins/utils";
  120. import { getStageDir } from "@/plugins/env";
  121. const logger = log4js.getLogger("scan");
  122. export default {
  123. name: "scan-paper",
  124. mixins: [timeMixins],
  125. components: {
  126. ImageContain,
  127. ScanResultTable,
  128. ManualBindDialog,
  129. SelectBatchNoDialog,
  130. },
  131. data() {
  132. return {
  133. task: this.$ls.get("task", {}),
  134. scanStatus: "INIT",
  135. scanStageList: [],
  136. errorStageList: [],
  137. selectList: [],
  138. statusDesc: {
  139. INIT: "开始扫描",
  140. SCAN: "扫描中",
  141. FINISH: "继续扫描",
  142. },
  143. user: this.$ls.get("user", {}),
  144. saving: false,
  145. maxCacheCount: 120,
  146. lastStudentCode: "",
  147. scanCount: 0,
  148. menus: [
  149. { code: "normal", name: "正常" },
  150. { code: "error", name: "异常" },
  151. ],
  152. curTab: "normal",
  153. // 非等待模式:delayMode:0
  154. looping: false,
  155. // 图片预览
  156. curPapers: [],
  157. curPaperIndex: 0,
  158. curPaper: { url: "" },
  159. stageDir: getStageDir(),
  160. };
  161. },
  162. computed: {
  163. ...mapState("client", ["ocrArea"]),
  164. canSave() {
  165. return (
  166. this.scanStatus === "FINISH" &&
  167. this.normalCount > 0 &&
  168. this.errorCount === 0
  169. );
  170. },
  171. canScan() {
  172. return this.errorCount + this.normalCount <= this.maxCacheCount;
  173. },
  174. canClear() {
  175. return this.studentCount > 0;
  176. },
  177. hasSelectedData() {
  178. return Boolean(this.selectList.length);
  179. },
  180. IS_DELAY_MODE() {
  181. return this.GLOBAL.delayMode === 1;
  182. },
  183. errorCount() {
  184. return calcSum(this.errorStageList.map((item) => item.papers.length));
  185. },
  186. normalCount() {
  187. return calcSum(this.scanStageList.map((item) => item.papers.length));
  188. },
  189. isNormalTab() {
  190. return this.curTab === "normal";
  191. },
  192. studentCount() {
  193. return this.isNormalTab
  194. ? this.scanStageList.length
  195. : this.errorStageList.length;
  196. },
  197. paperCount() {
  198. return this.isNormalTab ? this.normalCount : this.errorCount;
  199. },
  200. },
  201. created() {
  202. this.$on("go-back", this.goBackHandle);
  203. },
  204. beforeDestroy() {
  205. this.stopLoopScaningFile();
  206. this.clearFiles();
  207. },
  208. methods: {
  209. initData() {
  210. this.lastStudentCode = "";
  211. this.scanStageList = [];
  212. this.errorStageList = [];
  213. this.scanStatus = "INIT";
  214. this.curPapers = [];
  215. this.curPaperIndex = 0;
  216. this.curPaper = { url: "" };
  217. this.scanCount = 0;
  218. },
  219. clearFiles() {
  220. clearDir(this.stageDir);
  221. },
  222. async goBackHandle() {
  223. if (this.scanStageList.length) {
  224. const res = await this.$confirm(
  225. `当前存在未保存的扫描数据,确定要退出吗?`,
  226. "警告",
  227. {
  228. type: "warning",
  229. }
  230. ).catch(() => {});
  231. if (res !== "confirm") return;
  232. }
  233. this.$router.go(-1);
  234. logger.info(`99退出扫描`);
  235. },
  236. switchTab(tab) {
  237. this.curTab = tab;
  238. this.selectList = [];
  239. },
  240. // scan
  241. toScan() {
  242. if (!this.canScan) {
  243. this.$message.error("已超过最大缓存数量,请先保存数据再继续扫描!");
  244. return;
  245. }
  246. if (this.scanStatus === "INIT") {
  247. this.startTask();
  248. } else {
  249. this.continueTask();
  250. }
  251. },
  252. startTask() {
  253. logger.info(`01开始扫描`);
  254. this.continueTask();
  255. },
  256. continueTask() {
  257. this.scanStatus = "SCAN";
  258. if (this.IS_DELAY_MODE) {
  259. this.evokeScanExe();
  260. } else {
  261. this.evokeScanExeNotDelay();
  262. }
  263. },
  264. async evokeScanExe() {
  265. logger.info("02唤起扫描仪");
  266. await evokeScanner(this.GLOBAL.input).catch((error) => {
  267. console.error(error);
  268. });
  269. // 缓存已扫描的数据
  270. const res = getPreUploadFiles(this.GLOBAL.input, true);
  271. if (!res.succeed) {
  272. logger.error(`03扫描仪停止,故障:${res.errorMsg}`);
  273. this.$message.error(res.errorMsg);
  274. this.scanStatus = "FINISH";
  275. return;
  276. }
  277. logger.info(`03扫描仪停止,扫描数:${res.data.length}`);
  278. await this.stageScanImage(res.data);
  279. this.scanStatus = "FINISH";
  280. logger.info(`03-1完成条码解析`);
  281. },
  282. async evokeScanExeNotDelay() {
  283. logger.info("02唤起扫描仪");
  284. this.looping = true;
  285. this.loopScaningFile();
  286. await evokeScanner(this.GLOBAL.input).catch((error) => {
  287. console.error(error);
  288. });
  289. this.stopLoopScaningFile();
  290. await this.getScaningFile();
  291. const scanCount = this.scanStageList.length - this.scanCount;
  292. this.scanCount = this.scanStageList.length;
  293. // 已扫描的数据
  294. const res = getPreUploadFiles(this.GLOBAL.input);
  295. this.scanStatus = "FINISH";
  296. if (!res.succeed) {
  297. logger.error(
  298. `03扫描仪停止,扫描数:${scanCount},故障:${res.errorMsg}`
  299. );
  300. this.$message.error(res.errorMsg);
  301. return;
  302. }
  303. logger.info(`03扫描仪停止,扫描数:${scanCount}`);
  304. },
  305. async stageScanImage(imageList) {
  306. const ocrAreaContent = JSON.stringify(this.ocrArea);
  307. for (let i = 0, len = imageList.length; i < len; i++) {
  308. const fileInfo = {
  309. id: "",
  310. taskId: this.task.id,
  311. schoolId: this.task.schoolId,
  312. semesterId: this.task.semesterId,
  313. examId: this.task.examId,
  314. courseCode: this.task.courseCode,
  315. courseName: this.task.courseName,
  316. frontOriginImgPath: imageList[i].frontFile,
  317. versoOriginImgPath: imageList[i].versoFile,
  318. isFormal: 1,
  319. studentName: "",
  320. studentCode: "",
  321. ocrArea: ocrAreaContent,
  322. fileTypeId: "0",
  323. fileTypeName: "答题卡",
  324. roomOrClass: "",
  325. batchNo: "",
  326. clientUserId: this.user.id,
  327. clientUsername: this.user.loginName,
  328. clientUserLoginTime: this.user.loginTime,
  329. select: false,
  330. };
  331. const code = await decodeImageCode(
  332. fileInfo.frontOriginImgPath,
  333. this.ocrArea
  334. ).catch((err) => {
  335. console.error(err);
  336. logger.error(`03条码解析失败,${err}`);
  337. });
  338. fileInfo.studentCode = code || this.lastStudentCode;
  339. // 按照识别空自动绑定前一张code的规则,无论识别到的code是否合法,都应该作为最后一次识别的code
  340. // 否则,第一张进异常,后面空白条码页会自动进正常页面,对后续处理带来一定困扰
  341. if (fileInfo.studentCode) {
  342. this.lastStudentCode = fileInfo.studentCode;
  343. }
  344. fileInfo.id = `${fileInfo.studentCode || 0}-${randomCode(16)}`;
  345. const studentStage = this.scanStageList.find(
  346. (item) => item.studentCode === fileInfo.studentCode
  347. );
  348. if (studentStage) {
  349. studentStage.papers.push(fileInfo);
  350. continue;
  351. }
  352. if (fileInfo.studentCode) {
  353. const res = await getStudentInfo({
  354. examId: this.task.examId,
  355. courseCode: this.task.courseCode,
  356. studentCode: fileInfo.studentCode,
  357. }).catch(() => {});
  358. if (res) {
  359. fileInfo.studentName = res.studentName;
  360. this.scanStageList.push({
  361. id: res.id,
  362. studentCode: res.studentCode,
  363. studentName: res.studentName,
  364. courseCode: res.courseCode,
  365. courseName: res.courseName,
  366. teacher: res.teacher,
  367. teachClass: res.teachClass,
  368. collegeName: res.collegeName,
  369. majorName: res.majorName,
  370. className: res.className,
  371. score: res.score,
  372. remark: res.remark,
  373. select: false,
  374. papers: [fileInfo],
  375. });
  376. continue;
  377. }
  378. }
  379. const errorStudentStage = this.errorStageList.find(
  380. (item) => item.studentCode === fileInfo.studentCode
  381. );
  382. if (errorStudentStage) {
  383. errorStudentStage.papers.push(fileInfo);
  384. continue;
  385. }
  386. this.errorStageList.push({
  387. id: `none-${randomCode(16)}`,
  388. studentCode: fileInfo.studentCode,
  389. select: false,
  390. papers: [fileInfo],
  391. });
  392. }
  393. if (imageList.length) {
  394. const lastImg = imageList.pop();
  395. this.curPapers = [lastImg.frontFile, lastImg.versoFile];
  396. this.curPaperIndex = 0;
  397. this.setCurPaper(0);
  398. }
  399. },
  400. toSave() {
  401. if (this.errorStageList.length) {
  402. this.$message.error("还有异常数据未处理!");
  403. return;
  404. }
  405. if (!this.scanStageList.length) {
  406. this.$message.error("当前无要保存的数据!");
  407. return;
  408. }
  409. this.$refs.SelectBatchNoDialog.open();
  410. },
  411. async saveScanData(batchNo) {
  412. if (this.saving) return;
  413. this.saving = true;
  414. this.clearViewPapers();
  415. logger.info(`04-1开始保存数据`);
  416. try {
  417. let datas = this.scanStageList
  418. .map((item) => item.papers)
  419. .flat()
  420. .map((item) => {
  421. item.batchNo = batchNo;
  422. return item;
  423. });
  424. datas = await batchSaveImages(datas, this.task.courseCode);
  425. await db.batchSaveUploadInfo(datas);
  426. } catch (err) {
  427. console.error(err);
  428. logger.error(`04-1保存数据错误,${err}`);
  429. this.saving = false;
  430. this.$message.error("保存数据错误,请重新尝试!");
  431. return Promise.reject();
  432. }
  433. this.$message.success("保存成功!");
  434. this.saving = false;
  435. logger.info(`04-2保存数据成功`);
  436. this.clearFiles();
  437. this.initData();
  438. },
  439. // delay mode
  440. // 实时获取扫描图片
  441. async loopScaningFile() {
  442. this.clearSetTs();
  443. if (!this.looping) return;
  444. // const st = Date.now();
  445. await this.getScaningFile();
  446. // console.log(`耗时:${Date.now() - st}ms`);
  447. this.addSetTime(this.loopScaningFile, 1 * 1000);
  448. },
  449. stopLoopScaningFile() {
  450. this.clearSetTs();
  451. this.looping = false;
  452. },
  453. async getScaningFile() {
  454. const newScanFiles = getDirScanFile(this.GLOBAL.input);
  455. await this.stageScanImage(newScanFiles);
  456. },
  457. // table action
  458. toBind() {
  459. if (!this.selectList.length) return;
  460. this.$refs.ManualBindDialog.open();
  461. },
  462. bindConfirm(studentInfo) {
  463. if (this.isNormalTab) {
  464. this.normalBind(studentInfo);
  465. } else {
  466. this.errorBind(studentInfo);
  467. }
  468. this.clearViewPapers();
  469. this.$refs.scanResultTableRef.clearSelection();
  470. },
  471. normalBind(studentInfo) {
  472. const selectPaperIds = this.selectList
  473. .map((item) => item.papers.map((p) => p.id))
  474. .flat();
  475. let prevIndex = this.scanStageList.findIndex(
  476. (row) => row.id === this.selectList[0].id
  477. );
  478. // 删除选择的数据
  479. this.scanStageList.forEach((row) => {
  480. row.papers = row.papers.filter((p) => !selectPaperIds.includes(p.id));
  481. });
  482. this.scanStageList = this.scanStageList.filter(
  483. (row) => row.papers.length
  484. );
  485. // 绑定逻辑
  486. const preAddPapers = this.selectList
  487. .map((row) => {
  488. return row.papers.map((p) => {
  489. return {
  490. ...p,
  491. select: false,
  492. studentCode: studentInfo.studentCode,
  493. studentName: studentInfo.studentName,
  494. id: `${studentInfo.studentCode}-${randomCode(16)}`,
  495. };
  496. });
  497. })
  498. .flat();
  499. const stageStudent = this.scanStageList.find(
  500. (row) => row.studentCode === studentInfo.studentCode
  501. );
  502. if (stageStudent) {
  503. stageStudent.papers.push(...preAddPapers);
  504. return;
  505. }
  506. prevIndex = Math.max(
  507. 1,
  508. Math.min(prevIndex + 1, this.scanStageList.length)
  509. );
  510. this.scanStageList.splice(prevIndex, 0, {
  511. ...studentInfo,
  512. select: false,
  513. papers: preAddPapers,
  514. });
  515. },
  516. errorBind(studentInfo) {
  517. const selectPaperIds = this.selectList
  518. .map((item) => item.papers.map((p) => p.id))
  519. .flat();
  520. // 删除选择的数据
  521. this.errorStageList.forEach((row) => {
  522. row.papers = row.papers.filter((p) => !selectPaperIds.includes(p.id));
  523. });
  524. this.errorStageList = this.errorStageList.filter(
  525. (row) => row.papers.length
  526. );
  527. // 绑定逻辑
  528. const preAddPapers = this.selectList
  529. .map((row) => {
  530. return row.papers.map((p) => {
  531. return {
  532. ...p,
  533. select: false,
  534. studentCode: studentInfo.studentCode,
  535. studentName: studentInfo.studentName,
  536. id: `${studentInfo.studentCode}-${randomCode(16)}`,
  537. };
  538. });
  539. })
  540. .flat();
  541. const stageStudent = this.scanStageList.find(
  542. (row) => row.studentCode === studentInfo.studentCode
  543. );
  544. if (stageStudent) {
  545. stageStudent.papers.push(...preAddPapers);
  546. return;
  547. }
  548. this.scanStageList.push({
  549. ...studentInfo,
  550. select: false,
  551. papers: preAddPapers,
  552. });
  553. },
  554. async clearStage() {
  555. const name = this.isNormalTab ? "正常" : "异常";
  556. const res = await this.$confirm(
  557. `确定要清空所有【${name}】数据吗?`,
  558. "警告",
  559. {
  560. type: "warning",
  561. }
  562. ).catch(() => {});
  563. if (res !== "confirm") return;
  564. if (this.isNormalTab) {
  565. const files = this.scanStageList
  566. .map((row) =>
  567. row.papers.map((p) => [p.frontOriginImgPath, p.versoOriginImgPath])
  568. )
  569. .flat(2);
  570. deleteFiles(files);
  571. this.scanStageList = [];
  572. } else {
  573. const files = this.errorStageList
  574. .map((row) =>
  575. row.papers.map((p) => [p.frontOriginImgPath, p.versoOriginImgPath])
  576. )
  577. .flat(2);
  578. deleteFiles(files);
  579. this.errorStageList = [];
  580. }
  581. this.curPapers = [];
  582. this.curPaperIndex = 0;
  583. this.curPaper = { url: "" };
  584. logger.info(`99数据清空`);
  585. this.$message.success("数据已清空!");
  586. },
  587. selectChange(data) {
  588. this.selectList = data;
  589. },
  590. deletePaperHandle(deletedPapers) {
  591. this.curPapers = this.curPapers.filter((p) => !deletedPapers.includes(p));
  592. if (!this.curPapers.length) {
  593. this.curPaperIndex = 0;
  594. this.curPaper = { url: "" };
  595. return;
  596. }
  597. if (deletedPapers.includes(this.curPaper.url)) {
  598. this.curPaperIndex = 0;
  599. this.setCurPaper(0);
  600. return;
  601. }
  602. this.curPaperIndex = this.curPapers.indexOf(this.curPaper.url);
  603. },
  604. // image-preview
  605. rowClickHandle({ curPapers, curPaperIndex }) {
  606. this.curPapers = curPapers;
  607. this.curPaperIndex = curPaperIndex;
  608. this.setCurPaper(curPaperIndex);
  609. },
  610. clearViewPapers() {
  611. this.curPapers = [];
  612. this.curPaperIndex = 0;
  613. this.curPaper = { url: "" };
  614. },
  615. setCurPaper(index) {
  616. this.curPaper = { url: `file:///${this.curPapers[index]}` };
  617. },
  618. toPrevPaper() {
  619. if (this.curPaperIndex <= 0) {
  620. this.$message.error("没有上一页了");
  621. return;
  622. }
  623. this.setCurPaper(--this.curPaperIndex);
  624. },
  625. toNextPaper() {
  626. if (this.curPaperIndex >= this.curPapers.length - 1) {
  627. this.$message.error("没有下一页了");
  628. return;
  629. }
  630. this.setCurPaper(++this.curPaperIndex);
  631. },
  632. },
  633. };
  634. </script>