zhangjie 5 лет назад
Родитель
Сommit
15c770a822
40 измененных файлов с 1677 добавлено и 255 удалено
  1. 1 0
      package.json
  2. BIN
      public/database/client.rdb
  3. 18 0
      src/assets/styles/home.less
  4. 42 0
      src/assets/styles/icons.less
  5. 1 0
      src/assets/styles/index.less
  6. 30 0
      src/assets/styles/pages.less
  7. 12 14
      src/background.js
  8. 1 1
      src/mixins/uploadTaskMixin.js
  9. 16 0
      src/modules/client/api.js
  10. 59 0
      src/modules/client/components/ScanAreaDialog.vue
  11. 127 0
      src/modules/client/components/ScanAreaSteps.vue
  12. 175 0
      src/modules/client/components/ScanExceptionDialog.vue
  13. 105 0
      src/modules/client/components/steps/CodeArea.vue
  14. 79 0
      src/modules/client/components/steps/CoverArea.vue
  15. 162 0
      src/modules/client/components/steps/ImageOrientation.vue
  16. 74 0
      src/modules/client/components/steps/TailorArea.vue
  17. 22 4
      src/modules/client/router.js
  18. 4 1
      src/modules/client/store.js
  19. 90 0
      src/modules/client/views/Camera.vue
  20. 326 0
      src/modules/client/views/GroupScan.vue
  21. 15 0
      src/modules/client/views/LineScan.vue
  22. 85 0
      src/modules/client/views/ScanArea.vue
  23. 12 2
      src/modules/client/views/Subject.vue
  24. 4 4
      src/modules/example/router.js
  25. 0 81
      src/modules/login/fetchSmsMixins.js
  26. 0 20
      src/modules/login/router.js
  27. 0 82
      src/modules/login/views/Login.vue
  28. 0 22
      src/modules/login/views/LoginHome.vue
  29. 48 0
      src/plugins/codeInput.js
  30. 2 2
      src/plugins/db.js
  31. 3 0
      src/plugins/env.js
  32. 4 0
      src/plugins/globalVuePlugins.js
  33. 4 8
      src/plugins/imageOcr.js
  34. 8 8
      src/plugins/imageUpload.js
  35. 9 6
      src/router.js
  36. 2 0
      src/views/Home.vue
  37. 87 0
      src/views/Login.vue
  38. 0 0
      src/views/api.js
  39. 45 0
      vue.config.js
  40. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "axios": "^0.19.2",
     "core-js": "^3.6.4",
     "cropperjs": "^1.5.6",
+    "crypto": "^1.0.1",
     "deepmerge": "^4.2.2",
     "gm": "^1.23.1",
     "imagemagick": "^0.1.3",

BIN
public/database/client.rdb


+ 18 - 0
src/assets/styles/home.less

@@ -92,3 +92,21 @@
   line-height: 56px;
   text-align: center;
 }
+
+/* other */
+.img-contain {
+  display: block;
+  position: absolute;
+  max-width: 100%;
+  max-height: 100%;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  margin: auto;
+}
+
+/* iview */
+.ivu-progress-inner {
+  background: #e0e0e0;
+}

+ 42 - 0
src/assets/styles/icons.less

@@ -0,0 +1,42 @@
+/* icon */
+.icon {
+  height: 14px;
+  width: 14px;
+  display: inline-block;
+  vertical-align: top;
+  background-size: 100% 100%;
+  &-scan {
+    background-image: url(../images/icon-scan.png);
+  }
+  &-setting {
+    vertical-align: middle;
+    margin-right: 10px;
+    margin-top: -2px;
+    background-image: url(../images/icon-setting.png);
+  }
+  &-left {
+    height: 8px;
+    width: 8px;
+    vertical-align: middle;
+    margin-right: 10px;
+    background-image: url(../images/icon-left.png);
+  }
+  &-left-act {
+    background-image: url(../images/icon-left-act.png);
+  }
+  &-left-rotate {
+    height: 24px;
+    width: 24px;
+    background-image: url(../images/icon-left-rotate.png);
+  }
+  &-right-rotate {
+    height: 24px;
+    width: 24px;
+    background-image: url(../images/icon-right-rotate.png);
+  }
+  &-loading {
+    height: 80px;
+    width: 80px;
+    background-image: url(../images/icon-loading.png);
+  }
+}

+ 1 - 0
src/assets/styles/index.less

@@ -5,4 +5,5 @@
 @import "./home.less";
 @import "./login.less";
 @import "./pages.less";
+@import "./icons.less";
 @import "./common-component.less";

+ 30 - 0
src/assets/styles/pages.less

