ScanPaper.vue 17 KB

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