소스 검색

download add

zhangjie 4 년 전
부모
커밋
bba54612de

BIN
extra/database/org.rdb


+ 6 - 0
extra/font/simhei-subfont-intro.md

@@ -0,0 +1,6 @@
+# 字体说明
+
+- `simhei-subfont.ttf`字体文件通过`simhei.ttf`字体文件提取而来。
+- `simhei.ttf`是微软常规字体`黑体`。
+- 文件中只包含了`0123456789分`这 11 个字符。
+- [现在生成地址](https://www.fontke.com/tool/subfont/)

BIN
extra/font/simhei-subfont.ttf


+ 16 - 0
src/assets/styles/iview-custom.less

@@ -69,6 +69,22 @@
   }
 }
 
+.ivu-input-number {
+  border-radius: 10px;
+  line-height: 34px;
+  background-color: #212543 !important;
+  border-color: #212543 !important;
+  color: @fontSub;
+
+  .ivu-input-number-input {
+    padding: 0 15px;
+    background-color: #212543 !important;
+  }
+  .ivu-input-number-handler-wrap {
+    display: none;
+  }
+}
+
 // form -ok
 .ivu-form {
   .ivu-form-item-error-tip {

+ 24 - 0
src/assets/styles/manage.less

@@ -149,3 +149,27 @@
     }
   }
 }
+
+// paper-export
+.paper-export {
+  .ivu-form-item {
+    margin-bottom: 20px;
+  }
+  .ivu-radio-wrapper,
+  .ivu-checkbox-wrapper {
+    margin-right: 50px;
+    color: @dark-color-lighter;
+  }
+  .ivu-form-item-label {
+    color: @dark-color-lighter;
+  }
+  .ivu-progress-inner {
+    background-color: @background;
+  }
+}
+.export-filter {
+  padding: 15px 20px;
+}
+.export-task {
+  padding: 15px 20px;
+}

+ 14 - 0
src/constants/enumerate.js

@@ -20,3 +20,17 @@ export const CAFA_EXCEPTION_TYPE = {
   0: "缺考",
   1: "手工绑定"
 };
+
+// image-type
+export const IMAGE_TYPE = {
+  1: "原图",
+  2: "裁切图"
+};
+export const EXPORT_IMAGE_NAME_TYPE = {
+  1: "考号+姓名",
+  2: "流水号"
+};
+export const BOOLEAN_TYPE = {
+  0: "否",
+  1: "是"
+};

+ 34 - 1
src/modules/manage/api.js

