浏览代码

feat: 试卷扫描

zhangjie 11 月之前
父节点
当前提交
b5c4a6b661

+ 113 - 0
src/assets/styles/common-comp.scss

@@ -184,3 +184,116 @@
     background: rgba(55, 55, 55, 0.8);
   }
 }
+
+// image-contain
+.image-contain {
+  position: relative;
+  height: 100%;
+  overflow: hidden;
+
+  &-image {
+    position: absolute;
+    top: 0;
+    left: 50%;
+    width: 600px;
+    margin-left: -300px;
+    transition: width, height, transform 0.2s linear;
+    z-index: 8;
+    &-nosition {
+      transition: none;
+    }
+    &-move {
+      cursor: move;
+    }
+    > img {
+      display: block;
+      width: 100%;
+    }
+  }
+  &-guide {
+    position: absolute;
+    width: 80px;
+    height: 80px;
+    line-height: 80px;
+    top: 50%;
+    margin-top: -80px;
+    text-align: center;
+    color: #d0d0d0;
+    z-index: 9;
+    font-size: 60px;
+    text-shadow: 0 0 2px #333;
+    cursor: pointer;
+
+    &:hover {
+      color: #eee;
+    }
+    > i {
+      margin-top: -5px;
+    }
+    &-prev {
+      left: 0;
+    }
+    &-next {
+      right: 0;
+    }
+  }
+  &-action {
+    position: absolute;
+    height: 60px;
+    bottom: 0;
+    right: 0;
+    padding: 10px;
+    font-size: 30px;
+    color: #d0d0d0;
+    z-index: 99;
+    li {
+      display: inline-block;
+      vertical-align: top;
+      height: 40px;
+      width: 40px;
+      line-height: 40px;
+      margin: 0 5px;
+      text-align: center;
+      cursor: pointer;
+      transition: transform 0.2s linear;
+      // text-shadow: 0 0 1px #000;
+      color: #777;
+    }
+    li:hover {
+      // color: #000;
+      transform: scale(1.1, 1.1);
+    }
+    li.li-disabled {
+      cursor: not-allowed;
+      color: #d0d0d0 !important;
+      transform: none !important;
+    }
+  }
+
+  &-loading {
+    position: absolute;
+    width: 60px;
+    height: 60px;
+    top: 0;
+    left: 50%;
+    margin: 0 0 0 -30px;
+    color: $--color-border;
+    font-size: 50px;
+    text-align: center;
+    line-height: 60px;
+    z-index: 99;
+  }
+
+  &-none {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    color: #999;
+    text-align: center;
+    font-size: 20px;
+    > i {
+      font-size: 30px;
+    }
+  }
+}

+ 65 - 0
src/assets/styles/pages.scss

@@ -403,3 +403,68 @@
     }
   }
 }
+
+// scan-paper
+.scan-paper {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+
+  .scan-head {
+    flex-shrink: 0;
+    flex-grow: 0;
+  }
+
+  .scan-body {
+    flex-grow: 2;
+
+    display: flex;
+    justify-content: space-between;
+    align-items: stretch;
+
+    .scan-result {
+      width: 400px;
+      flex-grow: 0;
+      flex-shrink: 0;
+      margin-right: 16px;
+
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
+
+      &-head {
+        flex-grow: 0;
+        flex-shrink: 0;
+        margin-bottom: 10px;
+      }
+
+      &-foot {
+        flex-grow: 0;
+        flex-shrink: 0;
+        padding: 10px;
+        background-color: #fff;
+        border-bottom-left-radius: $--border-radius;
+        border-bottom-right-radius: $--border-radius;
+      }
+
+      &-table {
+        flex-grow: 2;
+        overflow: hidden;
+        background-color: #fff;
+        border-top-left-radius: $--border-radius;
+        border-top-right-radius: $--border-radius;
+        padding: 10px 10px 0;
+      }
+    }
+
+    .scan-content {
+      flex-grow: 2;
+      height: 100%;
+
+      background-color: #fff;
+      border-radius: $--border-radius;
+      padding: 16px;
+    }
+  }
+}

+ 276 - 0
src/components/ImageContain.vue