@@ -1,3 +1,33 @@
+/* subject */
+.subject-list {
+  position: relative;
+  z-index: 9;
+  overflow: hidden;
+  margin: 0 auto;
+  width: 760px;
+  padding: 30px;
+  border-radius: 3px;
+}
+.subject-list ul {
+  font-size: 0;
+  text-align: center;
+}
+.subject-list li {
+  width: 200px;
+  height: 200px;
+  font-size: 32px;
+  line-height: 200px;
+  margin: 0 15px;
+  display: inline-block;
+  vertical-align: top;
+  cursor: pointer;
+  color: hsl(0, 0%, 22%);
+  background-color: rgba(255, 255, 255, 0.7);
+}
+.subject-list li:hover {
+  background-color: rgba(255, 255, 255, 1);
+}
+
 /* camera */
 .camera {
   padding: 0 137px;

+ 12 - 14
src/background.js

@@ -94,20 +94,18 @@ app.on("ready", async () => {
       // await installVueDevtools();
       // 只需要安装一次,安装成功后注释。
       // windows 参照这个路径去安装 https://electronjs.org/docs/tutorial/devtools-extension
-      BrowserWindow
-        .addDevToolsExtension
-        // // macOS
-        // require("path").join(
-        //   require("os").homedir(),
-        //   "/Library/Application Support/Google/Chrome/Default/Extensions/nhdogjmejiglipccpnnnanhbledajbpd/5.3.3_0"
-        // )
-
-        // // window
-        // require("path").join(
-        //   require("os").homedir(),
-        //   "/AppData/Local/Google/Chrome/User Data/Default/Extensions/nhdogjmejiglipccpnnnanhbledajbpd/4.1.4_0"
-        // )
-        ();
+      // BrowserWindow.addDevToolsExtension(
+      //   // // macOS
+      //   // require("path").join(
+      //   //   require("os").homedir(),
+      //   //   "/Library/Application Support/Google/Chrome/Default/Extensions/nhdogjmejiglipccpnnnanhbledajbpd/5.3.3_0"
+      //   // )
+      //   // window
+      //   require("path").join(
+      //     require("os").homedir(),
+      //     "/AppData/Local/Google/Chrome/User Data/Default/Extensions/nhdogjmejiglipccpnnnanhbledajbpd/4.1.4_0"
+      //   )
+      // );
     } catch (e) {
       console.error("Vue Devtools failed to install:", e.toString());
     }

+ 1 - 1
src/mixins/uploadTaskMixin.js

@@ -1,5 +1,5 @@
 import db from "../plugins/db";
-import UploadTask from "../plugins/image-upload";
+import UploadTask from "../plugins/imageUpload";
 import { getLocalDate } from "../plugins/utils";
 
 /**

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

@@ -0,0 +1,16 @@
+import { $get, $post } from "@/plugins/axios";
+
+export const uploadImage = datas => {
+  return $post("/backend/course/updateCourseStatus", datas);
+};
+export const getStudentGroupByExamNumber = datas => {
+  return $post("/backend/course/updateCourseStatus", datas);
+};
+
+// course-manage
+export const courseList = datas => {
+  return $get("/backend/course/listCoursePage", datas);
+};
+export const updateCourseStatus = ({ id, status }) => {
+  return $post("/backend/course/updateCourseStatus", { id, status });
+};

+ 59 - 0
src/modules/client/components/ScanAreaDialog.vue

@@ -0,0 +1,59 @@
+<template>
+  <Modal
+    class="scan-area-dialog"
+    v-model="modalIsShow"
+    title="采集设置"
+    :mask-closable="false"
+    :closable="false"
+    fullscreen
+    footer-hide
+  >
+    <setting-steps
+      :image-url="curImage.url"
+      :cur-setting="curCollectConfig"
+      @on-finished="finished"
+      v-if="curImage.url && modalIsShow && curCollectConfig"
+    ></setting-steps>
+  </Modal>
+</template>
+
+<script>
+export default {
+  name: "scan-area-dialog",
+  props: {
+    curImage: {
+      type: Object,
+      default() {
+        return {
+          url: "",
+          name: ""
+        };
+      }
+    },
+    curCollectConfig: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      curSetting: null
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    finished(setting) {
+      this.$emit("confirm", setting);
+      this.cancel();
+    }
+  }
+};
+</script>

+ 127 - 0
src/modules/client/components/ScanAreaSteps.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="scan-area-steps">
+    <div class="step-body">
+      <component
+        :is="currentComponent"
+        :ref="currentComponent"
+        :image-url="imageUrl"
+        :cur-setting="newSetting"
+        @on-next="toNext"
+        @on-ready="stepReady"
+      ></component>
+    </div>
+
+    <div class="step-ctrl">
+      <Button
+        type="primary"
+        @click="prevStep"
+        v-if="!isFirstStep"
+        style="margin-right: 20px;"
+      >
+        上一步
+      </Button>
+      <Button type="primary" @click="nextStep" :disabled="nextHolder">
+        {{ isLastStep ? "完成" : "下一步" }}
+      </Button>
+    </div>
+  </div>
+</template>
+
+<script>
+import CodeArea from "./steps/CodeArea";
+import CoverArea from "./steps/CoverArea";
+import ImageOrientation from "./steps/ImageOrientation";
+import TailorArea from "./steps/TailorArea";
+import { deepCopy } from "../../../plugins/utils";
+
+const STEPS_LIST = [
+  {
+    name: "code-area",
+    title: "条形码区域"
+  },
+  {
+    name: "cover-area",
+    title: "覆盖区域"
+  },
+  {
+    name: "tailor-area",
+    title: "裁剪区域"
+  },
+  {
+    name: "image-orientation",
+    title: "试卷方向"
+  }
+];
+
+export default {
+  name: "scan-area-steps",
+  components: { CodeArea, CoverArea, ImageOrientation, TailorArea },
+  props: {
+    imageUrl: {
+      type: String,
+      require: true
+    },
+    curSetting: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      STEPS_LIST,
+      current: 0,
+      nextHolder: true,
+      dataReady: true,
+      newSetting: {}
+    };
+  },
+  created() {
+    this.newSetting = deepCopy(this.curSetting);
+  },
+  computed: {
+    currentComponent() {
+      return this.STEPS_LIST[this.current].name;
+    },
+    isFirstStep() {
+      return this.current === 0;
+    },
+    isLastStep() {
+      return this.current === this.lastStep;
+    },
+    lastStep() {
+      return this.STEPS_LIST.length - 1;
+    }
+  },
+  methods: {
+    prevStep() {
+      if (this.isFirstStep) return;
+      this.current -= 1;
+    },
+    nextStep() {
+      if (this.nextHolder) return;
+      this.$refs[this.currentComponent].checkValid();
+    },
+    toNext(setting) {
+      console.log(setting);
+
+      Object.entries(setting).map(([key, val]) => {
+        this.newSetting[key] =
+          typeof val === "object"
+            ? Object.assign({}, this.newSetting[key], val)
+            : val;
+      });
+      if (this.isLastStep) {
+        this.$emit("on-finished", this.newSetting);
+        return;
+      }
+      this.current += 1;
+      this.nextHolder = true;
+    },
+    stepReady() {
+      this.nextHolder = false;
+    }
+  }
+};
+</script>

+ 175 - 0
src/modules/client/components/ScanExceptionDialog.vue

@@ -0,0 +1,175 @@
+<template>
+  <Modal
+    class="scan-exception-dialog"
+    v-model="modalIsShow"
+    :mask-closable="false"
+    :closable="!curException.showAction"
+    fullscreen
+    footer-hide
+    @on-visible-change="visibleChange"
+  >
+    <div class="exception-title" slot="header">
+      <h2>{{ curException.message }}</h2>
+    </div>
+
+    <div class="home-main">
+      <div class="code-area-main exception-main">
+        <div class="code-area-cont code-area-cont-disabled">
+          <img :src="curImage.url" :alt="curImage.name" ref="editImage" />
+        </div>
+        <div class="code-area-preview">
+          <div class="code-area-spin">
+            <div
+              class="code-area-spin-img"
+              :style="spinStyle"
+              ref="CodeAreaSpinImg"
+            ></div>
+          </div>
+        </div>
+      </div>
+
+      <div class="exception-form" v-if="curException.showAction">
+        <i-form
+          ref="modalFormComp"
+          :model="modalForm"
+          :rules="rules"
+          :label-width="0"
+          inline
+        >
+          <form-item prop="examNumber">
+            <div class="input-append">
+              <i-input
+                size="default"
+                v-model.trim="modalForm.examNumber"
+                placeholder="请扫码输入"
+                style="width: 220px;"
+                autofocus
+              >
+                ></i-input
+              >
+            </div>
+          </form-item>
+          <form-item :label-width="0">
+            <i-button size="default" type="primary" @click="toHandInput"
+              >手工绑定</i-button
+            >
+            <i-button size="default" type="primary" @click="toRescan"
+              >重新采集</i-button
+            >
+            <i-button size="default" type="default" @click="toReset"
+              ><i class="icon-font icon-setting"></i>重新配置</i-button
+            >
+          </form-item>
+        </i-form>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<script>
+import Cropper from "cropperjs";
+
+export default {
+  name: "scan-exception-dialog",
+  props: {
+    curImage: {
+      type: Object,
+      default() {
+        return {
+          url: "",
+          name: ""
+        };
+      }
+    },
+    curException: {
+      type: Object,
+      default() {
+        return {
+          showAction: true,
+          message: ""
+        };
+      }
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      cropper: "",
+      spinStyle: {},
+      modalForm: {
+        examNumber: ""
+      },
+      rules: {
+        examNumber: [
+          {
+            required: true,
+            message: "请输入准考证号!",
+            trigger: "change"
+          }
+        ]
+      }
+    };
+  },
+  methods: {
+    initData() {
+      this.modalForm.examNumber = "";
+    },
+    visibleChange(visible) {
+      if (visible) {
+        this.initData();
+        this.$nextTick(() => {
+          this.initCropper();
+        });
+      } else {
+        if (this.cropper) {
+          this.cropper.destroy();
+          this.cropper = false;
+        }
+      }
+    },
+    initCropper() {
+      const _this = this;
+      const codeArea = this.curException.collectConfig.codeArea;
+
+      this.cropper = new Cropper(this.$refs.editImage, {
+        viewMode: 1,
+        checkCrossOrigin: false,
+        zoomable: false,
+        preview: _this.$refs.CodeAreaSpinImg,
+        ready() {
+          _this.cropper.setData(codeArea);
+          _this.spinStyle = {
+            transform: `translate(-50%, -50%) rotate(${codeArea.codeRotate}deg)`
+          };
+        }
+      });
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    toReset() {
+      this.$emit("on-reset");
+      this.cancel();
+    },
+    toRescan() {
+      this.$emit("on-rescan");
+      this.cancel();
+    },
+    async toHandInput() {
+      const valid = await this.$refs.modalFormComp.validate();
+      if (!valid) return;
+      this.$emit("on-hand-input", this.modalForm.examNumber, "MANUAL");
+      this.cancel();
+    }
+  },
+  beforeDestroy() {
+    if (this.cropper) {
+      this.cropper.destroy();
+      this.cropper = false;
+    }
+  }
+};
+</script>

+ 105 - 0
src/modules/client/components/steps/CodeArea.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="code-area">
+    <div class="part-head">
+      <h2>条码识别区域设置</h2>
+    </div>
+    <div class="code-area-main">
+      <div class="code-area-cont">
+        <img :src="imageUrl" ref="editImage" />
+      </div>
+      <div class="code-area-preview">
+        <div class="code-area-spin">
+          <div
+            class="code-area-spin-img"
+            :style="spinStyle"
+            ref="CodeAreaSpinImg"
+          ></div>
+        </div>
+        <div
+          class="code-area-icon"
+          title="顺时针旋转90度"
+          @click="toRotateSpin"
+        >
+          <i class="icon icon-right-rotate"></i>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Cropper from "cropperjs";
+
+export default {
+  name: "code-area",
+  props: {
+    imageUrl: {
+      type: String,
+      require: true
+    },
+    curSetting: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      cropper: "",
+      codeRotate: 0,
+      spinStyle: {}
+    };
+  },
+  mounted() {
+    this.initCropper();
+  },
+  methods: {
+    initCropper() {
+      const _this = this;
+      const defCodeArea = (this.curSetting && this.curSetting.codeArea) || {};
+      this.codeRotate = defCodeArea.codeRotate || 0;
+      this.spinStyle = {
+        transform: `translate(-50%, -50%) rotate(${this.codeRotate}deg)`
+      };
+
+      this.cropper = new Cropper(this.$refs.editImage, {
+        viewMode: 1,
+        checkCrossOrigin: false,
+        zoomable: false,
+        minCropBoxWidth: 10,
+        minCropBoxHeight: 10,
+        preview: _this.$refs.CodeAreaSpinImg,
+        ready() {
+          _this.cropper.setData(defCodeArea);
+          _this.$emit("on-ready");
+        }
+      });
+    },
+    toRotateSpin() {
+      this.codeRotate += 90;
+      if (this.codeRotate === 360) this.codeRotate = 0;
+
+      this.spinStyle = {
+        transform: `translate(-50%, -50%) rotate(${this.codeRotate}deg)`
+      };
+    },
+    checkValid() {
+      const codeArea = {
+        ...this.cropper.getData(),
+        codeRotate: this.codeRotate
+      };
+      this.$emit("on-next", { codeArea });
+    },
+    pass() {
+      this.$emit("on-next", { codeArea: {} });
+    }
+  },
+  beforeDestroy() {
+    if (this.cropper) {
+      this.cropper.destroy();
+      this.cropper = false;
+    }
+  }
+};
+</script>

+ 79 - 0
src/modules/client/components/steps/CoverArea.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="cover-area">
+    <div class="part-head">
+      <h2>覆盖区域设置</h2>
+    </div>
+    <div class="code-area-main">
+      <div class="code-area-cont">
+        <img :src="imageUrl" ref="editImage" />
+      </div>
+      <div class="code-area-preview">
+        <div class="code-area-spin">
+          <div class="code-area-spin-img" ref="CoverAreaSpinImg"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Cropper from "cropperjs";
+
+export default {
+  name: "cover-area",
+  props: {
+    imageUrl: {
+      type: String,
+      require: true
+    },
+    curSetting: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      cropper: ""
+    };
+  },
+  mounted() {
+    this.initCropper();
+  },
+  methods: {
+    initCropper() {
+      const _this = this;
+      const defCoverArea = (this.curSetting && this.curSetting.coverArea) || {};
+
+      this.cropper = new Cropper(this.$refs.editImage, {
+        viewMode: 1,
+        checkCrossOrigin: false,
+        zoomable: false,
+        minCropBoxWidth: 10,
+        minCropBoxHeight: 10,
+        preview: _this.$refs.CoverAreaSpinImg,
+        ready() {
+          _this.cropper.setData(defCoverArea);
+          _this.$emit("on-ready");
+        }
+      });
+    },
+    checkValid() {
+      const coverArea = {
+        ...this.cropper.getData()
+      };
+      this.$emit("on-next", { coverArea });
+    },
+    pass() {
+      this.$emit("on-next", { coverArea: {} });
+    }
+  },
+  beforeDestroy() {
+    if (this.cropper) {
+      this.cropper.destroy();
+      this.cropper = false;
+    }
+  }
+};
+</script>

+ 162 - 0
src/modules/client/components/steps/ImageOrientation.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="image-orientation">
+    <div class="part-head">
+      <h2>图片方向设置</h2>
+    </div>
+    <div class="image-orient-main">
+      <div class="image-orient-cont">
+        <img
+          :src="imageUrl"
+          :style="contStyle"
+          ref="editImage"
+          @load="imageLoad"
+        />
+      </div>
+      <div class="image-cover-box" :style="coverBoxStyle">
+        <div
+          class="image-cover-item"
+          v-for="(item, index) in coverItems"
+          :key="index"
+          :style="item"
+        ></div>
+      </div>
+      <div class="image-orient-btns">
+        <div
+          class="image-orient-icon"
+          title="逆时针旋转90度"
+          @click="toRotate(0)"
+        >
+          <i class="icon icon-left-rotate"></i>
+        </div>
+        <div
+          class="image-orient-icon"
+          title="顺时针旋转90度"
+          @click="toRotate(1)"
+        >
+          <i class="icon icon-right-rotate"></i>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "image-orientation",
+  props: {
+    imageUrl: {
+      type: String,
+      require: true
+    },
+    curSetting: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      contStyle: {},
+      picRotate: 0,
+      coverItems: [],
+      coverBoxStyle: {}
+    };
+  },
+  mounted() {
+    const defImageRotate =
+      (this.curSetting && this.curSetting.imageRotate) || 0;
+    this.picRotate = defImageRotate;
+    this.contStyle = {
+      transform: `translate(-50%, -50%) rotate(${this.picRotate}deg)`
+    };
+    this.$emit("on-ready");
+  },
+  methods: {
+    toRotate(type) {
+      if (type) {
+        this.picRotate += 90;
+        if (this.picRotate === 360) this.picRotate = 0;
+      } else {
+        if (this.picRotate === 0) this.picRotate = 360;
+        this.picRotate -= 90;
+      }
+
+      this.contStyle = {
+        transform: `translate(-50%, -50%) rotate(${this.picRotate}deg)`
+      };
+      this.coverBoxStyle.transform = this.contStyle.transform;
+    },
+    imageLoad() {
+      const imgDom = this.$refs.editImage;
+      const rate = imgDom.clientWidth / imgDom.naturalWidth;
+
+      this.coverBoxStyle = {
+        width: imgDom.clientWidth + "px",
+        height: imgDom.clientHeight + "px",
+        transform: this.contStyle.transform
+      };
+      const codeAreaSize = this.transformSize(this.curSetting.codeArea, rate);
+      this.coverItems.push({
+        width: codeAreaSize.width,
+        height: codeAreaSize.height,
+        top: codeAreaSize.y,
+        left: codeAreaSize.x
+      });
+      const coverAreaSize = this.transformSize(this.curSetting.coverArea, rate);
+      this.coverItems.push({
+        width: coverAreaSize.width,
+        height: coverAreaSize.height,
+        top: coverAreaSize.y,
+        left: coverAreaSize.x
+      });
+      this.getTailorCoverArea(imgDom).map(item => {
+        this.coverItems.push(this.transformSize(item, rate));
+      });
+    },
+    getTailorCoverArea(imgDom) {
+      const tailorArea = this.curSetting.tailorArea;
+      const { naturalWidth, naturalHeight } = imgDom;
+      return [
+        {
+          width: tailorArea.x,
+          height: naturalHeight,
+          top: 0,
+          left: 0
+        },
+        {
+          width: naturalWidth - tailorArea.x - tailorArea.width,
+          height: naturalHeight,
+          top: 0,
+          right: 0
+        },
+        {
+          width: naturalWidth,
+          height: tailorArea.y,
+          top: 0,
+          left: 0
+        },
+        {
+          width: naturalWidth,
+          height: naturalHeight - tailorArea.y - tailorArea.height,
+          bottom: 0,
+          left: 0
+        }
+      ];
+    },
+    transformSize(sizes, rate) {
+      let newSizes = {};
+      Object.keys(sizes).map(key => {
+        newSizes[key] = sizes[key] * rate + "px";
+      });
+      return newSizes;
+    },
+    checkValid() {
+      this.$emit("on-next", { imageRotate: this.picRotate });
+    },
+    pass() {
+      this.$emit("on-next", { imageRotate: null });
+    }
+  }
+};
+</script>

+ 74 - 0
src/modules/client/components/steps/TailorArea.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="tailor-area">
+    <div class="part-head">
+      <h2>裁剪区域设置</h2>
+    </div>
+    <div class="code-area-main">
+      <div class="code-area-cont">
+        <img :src="imageUrl" ref="editImage" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Cropper from "cropperjs";
+
+export default {
+  name: "tailor-area",
+  props: {
+    imageUrl: {
+      type: String,
+      require: true
+    },
+    curSetting: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      cropper: ""
+    };
+  },
+  mounted() {
+    this.initCropper();
+  },
+  methods: {
+    initCropper() {
+      const _this = this;
+      const defTailorArea =
+        (this.curSetting && this.curSetting.tailorArea) || {};
+
+      this.cropper = new Cropper(this.$refs.editImage, {
+        viewMode: 1,
+        checkCrossOrigin: false,
+        zoomable: false,
+        minCropBoxWidth: 10,
+        minCropBoxHeight: 10,
+        ready() {
+          _this.cropper.setData(defTailorArea);
+          _this.$emit("on-ready");
+        }
+      });
+    },
+    checkValid() {
+      const tailorArea = {
+        ...this.cropper.getData()
+      };
+      this.$emit("on-next", { tailorArea });
+    },
+    pass() {
+      this.$emit("on-next", { tailorArea: {} });
+    }
+  },
+  beforeDestroy() {
+    if (this.cropper) {
+      this.cropper.destroy();
+      this.cropper = false;
+    }
+  }
+};
+</script>

+ 22 - 4
src/modules/client/router.js

@@ -1,9 +1,27 @@
-import DataManage from "./views/DataManage.vue";
+import Subject from "./views/Subject.vue";
+import Camera from "./views/Camera.vue";
+import ScanArea from "./views/ScanArea.vue";
+import GroupScan from "./views/GroupScan.vue";
 
 export default [
   {
-    path: "/example/data-manage",
-    name: "DataManage",
-    component: DataManage
+    path: "/subject",
+    name: "Subject",
+    component: Subject
+  },
+  {
+    path: "/camera",
+    name: "Camera",
+    component: Camera
+  },
+  {
+    path: "/scan-area",
+    name: "ScanArea",
+    component: ScanArea
+  },
+  {
+    path: "/group-scan",
+    name: "GroupScan",
+    component: GroupScan
   }
 ];

+ 4 - 1
src/modules/client/store.js

@@ -1,7 +1,7 @@
 import db from "../../plugins/db";
 
 const state = {
-  user: {},
+  camera: "", // 相机编号
   curSubject: {},
   scanArea: {},
   scanNo: 0, // 已采集数量(当天登录)
@@ -10,6 +10,9 @@ const state = {
 };
 
 const mutations = {
+  setCamera(state, camera) {
+    state.camera = camera;
+  },
   setShowToLoginModel(state, showToLoginModel) {
     state.showToLoginModel = showToLoginModel;
   },

+ 90 - 0
src/modules/client/views/Camera.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="camera">
+    <div class="part-head">
+      <i-button size="default" type="default" @click="goBack"
+        ><i class="icon icon-left"></i>返回</i-button
+      >
+      <h2>扫描获取相机编码</h2>
+    </div>
+    <div class="camera-form">
+      <Form
+        ref="modalFormComp"
+        :model="modalForm"
+        :rules="rules"
+        :label-width="0"
+      >
+        <FormItem prop="camera">
+          <Input
+            size="large"
+            v-model.trim="modalForm.camera"
+            placeholder="相机编码"
+            key="1"
+            autofocus
+            v-if="canEdit"
+          >
+          </Input>
+          <Input
+            size="large"
+            v-model.trim="modalForm.camera"
+            placeholder="扫描相机编码"
+            :key="2"
+            autofocus
+            v-code-input.prevent="{ inputOver }"
+            v-else
+          >
+          </Input>
+        </FormItem>
+        <div class="camera-write" @click="toHandInput">
+          <span>手工输入</span>
+        </div>
+      </Form>
+      <div class="camera-btn">
+        <i-button size="large" type="primary" long @click="submit"
+          >确认</i-button
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "camera",
+  data() {
+    return {
+      canEdit: false,
+      modalForm: {
+        camera: ""
+      },
+      rules: {
+        camera: [
+          {
+            required: true,
+            message: "相机编码不能为空",
+            trigger: "change"
+          }
+        ]
+      }
+    };
+  },
+  methods: {
+    goBack() {
+      this.$router.push({ name: "Login" });
+    },
+    inputOver(code) {
+      this.modalForm.camera = code;
+    },
+    toHandInput() {
+      this.modalForm.camera = "";
+      this.canEdit = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate();
+      if (!valid) return;
+
+      this.$store.commit("setCamera", this.modalForm.camera);
+      this.$router.push({ name: "Setting" });
+    }
+  }
+};
+</script>

+ 326 - 0
src/modules/client/views/GroupScan.vue

@@ -0,0 +1,326 @@
+<template>
+  <div class="group-scan">
+    <div class="home-header">
+      <div class="head-logo">
+        <h1>试卷采集</h1>
+      </div>
+      <div class="head-back">
+        <i-button type="default" @click="goBack"
+          ><i class="icon-font icon-left"></i>返回</i-button
+        >
+      </div>
+    </div>
+    <div class="home-body">
+      <div class="home-main home-main-split">
+        <div
+          class="scan-list student-list"
+          id="student-list"
+          v-if="studentSerialList.length"
+        >
+          <div
+            v-for="(student, index) in studentSerialList"
+            :key="index"
+            :class="[
+              'student-item',
+              {
+                'student-current': student.isCurrent,
+                'student-over': student.isClient
+              }
+            ]"
+          >
+            <span>{{ student.name }}</span>
+            <span>{{ student.examNumber }}</span>
+          </div>
+          <div class="scan-btns">
+            <i-button type="primary" @click="scanOver" :disabled="holding"
+              >扫描完毕</i-button
+            >
+            <i-button type="primary" @click="allReScan">整包重扫</i-button>
+          </div>
+        </div>
+        <div class="scan-image">
+          <div class="scan-main scan-waiting" v-if="isWaiting">
+            <p class="scan-waiting-icon"></p>
+            <p class="scan-waiting-tips">等待采集试卷</p>
+          </div>
+          <div class="scan-main scan-picture" v-else>
+            <img class="img-contain" :src="curImage.url" :alt="curImage.name" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- reset current image scan area dialog -->
+    <scan-area-dialog
+      :cur-image="curImage"
+      :cur-collect-config="curCollectConfig"
+      @confirm="studentConfigChange"
+      ref="ScanAreaDialog"
+    ></scan-area-dialog>
+    <!-- scan exception dialog -->
+    <scan-exception-dialog
+      :cur-image="curImage"
+      :cur-exception="curException"
+      @on-reset="resetConfig"
+      @on-rescan="restartInitFile"
+      @on-hand-input="examNumberValid"
+      ref="ScanExceptionDialog"
+    ></scan-exception-dialog>
+  </div>
+</template>
+
+<script>
+const fs = require("fs");
+import { getStudentGroupByExamNumber } from "../api";
+import {
+  decodeImageCode,
+  getEarliestFile,
+  saveFormalImage
+} from "../../../plugins/imageOcr";
+import { formatDate } from "../../../plugins/utils";
+import ScanAreaDialog from "../components/ScanAreaDialog";
+import ScanExceptionDialog from "../components/ScanExceptionDialog";
+
+export default {
+  name: "group-scan",
+  components: { ScanAreaDialog, ScanExceptionDialog },
+  data() {
+    return {
+      isWaiting: true,
+      students: [],
+      curStudent: {},
+      curCollectConfig: {},
+      curException: {
+        showAction: true,
+        message: ""
+      },
+      setT: "",
+      holding: false,
+      curImage: {
+        url: "",
+        name: ""
+      }
+    };
+  },
+  computed: {
+    user() {
+      return this.$store.state.user;
+    },
+    curSubject() {
+      return this.$store.state.curSubject;
+    },
+    studentSerialList() {
+      return this.students.sort((a, b) => {
+        if (a.isClient) return -1;
+        if (b.isClient) return 1;
+        if (a.isCurrent) return -1;
+        if (b.isCurrent) return 1;
+        return 0;
+      });
+    },
+    scanNo() {
+      return this.students.filter(item => item.isClient).length;
+    },
+    taskNo() {
+      return this.students.length;
+    },
+    isFinished() {
+      return this.taskNo && this.scanNo === this.taskNo;
+    }
+  },
+  mounted() {
+    this.getInitFile();
+    // this.curImage = {
+    //   name: "1901130043.jpg",
+    //   url: "E:\\newspace\\msyj-client\\in\\1901130043.jpg"
+    // };
+  },
+  methods: {
+    getInitFile() {
+      if (this.isFinished) {
+        this.isWaiting = true;
+        if (this.setT) clearTimeout(this.setT);
+        return;
+      }
+      this.curImage = getEarliestFile();
+
+      if (this.curImage.url) {
+        if (this.setT) clearTimeout(this.setT);
+        this.isWaiting = false;
+        this.$nextTick(() => {
+          this.startDecodeTask(this.curSubject.collectConfig.codeArea);
+        });
+      } else {
+        this.setT = setTimeout(() => {
+          this.getInitFile();
+        }, 1000);
+      }
+    },
+    restartInitFile() {
+      this.isWaiting = true;
+      this.curStudent = {};
+      if (this.setT) clearTimeout(this.setT);
+      this.holding = false;
+      this.curImage = { url: "", name: "" };
+
+      this.$nextTick(() => {
+        this.getInitFile();
+      });
+    },
+    async startDecodeTask(codeArea) {
+      const examNumber = await decodeImageCode(
+        this.curImage.url,
+        codeArea
+      ).catch(error => {
+        const content = `图像:${this.curImage.name},解析错误,错误信息:${error}`;
+
+        this.$Notice.error({ title: "错误提示", desc: content, duration: 0 });
+      });
+
+      if (examNumber) {
+        this.examNumberValid(examNumber);
+      } else {
+        // 未解析到考号异常
+        this.curException = {
+          showAction: true,
+          message: "条形码解析错误!",
+          collectConfig: this.getCurCollectConfig()
+        };
+        this.$refs.ScanExceptionDialog.open();
+      }
+    },
+    async examNumberValid(examNumber, type = "AUTO") {
+      const validRes = await this.checkStudentValid(examNumber);
+      if (validRes.valid) {
+        // 保存扫描到的试卷
+        this.toSaveStudent(examNumber, type);
+      } else {
+        // 考号不合法异常
+        this.curException = {
+          showAction: false,
+          message: validRes.message
+        };
+        this.$refs.ScanExceptionDialog.open();
+      }
+    },
+    async checkStudentValid(examNumber) {
+      let validInfo = { valid: true, message: "" };
+      if (!this.students.length) {
+        const students = await getStudentGroupByExamNumber(examNumber);
+        if (students && students.length) {
+          this.students = students.map(item => {
+            return {
+              ...item,
+              isClient: false,
+              isCurrent: false,
+              isManual: false,
+              collectConfig: null
+            };
+          });
+        } else {
+          validInfo = {
+            valid: false,
+            message: "学生数据没有上传,请重新采集!"
+          };
+          return validInfo;
+        }
+        // 请求远程当前学生所在组的所有学生信息。
+        // 通过当前学生examNumber获取到的学生列表为空时,抛出异常
+      }
+
+      this.curStudent = this.students.find(
+        item => item.examNumber === examNumber
+      );
+      if (!this.curStudent) {
+        validInfo = {
+          valid: false,
+          message: "当前考场没有当前学生,请重新采集!"
+        };
+      } else {
+        this.curStudent.isCurrent = true;
+      }
+      return validInfo;
+    },
+    async toSaveStudent(examNumber, type) {
+      await saveFormalImage(
+        this.curImage.url,
+        {
+          workId: this.user.workId,
+          subjectId: this.curSubject.id,
+          examNumber
+        },
+        this.getCurCollectConfig()
+      );
+      this.curStudent = Object.assign(this.curStudent, {
+        isCurrent: false,
+        isClient: true,
+        isManual: type === "MANUAL"
+      });
+
+      // 删除扫描文件,继续开始下一个任务
+      fs.unlinkSync(this.curImage.url);
+      this.restartInitFile();
+    },
+    checkAllStudentIsClient() {
+      return !this.students.some(item => !item.isClient);
+    },
+    async scanOver() {
+      if (this.holding) return;
+      this.holding = true;
+      if (!this.checkAllStudentIsClient()) {
+        this.$Message.error("当前考场试卷没有扫完!");
+        this.holding = false;
+        return;
+      }
+      // 添加上传任务
+      for (let i = 0, len = this.students.length; i < len; i++) {
+        const curStudent = this.students[i];
+        await this.$parent.addUploadTask({
+          workId: this.user.workId,
+          workName: this.user.workName,
+          subjectId: this.curSubject.id,
+          subjectName: this.curSubject.name,
+          examNumber: curStudent.examNumber,
+          studentName: curStudent.name,
+          siteCode: curStudent.siteCode,
+          roomCode: curStudent.roomCode,
+          createdTime: formatDate(),
+          isManual: curStudent.isManual,
+          clientUserId: this.user.id,
+          clientUsername: this.user.name,
+          clientUserLoginTime: this.user.clientUserLoginTime
+        });
+      }
+
+      this.allReScan();
+    },
+    allReScan() {
+      this.students = [];
+      this.restartInitFile();
+    },
+    getCurCollectConfig() {
+      return this.curStudent.collectConfig || this.curSubject.collectConfig;
+    },
+    // scan-exception
+    resetConfig() {
+      this.curCollectConfig = this.getCurCollectConfig();
+      this.$refs.ScanAreaDialog.open();
+    },
+    studentConfigChange(setting) {
+      this.curStudent.collectConfig = setting;
+      this.startDecodeTask(setting.codeArea);
+    },
+    goBack() {
+      this.$confirm({
+        content: "当前正处于采集状态,确定要退出吗?",
+        onOk: () => {
+          this.$router.go(-1);
+        }
+      });
+    }
+  },
+  beforeDestroy() {
+    if (this.setT) clearTimeout(this.setT);
+  }
+};
+</script>

+ 15 - 0
src/modules/client/views/LineScan.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="line-scan">
+    line-scan
+  </div>
+</template>
+
+<script>
+export default {
+  name: "line-scan",
+  data() {
+    return {};
+  },
+  methods: {}
+};
+</script>

+ 85 - 0
src/modules/client/views/ScanArea.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="scan-area">
+    <div class="home-header">
+      <div class="head-logo">
+        <h1>试卷采集区域设置</h1>
+      </div>
+      <div class="head-back">
+        <i-button type="default" @click="goback"
+          ><i class="icon icon-left"></i>返回</i-button
+        >
+      </div>
+    </div>
+    <div class="home-body">
+      <div class="home-main">
+        <scan-area-steps
+          :image-url="curImage.url"
+          :cur-setting="curSetting"
+          @on-finished="finished"
+          v-if="curImage.url"
+        ></scan-area-steps>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import ScanAreaSteps from "../components/ScanAreaSteps";
+import { getEarliestFile } from "../../../plugins/imageOcr";
+import { mapActions } from "vuex";
+
+export default {
+  name: "scan-area",
+  components: {
+    ScanAreaSteps
+  },
+  data() {
+    return {
+      curImage: {
+        url: "",
+        name: ""
+      },
+      curSetting: {},
+      config: {
+        isPackageMode: true
+      },
+      setT: ""
+    };
+  },
+  computed: {
+    curSubject() {
+      return this.$store.state.curSubject;
+    }
+  },
+  created() {
+    this.curSetting = this.curSubject.collectConfig || {};
+    this.getInitFile();
+  },
+  methods: {
+    ...mapActions("client", ["setCurSubject"]),
+    getInitFile() {
+      this.curImage = getEarliestFile();
+      if (this.curImage.url) {
+        if (this.setT) clearTimeout(this.setT);
+      } else {
+        this.setT = setTimeout(() => {
+          this.getInitFile();
+        }, 1000);
+      }
+    },
+    finished(setting) {
+      console.log(setting);
+      const curSubject = Object.assign({}, this.curSubject, {
+        collectConfig: setting
+      });
+      this.setCurSubject(curSubject);
+
+      const scanName = this.config.isPackageMode ? "GroupScan" : "LineScan";
+      this.$router.push({ name: scanName });
+    }
+  },
+  beforeDestroy() {
+    if (this.setT) clearTimeout(this.setT);
+  }
+};
+</script>

+ 12 - 2
src/modules/client/views/Subject.vue

@@ -24,7 +24,17 @@ export default {
   name: "subject",
   data() {
     return {
-      subjects: this.$ls.get("subjects", [])
+      // subjects: this.$ls.get("subjects", [])
+      subjects: [
+        {
+          id: 1,
+          name: "素描"
+        },
+        {
+          id: 2,
+          name: "绘画"
+        }
+      ]
     };
   },
   computed: {
@@ -43,7 +53,7 @@ export default {
       //   name: "Entrance"
       // });
       this.$router.push({
-        name: "Setting"
+        name: "Camera"
       });
     }
   }

+ 4 - 4
src/modules/example/router.js

@@ -1,9 +1,9 @@
-import Subject from "./views/Subject.vue";
+import DataManage from "./views/DataManage.vue";
 
 export default [
   {
-    path: "/subject",
-    name: "Subject",
-    component: Subject
+    path: "/example/data-manage",
+    name: "DataManage",
+    component: DataManage
   }
 ];

+ 0 - 81
src/modules/login/fetchSmsMixins.js

@@ -1,81 +0,0 @@
-import { getSmsCode } from "./api";
-
-const wstorage = {
-  set(key, value, expire = null) {
-    window.localStorage.setItem(key, JSON.stringify({ value, expire }));
-  },
-  get(key, defaultVal = null) {
-    const lsvalue = JSON.parse(window.localStorage.getItem(key));
-    return lsvalue && (!lsvalue.expire || lsvalue.expire > new Date().getTime())
-      ? lsvalue.value
-      : defaultVal;
-  },
-  remove(key) {
-    window.localStorage.removeItem(key);
-  }
-};
-
-const codeWaitingTime = 60;
-
-export default {
-  data() {
-    return {
-      isFetchingCode: false,
-      codeContent: "获取验证码",
-      codeWaitingTime,
-      time: codeWaitingTime
-    };
-  },
-  methods: {
-    setWaitingTime() {
-      let codetime = wstorage.get(this.nameWaitTime);
-      if (codetime) {
-        let num = Math.floor((codetime.expire - new Date().getTime()) / 1000);
-        if (num > 0) {
-          this.time = num;
-          this.isFetchingCode = true;
-          this.changeContent();
-        }
-      }
-    },
-    fetchSmsCode() {
-      this.$refs.modalFormComp.validateField("phone", async valid => {
-        if (valid) return;
-        this.isFetchingCode = true;
-        const data = await getSmsCode(this.modalForm.phone).catch(() => {
-          this.isFetchingCode = false;
-        });
-        if (!data) return;
-        this.changeContent();
-      });
-    },
-    changeContent() {
-      if (!this.isFetchingCode) return;
-      this.codeContent = "倒计时" + this.time + "s";
-      const circleTime = time => {
-        let t = setInterval(() => {
-          if (time > 1) {
-            time--;
-            let expire = new Date().getTime() + time * 1000;
-            wstorage.set(
-              this.nameWaitTime,
-              {
-                time,
-                expire
-              },
-              expire
-            );
-            this.codeContent = "倒计时" + time + "s";
-          } else {
-            this.time = this.codeWaitingTime;
-            wstorage.remove(this.nameWaitTime);
-            this.codeContent = "获取验证码";
-            this.isFetchingCode = false;
-            clearInterval(t);
-          }
-        }, 1e3);
-      };
-      circleTime(this.time);
-    }
-  }
-};

+ 0 - 20
src/modules/login/router.js

@@ -1,20 +0,0 @@
-import LoginHome from "./views/LoginHome";
-import Login from "./views/Login";
-
-export default [
-  {
-    path: "/login-home",
-    component: LoginHome,
-    children: [
-      {
-        path: "/login",
-        name: "Login",
-        component: Login,
-        meta: {
-          title: "登录",
-          noRequire: true
-        }
-      }
-    ]
-  }
-];

+ 0 - 82
src/modules/login/views/Login.vue

@@ -1,82 +0,0 @@
-<template>
-  <div class="login login-box">
-    <div class="login-title">
-      <h1>美术阅卷系统登录</h1>
-    </div>
-    <div class="login-form">
-      <Form ref="loginForm" :model="loginModel" :rules="loginRules">
-        <FormItem prop="loginName">
-          <Input
-            v-model.trim="loginModel.loginName"
-            prefix="md-person"
-            placeholder="请输入用户名"
-            clearable
-          ></Input>
-        </FormItem>
-        <FormItem prop="password">
-          <Input
-            type="password"
-            v-model.trim="loginModel.password"
-            prefix="md-lock"
-            placeholder="请输入密码"
-            clearable
-          ></Input>
-        </FormItem>
-        <FormItem>
-          <Button
-            long
-            type="primary"
-            :disabled="isSubmit"
-            @click="submit('loginForm')"
-            >登录</Button
-          >
-        </FormItem>
-      </Form>
-    </div>
-  </div>
-</template>
-
-<script>
-import { username, password } from "@/plugins/formRules";
-import { login } from "../api";
-
-export default {
-  name: "login",
-  data() {
-    return {
-      loginModel: {
-        loginName: "test1",
-        password: "123456"
-      },
-      loginRules: {
-        loginName: username,
-        password
-      },
-      isSubmit: false
-    };
-  },
-  mounted() {
-    this.$ls.clear();
-  },
-  methods: {
-    async submit(name) {
-      const valid = await this.$refs[name].validate();
-      if (!valid) return;
-
-      if (this.isSubmit) return;
-      this.isSubmit = true;
-      const data = await login(this.loginModel).catch(() => {
-        this.isSubmit = false;
-      });
-      if (!data) return;
-
-      this.isSubmit = false;
-      this.$ls.set("user", data, this.GLOBAL.authTimeout);
-      this.$store.commit("setUser", data);
-      this.$router.push({
-        name: "Subject"
-      });
-    }
-  }
-};
-</script>

+ 0 - 22
src/modules/login/views/LoginHome.vue

@@ -1,22 +0,0 @@
-<template>
-  <div class="login-home">
-    <router-view></router-view>
-
-    <view-footer></view-footer>
-  </div>
-</template>
-
-<script>
-import ViewFooter from "@/components/ViewFooter";
-
-export default {
-  name: "login-home",
-  components: {
-    ViewFooter
-  },
-  data() {
-    return {};
-  },
-  methods: {}
-};
-</script>

+ 48 - 0
src/plugins/codeInput.js

@@ -0,0 +1,48 @@
+export default {
+  inserted(el, { value, modifiers }, vnode) {
+    let lastTime = null;
+    let nextTime = null;
+    let code = "";
+    let setT = "";
+    el.focus();
+    el.addEventListener("keydown", function(e) {
+      if (modifiers.prevent) {
+        e.preventDefault();
+      }
+
+      const keycode = e.keyCode || e.which || e.charCode;
+      nextTime = new Date().getTime();
+      if (setT) clearTimeout(setT);
+      // 扫码枪输入完毕之后,会自动输入一个回车
+      if (keycode === 13) {
+        if (lastTime && nextTime - lastTime < 30) {
+          if (value && value.inputOver) value.inputOver(code);
+        }
+        code = "";
+        lastTime = null;
+        e.preventDefault();
+      } else {
+        if (!lastTime) {
+          code = String.fromCharCode(keycode);
+          lastTime = nextTime;
+        } else {
+          if (nextTime - lastTime < 30) {
+            code += String.fromCharCode(keycode);
+            lastTime = nextTime;
+          } else {
+            code = "";
+            lastTime = null;
+          }
+          if (value && value.inputOver) value.inputOver(code);
+        }
+        // bug:当手动输入之后,再扫描输入时,会首字符缺失
+        // fix:设置定时清理上次输入和时间,暂时解决上述bug
+        setT = setTimeout(() => {
+          code = "";
+          lastTime = null;
+          if (value && value.inputOver) value.inputOver(code);
+        }, 80);
+      }
+    });
+  }
+};

+ 2 - 2
src/plugins/db.js

@@ -1,5 +1,5 @@
-const { getDatabasePath } = require("./env");
-const { formatDate } = require("./utils");
+import { getDatabasePath } from "./env";
+import { formatDate } from "./utils";
 const path = require("path");
 const fs = require("fs");
 const sqlite = require("sqlite3").verbose();

+ 3 - 0
src/plugins/env.js

@@ -3,6 +3,7 @@ const fs = require("fs");
 const { getLocalDate } = require("./utils");
 
 const homePath = path.join(__dirname, "../../");
+const __static = "";
 
 function initPath() {
   const paths = [
@@ -77,6 +78,8 @@ function makeDirSync(pathContent) {
 export {
   initPath,
   getPath,
+  getToolPath,
+  getInImgPath,
   getOutImgPath,
   getTmpImgPath,
   getOutputImagePath,

+ 4 - 0
src/plugins/globalVuePlugins.js

@@ -1,5 +1,6 @@
 import { objAssign, randomCode, tableAction } from "@/plugins/utils";
 import globalMixins from "./mixins";
+import CodeInput from "./codeInput";
 import ViewHeader from "@/components/ViewHeader.vue";
 import ViewFooter from "@/components/ViewFooter.vue";
 
@@ -22,5 +23,8 @@ export default {
 
     //全局 mixins
     Vue.mixin(globalMixins);
+
+    // 全局 directive
+    Vue.directive("CodeInput", CodeInput);
   }
 };

+ 4 - 8
src/plugins/image-ocr.js → src/plugins/imageOcr.js

@@ -1,11 +1,11 @@
-const {
+import {
   getTmpImgPath,
   getInImgPath,
   getOutImgPath,
   getToolPath,
   getImgDecodeTool
-} = require("./env");
-const { randomCode } = require("./utils");
+} from "./env";
+import { randomCode } from "./utils";
 const fs = require("fs");
 const path = require("path");
 const childProcess = require("child_process");
@@ -119,8 +119,4 @@ function getEarliestFile(dir) {
   };
 }
 
-module.exports = {
-  decodeImageCode,
-  saveFormalImage,
-  getEarliestFile
-};
+export { decodeImageCode, saveFormalImage, getEarliestFile };

+ 8 - 8
src/plugins/image-upload.js → src/plugins/imageUpload.js

@@ -1,6 +1,6 @@
 const fs = require("fs");
 const crypto = require("crypto");
-const { uploadImage } = require("../main-script/api");
+import { uploadImage } from "../modules/client/api";
 
 /**
  * 文件上传
@@ -29,12 +29,12 @@ function toUploadImg(filePath, options) {
  * 获取文件的MD5
  * @param {String} source 文件路径
  */
-function getMD5(source) {
-  const buffer = fs.readFileSync(source);
-  let fsHash = crypto.createHash("md5");
-  fsHash.update(buffer);
-  return fsHash.digest("hex");
-}
+// function getMD5(source) {
+//   const buffer = fs.readFileSync(source);
+//   let fsHash = crypto.createHash("md5");
+//   fsHash.update(buffer);
+//   return fsHash.digest("hex");
+// }
 
 class UploadTask {
   constructor({ taskList, uploadSuccessCallback, uploadTaskOverCallback }) {
@@ -91,4 +91,4 @@ class UploadTask {
   }
 }
 
-module.exports = UploadTask;
+export default UploadTask;

+ 9 - 6
src/router.js

@@ -2,8 +2,8 @@ import Vue from "vue";
 import Router from "vue-router";
 
 import Home from "./views/Home";
+import Login from "./views/Login";
 // modules
-import login from "./modules/login/router";
 import client from "./modules/client/router";
 
 Vue.use(Router);
@@ -22,13 +22,16 @@ export default new Router({
       redirect: { name: "Login" }
     },
     {
-      path: "home",
+      path: "/login",
+      name: "Login",
+      component: Login
+    },
+    {
+      path: "/home",
       name: "Home",
-      components: Home,
+      component: Home,
       children: [...client]
-    },
-    ...login
-
+    }
     // [lazy-loaded] route level code-splitting
     // {
     //   path: "/about",

+ 2 - 0
src/views/Home.vue

@@ -56,6 +56,8 @@ export default {
     }
   },
   created() {
+    // TODO:遵循file-upload项目的逻辑
+    // 探索多线程编程
     // this.initStore();
     // this.initUploadProgress();
   },

+ 87 - 0
src/views/Login.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="login">
+    <div class="login-home">
+      <div class="login-title">
+        <h1>美术阅卷系统登录</h1>
+      </div>
+      <div class="login-box">
+        <h2>欢迎登录</h2>
+        <div class="login-form">
+          <Form ref="loginForm" :model="loginModel" :rules="loginRules">
+            <FormItem prop="loginName">
+              <Input
+                v-model.trim="loginModel.loginName"
+                prefix="md-person"
+                placeholder="请输入用户名"
+                clearable
+              ></Input>
+            </FormItem>
+            <FormItem prop="password">
+              <Input
+                type="password"
+                v-model.trim="loginModel.password"
+                prefix="md-lock"
+                placeholder="请输入密码"
+                clearable
+              ></Input>
+            </FormItem>
+            <FormItem>
+              <Button
+                long
+                type="primary"
+                :disabled="isSubmit"
+                @click="submit('loginForm')"
+                >登录</Button
+              >
+            </FormItem>
+          </Form>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { username, password } from "@/plugins/formRules";
+import { login } from "./api";
+
+export default {
+  name: "login",
+  data() {
+    return {
+      loginModel: {
+        loginName: "test1",
+        password: "123456"
+      },
+      loginRules: {
+        loginName: username,
+        password
+      },
+      isSubmit: false
+    };
+  },
+  mounted() {
+    this.$ls.clear();
+  },
+  methods: {
+    async submit(name) {
+      const valid = await this.$refs[name].validate();
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await login(this.loginModel).catch(() => {
+        this.isSubmit = false;
+      });
+      if (!data) return;
+
+      this.isSubmit = false;
+      this.$ls.set("user", data, this.GLOBAL.authTimeout);
+      this.$store.commit("setUser", data);
+      this.$router.push({
+        name: "Subject"
+      });
+    }
+  }
+};
+</script>

+ 0 - 0
src/modules/login/api.js → src/views/api.js


+ 45 - 0
vue.config.js

@@ -0,0 +1,45 @@
+var webpack = require("webpack");
+var TerserPlugin = require("terser-webpack-plugin");
+var devProxy = {};
+try {
+  devProxy = require("./dev-proxy");
+} catch (error) {}
+
+var proxy = process.env.NODE_ENV === "production" ? {} : devProxy;
+
+// 配置手册: https://cli.vuejs.org/zh/config/#vue-config-js
+var config = {
+  // publicPath: './',
+  devServer: {
+    port: 8066
+  }
+};
+
+// compress配置手册:https://github.com/mishoo/UglifyJS2/tree/harmony#compress-options
+if (process.env.NODE_ENV === "production") {
+  config.configureWebpack = {
+    plugins: [],
+    optimization: {
+      minimizer: [
+        new TerserPlugin({
+          terserOptions: { compress: { drop_console: true } }
+        })
+      ]
+    }
+  };
+}
+
+if (proxy && Object.keys(proxy).length) {
+  config.devServer.proxy = proxy;
+}
+
+// 解决iview自定义主题导入less报错
+config.css = {
+  loaderOptions: {
+    less: {
+      javascriptEnabled: true
+    }
+  }
+};
+
+module.exports = config;

+ 5 - 0
yarn.lock

@@ -2985,6 +2985,11 @@ crypto-random-string@^1.0.0:
   resolved "https://registry.npm.taobao.org/crypto-random-string/download/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
   integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
 
+crypto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npm.taobao.org/crypto/download/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
+  integrity sha1-KvG3ytgXXSTIobB3glV5SiGAMDc=
+
 css-color-names@0.0.4, css-color-names@^0.0.4:
   version "0.0.4"
   resolved "https://registry.npm.taobao.org/css-color-names/download/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"