@@ -1,4 +1,4 @@
-import { $post } from "@/plugins/axios";
+import { $post, $get } from "@/plugins/axios";
 import db from "@/plugins/db";
 
 export const paperPageList = datas => {
@@ -14,3 +14,36 @@ export const absentPaper = paperId => {
 export const absentLocalPaper = (id, missing) => {
   return db.absentLocalPaper(id, missing);
 };
+
+// export paper
+export const workList = () => {
+  return $get("/api/admin/works");
+};
+export const exportDataList = datas => {
+  return $get(`/api/file/image/getExportData`, datas);
+};
+//
+export const getUnfinishTask = taskId => {
+  return db.getUnfinishTask(taskId);
+};
+export const getDownloadTaskCount = params => {
+  return db.getDownloadTaskCount(params);
+};
+export const getDownloadTaskList = taskId => {
+  return db.getDownloadTaskList(taskId);
+};
+export const addExportTask = datas => {
+  return db.addExportTask(datas);
+};
+export const addExportTaskDetail = datas => {
+  return db.addExportTaskDetail(datas);
+};
+export const updateTaskFinish = datas => {
+  return db.updateTaskFinish(datas);
+};
+export const updateDownloadTaskDownload = datas => {
+  return db.updateDownloadTaskDownload(datas);
+};
+export const donloadExportFile = datas => {
+  return $get(`/api/file/image/exportScorePicturesFromCollect`, datas);
+};

+ 7 - 1
src/modules/manage/router.js

@@ -1,9 +1,15 @@
-import PaperManage from "./views/PaperManage.vue";
+import PaperManage from "./views/PaperManage";
+import PaperExport from "./views/PaperExport";
 
 export default [
   {
     path: "/paper-manage",
     name: "PaperManage",
     component: PaperManage
+  },
+  {
+    path: "/paper-export",
+    name: "PaperExport",
+    component: PaperExport
   }
 ];

+ 545 - 0
src/modules/manage/views/PaperExport.vue

@@ -0,0 +1,545 @@
+<template>
+  <div class="paper-export part-box">
+    <div class="part-head">
+      <h2>分数图片导出</h2>
+    </div>
+    <div class="export-filter">
+      <Form :model="scoreFilter" ref="scoreForm" :label-width="140">
+        <FormItem label="工作文件夹:">
+          <Select
+            v-model="scoreFilter.examId"
+            placeholder="请选择工作文件夹"
+            style="width: 400px"
+            :disabled="taskRunning"
+          >
+            <Option
+              v-for="item in exams"
+              :key="item.id"
+              :value="item.id"
+              :label="item.name"
+            ></Option>
+          </Select>
+        </FormItem>
+        <FormItem prop="imageType" label="图片类型:">
+          <RadioGroup v-model="scoreFilter.imageType">
+            <Radio
+              size="large"
+              v-for="(val, key) in IMAGE_TYPE"
+              :key="key"
+              :label="key * 1"
+              :disabled="taskRunning"
+              >{{ val }}</Radio
+            >
+          </RadioGroup>
+        </FormItem>
+        <FormItem prop="isWatermark" label="是否分数水印:">
+          <RadioGroup v-model="scoreFilter.isWatermark">
+            <Radio
+              size="large"
+              v-for="(val, key) in BOOLEAN_TYPE"
+              :key="key"
+              :label="key * 1"
+              :disabled="taskRunning"
+              >{{ val }}</Radio
+            >
+          </RadioGroup>
+        </FormItem>
+        <FormItem prop="isResume" label="是否续传:">
+          <RadioGroup v-model="scoreFilter.isResume">
+            <Radio
+              size="large"
+              v-for="(val, key) in BOOLEAN_TYPE"
+              :key="key"
+              :label="key * 1"
+              :disabled="taskRunning"
+              >{{ val }}</Radio
+            >
+          </RadioGroup>
+        </FormItem>
+        <FormItem prop="nameRule" label="图片命名规则:">
+          <RadioGroup v-model="scoreFilter.nameRule">
+            <Radio
+              size="large"
+              v-for="(val, key) in EXPORT_IMAGE_NAME_TYPE"
+              :key="key"
+              :label="key * 1"
+              :disabled="taskRunning"
+              >{{ val }}</Radio
+            >
+          </RadioGroup>
+        </FormItem>
+        <FormItem label="分数段:">
+          <InputNumber
+            v-model="scoreFilter.startScore"
+            :min="1"
+            :max="1000"
+            :precision="0"
+            placeholder="输入起始分数"
+            style="width: 120px"
+            :disabled="taskRunning"
+            clearable
+          ></InputNumber>
+          <span style="margin: 0 10px;"></span>
+          <InputNumber
+            v-model="scoreFilter.endScore"
+            :min="scoreFilter.startScore"
+            :max="1000"
+            :precision="0"
+            :active-change="false"
+            placeholder="输入终止分数"
+            style="width: 120px"
+            :disabled="taskRunning"
+            clearable
+          ></InputNumber>
+        </FormItem>
+        <FormItem label="本地保存路径:">
+          <Input
+            v-model="scoreFilter.outputDir"
+            readonly
+            style="width: 600px;margin-right: 15px;"
+          ></Input>
+          <Button
+            class="export-paper-btn"
+            type="primary"
+            @click="seleteOutpath"
+            :disabled="taskRunning"
+            >选择</Button
+          >
+        </FormItem>
+        <FormItem label="转存规则:">
+          <Checkbox
+            v-for="item in SAVE_PATH_RULES"
+            :key="item.val"
+            v-model="item.seleted"
+            :disabled="item.disabled || taskRunning"
+            >{{ item.name }}</Checkbox
+          >
+        </FormItem>
+        <FormItem>
+          <Button
+            class="export-paper-btn"
+            type="primary"
+            @click="toExportScore"
+            :disabled="taskRunning"
+            >开始导出</Button
+          >
+        </FormItem>
+      </Form>
+    </div>
+
+    <!-- 任务信息 -->
+    <div class="part-head">
+      <h2>任务信息</h2>
+    </div>
+    <div class="export-task">
+      <Form :label-width="140">
+        <FormItem label="任务进度:">
+          <Progress :percent="progress" />
+        </FormItem>
+        <FormItem label="任务数量:">
+          <p>{{ finishedTaskCount }} / {{ taskTotalCount }}</p>
+        </FormItem>
+        <FormItem label="开始时间:">
+          <p>{{ startProcessTimeFormat }}</p>
+        </FormItem>
+        <template v-if="!taskOver">
+          <FormItem label="预计剩余时间:">
+            <p>{{ predictTime }}</p>
+          </FormItem>
+          <FormItem label="当前下载图片:">
+            <p>
+              {{ curDownloadTask.studentName }} -
+              {{ curDownloadTask.examNumber }}
+            </p>
+          </FormItem>
+        </template>
+        <template v-else>
+          <FormItem label="结束时间:">
+            <p>{{ endProcessTimeFormat }}</p>
+          </FormItem>
+          <FormItem label="任务总耗时:">
+            <p>{{ processDuritionTime }}</p>
+          </FormItem>
+        </template>
+      </Form>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  IMAGE_TYPE,
+  EXPORT_IMAGE_NAME_TYPE,
+  BOOLEAN_TYPE
+} from "@/constants/enumerate";
+import { formatDate, timeNumberToText, qsParams } from "@/plugins/utils";
+import {
+  workList,
+  getUnfinishTask,
+  exportDataList,
+  addExportTask,
+  addExportTaskDetail,
+  getDownloadTaskList,
+  updateTaskFinish,
+  updateDownloadTaskDownload,
+  getDownloadTaskCount
+} from "../api";
+import { downloadFile, downloadServerFile } from "@/plugins/imageOcr";
+
+const remote = require("electron").remote;
+const path = require("path");
+
+let undownloadTaskList = [];
+
+const SAVE_PATH_RULES = [
+  {
+    name: "考区",
+    val: "${areaName}${areaCode}",
+    seleted: false,
+    disabled: false
+  },
+  {
+    name: "学校",
+    val: "${school}",
+    seleted: false,
+    disabled: false
+  },
+  {
+    name: "科目",
+    val: "${subjectName}",
+    seleted: true,
+    disabled: true
+  }
+];
+
+export default {
+  name: "paper-export",
+  data() {
+    return {
+      scoreFilter: {
+        examId: null,
+        imageType: 1,
+        isWatermark: 1,
+        isResume: 1,
+        nameRule: 1,
+        startScore: null,
+        endScore: null,
+        outputDir: "",
+        savePathRule: ""
+      },
+      exams: [],
+      IMAGE_TYPE,
+      EXPORT_IMAGE_NAME_TYPE,
+      BOOLEAN_TYPE,
+      SAVE_PATH_RULES,
+      // task
+      taskRunning: false,
+      taskOver: false,
+      pageSize: 100,
+      curExportTask: {},
+      curDownloadTask: {
+        path: "",
+        filename: ""
+      },
+      taskTotalCount: 0,
+      finishedTaskCount: 0,
+      predictTime: "",
+      startProcessTime: 0,
+      startDownloadTime: 0,
+      endProcessTime: 0,
+      processDuritionTime: "",
+      taskStarttime: 0,
+      alertTips: {
+        type: "info",
+        message: "准备执行任务"
+      }
+    };
+  },
+  computed: {
+    progress() {
+      return this.taskTotalCount
+        ? Math.floor((this.finishedTaskCount * 100) / this.taskTotalCount)
+        : 0;
+    },
+    startProcessTimeFormat() {
+      return this.startProcessTime
+        ? formatDate("YYYY-MM-DD HH:mm:ss", new Date(this.startProcessTime))
+        : "-";
+    },
+    endProcessTimeFormat() {
+      return this.endProcessTime
+        ? formatDate("YYYY-MM-DD HH:mm:ss", new Date(this.endProcessTime))
+        : "-";
+    }
+  },
+  mounted() {
+    this.scoreFilter.examId = this.$ls.get("user", { examId: "" }).examId;
+    this.getExams();
+    this.checkHasUnfinishTask();
+  },
+  methods: {
+    async text() {
+      await downloadFile({
+        url:
+          "https://msmk-prod.oss-cn-beijing.aliyuncs.com/ms-test/upload/sheet/51/SC/1/1901040084.jpg",
+        outpath: "E:\\meishuMS\\output\\1901040084.jpg",
+        score: 88,
+        isWatermark: true
+      });
+    },
+    async getExams() {
+      const data = await workList();
+      this.exams = data.map(item => {
+        return {
+          id: item.id,
+          name: item.name,
+          active: item.active,
+          createdOn: item.createdOn
+        };
+      });
+    },
+    async checkHasUnfinishTask() {
+      const task = await getUnfinishTask().catch(() => {});
+      if (!task) return;
+
+      this.$Modal.confirm({
+        content: "当前还有未完成的导出任务,是否继续进行?",
+        onOk: () => {
+          console.log(task);
+          this.curExportTask = task;
+          this.scoreFilter = { ...task };
+          this.scoreFilter.examId = task.examId * 1;
+          const saveRules = this.scoreFilter.savePathRule.split("/");
+          this.SAVE_PATH_RULES.map(item => {
+            item.seleted = saveRules.includes(item.val);
+          });
+          this.startProcessTime = Date.now();
+          this.startDownload();
+        },
+        onCancel: async () => {
+          await updateTaskFinish(task.id);
+        }
+      });
+    },
+    async toExportScore() {
+      if (this.taskRunning) return;
+
+      if (!this.scoreFilter.outputDir) {
+        this.$Message.error("请选择本地保存路径");
+        return;
+      }
+      this.taskRunning = true;
+
+      this.scoreFilter.savePathRule = this.SAVE_PATH_RULES.filter(
+        item => item.seleted
+      )
+        .map(item => item.val)
+        .join("/");
+
+      this.startProcessTime = Date.now();
+      this.alertTips = {
+        type: "info",
+        message: "正在下载任务数据"
+      };
+      const exam = this.exams.find(item => item.id === this.scoreFilter.examId);
+      // 保存本地导出任务
+      this.curExportTask = {
+        ...this.scoreFilter,
+        workId: this.scoreFilter.examId,
+        examName: exam.name,
+        id: ""
+      };
+      const taskId = await addExportTask(this.curExportTask).catch(() => {});
+
+      if (!taskId) {
+        console.log("本地添加任务失败!");
+        this.taskRunning = false;
+        return;
+      }
+      this.curExportTask.id = taskId;
+
+      // 获取导出任务详情数据
+      const dataList = await exportDataList(this.curExportTask);
+      // 记录导出任务详情数据
+      const len = dataList.length;
+      const pageCount = Math.ceil(len / this.pageSize);
+      for (let i = 0; i < pageCount; i++) {
+        const startNo = i * this.pageSize;
+        const taskList = dataList
+          .slice(startNo, startNo + this.pageSize)
+          .map((item, index) => {
+            const serialNo = startNo + index + 1;
+            const filename =
+              this.curExportTask.nameRule === 1
+                ? `${item.examNumber}-${item.studentName}`
+                : serialNo;
+            return {
+              ...item,
+              examId: this.curExportTask.examId,
+              examName: this.curExportTask.examName,
+              taskId,
+              serialNo,
+              filename: filename + path.extname(item.url)
+            };
+          });
+        await addExportTaskDetail(taskList).catch(error => {
+          console.log(error);
+        });
+      }
+      // 开启下载
+      this.startDownload();
+    },
+    seleteOutpath() {
+      remote.dialog.showOpenDialog(
+        {
+          title: "选择导出目录",
+          properties: ["openDirectory"]
+        },
+        folderPaths => {
+          if (folderPaths && folderPaths[0]) {
+            this.scoreFilter.outputDir = folderPaths[0];
+          }
+        }
+      );
+    },
+    initTask() {
+      this.taskRunning = false;
+      this.taskOver = false;
+      this.curDownloadTask = {};
+      this.taskTotalCount = 0;
+      this.finishedTaskCount = 0;
+      this.predictTime = "";
+      this.alertTips = {
+        type: "info",
+        message: "准备执行任务"
+      };
+      this.startProcessTime = 0;
+      this.endProcessTime = 0;
+      this.processDuritionTime = "";
+    },
+    async startDownload() {
+      this.alertTips = {
+        type: "info",
+        message: "正在下载图片"
+      };
+      const finishedTaskCount = await getDownloadTaskCount({
+        taskId: this.curExportTask.id,
+        isDownload: 1
+      }).catch(() => {});
+      this.finishedTaskCount = finishedTaskCount || 0;
+
+      this.taskRunning = true;
+      undownloadTaskList = await getDownloadTaskList(
+        this.curExportTask.id
+      ).catch(() => {
+        this.taskRunning = false;
+      });
+      if (!undownloadTaskList || !undownloadTaskList.length) return;
+
+      this.taskTotalCount = this.finishedTaskCount + undownloadTaskList.length;
+      this.startDownloadTime = Date.now();
+      this.runTask();
+    },
+    async runTask() {
+      if (!this.taskRunning) return;
+
+      if (!undownloadTaskList.length) {
+        this.stopTask();
+        return;
+      }
+
+      this.curDownloadTask = undownloadTaskList.shift();
+      this.curDownloadTask.isWatermark = this.curExportTask.isWatermark;
+      this.curDownloadTask.outpath = this.getDownloadFilePath(
+        this.curExportTask,
+        this.curDownloadTask
+      );
+
+      const result = await this.download(1);
+
+      if (result) {
+        console.log(result);
+        await updateDownloadTaskDownload(this.curDownloadTask.id).catch(err => {
+          console.log(err);
+        });
+      }
+
+      this.curTaskOver();
+    },
+    async download(isLocalDownload) {
+      // 本地加水印
+      if (isLocalDownload) {
+        return await downloadFile(this.curDownloadTask).catch(() => {
+          console.log(`${this.curDownloadTask.studentName}下载失败!`);
+        });
+      }
+      // 服务端打水印
+      const domain =
+        process.env.NODE_ENV === "production"
+          ? this.GLOBAL.domain
+          : "https://msmk.qmth.com.cn/";
+      this.curDownloadTask.url =
+        domain +
+        "/api/file/image/exportScorePicturesFromCollect?" +
+        qsParams({
+          workId: this.curExportTask.examId,
+          imageType: this.curExportTask.imageType,
+          isWatermark: this.curExportTask.isWatermark,
+          nameRule: this.curExportTask.nameRule,
+          studentId: this.curDownloadTask.studentId,
+          studentName: this.curDownloadTask.studentName,
+          subject: this.curDownloadTask.subject,
+          areaCode: this.curDownloadTask.areaCode,
+          examNumber: this.curDownloadTask.examNumber,
+          score: this.curDownloadTask.score,
+          idx: this.curDownloadTask.serialNo
+        });
+      this.curDownloadTask.isWatermark = 0;
+      const result = await downloadServerFile({
+        url: encodeURI(this.curDownloadTask.url),
+        outpath: this.curDownloadTask.outpath
+      }).catch(() => {
+        console.log(`${this.curDownloadTask.studentName}下载失败!`);
+      });
+
+      return result;
+    },
+    curTaskOver() {
+      // 准备开启下一个下载任务
+      this.finishedTaskCount++;
+      this.getPredictTime();
+      setTimeout(() => {
+        this.runTask();
+      });
+    },
+    getDownloadFilePath(task, paper) {
+      let fileDir = task.savePathRule;
+      Object.keys(paper).map(k => {
+        fileDir = fileDir.replace("${" + k + "}", paper[k]);
+      });
+      return path.join(task.outputDir, fileDir, paper.filename);
+    },
+    async stopTask() {
+      undownloadTaskList = [];
+      this.taskRunning = false;
+      this.taskOver = true;
+      this.endProcessTime = Date.now();
+      this.processDuritionTime = timeNumberToText(
+        this.endProcessTime - this.startProcessTime
+      );
+      this.alertTips = {
+        type: "success",
+        message: "任务执行完毕!"
+      };
+      await updateTaskFinish(this.curExportTask.id).catch(() => {});
+    },
+    getPredictTime() {
+      const hasUsedTime = Date.now() - this.startDownloadTime;
+      const predictRunTime =
+        (hasUsedTime / this.finishedTaskCount) *
+        (this.taskTotalCount - this.finishedTaskCount);
+      this.predictTime = timeNumberToText(predictRunTime);
+    }
+  }
+};
+</script>

+ 155 - 7
src/plugins/db.js

@@ -21,7 +21,7 @@ function init() {
 }
 
 function saveUploadInfo(params) {
-  const sql = `INSERT INTO scan (examId, examName, subjectId, subjectName, examNumber, studentName, siteCode, roomCode, formalImgPath, sliceImgPath,isManual,imageEncrypt,level,clientUserId, clientUsername, clientUserLoginTime, isUpload,createdTime, fininshTime) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
+  const sql = `INSERT INTO scan (examId, examName, subjectId, subjectName, examNumber, studentName, siteCode, roomCode, formalImgPath, sliceImgPath,isManual,imageEncrypt,level,clientUserId, clientUsername, clientUserLoginTime, isUpload,createdTime, finishTime) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
   const datas = [
     params.examId,
     params.examName,
@@ -80,7 +80,7 @@ function countScanList(params) {
 }
 
 function getUploadCount(limitTime) {
-  const sql = `SELECT COUNT(1) AS count FROM scan WHERE strftime('%s',fininshTime, 'utc') >= '${limitTime}' AND isUpload = 1`;
+  const sql = `SELECT COUNT(1) AS count FROM scan WHERE strftime('%s',finishTime, 'utc') >= '${limitTime}' AND isUpload = 1`;
 
   return new Promise((resolve, reject) => {
     db.all(sql, (err, rows) => {
@@ -116,10 +116,10 @@ function getHistory(limitTime, subjectId) {
 }
 
 function updateUploadState(id) {
-  const sql = `UPDATE scan SET isUpload=$isUpload,fininshTime=$fininshTime WHERE id=$id`;
+  const sql = `UPDATE scan SET isUpload=$isUpload,finishTime=$finishTime WHERE id=$id`;
   const datas = {
     $isUpload: 1,
-    $fininshTime: formatDate(),
+    $finishTime: formatDate(),
     $id: id
   };
   return new Promise((resolve, reject) => {
@@ -288,7 +288,7 @@ function absentLocalPaper(id, missing) {
   });
 }
 
-function uploadLocalPaperId(id, paperId) {
+function updateLocalPaperId(id, paperId) {
   const sql = `UPDATE scan SET paperId=$paperId WHERE id=$id`;
   const datas = {
     $paperId: paperId,
@@ -317,6 +317,147 @@ function deleteScanById(id) {
   });
 }
 
+// export data
+function addExportTask(params) {
+  const sql = `INSERT INTO export_task (examId, examName,imageType,isWatermark,isResume,nameRule,startScore,endScore,outputDir,savePathRule,isFinish,createdTime,finishTime) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`;
+  const datas = [
+    params.examId,
+    params.examName,
+    params.imageType,
+    params.isWatermark,
+    params.isResume,
+    params.nameRule,
+    params.startScore,
+    params.endScore,
+    params.outputDir,
+    params.savePathRule,
+    0, // isFinish
+    formatDate(), // createdTime
+    null
+  ];
+
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, function(err) {
+      console.log(err);
+      if (err) reject(err);
+      resolve(this.lastID);
+    });
+  });
+}
+
+/**
+ *
+ * @param {Array} paramList 参数列表
+ */
+function addExportTaskDetail(paramList) {
+  console.log(paramList);
+  const listVals = paramList.map(params => {
+    const datas = [
+      params.serialNo,
+      params.taskId,
+      params.examId,
+      params.examName,
+      params.school,
+      params.studentId,
+      params.studentName,
+      params.subject,
+      params.subjectName,
+      params.areaCode,
+      params.areaName,
+      params.examNumber,
+      params.score,
+      params.url,
+      params.filename,
+      0, // isDownload
+      formatDate(), // createdTime
+      ""
+    ];
+    const nitem = datas.map(val =>
+      typeof val === "string" ? `'${val}'` : val
+    );
+    return `(${nitem.join(",")})`;
+  });
+  const vals = listVals.join(",");
+
+  const sql = `INSERT INTO export_task_detail (serialNo,taskId, examId, examName,school,studentId,studentName,subject,subjectName,areaCode,areaName,examNumber,score,url,filename,isDownload,createdTime,finishTime) VALUES ${vals}`;
+
+  return new Promise((resolve, reject) => {
+    db.run(sql, function(err) {
+      console.log(err);
+      if (err) reject(err);
+      resolve(true);
+    });
+  });
+}
+
+function getUnfinishTask() {
+  const sql = `SELECT * FROM export_task WHERE isFinish = 0 LIMIT 1;`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("get task fail!");
+      resolve(rows[0]);
+    });
+  });
+}
+
+function getDownloadTaskList(taskId) {
+  const sql = `SELECT * FROM export_task_detail WHERE taskId = '${taskId}' and isDownload = 0`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("get task list fail!");
+
+      resolve(rows);
+    });
+  });
+}
+
+function getDownloadTaskCount(params) {
+  const { where, whereData } = serializeWhere(params);
+  const sql = `SELECT COUNT(1) AS count FROM export_task_detail WHERE ${where}`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, whereData, (err, rows) => {
+      if (err) reject("count task list fail!");
+
+      resolve(rows[0].count);
+    });
+  });
+}
+
+function updateDownloadTaskDownload(id) {
+  const sql = `UPDATE export_task_detail SET isDownload=$isDownload,finishTime=$finishTime WHERE id=$id`;
+  const datas = {
+    $id: id,
+    $isDownload: 1,
+    $finishTime: formatDate()
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update download task info fail!");
+
+      resolve(true);
+    });
+  });
+}
+
+function updateTaskFinish(id) {
+  const sql = `UPDATE export_task SET isFinish=$isFinish,finishTime=$finishTime WHERE id=$id`;
+  const datas = {
+    $id: id,
+    $isFinish: 1,
+    $finishTime: formatDate()
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update export task info fail!");
+
+      resolve(true);
+    });
+  });
+}
+
 export default {
   init,
   saveUploadInfo,
@@ -333,6 +474,13 @@ export default {
   getAreas,
   getPaperList,
   absentLocalPaper,
-  uploadLocalPaperId,
-  deleteScanById
+  updateLocalPaperId,
+  deleteScanById,
+  addExportTask,
+  getUnfinishTask,
+  addExportTaskDetail,
+  getDownloadTaskList,
+  getDownloadTaskCount,
+  updateDownloadTaskDownload,
+  updateTaskFinish
 };

+ 6 - 0
src/plugins/env.js

@@ -54,6 +54,11 @@ function getImgDecodeTool() {
   return path.join(getExtraDir("zxing"), "zxing.exe");
 }
 
+function getFontPath() {
+  // 文件名必须是英文,否则会报错
+  return path.join(getExtraDir("font"), "simhei-subfont.ttf");
+}
+
 /**
  *
  * @param {String} pathContent 目录路径
@@ -99,5 +104,6 @@ export {
   makeDirSync,
   getDatabaseDir,
   getImgDecodeTool,
+  getFontPath,
   initConfigData
 };

+ 71 - 1
src/plugins/imageOcr.js

@@ -4,9 +4,11 @@ import {
   getOutputDir,
   getExtraDir,
   getImgDecodeTool,
+  getFontPath,
   makeDirSync
 } from "./env";
 import { randomCode, formatDate } from "./utils";
+const request = require("request");
 const fs = require("fs");
 const path = require("path");
 const childProcess = require("child_process");
@@ -183,4 +185,72 @@ function getEarliestFile(dir) {
   };
 }
 
-export { decodeImageCode, saveOutputImage, getEarliestFile, rotateImage };
+// 下载水印图片
+function downloadFile(task) {
+  const fileDir = path.dirname(task.outpath);
+  if (!fs.existsSync(fileDir)) makeDirSync(fileDir);
+  const fontPath = getFontPath();
+
+  return new Promise((resolve, reject) => {
+    const t1 = Date.now();
+    if (task.isWatermark) {
+      gm(request(task.url)).size({ bufferStream: true }, function(err, size) {
+        if (err) {
+          reject(err);
+        }
+        // 添加水印
+        this.fontSize(100)
+          .fill("#E33E33")
+          .font(fontPath)
+          .drawText(10, size.height / 2 - 100, `${task.score}分`, "Center");
+
+        this.write(task.outpath, function(err) {
+          console.log(Date.now() - t1);
+          if (err) {
+            reject(err);
+          }
+          resolve(task.outpath);
+        });
+      });
+    } else {
+      gm(request(task.url)).write(task.outpath, function(err) {
+        console.log(Date.now() - t1);
+        if (err) {
+          reject(err);
+        }
+        resolve(task.outpath);
+      });
+    }
+  });
+}
+
+function downloadServerFile(task) {
+  const fileDir = path.dirname(task.outpath);
+  if (!fs.existsSync(fileDir)) makeDirSync(fileDir);
+  const t1 = Date.now();
+
+  return new Promise((resolve, reject) => {
+    request
+      .get(task)
+      .pipe(fs.createWriteStream(task.outpath))
+      .on("error", e => {
+        console.log(e);
+        reject(`write file error: ${task.outpath}`);
+      })
+      .on("finish", () => {
+        console.log(Date.now() - t1);
+
+        console.log(`file loaded: ${task.outpath}`);
+        resolve(task.outpath);
+      });
+  });
+}
+
+export {
+  decodeImageCode,
+  saveOutputImage,
+  getEarliestFile,
+  rotateImage,
+  downloadFile,
+  downloadServerFile
+};

+ 1 - 1
src/plugins/imageUpload.js

@@ -127,7 +127,7 @@ class UploadTask {
 
       if (updateStdRes && saveLogRes) {
         // 更新paperId
-        await db.uploadLocalPaperId(curTask.id, updateStdRes.paperId);
+        await db.updateLocalPaperId(curTask.id, updateStdRes.paperId);
         this.uploadSuccessCallback(curTask);
       }
     }

+ 37 - 1
src/plugins/utils.js

@@ -197,6 +197,41 @@ function removeHtmlTag(str) {
   return str.replace(/<[^>]+>/g, "");
 }
 
+/**
+ *  获取时间长度文字
+ * @param {Number} timeNumber 时间数值,单位:毫秒
+ */
+function timeNumberToText(timeNumber) {
+  const DAY_TIME = 24 * 60 * 60 * 1000;
+  const HOUR_TIME = 60 * 60 * 1000;
+  const MINUTE_TIME = 60 * 1000;
+  const SECOND_TIME = 1000;
+  let [day, hour, minute, second] = [0, 0, 0, 0];
+  let residueTime = timeNumber;
+
+  if (residueTime >= DAY_TIME) {
+    day = Math.floor(residueTime / DAY_TIME);
+    residueTime -= day * DAY_TIME;
+    day += "天";
+  }
+  if (residueTime >= HOUR_TIME) {
+    hour = Math.floor(residueTime / HOUR_TIME);
+    residueTime -= hour * HOUR_TIME;
+    hour += "小时";
+  }
+  if (residueTime >= MINUTE_TIME) {
+    minute = Math.floor(residueTime / MINUTE_TIME);
+    residueTime -= minute * MINUTE_TIME;
+    minute += "分";
+  }
+  if (residueTime >= SECOND_TIME) {
+    second = Math.round(residueTime / SECOND_TIME);
+    second += "秒";
+  }
+
+  return [day, hour, minute, second].filter(item => !!item).join("");
+}
+
 export {
   objTypeOf,
   deepCopy,
@@ -207,5 +242,6 @@ export {
   qsParams,
   formatDate,
   removeHtmlTag,
-  getLocalDate
+  getLocalDate,
+  timeNumberToText
 };

+ 1 - 0
src/views/Home.vue

@@ -50,6 +50,7 @@ export default {
       isDev: process.env.NODE_ENV === "development",
       // 路由历史只前进,不后退
       backRouters: {
+        PaperExport: "Login",
         ActionType: "Login",
         PaperManage: "ActionType",
         Subject: "ActionType",

+ 6 - 1
src/views/Login.vue

@@ -126,9 +126,14 @@ export default {
         packageScan: data.paramSetting.packageScan,
         paperStage: data.paramSetting.paperStage
       });
+      const routerName =
+        data.roleCode === "ADMIN" ? "PaperExport" : "ActionType";
       this.$router.push({
-        name: "ActionType"
+        name: routerName
       });
+      // this.$router.push({
+      //   name: "PaperExport"
+      // });
     }
   }
 };

+ 1 - 0
vue.config.js

@@ -17,6 +17,7 @@ var config = {
     electronBuilder: {
       builderOptions: {
         extraFiles: [
+          "extra/font/**",
           "extra/imagemagick/**",
           "extra/zxing/**",
           "extra/database/org.rdb",