Browse Source

feat: 其他扫描

zhangjie 11 months ago
parent
commit
b5de06589e

+ 64 - 0
src/components/base/FileTypeSelect.vue

@@ -0,0 +1,64 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="file-type-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.name"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { fileTypeQuery } from "../../modules/client/api";
+
+export default {
+  name: "file-type-select",
+  props: {
+    value: { type: [Number, String], default: "" },
+    placeholder: { type: String, default: "请选择" },
+    disabled: { type: Boolean, default: false },
+    clearable: { type: Boolean, default: true },
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+      const res = await fileTypeQuery();
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find((item) => item.name === this.selected)
+      );
+    },
+  },
+};
+</script>

+ 87 - 0
src/components/base/RoomClassSelect.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="room-class-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.name"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { roomQuery, classQuery } from "../../modules/client/api";
+
+export default {
+  name: "room-class-select",
+  props: {
+    value: { type: [Number, String], default: "" },
+    placeholder: { type: String, default: "请选择" },
+    disabled: { type: Boolean, default: false },
+    clearable: { type: Boolean, default: true },
+    type: { type: String, default: "room" },
+    filterData: {
+      type: Object,
+      default() {
+        return {
+          examId: "",
+          courseCode: "",
+        };
+      },
+    },
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+    "filterData.courseCode": {
+      handler(val, oldval) {
+        console.log(val);
+        if (val !== oldval) {
+          this.search();
+          this.$emit("input", "");
+          this.$emit("change", {});
+        }
+      },
+    },
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+      if (!this.filterData.courseCode) return;
+
+      const func = this.type === "room" ? roomQuery : classQuery;
+      const res = await func(this.filterData);
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find((item) => item.name === this.selected)
+      );
+    },
+  },
+};
+</script>

+ 14 - 0
src/modules/client/api.js