@@ -0,0 +1,276 @@
+<template>
+  <div class="image-contain">
+    <div
+      v-if="showGuide"
+      :class="[`${prefixCls}-guide`, `${prefixCls}-guide-prev`]"
+      @click.stop="showPrev"
+    >
+      <i class="el-icon-arrow-left"></i>
+    </div>
+    <div
+      v-if="showGuide"
+      :class="[`${prefixCls}-guide`, `${prefixCls}-guide-next`]"
+      @click.stop="showNext"
+    >
+      <i class="el-icon-arrow-right"></i>
+    </div>
+
+    <!-- image -->
+    <div
+      :class="[
+        `${prefixCls}-image`,
+        { [`${prefixCls}-image-nosition`]: nosition },
+      ]"
+      :style="styles"
+      v-move-ele.prevent.stop="{ mouseMove }"
+    >
+      <img
+        :key="image.url"
+        :src="image.url"
+        :alt="imageName"
+        ref="PreviewImgDetail"
+        @load="resizeImage"
+      />
+    </div>
+    <div :class="[`${prefixCls}-none`]" v-if="!image.url">
+      <i class="el-icon-picture"></i>
+      <p>暂无数据</p>
+    </div>
+
+    <div :class="[`${prefixCls}-loading`]" v-show="loading">
+      <i class="el-icon-loading"></i>
+    </div>
+    <!-- action -->
+    <div v-if="showAction" :class="[`${prefixCls}-action`]">
+      <ul>
+        <li title="合适大小" @click.stop="toOrigin">
+          <i class="el-icon-rank" />
+        </li>
+        <li title="旋转" @click.stop="toRotate">
+          <i class="el-icon-refresh-right"></i>
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+import MoveEle from "../plugins/move-ele";
+const prefixCls = "image-contain";
+
+export default {
+  name: "image-contain",
+  props: {
+    image: {
+      type: Object,
+      default() {
+        return { url: "", filename: "" };
+      },
+    },
+    showAction: {
+      type: Boolean,
+      default: true,
+    },
+    showGuide: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  directives: { MoveEle },
+  data() {
+    return {
+      prefixCls,
+      styles: { width: "", height: "", top: "", left: "", transform: "" },
+      initWidth: 500,
+      minScale: 0.5,
+      maxScale: 5,
+      transform: {
+        scale: 1,
+        rotate: 0,
+      },
+      loading: false,
+      loadingSetT: null,
+      nosition: false,
+    };
+  },
+  // watch: {
+  //   "image.url": {
+  //     handler(val) {
+  //       if (val) {
+  //         this.loadingSetT = setTimeout(() => {
+  //           this.loading = true;
+  //         }, 300);
+  //         this.styles = {
+  //           width: "",
+  //           height: "",
+  //           top: "",
+  //           left: "",
+  //           transform: ""
+  //         };
+  //       }
+  //     }
+  //   }
+  // },
+  computed: {
+    imageName() {
+      if (this.image.filename) return this.image.filename;
+
+      const st1 = this.image.url.split("?")[0] || "";
+      const st2 = st1.split("/").slice(-1) || "";
+      return st2;
+    },
+  },
+  mounted() {
+    this.registWheelHandle();
+  },
+  methods: {
+    resizeImage() {
+      const imgDom = this.$refs.PreviewImgDetail;
+      const { naturalWidth, naturalHeight } = imgDom;
+      const imageSize = this.getImageSizePos({
+        win: {
+          width: this.$el.clientWidth,
+          height: this.$el.clientHeight,
+        },
+        img: {
+          width: naturalWidth,
+          height: naturalHeight,
+        },
+        rotate: 0,
+      });
+
+      this.styles = Object.assign(this.styles, {
+        width: imageSize.width + "px",
+        height: imageSize.height + "px",
+        top: imageSize.top + "px",
+        left: imageSize.left + "px",
+        marginLeft: "auto",
+        transform: "none",
+      });
+      this.transform = {
+        scale: 1,
+        rotate: 0,
+      };
+      this.loading = false;
+      setTimeout(() => {
+        this.nosition = false;
+      }, 100);
+    },
+    getImageSizePos({ win, img, rotate }) {
+      const imageSize = {
+        width: 0,
+        height: 0,
+        top: 0,
+        left: 0,
+      };
+      const isHorizontal = !!(rotate % 180);
+
+      const rateWin = isHorizontal
+        ? win.height / win.width
+        : win.width / win.height;
+      const hwin = isHorizontal
+        ? {
+            width: win.height,
+            height: win.width,
+          }
+        : win;
+
+      const rateImg = img.width / img.height;
+
+      if (rateImg <= rateWin) {
+        imageSize.height = Math.min(hwin.height, img.height);
+        imageSize.width = Math.floor(
+          (imageSize.height * img.width) / img.height
+        );
+      } else {
+        imageSize.width = Math.min(hwin.width, img.width);
+        imageSize.height = Math.floor(
+          (imageSize.width * img.height) / img.width
+        );
+      }
+      imageSize.left = (win.width - imageSize.width) / 2;
+      imageSize.top = (win.height - imageSize.height) / 2;
+      return imageSize;
+    },
+    showPrev() {
+      this.$emit("on-prev");
+    },
+    showNext() {
+      this.$emit("on-next");
+    },
+    // dome-move
+    registWheelHandle() {
+      this.$el.addEventListener("wheel", (e) => {
+        e.preventDefault();
+        this.mouseWheel(e.wheelDeltaY);
+      });
+    },
+    mouseMove({ left, top }) {
+      this.styles.left = left + "px";
+      this.styles.top = top + "px";
+    },
+    mouseWheel(delta) {
+      if (delta < 0) {
+        this.toMagnify();
+      } else {
+        this.toShrink();
+      }
+    },
+    setStyleTransform() {
+      const { scale, rotate } = this.transform;
+      this.styles.transform = `scale(${scale}, ${scale}) rotate(${rotate}deg)`;
+    },
+    toOrigin() {
+      this.transform.scale = 1;
+      this.setStyleTransform();
+      this.resizeImage();
+    },
+    toMagnify() {
+      const scale = (this.transform.scale * 1.2).toFixed(2);
+      this.transform.scale = scale >= this.maxScale ? this.maxScale : scale;
+      this.setStyleTransform();
+    },
+    toShrink() {
+      const scale = (this.transform.scale * 0.75).toFixed(2);
+      this.transform.scale = scale <= this.minScale ? this.minScale : scale;
+      this.setStyleTransform();
+    },
+    toRotate() {
+      this.transform.rotate = this.transform.rotate + 90;
+      this.setStyleTransform();
+      // 调整图片尺寸
+      const { naturalWidth, naturalHeight } = this.$refs.PreviewImgDetail;
+      const imageSize = this.getImageSizePos({
+        win: {
+          width: this.$el.clientWidth,
+          height: this.$el.clientHeight,
+        },
+        img: {
+          width: naturalWidth,
+          height: naturalHeight,
+        },
+        rotate: this.transform.rotate,
+      });
+
+      this.styles = Object.assign(this.styles, {
+        width: imageSize.width + "px",
+        height: imageSize.height + "px",
+        top: imageSize.top + "px",
+        left: imageSize.left + "px",
+      });
+      // 360度无缝切换到0度
+      if (this.transform.rotate >= 360) {
+        setTimeout(() => {
+          this.nosition = true;
+          this.transform.rotate = 0;
+          this.setStyleTransform();
+          setTimeout(() => {
+            this.nosition = false;
+          }, 100);
+        }, 200);
+        // 200ms当次旋转动画持续时间
+      }
+    },
+  },
+};
+</script>

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

@@ -26,3 +26,9 @@ export const uploadImage = (isFormal, datas, config = {}) => {
 export const taskInfos = (datas) => {
   return $postParam(`/api/admin/client/task/get`, datas);
 };
+export const getStudentInfo = (datas) => {
+  return $postParam(`/api/admin/com/task/get`, datas);
+};
+export const updateStudent = (datas) => {
+  return $post("/api/admin/exam/student/save", datas);
+};

+ 0 - 76
src/modules/client/components/HandleInputDialog.vue

@@ -1,76 +0,0 @@
-<template>
-  <el-dialog
-    :visible.sync="modalIsShow"
-    title="手动输入"
-    top="10vh"
-    width="460px"
-    :close-on-click-modal="false"
-    :close-on-press-escape="false"
-    :show-close="false"
-    append-to-body
-    @open="visibleChange"
-  >
-    <el-form
-      ref="modalFormComp"
-      :model="modalForm"
-      :rules="rules"
-      label-width="80px"
-      @submit.native.prevent
-    >
-      <el-form-item prop="studentCode" label="学号:">
-        <el-input
-          v-model.trim="modalForm.studentCode"
-          placeholder="请输入第一张试卷学号"
-          clearable
-        ></el-input>
-      </el-form-item>
-    </el-form>
-    <div slot="footer">
-      <el-button type="primary" @click="submit">确认</el-button>
-    </div>
-  </el-dialog>
-</template>
-
-<script>
-export default {
-  name: "handle-input-dialog",
-  data() {
-    return {
-      modalIsShow: false,
-      modalForm: { studentCode: "" },
-      rules: {
-        studentCode: [
-          {
-            required: true,
-            message: "请输入第一张试卷学号",
-            trigger: "change",
-          },
-          {
-            message: "学号只能由数字、字母和下划线组成,长度1-30个字符",
-            pattern: /^[a-zA-Z0-9_-]{1,30}$/,
-            trigger: "change",
-          },
-        ],
-      },
-    };
-  },
-  methods: {
-    visibleChange() {
-      this.modalForm.studentCode = "";
-    },
-    cancel() {
-      this.modalIsShow = false;
-    },
-    open() {
-      this.modalIsShow = true;
-    },
-    async submit() {
-      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
-      if (!valid) return;
-
-      this.$emit("confirm", this.modalForm.studentCode);
-      this.cancel();
-    },
-  },
-};
-</script>

+ 333 - 0
src/modules/client/components/ManualBindDialog.vue

@@ -0,0 +1,333 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="手动输入"
+    top="10vh"
+    width="460px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :show-close="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="80px"
+      @submit.native.prevent
+    >
+      <el-form-item prop="studentCode" label="学号:">
+        <div @keyup.enter="searchStudent">
+          <el-input
+            v-model.trim="modalForm.studentCode"
+            placeholder="请输入学号"
+            clearable
+          ></el-input>
+        </div>
+        <p>注意:请输入学号后回车</p>
+      </el-form-item>
+      <el-form-item prop="studentCode" label="姓名:">
+        <el-input
+          v-model.trim="modalForm.studentName"
+          placeholder="请输入姓名"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="courseName" label="课程名称:">
+        <el-input
+          v-model.trim="modalForm.courseName"
+          placeholder="请输入课程名称"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="courseCode" label="课程代码:">
+        <el-input
+          v-model.trim="modalForm.courseCode"
+          placeholder="请输入课程代码"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="teacher" label="任课老师:">
+        <el-input
+          v-model.trim="modalForm.teacher"
+          placeholder="请输入任课老师"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="teachClass" label="教学班:">
+        <el-input
+          v-model.trim="modalForm.teachClass"
+          placeholder="请输入教学班"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="collegeName" label="学院:">
+        <el-input
+          v-model.trim="modalForm.collegeName"
+          placeholder="请输入学院"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="majorName" label="专业:">
+        <el-input
+          v-model.trim="modalForm.majorName"
+          placeholder="请输入专业"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="className" label="班级:">
+        <el-input
+          v-model.trim="modalForm.className"
+          placeholder="请输入班级"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="score" label="成绩:">
+        <el-input-number
+          v-model="modalForm.score"
+          :min="0"
+          :max="999"
+          :step="1"
+          step-strictly
+          :controls="false"
+          :disabled="editDisabled"
+        ></el-input-number>
+      </el-form-item>
+      <el-form-item prop="remark" label="备注:">
+        <el-input
+          v-model.trim="modalForm.remark"
+          placeholder="请输入备注"
+          clearable
+          :disabled="editDisabled"
+        ></el-input>
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" :disabled="!canSubmit" @click="submit"
+        >绑定</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { getStudentInfo, updateStudent } from "../api";
+
+const initModalForm = {
+  id: null,
+  studentCode: "",
+  studentName: "",
+  courseCode: "",
+  courseName: "",
+  teacher: "",
+  teachClass: "",
+  collegeName: "",
+  majorName: "",
+  className: "",
+  score: undefined,
+  remark: "",
+};
+
+export default {
+  name: "manual-bind-dialog",
+  props: {
+    datas: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      releaseStudent: null,
+      modalForm: { ...initModalForm },
+      rules: {
+        studentName: [
+          {
+            required: true,
+            message: "请输入姓名",
+            trigger: "change",
+          },
+          {
+            message: "姓名不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        studentCode: [
+          {
+            required: true,
+            message: "请输入学号",
+            trigger: "change",
+          },
+          {
+            message: "学号只能由数字、字母和下划线组成,长度1-30个字符",
+            pattern: /^[a-zA-Z0-9_-]{1,30}$/,
+            trigger: "change",
+          },
+        ],
+        collegeName: [
+          {
+            message: "学院不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        majorName: [
+          {
+            message: "专业不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        className: [
+          {
+            message: "班级不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        teacher: [
+          {
+            message: "任课老师不能超过30字符",
+            max: 30,
+            trigger: "change",
+          },
+        ],
+        teachClass: [
+          {
+            message: "教学班不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        courseName: [
+          {
+            required: true,
+            message: "请输入课程名称",
+            trigger: "change",
+          },
+          {
+            message: "课程名称不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        courseCode: [
+          {
+            required: true,
+            message: "请输入课程代码",
+            trigger: "change",
+          },
+          {
+            message: "课程代码不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        examRoom: [
+          {
+            message: "考场不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        remark: [
+          {
+            message: "备注不能超过50字符",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  computed: {
+    editDisabled() {
+      return Boolean(this.releaseStudent);
+    },
+    canSubmit() {
+      return (
+        this.releaseStudent &&
+        this.releaseStudent.studentCode === this.modalForm.studentCode
+      );
+    },
+  },
+  methods: {
+    visibleChange() {
+      if (this.datas.length === 1) {
+        this.modalForm = this.$objAssign(initModalForm, this.datas[0]);
+        if (this.datas[0].studentName) {
+          this.releaseStudent = { ...this.modalForm };
+        } else {
+          this.releaseStudent = null;
+        }
+      } else {
+        this.modalForm = { ...initModalForm };
+        this.releaseStudent = null;
+      }
+
+      this.$nextTick(() => {
+        this.$refs.modalFormComp.clearValidate();
+      });
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async searchStudent() {
+      const valid = await this.$refs.modalFormComp
+        .validateField("studentCode")
+        .catch(() => {});
+      if (!valid) return;
+
+      const res = await getStudentInfo({
+        // courseCode: this.task.courseCode,
+        studentCode: this.modalForm.studentCode,
+      }).catch(() => {});
+      if (res) {
+        this.releaseStudent = res;
+        this.modalForm = this.$objAssign(this.modalForm, res);
+      } else {
+        this.releaseStudent = null;
+        this.modalForm = {
+          ...initModalForm,
+          studentCode: this.modalForm.studentCode,
+        };
+      }
+
+      this.$nextTick(() => {
+        this.$refs.modalFormComp.clearValidate();
+      });
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (!this.editDisabled) {
+        // 新创建的考生,先保存
+        const res = await updateStudent(this.modalForm).catch(() => {});
+        if (!res) return;
+        this.modalForm.id = res;
+      }
+
+      this.$emit("confirm", this.modalForm);
+      this.cancel();
+    },
+  },
+};
+</script>

+ 197 - 0
src/modules/client/components/ScanResultTable.vue

@@ -0,0 +1,197 @@
+<template>
+  <el-table :data="datas" @row-click="studentClickHandle">
+    <el-table-column type="expand">
+      <template slot-scope="scope">
+        <el-table :data="scope.row.papers" @row-click="paperClickHandle">
+          <el-table-column width="55" align="center">
+            <template slot-scope="props">
+              <el-checkbox
+                v-model="props.row.select"
+                @change="selectionChange"
+              ></el-checkbox>
+            </template>
+          </el-table-column>
+          <el-table-column label="文件名">
+            <template slot-scope="props"> 第{{ props.$index + 1 }}张 </template>
+          </el-table-column>
+          <el-table-column class-name="action-column" label="操作" width="60">
+            <template slot-scope="props">
+              <el-button
+                class="btn-danger"
+                type="text"
+                @click="toDeletePaper(scope.row, props.row)"
+              >
+                删除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+    </el-table-column>
+    <el-table-column width="55" align="center">
+      <template slot-scope="scope">
+        <el-checkbox
+          v-model="scope.row.select"
+          @change="selectionChange"
+        ></el-checkbox>
+      </template>
+    </el-table-column>
+    <el-table-column
+      prop="studentCode"
+      label="学号"
+      width="120"
+    ></el-table-column>
+    <el-table-column
+      v-if="isNormalTab"
+      prop="studentName"
+      label="姓名"
+    ></el-table-column>
+    <el-table-column prop="paperCount" label="张数" width="60">
+      <template slot-scope="scope">
+        {{ scope.row.papers.length }}
+      </template>
+    </el-table-column>
+    <el-table-column class-name="action-column" label="操作" width="60">
+      <template slot-scope="scope">
+        <el-button
+          class="btn-danger"
+          type="text"
+          @click="toDeleteUser(scope.row)"
+        >
+          删除
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+import { deleteFiles } from "../../../plugins/imageOcr";
+
+export default {
+  name: "scan-result-table",
+  props: {
+    datas: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    tab: {
+      type: String,
+      default: "normal",
+    },
+  },
+  data() {
+    return { selectList: [] };
+  },
+  computed: {
+    isNormalTab() {
+      return this.curTab === "normal";
+    },
+  },
+  methods: {
+    // table action
+    selectionChange() {
+      const selectList = [];
+      this.datas.forEach((row) => {
+        if (row.select) {
+          row.papers.forEach((p) => (p.select = true));
+        } else {
+          const paperSelected = !row.papers.some((p) => !p.select);
+          row.select = paperSelected;
+        }
+
+        const selectPapers = row.papers.filter((p) => p.select);
+        if (selectPapers.length) {
+          selectList.push({
+            ...row,
+            papers: selectPapers,
+          });
+        }
+      });
+
+      this.selectList = selectList;
+      this.$emit("select-change", this.selectList);
+    },
+    clearSelection() {
+      this.datas.forEach((row) => {
+        row.select = false;
+        row.papers.forEach((p) => (p.select = false));
+      });
+      this.selectList = [];
+    },
+    async toDeleteUser(row) {
+      const res = await this.$confirm(
+        `确定要删除学生【${row.studentName || row.studentCode}】所有数据吗?`,
+        "警告",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (res !== "confirm") return;
+
+      const selectedFiles = row.papers
+        .map((item) => [item.frontOriginImgPath, item.versoOriginImgPath])
+        .flat();
+      deleteFiles(selectedFiles);
+
+      this.datas = this.datas.filter(
+        (item) => item.studentCode !== row.studentCode
+      );
+
+      this.selectionChange();
+      this.$emit("delete-paper", selectedFiles);
+    },
+    async toDeletePaper(row, paper) {
+      const res = await this.$confirm(`确定要删除当前图片吗?`, "警告", {
+        type: "warning",
+      }).catch(() => {});
+      if (res !== "confirm") return;
+
+      const selectedFiles = [
+        paper.frontOriginImgPath,
+        paper.versoOriginImgPath,
+      ];
+      deleteFiles(selectedFiles);
+
+      row.papers = row.papers.filter((item) => item.id !== paper.id);
+      if (!row.papers.length) {
+        this.datas = this.datas.filter(
+          (item) => item.studentCode !== row.studentCode
+        );
+      }
+
+      this.selectionChange();
+      this.$emit("delete-paper", selectedFiles);
+    },
+    studentClickHandle(row) {
+      const curPapers = row.papers
+        .map((item) => [item.frontOriginImgPath, item.versoOriginImgPath])
+        .flat();
+      const curPaperIndex = 0;
+      this.$emit("row-click", {
+        curPaperIndex,
+        curPapers,
+      });
+    },
+    paperClickHandle(paper) {
+      const row = this.datas.find((item) =>
+        item.papers.some((elem) => elem.id === paper.id)
+      );
+      if (!row) return;
+
+      const curPapers = row.papers
+        .map((item) => [item.frontOriginImgPath, item.versoOriginImgPath])
+        .flat();
+      const curPaperIndex = curPapers.findIndex(
+        (item) => item === paper.frontOriginImgPath
+      );
+      this.$emit("row-click", {
+        curPaperIndex,
+        curPapers,
+      });
+    },
+  },
+};
+</script>

+ 1 - 1
src/modules/client/components/ScanTaskProcessDialog.vue

@@ -107,7 +107,7 @@ import {
 import db from "../../../plugins/db";
 import { evokeScanner } from "../../../plugins/scanner";
 import SimpleImagePreview from "@/components/SimpleImagePreview.vue";
-import HandleInputDialog from "./HandleInputDialog.vue";
+import HandleInputDialog from "./ManualBindDialog.vue";
 import timeMixins from "@/mixins/setTimeMixins";
 import log4js from "@/plugins/logger";
 const logger = log4js.getLogger("scan");

+ 6 - 0
src/modules/client/router.js

@@ -1,4 +1,5 @@
 import Scan from "./views/Scan.vue";
+import ScanPaper from "./views/ScanPaper.vue";
 
 export default [
   {
@@ -6,4 +7,9 @@ export default [
     name: "Scan",
     component: Scan,
   },
+  {
+    path: "/scan-paper",
+    name: "ScanPaper",
+    component: ScanPaper,
+  },
 ];

+ 605 - 0
src/modules/client/views/ScanPaper.vue

@@ -0,0 +1,605 @@
+<template>
+  <div class="scan-paper">
+    <div class="part-box part-box-pad part-box-flex scan-head">
+      <div>
+        <h2>课程(代码):{{ task.courseName }}({{ task.courseCode }})</h2>
+      </div>
+      <div>
+        <el-button :disabled="!hasSelectedData" type="primary" @click="toBind"
+          >重新绑定</el-button
+        >
+        <el-button :disabled="!canSave" 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="scan-body">
+      <div class="scan-result">
+        <div class="mb-4 tab-btns scan-result-head">
+          <el-button
+            size="medium"
+            :type="curTab === 'normal' ? 'primary' : 'default'"
+            @click="switchTab('normal')"
+            >正常 <span>[{{ normalCount }}]</span></el-button
+          >
+          <el-button
+            size="medium"
+            :type="curTab === 'error' ? 'danger' : 'default'"
+            @click="switchTab('error')"
+            >异常
+            <span class="color-danger">[{{ errorCount }}]</span></el-button
+          >
+        </div>
+        <div class="scan-result-table">
+          <scan-result-table
+            v-if="isNormalTab"
+            ref="scanResultTableRef"
+            :datas="scanStageList"
+            @row-click="rowClickHandle"
+            @select-change="selectChange"
+            @delete-paper="deletePaperHandle"
+          ></scan-result-table>
+          <scan-result-table
+            v-else
+            ref="scanResultTableRef"
+            :datas="errorStageList"
+            @row-click="rowClickHandle"
+            @select-change="selectChange"
+            @delete-paper="deletePaperHandle"
+          ></scan-result-table>
+        </div>
+        <div class="scan-result-foot">
+          共{{ studentCount }}人,{{ paperCount }}张图片
+        </div>
+      </div>
+      <div class="scan-content">
+        <image-contain
+          v-if="curPaper && curPaper.url"
+          ref="ImageContain"
+          :image="curPaper"
+          @on-prev="toPrevPaper"
+          @on-next="toNextPaper"
+        ></image-contain>
+      </div>
+    </div>
+
+    <!-- ManualBindDialog -->
+    <manual-bind-dialog
+      ref="ManualBindDialog"
+      :datas="selectList"
+      @confirm="bindConfirm"
+    ></manual-bind-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import {
+  getPreUploadFiles,
+  saveOutputImage,
+  clearDir,
+  decodeImageCode,
+  getDirScanFile,
+} from "../../../plugins/imageOcr";
+import db from "../../../plugins/db";
+import { evokeScanner } from "../../../plugins/scanner";
+import ImageContain from "@/components/ImageContain.vue";
+import ScanResultTable from "../components/ScanResultTable.vue";
+import ManualBindDialog from "../components/ManualBindDialog.vue";
+import timeMixins from "@/mixins/setTimeMixins";
+import { getStudentInfo } from "../api";
+import log4js from "@/plugins/logger";
+import { randomCode, calcSum } from "@/plugins/utils";
+import { getStageDir } from "@/plugins/env";
+const logger = log4js.getLogger("scan");
+
+export default {
+  name: "scan-paper",
+  mixins: [timeMixins],
+  components: { ImageContain, ScanResultTable, ManualBindDialog },
+  data() {
+    return {
+      task: this.$ls.get("task", {}),
+      scanStatus: "INIT",
+      scanStageList: [],
+      errorStageList: [],
+      selectList: [],
+      statusDesc: {
+        INIT: "开始扫描",
+        SCAN: "扫描中",
+        FINISH: "继续扫描",
+      },
+      user: this.$ls.get("user", {}),
+      saving: false,
+      maxCacheCount: 120,
+      lastStudentCode: "",
+      scanCount: 0,
+      menus: [
+        { code: "normal", name: "正常" },
+        { code: "error", name: "异常" },
+      ],
+      curTab: "normal",
+      // 非等待模式:delayMode:0
+      looping: false,
+      // 图片预览
+      curPapers: [],
+      curPaperIndex: 0,
+      curPaper: { url: "" },
+      stageDir: getStageDir(),
+    };
+  },
+  computed: {
+    ...mapState("client", ["ocrArea"]),
+    canSave() {
+      return this.scanStatus === "FINISH" && this.scanStageList.length;
+    },
+    canScan() {
+      return this.scanStageList.length <= this.maxCacheCount;
+    },
+    hasSelectedData() {
+      return Boolean(this.selectList.length);
+    },
+    IS_DELAY_MODE() {
+      return this.GLOBAL.delayMode === 1;
+    },
+    errorCount() {
+      return calcSum(this.errorStageList.map((item) => item.papers.length));
+    },
+    normalCount() {
+      return calcSum(this.scanStageList.map((item) => item.papers.length));
+    },
+    isNormalTab() {
+      return this.curTab === "normal";
+    },
+    studentCount() {
+      return this.isNormalTab
+        ? this.scanStageList.length
+        : this.errorStageList.length;
+    },
+    paperCount() {
+      return this.isNormalTab ? this.normalCount : this.errorCount;
+    },
+  },
+  beforeDestroy() {
+    this.stopLoopScaningFile();
+  },
+  methods: {
+    initData() {
+      this.lastStudentCode = "";
+      this.scanStageList = [];
+      this.errorStageList = [];
+      this.scanStatus = "INIT";
+      this.curPagePapers = [];
+      this.curPagePaperIndex = 0;
+      this.curPagePaper = { url: "" };
+      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退出扫描`);
+    },
+    switchTab(tab) {
+      this.$refs.scanResultTableRef.clearSelection();
+      this.curTab = tab;
+      this.selectList = [];
+    },
+    // scan
+    toScan() {
+      if (!this.canScan) {
+        this.$message.error("已超过最大缓存数量,请先保存数据再继续扫描!");
+        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) {
+      const ocrAreaContent = JSON.stringify(this.ocrArea);
+      for (let i = 0, len = imageList.length; i < len; i++) {
+        const fileInfo = {
+          id: "",
+          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: 1,
+          studentName: "",
+          studentCode: "",
+          ocrArea: ocrAreaContent,
+          clientUserId: this.user.id,
+          clientUsername: this.user.loginName,
+          clientUserLoginTime: this.user.loginTime,
+          select: false,
+        };
+
+        const code = await decodeImageCode(
+          fileInfo.frontOriginImgPath,
+          this.ocrArea
+        ).catch((err) => {
+          console.error(err);
+          logger.error(`03条码解析失败,${err}`);
+        });
+        fileInfo.studentCode = code || this.lastStudentCode;
+        // 按照识别空自动绑定前一张code的规则,无论识别到的code是否合法,都应该作为最后一次识别的code
+        // 否则,第一张进异常,后面空白条码页会自动进正常页面,对后续处理带来一定困扰
+        if (fileInfo.studentCode) {
+          this.lastStudentCode = fileInfo.studentCode;
+        }
+
+        fileInfo.id = `${fileInfo.studentCode || 0}-${randomCode(16)}`;
+
+        const studentStage = this.scanStageList.find(
+          (item) => item.studentCode === fileInfo.studentCode
+        );
+        if (studentStage) {
+          studentStage.papers.push(fileInfo);
+          continue;
+        }
+
+        const res = await getStudentInfo({
+          courseCode: this.task.courseCode,
+          studentCode: fileInfo.studentCode,
+        }).catch(() => {});
+        if (res) {
+          fileInfo.studentName = res.studentName;
+          this.scanStageList.push({
+            id: res.id,
+            studentCode: res.studentCode,
+            studentName: res.studentName,
+            courseCode: res.courseCode,
+            courseName: res.courseName,
+            teacher: res.teacher,
+            teachClass: res.teachClass,
+            collegeName: res.collegeName,
+            majorName: res.majorName,
+            className: res.className,
+            score: res.score,
+            remark: res.remark,
+            select: false,
+            papers: [fileInfo],
+          });
+          continue;
+        }
+
+        const errorStudentStage = this.errorStageList.find(
+          (item) => item.studentCode === fileInfo.studentCode
+        );
+        if (errorStudentStage) {
+          errorStudentStage.papers.push(fileInfo);
+          continue;
+        }
+
+        this.errorStageList.push({
+          id: `none-${randomCode(16)}`,
+          studentCode: fileInfo.studentCode,
+          select: false,
+          papers: [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);
+    },
+    async toSave() {
+      if (this.errorStageList.length) {
+        this.$message.error("还有异常数据未处理!");
+        return;
+      }
+
+      if (!this.scanStageList.length) {
+        this.$message.error("当前无要保存的数据!");
+        return;
+      }
+
+      if (this.saving) return;
+      this.saving = true;
+
+      const datas = this.scanStageList.map((item) => item.papers).flat();
+      // TODO: 使用批量保存,有时间再做
+      logger.info(`04-1开始保存数据`);
+      for (let i = 0, len = datas.length; i < len; i++) {
+        const fileInfo = datas[i];
+
+        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, true);
+    },
+    // table action
+    toBind() {
+      if (!this.selectList.length) return;
+      this.$refs.ManualBindDialog.open();
+    },
+    bindConfirm(studentInfo) {
+      if (this.isNormalTab) {
+        this.normalBind(studentInfo);
+      } else {
+        this.errorBind(studentInfo);
+      }
+      this.clearViewPapers();
+      this.$refs.scanResultTableRef.clearSelection();
+    },
+    normalBind(studentInfo) {
+      const selectPaperIds = this.selectList
+        .map((item) => item.papers.map((p) => p.id))
+        .flat();
+      let prevIndex = this.scanStageList.findIndex(
+        (row) => row.id === this.selectList[0].id
+      );
+
+      // 删除选择的数据
+      this.scanStageList.forEach((row) => {
+        row.papers = row.papers.filter((p) => !selectPaperIds.includes(p.id));
+      });
+
+      this.scanStageList = this.scanStageList.filter(
+        (row) => row.papers.length
+      );
+
+      // 绑定逻辑
+      const preAddPapers = this.selectList
+        .map((row) => {
+          return row.papers.map((p) => {
+            return {
+              ...p,
+              studentCode: studentInfo.studentCode,
+              studentName: studentInfo.studentName,
+              id: `${stageStudent.studentCode}-${randomCode(16)}`,
+            };
+          });
+        })
+        .flat();
+
+      const stageStudent = this.scanStageList.find(
+        (row) => row.studentCode === studentInfo.studentCode
+      );
+      if (stageStudent) {
+        stageStudent.papers.push(...preAddPapers);
+        return;
+      }
+
+      prevIndex = Math.max(
+        0,
+        Math.min(prevIndex, this.scanStageList.length - 1)
+      );
+
+      this.scanStageList.splice(prevIndex, 0, {
+        ...studentInfo,
+        papers: preAddPapers,
+      });
+    },
+    errorBind(studentInfo) {
+      const selectPaperIds = this.selectList
+        .map((item) => item.papers.map((p) => p.id))
+        .flat();
+
+      // 删除选择的数据
+      this.errorStageList.forEach((row) => {
+        row.papers = row.papers.filter((p) => !selectPaperIds.includes(p.id));
+      });
+
+      this.errorStageList = this.errorStageList.filter(
+        (row) => row.papers.length
+      );
+
+      // 绑定逻辑
+      const preAddPapers = this.selectList
+        .map((row) => {
+          return row.papers.map((p) => {
+            return {
+              ...p,
+              studentCode: studentInfo.studentCode,
+              studentName: studentInfo.studentName,
+              id: `${stageStudent.studentCode}-${randomCode(16)}`,
+            };
+          });
+        })
+        .flat();
+
+      const stageStudent = this.scanStageList.find(
+        (row) => row.studentCode === studentInfo.studentCode
+      );
+      if (stageStudent) {
+        stageStudent.papers.push(...preAddPapers);
+        return;
+      }
+
+      this.scanStageList.push({
+        ...studentInfo,
+        papers: preAddPapers,
+      });
+    },
+    async clearStage() {
+      const res = await this.$confirm(`确定要清空数据吗?`, "警告", {
+        type: "warning",
+      }).catch(() => {});
+      if (res !== "confirm") return;
+
+      this.initData();
+      this.clearFiles();
+      logger.info(`99数据清空`);
+      this.$message.success("数据已清空!");
+    },
+    selectChange(data) {
+      this.selectList = data;
+    },
+    deletePaperHandle(deletedPapers) {
+      this.curPapers = this.curPapers.filter((p) => !deletedPapers.includes(p));
+      if (!this.curPapers.length) {
+        this.curPaperIndex = 0;
+        this.curPaper = { url: "" };
+        return;
+      }
+
+      if (deletedPapers.includes(this.curPaper.url)) {
+        this.curPaperIndex = 0;
+        this.setCurPaper(0);
+        return;
+      }
+
+      this.curPaperIndex = this.curPapers.indexOf(this.curPaper.url);
+    },
+    // image-preview
+    rowClickHandle({ curPapers, curPagePaperIndex }) {
+      this.curPapers = curPapers;
+      this.curPaperIndex = curPagePaperIndex;
+      this.setCurPaper(curPagePaperIndex);
+    },
+    clearViewPapers() {
+      this.curPapers = [];
+      this.curPaperIndex = 0;
+      this.curPaper = { url: "" };
+    },
+    setCurPaper(index) {
+      this.curPaper = { url: this.curPapers[index] };
+    },
+    toPrevPaper() {
+      if (this.curPaperIndex <= 0) {
+        this.$message.error("没有上一页了");
+        return;
+      }
+      this.setCurPaper(this.curPaperIndex - 1);
+    },
+    toNextPaper() {
+      if (this.curPaperIndex >= this.curPapers.length - 1) {
+        this.$message.error("没有下一页了");
+        return;
+      }
+      this.setCurPaper(this.curPaperIndex + 1);
+    },
+  },
+};
+</script>

+ 5 - 0
src/plugins/env.js

@@ -17,6 +17,7 @@ function initPath() {
   const paths = [
     storePath,
     getInputDir(),
+    getStageDir(),
     getStoresDir("out"),
     getOutputDir("origin"),
     getTmpDir(),
@@ -53,6 +54,9 @@ function getImgDecodeTool() {
 function getInputDir() {
   return getStoresDir("in");
 }
+function getStageDir() {
+  return getStoresDir("stage");
+}
 function getOutputDir(type) {
   return path.join(getStoresDir("out"), type);
 }
@@ -144,6 +148,7 @@ export {
   getImagicTool,
   getImgDecodeTool,
   getInputDir,
+  getStageDir,
   getOutputDir,
   getScanExePath,
   getTmpDir,

+ 35 - 21
src/plugins/imageOcr.js

@@ -1,5 +1,6 @@
 import {
   getInputDir,
+  getStageDir,
   getOutputDir,
   makeDirSync,
   getImgDecodeTool,
@@ -24,7 +25,6 @@ export function saveOutputImage(scaningImageList, paperInfo) {
     const imagePath = scaningImageList[i];
     const originImageFile = saveOriginImage(imagePath, paperInfo);
     outputOriginPaths.push(originImageFile);
-    fs.unlinkSync(imagePath);
   }
 
   return outputOriginPaths;
@@ -36,7 +36,7 @@ function saveOriginImage(imagePath, paperInfo) {
 
   if (!fs.existsSync(outputDir)) makeDirSync(outputDir);
   const outputOriginPath = path.join(outputDir, path.basename(imagePath));
-  fs.copyFileSync(imagePath, outputOriginPath);
+  fs.renameSync(imagePath, outputOriginPath);
   return outputOriginPath;
 }
 
@@ -129,8 +129,9 @@ export function getScanFileBasename(filepath) {
     .join("_");
 }
 
-export function getPreUploadFiles(dir) {
+export function getPreUploadFiles(dir, moveImg = false) {
   const ddir = dir || getInputDir();
+  const stageDdir = getStageDir();
   const files = fs
     .readdirSync(ddir)
     .filter((fileName) => fileName.toLowerCase().match(/\.(json)/));
@@ -138,7 +139,11 @@ export function getPreUploadFiles(dir) {
   let imageList = [];
   if (!files.length) return { succeed: false, errorMsg: "当前无扫描文件!" };
 
-  const fileCont = fs.readFileSync(path.join(ddir, files[0]));
+  files.forEach((filename) => {
+    fs.renameSync(path.join(ddir, filename), path.join(stageDdir, filename));
+  });
+
+  const fileCont = fs.readFileSync(path.join(stageDdir, files[0]));
   const fileInfo = JSON.parse(fileCont);
 
   if (!fileInfo.succeed) {
@@ -146,10 +151,17 @@ export function getPreUploadFiles(dir) {
   }
 
   imageList = (fileInfo.images || []).map((item) => {
+    const frontFile = path.join(stageDdir, path.basename(item.front));
+    const versoFile = path.join(stageDdir, path.basename(item.back));
+
+    if (moveImg) {
+      fs.renameSync(item.front, frontFile);
+      fs.renameSync(item.back, versoFile);
+    }
+
     return {
-      frontFile: item.front,
-      versoFile: item.back,
-      basename: getScanFileBasename(item.front),
+      frontFile,
+      versoFile,
     };
   });
   if (!imageList.length)
@@ -177,17 +189,13 @@ export function renamePreUploadJsonFile(dir) {
  * 获取最早添加的文件
  * @param {String} dir 图片目录
  */
-export function getDirScanFile(dir, cacheFilenams = []) {
-  const st = Date.now();
+export function getDirScanFile(dir) {
+  // const st = Date.now();
+  const stageDdir = getStageDir();
   const ddir = dir || getInputDir();
   const files = fs
     .readdirSync(ddir)
-    .filter((fileName) => {
-      const formatValid = fileName.toLowerCase().match(/\.(jpg|png|jpeg)/);
-      const fileBasename = getScanFileBasename(fileName);
-      const nameValid = !cacheFilenams.includes(fileBasename);
-      return formatValid && nameValid;
-    })
+    .filter((fileName) => fileName.toLowerCase().match(/\.(jpg|png|jpeg)/))
     .map((fileName) => {
       return {
         name: fileName,
@@ -201,16 +209,22 @@ export function getDirScanFile(dir, cacheFilenams = []) {
   const nFiles = [];
   const len = Math.floor(files.length / 2);
   for (let i = 0; i < len; i++) {
-    const frontUrl = files[2 * i].name;
-    const versoUrl = files[2 * i + 1].name;
+    const frontName = files[2 * i].name;
+    const versoName = files[2 * i + 1].name;
+    const frontFile = path.join(stageDdir, frontName);
+    const versoFile = path.join(stageDdir, versoName);
+
+    fs.renameSync(path.join(ddir, frontName), frontFile);
+    fs.renameSync(path.join(ddir, versoName), versoFile);
+
     nFiles[i] = {
-      frontFile: path.join(ddir, frontUrl),
-      versoFile: path.join(ddir, versoUrl),
-      basename: getScanFileBasename(frontUrl),
+      frontFile,
+      versoFile,
+      basename: getScanFileBasename(frontName),
     };
   }
   // console.log(nFiles);
-  console.log(`getDirScanFile耗时:${Date.now() - st}`);
+  // console.log(`getDirScanFile耗时:${Date.now() - st}`);
   return nFiles;
 }
 

+ 18 - 19
src/plugins/utils.js

@@ -4,7 +4,7 @@
  * 判断对象类型
  * @param {*} obj 对象
  */
-function objTypeOf(obj) {
+export function objTypeOf(obj) {
   const toString = Object.prototype.toString;
   const map = {
     "[object Boolean]": "boolean",
@@ -36,7 +36,7 @@ function objTypeOf(obj) {
  * @param {Object} target 目标对象
  * @param {Object} sources 源对象
  */
-function objAssign(target, sources) {
+export function objAssign(target, sources) {
   let targ = { ...target };
   for (let k in targ) {
     targ[k] = Object.prototype.hasOwnProperty.call(sources, k)
@@ -51,7 +51,7 @@ function objAssign(target, sources) {
  * @param {Number} len 推荐8的倍数
  *
  */
-function randomCode(len = 16) {
+export function randomCode(len = 16) {
   if (len <= 0) return;
   let steps = Math.ceil(len / 8);
   let stepNums = [];
@@ -67,7 +67,7 @@ function randomCode(len = 16) {
  * 序列化参数
  * @param {Object} params 参数对象
  */
-function qsParams(params) {
+export function qsParams(params) {
   return Object.entries(params)
     .map(([key, val]) => `${key}=${val}`)
     .join("&");
@@ -78,7 +78,7 @@ function qsParams(params) {
  * @param {String} format 时间格式
  * @param {Date} date 需要格式化的时间对象
  */
-function formatDate(format = "YYYY-MM-DD HH:mm:ss", date = new Date()) {
+export function formatDate(format = "YYYY-MM-DD HH:mm:ss", date = new Date()) {
   if (objTypeOf(date) !== "date") return;
   const options = {
     "Y+": date.getFullYear(),
@@ -101,7 +101,7 @@ function formatDate(format = "YYYY-MM-DD HH:mm:ss", date = new Date()) {
 /**
  * 获取本地日期,格式YYYY-M-D
  */
-function getLocalDate() {
+export function getLocalDate() {
   return formatDate("YYYY-MM-DD");
 }
 
@@ -109,7 +109,7 @@ function getLocalDate() {
  * 清除html标签
  * @param {String} str html字符串
  */
-function removeHtmlTag(str) {
+export function removeHtmlTag(str) {
   return str.replace(/<[^>]+>/g, "");
 }
 
@@ -117,7 +117,7 @@ function removeHtmlTag(str) {
  *  获取时间长度文字
  * @param {Number} timeNumber 时间数值,单位:毫秒
  */
-function timeNumberToText(timeNumber) {
+export function timeNumberToText(timeNumber) {
   const DAY_TIME = 24 * 60 * 60 * 1000;
   const HOUR_TIME = 60 * 60 * 1000;
   const MINUTE_TIME = 60 * 1000;
@@ -148,14 +148,13 @@ function timeNumberToText(timeNumber) {
   return [day, hour, minute, second].filter((item) => !!item).join("");
 }
 
-export {
-  objTypeOf,
-  // deepCopy,
-  objAssign,
-  randomCode,
-  qsParams,
-  formatDate,
-  removeHtmlTag,
-  getLocalDate,
-  timeNumberToText,
-};
+/**
+ * 计算总数
+ * @param {Array} dataList 需要统计的数组
+ */
+export function calcSum(dataList) {
+  if (!dataList.length) return 0;
+  return dataList.reduce(function (total, item) {
+    return total + item;
+  }, 0);
+}