@@ -12,6 +12,20 @@ export const commonExamQuery = (data) => {
 export const commonCourseQuery = (datas) => {
   return $postParam("/api/admin/common/course/query", datas);
 };
+// 文件类型
+export const fileTypeQuery = () => {
+  return $postParam("/api/admin/common/course/query", {});
+};
+// 考场
+export const roomQuery = (datas) => {
+  // examId,courseCode
+  return $postParam("/api/admin/common/room/query", datas);
+};
+// 班级
+export const classQuery = (datas) => {
+  // examId,courseCode
+  return $postParam("/api/admin/common/class/query", datas);
+};
 
 // scan
 export const uploadImage = (isFormal, datas, config = {}) => {

+ 104 - 0
src/modules/client/components/SelectBatchNoDialog.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-dialog
+    class="ocr-area-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="dialogOpened"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      :key="modalForm.id"
+      label-width="100px"
+    >
+      <el-form-item prop="serialNo" label="批次号:">
+        <el-select
+          v-model.trim="modalForm.code"
+          filterable
+          placeholder="编码"
+          :disabled="!!originCode"
+        >
+          <el-option
+            v-for="item in codes"
+            :key="item"
+            :label="code"
+            :value="code"
+          ></el-option>
+        </el-select>
+        <el-input-number
+          v-model="modalForm.serialNo"
+          placeholder="请输入编号"
+          :min="1"
+          :max="9999"
+          :step="1"
+          step-strictly
+          :controls="false"
+        ></el-input-number>
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer">
+      <el-button type="primary" @click="confirm">确定</el-button>
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: "select-batch-no-dialog",
+  data() {
+    return {
+      modalIsShow: false,
+      modalForm: { code: "", serialNo: 1 },
+      originCode: "",
+      codes: "abcdefghijklmnopqrstuvwxyz".toUpperCase().split(""),
+      rules: {
+        serialNo: [
+          {
+            required: true,
+            message: "请输入批次号",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    dialogOpened() {
+      const batchNo = this.$ls.get("batchNo", "");
+      if (batchNo) {
+        this.modalForm.code = batchNo[0];
+        this.modalForm.serialNo = Number(batchNo.substring(1) || "0") + 1;
+      } else {
+        this.modalForm.code = "";
+        this.modalForm.serialNo = 1;
+      }
+      this.originCode = this.modalForm.code;
+
+      this.$nextTick(() => {
+        this.$refs.modalFormComp.clearValidate();
+      });
+    },
+    async confirm() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      const batchNo = `${this.modalForm.code}${this.modalForm.serialNo}`;
+
+      this.$ls.set("batchNo", batchNo);
+      this.$emit("confirm", batchNo);
+      this.cancel();
+    },
+  },
+};
+</script>

+ 362 - 3
src/modules/client/views/ScanOther.vue

@@ -1,13 +1,372 @@
 <template>
-  <div class="scan-other">scan-other</div>
+  <div class="scan-other">
+    <div class="part-box part-box-filter part-box-flex">
+      <el-form
+        ref="modalFormComp"
+        :model="modalForm"
+        :rules="rules"
+        label-position="left"
+        label-width="85px"
+        inline
+      >
+        <el-form-item prop="roomOrClass" label="考场/班级:">
+          <room-class-select
+            v-model="modalForm.roomOrClass"
+            placeholder="请选择考场/班级"
+            :disabled="isScaning"
+          >
+          </room-class-select>
+        </el-form-item>
+        <el-form-item prop="fileType" label="文件类型:">
+          <file-type-select
+            v-model="modalForm.fileType"
+            placeholder="请选择文件类型"
+            :disabled="isScaning"
+          >
+          </file-type-select>
+        </el-form-item>
+      </el-form>
+      <div class="part-box-action">
+        <el-button :disabled="!canClear" type="danger" @click="clearStage"
+          >清空</el-button
+        >
+        <el-button
+          :disabled="!canSave"
+          :loading="saving"
+          type="primary"
+          @click="toSave"
+          >保存</el-button
+        >
+        <el-button
+          type="primary"
+          :loading="scanStatus === 'SCAN'"
+          :disabled="!canScan"
+          @click="toScan"
+        >
+          {{ statusDesc[scanStatus] }}
+        </el-button>
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <el-table ref="TableList" size="medium" :data="dataList">
+        <el-table-column
+          prop="文件名"
+          label="任务名称"
+          min-width="300"
+        ></el-table-column>
+        <el-table-column
+          prop="roomOrClass"
+          label="考场/班级"
+          min-width="160"
+        ></el-table-column>
+        <el-table-column
+          prop="fileType"
+          label="文件类型"
+          width="160"
+        ></el-table-column>
+      </el-table>
+    </div>
+
+    <!-- SelectBatchNoDialog -->
+    <select-batch-no-dialog
+      ref="SelectBatchNoDialog"
+      @confirm="saveScanData"
+    ></select-batch-no-dialog>
+  </div>
 </template>
 
 <script>
+import {
+  getPreUploadFiles,
+  saveOutputImage,
+  clearDir,
+  getDirScanFile,
+} from "../../../plugins/imageOcr";
+import db from "../../../plugins/db";
+import { evokeScanner } from "../../../plugins/scanner";
+
+import FileTypeSelect from "@/components/base/FileTypeSelect.vue";
+import RoomClassSelect from "@/components/base/RoomClassSelect.vue";
+import SelectBatchNoDialog from "../components/SelectBatchNoDialog.vue";
+
+import timeMixins from "@/mixins/setTimeMixins";
+import log4js from "@/plugins/logger";
+import { randomCode } from "@/plugins/utils";
+import { getStageDir } from "@/plugins/env";
+const logger = log4js.getLogger("scan");
+
 export default {
   name: "scan-other",
+  components: { RoomClassSelect, FileTypeSelect, SelectBatchNoDialog },
+  mixins: [timeMixins],
   data() {
-    return {};
+    return {
+      task: this.$ls.get("task", {}),
+      scanStatus: "INIT",
+      scanStageList: [],
+      statusDesc: {
+        INIT: "开始扫描",
+        SCAN: "扫描中",
+        FINISH: "继续扫描",
+      },
+      user: this.$ls.get("user", {}),
+      saving: false,
+      maxCacheCount: 120,
+      scanCount: 0,
+      modalForm: {
+        roomOrClass: "",
+        fileType: "",
+      },
+      rules: {
+        roomOrClass: [
+          {
+            required: true,
+            message: "请选择考场/班级",
+            trigger: "change",
+          },
+        ],
+        fileType: [
+          {
+            required: true,
+            message: "请选择文件类型",
+            trigger: "change",
+          },
+        ],
+      },
+      // 非等待模式:delayMode:0
+      looping: false,
+      stageDir: getStageDir(),
+    };
+  },
+  computed: {
+    paperCount() {
+      return this.scanStageList.length;
+    },
+    canSave() {
+      return this.scanStatus === "FINISH" && this.paperCount > 0;
+    },
+    canScan() {
+      return (
+        this.scanStatus !== "SCAN" && this.paperCount <= this.maxCacheCount
+      );
+    },
+    canClear() {
+      return this.paperCount > 0;
+    },
+    isScaning() {
+      return this.scanStatus === "SCAN";
+    },
+    IS_DELAY_MODE() {
+      return this.GLOBAL.delayMode === 1;
+    },
+  },
+  beforeDestroy() {
+    this.stopLoopScaningFile();
+  },
+  methods: {
+    initData() {
+      this.scanStageList = [];
+      this.scanStatus = "INIT";
+      this.scanCount = 0;
+    },
+    clearFiles() {
+      clearDir(this.stageDir);
+    },
+    async goBackHandle() {
+      if (this.scanStageList.length) {
+        const res = await this.$confirm(
+          `当前存在未保存的扫描数据,确定要退出吗?`,
+          "警告",
+          {
+            type: "warning",
+          }
+        ).catch(() => {});
+        if (res !== "confirm") return;
+      }
+
+      logger.info(`99退出扫描`);
+    },
+    // scan
+    async toScan() {
+      if (!this.canScan) {
+        this.$message.error("已超过最大缓存数量,请先保存数据再继续扫描!");
+        return;
+      }
+
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.scanStatus === "INIT") {
+        this.startTask();
+      } else {
+        this.continueTask();
+      }
+    },
+    startTask() {
+      logger.info(`01开始扫描`);
+      this.continueTask();
+    },
+    continueTask() {
+      this.scanStatus = "SCAN";
+      if (this.IS_DELAY_MODE) {
+        this.evokeScanExe();
+      } else {
+        this.evokeScanExeNotDelay();
+      }
+    },
+    async evokeScanExe() {
+      logger.info("02唤起扫描仪");
+      await evokeScanner(this.GLOBAL.input).catch((error) => {
+        console.error(error);
+      });
+
+      // 缓存已扫描的数据
+      const res = getPreUploadFiles(this.GLOBAL.input, true);
+      if (!res.succeed) {
+        logger.error(`03扫描仪停止,故障:${res.errorMsg}`);
+        this.$message.error(res.errorMsg);
+        this.scanStatus = "FINISH";
+        return;
+      }
+      logger.info(`03扫描仪停止,扫描数:${res.data.length}`);
+      await this.stageScanImage(res.data);
+      this.scanStatus = "FINISH";
+      logger.info(`03-1完成条码解析`);
+    },
+    async evokeScanExeNotDelay() {
+      logger.info("02唤起扫描仪");
+      this.looping = true;
+      this.loopScaningFile();
+
+      await evokeScanner(this.GLOBAL.input).catch((error) => {
+        console.error(error);
+      });
+      this.stopLoopScaningFile();
+      await this.getScaningFile();
+
+      const scanCount = this.scanStageList.length - this.scanCount;
+      this.scanCount = this.scanStageList.length;
+
+      // 已扫描的数据
+      const res = getPreUploadFiles(this.GLOBAL.input);
+      this.scanStatus = "FINISH";
+      if (!res.succeed) {
+        logger.error(
+          `03扫描仪停止,扫描数:${scanCount},故障:${res.errorMsg}`
+        );
+        this.$message.error(res.errorMsg);
+        return;
+      }
+      logger.info(`03扫描仪停止,扫描数:${scanCount}`);
+    },
+    async stageScanImage(imageList) {
+      for (let i = 0, len = imageList.length; i < len; i++) {
+        const fileInfo = {
+          id: randomCode(16),
+          taskId: this.task.id,
+          schoolId: this.task.schoolId,
+          semesterId: this.task.semesterId,
+          examId: this.task.examId,
+          courseCode: this.task.courseCode,
+          courseName: this.task.courseName,
+          frontOriginImgPath: imageList[i].frontFile,
+          versoOriginImgPath: imageList[i].versoFile,
+          isFormal: 0,
+          studentName: "",
+          studentCode: "",
+          ocrArea: "",
+          fileType: this.modalForm.fileType,
+          roomOrClass: this.modalForm.roomOrClass,
+          batchNo: "",
+          clientUserId: this.user.id,
+          clientUsername: this.user.loginName,
+          clientUserLoginTime: this.user.loginTime,
+        };
+
+        this.scanStageList.push(fileInfo);
+      }
+    },
+    async saveScanItem(fileInfo) {
+      const ouputImageList = saveOutputImage(
+        [fileInfo.frontOriginImgPath, fileInfo.versoOriginImgPath],
+        {
+          courseCode: this.task.courseCode,
+        }
+      );
+
+      fileInfo.frontOriginImgPath = ouputImageList[0];
+      fileInfo.versoOriginImgPath = ouputImageList[1];
+
+      await db.saveUploadInfo(fileInfo);
+    },
+    toSave() {
+      if (!this.scanStageList.length) {
+        this.$message.error("当前无要保存的数据!");
+        return;
+      }
+
+      this.$refs.selectBatchNoDialog.open();
+    },
+    async saveScanData(batchNo) {
+      if (this.saving) return;
+      this.saving = true;
+
+      // TODO: 使用批量保存,有时间再做
+      logger.info(`04-1开始保存数据`);
+      for (let i = 0, len = this.scanStageList.length; i < len; i++) {
+        const fileInfo = this.scanStageList[i];
+        fileInfo.batchNo = batchNo;
+
+        let res = true;
+        await this.saveScanItem(fileInfo).catch((err) => {
+          res = false;
+          console.error(err);
+          logger.error(`04-1保存数据错误,${err}`);
+        });
+        if (!res) {
+          this.saving = false;
+          this.$message.error("保存数据错误,请重新尝试!");
+          return Promise.reject();
+        }
+      }
+
+      this.$message.success("保存成功!");
+      this.saving = false;
+      logger.info(`04-2保存数据成功`);
+      this.clearFiles();
+      this.initData();
+    },
+    // delay mode
+    // 实时获取扫描图片
+    async loopScaningFile() {
+      this.clearSetTs();
+      if (!this.looping) return;
+      await this.getScaningFile();
+
+      this.addSetTime(this.loopScaningFile, 1 * 1000);
+    },
+    stopLoopScaningFile() {
+      this.clearSetTs();
+      this.looping = false;
+    },
+    async getScaningFile() {
+      const newScanFiles = getDirScanFile(this.GLOBAL.input);
+      await this.stageScanImage(newScanFiles);
+    },
+    // table action
+    async clearStage() {
+      const res = await this.$confirm(`确定要清空所有数据吗?`, "警告", {
+        type: "warning",
+      }).catch(() => {});
+      if (res !== "confirm") return;
+
+      this.clearFiles();
+      this.initData();
+
+      logger.info(`99数据清空`);
+      this.$message.success("数据已清空!");
+    },
   },
-  methods: {},
 };
 </script>

+ 22 - 4
src/modules/client/views/ScanPaper.vue

@@ -85,6 +85,11 @@
       :datas="selectList"
       @confirm="bindConfirm"
     ></manual-bind-dialog>
+    <!-- SelectBatchNoDialog -->
+    <select-batch-no-dialog
+      ref="SelectBatchNoDialog"
+      @confirm="saveScanData"
+    ></select-batch-no-dialog>
   </div>
 </template>
 
@@ -103,6 +108,8 @@ import { evokeScanner } from "../../../plugins/scanner";
 import ImageContain from "@/components/ImageContain.vue";
 import ScanResultTable from "../components/ScanResultTable.vue";
 import ManualBindDialog from "../components/ManualBindDialog.vue";
+import SelectBatchNoDialog from "../components/SelectBatchNoDialog.vue";
+
 import timeMixins from "@/mixins/setTimeMixins";
 import { getStudentInfo } from "../api";
 import log4js from "@/plugins/logger";
@@ -113,7 +120,12 @@ const logger = log4js.getLogger("scan");
 export default {
   name: "scan-paper",
   mixins: [timeMixins],
-  components: { ImageContain, ScanResultTable, ManualBindDialog },
+  components: {
+    ImageContain,
+    ScanResultTable,
+    ManualBindDialog,
+    SelectBatchNoDialog,
+  },
   data() {
     return {
       task: this.$ls.get("task", {}),
@@ -305,6 +317,9 @@ export default {
           studentName: "",
           studentCode: "",
           ocrArea: ocrAreaContent,
+          fileType: "答题卡",
+          roomOrClass: "",
+          batchNo: "",
           clientUserId: this.user.id,
           clientUsername: this.user.loginName,
           clientUserLoginTime: this.user.loginTime,
@@ -389,7 +404,7 @@ export default {
 
       await db.saveUploadInfo(fileInfo);
     },
-    async toSave() {
+    toSave() {
       if (this.errorStageList.length) {
         this.$message.error("还有异常数据未处理!");
         return;
@@ -399,7 +414,9 @@ export default {
         this.$message.error("当前无要保存的数据!");
         return;
       }
-
+      this.$refs.selectBatchNoDialog.open();
+    },
+    async saveScanData(batchNo) {
       if (this.saving) return;
       this.saving = true;
 
@@ -408,6 +425,7 @@ export default {
       logger.info(`04-1开始保存数据`);
       for (let i = 0, len = datas.length; i < len; i++) {
         const fileInfo = datas[i];
+        fileInfo.batchNo = batchNo;
 
         let res = true;
         await this.saveScanItem(fileInfo).catch((err) => {
@@ -443,7 +461,7 @@ export default {
     },
     async getScaningFile() {
       const newScanFiles = getDirScanFile(this.GLOBAL.input);
-      await this.stageScanImage(newScanFiles, true);
+      await this.stageScanImage(newScanFiles);
     },
     // table action
     toBind() {

+ 22 - 1
src/plugins/db.js

@@ -60,7 +60,7 @@ function serializeWhere(params) {
 
 // scan
 function saveUploadInfo(params) {
-  const sql = `INSERT INTO scan (taskId, schoolId, semesterId, examId, courseCode, courseName, frontOriginImgPath, versoOriginImgPath, studentCode, ocrArea, isFormal, clientUserId, clientUsername, clientUserLoginTime, isUpload,createdTime, finishTime) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
+  const sql = `INSERT INTO scan (taskId, schoolId, semesterId, examId, courseCode, courseName, frontOriginImgPath, versoOriginImgPath, studentCode, ocrArea,fileType,roomOrClass,batchNo, isFormal, clientUserId, clientUsername, clientUserLoginTime, isUpload,createdTime, finishTime) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
   const datas = [
     params.taskId,
     params.schoolId,
@@ -72,6 +72,9 @@ function saveUploadInfo(params) {
     params.versoOriginImgPath,
     params.studentCode,
     params.ocrArea,
+    params.fileType,
+    params.roomOrClass,
+    params.batchNo,
     params.isFormal,
     params.clientUserId,
     params.clientUsername,
@@ -90,6 +93,23 @@ function saveUploadInfo(params) {
   });
 }
 
+function batchSaveUploadInfo(datas) {
+  return new Promise((resolve, reject) => {
+    const sql = `INSERT INTO scan (taskId, schoolId, semesterId, examId, courseCode, courseName, frontOriginImgPath, versoOriginImgPath, studentCode, ocrArea,fileType,roomOrClass,batchNo, isFormal, clientUserId, clientUsername, clientUserLoginTime, isUpload,createdTime, finishTime) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
+    db.serialize(() => {
+      const stmt = db.prepare(sql);
+
+      for (let i = 0; i < datas.length; i++) {
+        stmt.run(datas[i]);
+      }
+      stmt.finalize((err) => {
+        if (err) reject(err);
+        resolve();
+      });
+    });
+  });
+}
+
 function searchUploadList(params) {
   const { where, whereData } = serializeWhere(params);
 
@@ -226,6 +246,7 @@ export default {
   init,
   // scan
   saveUploadInfo,
+  batchSaveUploadInfo,
   searchUploadList,
   searchHistoryList,
   countScanList,

+ 3 - 0
src/plugins/db.sql

@@ -27,6 +27,9 @@ CREATE TABLE "scan" (
   "versoOriginImgPath" TEXT,
   "studentCode" TEXT,
   "ocrArea" TEXT,
+  "fileType" TEXT NOT NULL DEFAULT '0',
+  "roomOrClass" TEXT,
+  "batchNo" TEXT NOT NULL,
   "isFormal" integer NOT NULL DEFAULT 0,
   "isUpload" integer NOT NULL DEFAULT 0,
   "clientUserId" text NOT NULL,