zhangjie 2 лет назад
Сommit
c8f63135df
96 измененных файлов с 5231 добавлено и 0 удалено
  1. 2 0
      .env
  2. 2 0
      .env.production
  3. 22 0
      .gitignore
  4. 126 0
      README.md
  5. 3 0
      babel.config.js
  6. 4 0
      config.sample.json
  7. BIN
      extra/database/org.rdb
  8. 59 0
      package.json
  9. BIN
      public/favicon.ico
  10. BIN
      public/img/icons/android-chrome-192x192.png
  11. BIN
      public/img/icons/android-chrome-512x512.png
  12. BIN
      public/img/icons/apple-touch-icon-120x120.png
  13. BIN
      public/img/icons/apple-touch-icon-152x152.png
  14. BIN
      public/img/icons/apple-touch-icon-180x180.png
  15. BIN
      public/img/icons/apple-touch-icon-60x60.png
  16. BIN
      public/img/icons/apple-touch-icon-76x76.png
  17. BIN
      public/img/icons/apple-touch-icon.png
  18. BIN
      public/img/icons/favicon-16x16.png
  19. BIN
      public/img/icons/favicon-32x32.png
  20. BIN
      public/img/icons/msapplication-icon-144x144.png
  21. BIN
      public/img/icons/mstile-150x150.png
  22. 149 0
      public/img/icons/safari-pinned-tab.svg
  23. 40 0
      public/index.html
  24. 29 0
      src/App.vue
  25. BIN
      src/assets/images/icon-arrow-right.png
  26. BIN
      src/assets/images/icon-close-act.png
  27. BIN
      src/assets/images/icon-close.png
  28. BIN
      src/assets/images/icon-error-big.png
  29. BIN
      src/assets/images/icon-error-hollow.png
  30. BIN
      src/assets/images/icon-error-tips.png
  31. BIN
      src/assets/images/icon-handle.png
  32. BIN
      src/assets/images/icon-level.png
  33. BIN
      src/assets/images/icon-loading.png
  34. BIN
      src/assets/images/icon-lock.png
  35. BIN
      src/assets/images/icon-max-act.png
  36. BIN
      src/assets/images/icon-max.png
  37. BIN
      src/assets/images/icon-more.png
  38. BIN
      src/assets/images/icon-rotate-left-act.png
  39. BIN
      src/assets/images/icon-rotate-left.png
  40. BIN
      src/assets/images/icon-rotate-right-act.png
  41. BIN
      src/assets/images/icon-rotate-right.png
  42. BIN
      src/assets/images/icon-scan.png
  43. BIN
      src/assets/images/icon-subject.png
  44. BIN
      src/assets/images/icon-user-act.png
  45. BIN
      src/assets/images/icon-user-gray.png
  46. BIN
      src/assets/images/icon-user.png
  47. BIN
      src/assets/images/login-back-left.png
  48. BIN
      src/assets/images/login-back-mid.png
  49. BIN
      src/assets/images/login-back-right.png
  50. BIN
      src/assets/images/subject-back-one.png
  51. BIN
      src/assets/images/subject-back-three.png
  52. BIN
      src/assets/images/subject-back-two.png
  53. 490 0
      src/assets/styles/base.scss
  54. 664 0
      src/assets/styles/element-ui-costom.scss
  55. 134 0
      src/assets/styles/home.scss
  56. 8 0
      src/assets/styles/index.scss
  57. 118 0
      src/assets/styles/login.scss
  58. 0 0
      src/assets/styles/pages.scss
  59. 43 0
      src/assets/styles/variables.scss
  60. 136 0
      src/background.js
  61. 110 0
      src/components/ImageFlexContain.vue
  62. 12 0
      src/components/MoveBar.vue
  63. 296 0
      src/components/SimpleImagePreview.vue
  64. 15 0
      src/components/ViewFooter.vue
  65. 42 0
      src/components/ViewHeader.vue
  66. 56 0
      src/components/move-ele.js
  67. 4 0
      src/config.js
  68. 15 0
      src/constants/enumerate.js
  69. 119 0
      src/main.js
  70. 20 0
      src/mixins/authUnvalidMixin.js
  71. 40 0
      src/mixins/initStoreMixin.js
  72. 17 0
      src/mixins/setTimeMixins.js
  73. 65 0
      src/mixins/uploadTaskMixin.js
  74. 35 0
      src/modules/client/api.js
  75. 10 0
      src/modules/client/router.js
  76. 38 0
      src/modules/client/store.js
  77. 16 0
      src/modules/client/views/TaskManage.vue
  78. 0 0
      src/modules/login/api.js
  79. 226 0
      src/modules/login/views/Login.vue
  80. 16 0
      src/modules/login/views/LoginHome.vue
  81. 207 0
      src/plugins/axios.js
  82. 816 0
      src/plugins/db.js
  83. 43 0
      src/plugins/db.sql
  84. 130 0
      src/plugins/env.js
  85. 84 0
      src/plugins/formRules.js
  86. 32 0
      src/plugins/globalVuePlugins.js
  87. 86 0
      src/plugins/imageOcr.js
  88. 155 0
      src/plugins/imageUpload.js
  89. 21 0
      src/plugins/logger.js
  90. 18 0
      src/plugins/mixins.js
  91. 163 0
      src/plugins/utils.js
  92. 54 0
      src/router.js
  93. 25 0
      src/store.js
  94. 70 0
      src/views/Home.vue
  95. 79 0
      src/views/Layout.vue
  96. 67 0
      vue.config.js

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+NODE_ENV=development
+VUE_APP_DOMAIN=

+ 2 - 0
.env.production

@@ -0,0 +1,2 @@
+NODE_ENV=production
+VUE_APP_DOMAIN=

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+.DS_Store
+node_modules
+/dist
+modules-old*
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw*

+ 126 - 0
README.md

@@ -0,0 +1,126 @@
+# paper-library-client 试卷电子化采集端系统
+
+## 项目操作
+
+#### 项目安装
+
+```
+yarn install
+```
+
+#### 开发模式
+
+```
+yarn start
+```
+
+#### 项目打包
+
+```
+yarn run electron:build
+```
+
+#### lint 项目文件,并修正格式
+
+```
+yarn run lint
+```
+
+### 自定义配置
+
+- See [Configuration Reference](https://cli.vuejs.org/config/).
+- See [Vue CLI Plugin Electron Builder](https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html).
+- See [electron-builder Common Configuration](https://www.electron.build/configuration/configuration).
+
+## 系统开发目录说明
+
+- 开发目录:`当前代码目录`
+- 开发运行环境目录:`${开发目录}/node_modules/electron/dist/`
+- 正式运行环境目录:
+  - window: `C:~\AppData\Local\Programs\{系统名称}\`
+
+## 关于使用第三方工具的处理办法
+
+**所有第三方工具统一存放在根目录`extra`文件夹中**
+
+- 设置文件`vue.config.js`
+
+> [配置 api](https://www.electron.build/configuration/contents#extrafiles)
+
+```js
+// config中新增如下配置
+pluginOptions: {
+  electronBuilder: {
+    builderOptions: {
+      extraFiles: ["extra/**"]
+    }
+  }
+},
+```
+
+- 设置`plugins/env.js`
+
+```js
+// 运行系统的根目录
+const homePath = path.dirname(process.execPath);
+// 开放环境中,使用开发代码的根目录;正式包中,使用运行系统的根目录。
+const extraPath =
+  process.env.NODE_ENV === "production"
+    ? path.join(homePath, "extra")
+    : path.join(__static, "../extra");
+```
+
+## 配置打包参数(可选)
+
+```json
+{
+  "pluginOptions": {
+    "electronBuilder": {
+      "builderOptions": {
+        "appId": "com.example.app",
+        "productName": "aDemo", // 项目名,也是生成的安装文件名,即aDemo.exe
+        "copyright": "Copyright © 2020", //版权信息
+        "directories": {
+          "output": "./dist" //输出文件路径
+        },
+        "win": {
+          // win相关配置
+          "icon": "./shanqis.ico", // 图标,当前图标在根目录下,注意这里有两个坑
+          "target": [
+            {
+              "target": "nsis", // 利用nsis制作安装程序
+              "arch": [
+                "x64" // 64位
+              ]
+            }
+          ]
+        },
+        "nsis": {
+          "oneClick": false, // 是否一键安装
+          "allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
+          "allowToChangeInstallationDirectory": true, // 允许修改安装目录
+          "installerIcon": "./shanqis.ico", // 安装图标
+          "uninstallerIcon": "./shanqis.ico", //卸载图标
+          "installerHeaderIcon": "./shanqis.ico", // 安装时头部图标
+          "createDesktopShortcut": true, // 创建桌面图标
+          "createStartMenuShortcut": true, // 创建开始菜单图标
+          "shortcutName": "demo" // 图标名称
+        }
+      }
+    }
+  }
+}
+```
+
+## config.json 配置说明
+
+- 根目录下会有一个`config.sample.json`文件,当需要设置配置参数时,可以复制一份,保存为`config.json`文件。
+- 系统内部预设了一组 config 参数,当根目录`config.json`文件中的参数有缺失时,默认使用预设 config 参数。
+- `input`默认为`/stores/in/`
+
+## postinstall
+
+- 32/64 切换,`"postinstall": "electron-builder install-app-deps --arch=ia32"`
+
+
+- ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ yarn install  

+ 3 - 0
babel.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  presets: ["@vue/cli-plugin-babel/preset"]
+};

+ 4 - 0
config.sample.json

@@ -0,0 +1,4 @@
+{
+  "domain": "http://localhost:9000",
+  "input": ""
+}

BIN
extra/database/org.rdb


+ 59 - 0
package.json

@@ -0,0 +1,59 @@
+{
+  "name": "paper-library-client",
+  "version": "1.0.0",
+  "description": "paper-library client",
+  "scripts": {
+    "start": "yarn run e:serve",
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint",
+    "win:build": "vue-cli-service electron:build",
+    "e:build": "vue-cli-service electron:build --win --x64 --ia32",
+    "e:serve": "vue-cli-service electron:serve",
+    "postinstall": "electron-builder install-app-deps",
+    "postuninstall": "electron-builder install-app-deps"
+  },
+  "main": "background.js",
+  "dependencies": {
+    "axios": "^0.19.2",
+    "core-js": "^3.6.4",
+    "crypto": "^1.0.1",
+    "deepmerge": "^4.2.2",
+    "element-ui": "^2.14.1",
+    "log4js": "^6.3.0",
+    "sqlite3": "^4.2.0",
+    "vue": "^2.6.11",
+    "vue-ls": "^3.2.2",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.3.0",
+    "@vue/cli-plugin-eslint": "~4.3.0",
+    "@vue/cli-plugin-router": "~4.3.0",
+    "@vue/cli-plugin-vuex": "~4.3.0",
+    "@vue/cli-service": "~4.3.0",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "babel-eslint": "^10.1.0",
+    "electron": "^9.3.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.1.1",
+    "eslint-plugin-vue": "^6.2.2",
+    "sass": "^1.26.5",
+    "sass-loader": "^8.0.2",
+    "lint-staged": "^9.5.0",
+    "prettier": "^1.19.1",
+    "terser-webpack-plugin": "^3.0.1",
+    "vue-cli-plugin-electron-builder": "~1.4.6",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.{js,jsx,vue}": [
+      "vue-cli-service lint",
+      "git add"
+    ]
+  }
+}

BIN
public/favicon.ico


BIN
public/img/icons/android-chrome-192x192.png


BIN
public/img/icons/android-chrome-512x512.png


BIN
public/img/icons/apple-touch-icon-120x120.png


BIN
public/img/icons/apple-touch-icon-152x152.png


BIN
public/img/icons/apple-touch-icon-180x180.png


BIN
public/img/icons/apple-touch-icon-60x60.png


BIN
public/img/icons/apple-touch-icon-76x76.png


BIN
public/img/icons/apple-touch-icon.png


BIN
public/img/icons/favicon-16x16.png


BIN
public/img/icons/favicon-32x32.png


BIN
public/img/icons/msapplication-icon-144x144.png


BIN
public/img/icons/mstile-150x150.png


+ 149 - 0
public/img/icons/safari-pinned-tab.svg

@@ -0,0 +1,149 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.11, written by Peter Selinger 2001-2013
+</metadata>
+<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
+fill="#000000" stroke="none">
+<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
+-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
+173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
+13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
+20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
+-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
+11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
+-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
+-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
+11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
+-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
+36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
+-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
+-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
+-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
+10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
+-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
+221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
+-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
+-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
+-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
+-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
+-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
+30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
+-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
+-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
+96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
+-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
+-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
+-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
+-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
+-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
+30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
+342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
+22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
+-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
+-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
+-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
+-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
+-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
+17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
+-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
+-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
+-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
+72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
+35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
+-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
+-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
+0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
+-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
+-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
+-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
+11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
+-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
+12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
+145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
+-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
+-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
+-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
+248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
+29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
+-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
+-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
+-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
+109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
+-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
+-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
+-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
+38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
+34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
+136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
+67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
+150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
+116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
+113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
+175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
+89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
+11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
+6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
+430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
+220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
+24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
+478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
+394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
+55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
+175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
+88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
+155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
+11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
+14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
+51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
+37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
+55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
+55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
+295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
+16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
+157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
+200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
+241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
+61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
+28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
+62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
+56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
+350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
+90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
+121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
+60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
+167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
+171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
+97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
+111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
+11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
+13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
+101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
+19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
+54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
+0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
+90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
+108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
+20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
+-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
+-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
+-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
+-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
+-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
+-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
+-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
+-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
+396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
+110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
+-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
+-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
+583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
+408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
+561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
+-9615 0 20 -32z"/>
+</g>
+</svg>

+ 40 - 0
public/index.html

@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
+    <title><%= htmlWebpackPlugin.options.title %></title>
+    <script>
+      (function() {
+        // 按Ctrl+Shift+U,放开U,在一秒内再按Ctrl+Shift+P,可以调出开发者工具
+        let firstStepKey = false;
+        document.addEventListener("keydown", function(e) {
+          if (e.ctrlKey && e.shiftKey && e.code === "KeyU") {
+            firstStepKey = true;
+            setTimeout(() => {
+              firstStepKey = false;
+            }, 1000);
+          }
+          if (firstStepKey && e.ctrlKey && e.shiftKey && e.code === "KeyP") {
+            require("electron")
+              .remote.getCurrentWindow()
+              .toggleDevTools();
+          }
+        });
+      })();
+    </script>
+  </head>
+  <body>
+    <noscript>
+      <strong
+        >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
+        properly without JavaScript enabled. Please enable it to
+        continue.</strong
+      >
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 29 - 0
src/App.vue

@@ -0,0 +1,29 @@
+<template>
+  <div id="app">
+    <move-bar></move-bar>
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: "app",
+  data() {
+    return {};
+  },
+  computed: {
+    hardwareCheckResult() {
+      return this.$store.state.hardwareCheckResult;
+    }
+  },
+  watch: {
+    hardwareCheckResult(val) {
+      if (val.success) {
+        console.log("成功!");
+      } else {
+        console.error("失败!");
+      }
+    }
+  }
+};
+</script>

BIN
src/assets/images/icon-arrow-right.png


BIN
src/assets/images/icon-close-act.png


BIN
src/assets/images/icon-close.png


BIN
src/assets/images/icon-error-big.png


BIN
src/assets/images/icon-error-hollow.png


BIN
src/assets/images/icon-error-tips.png


BIN
src/assets/images/icon-handle.png


BIN
src/assets/images/icon-level.png


BIN
src/assets/images/icon-loading.png


BIN
src/assets/images/icon-lock.png


BIN
src/assets/images/icon-max-act.png


BIN
src/assets/images/icon-max.png


BIN
src/assets/images/icon-more.png


BIN
src/assets/images/icon-rotate-left-act.png


BIN
src/assets/images/icon-rotate-left.png


BIN
src/assets/images/icon-rotate-right-act.png


BIN
src/assets/images/icon-rotate-right.png


BIN
src/assets/images/icon-scan.png


BIN
src/assets/images/icon-subject.png


BIN
src/assets/images/icon-user-act.png


BIN
src/assets/images/icon-user-gray.png


BIN
src/assets/images/icon-user.png


BIN
src/assets/images/login-back-left.png


BIN
src/assets/images/login-back-mid.png


BIN
src/assets/images/login-back-right.png


BIN
src/assets/images/subject-back-one.png


BIN
src/assets/images/subject-back-three.png


BIN
src/assets/images/subject-back-two.png


+ 490 - 0
src/assets/styles/base.scss

@@ -0,0 +1,490 @@
+/* reset */
+body,
+div,
+ul,
+ol,
+li,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+input,
+p,
+tr,
+th,
+td,
+span,
+a,
+header,
+footer,
+i {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+}
+li {
+  list-style: none;
+}
+em,
+i,
+u {
+  font-style: normal;
+}
+input {
+  outline: none;
+  border: none;
+  background: rgba(245, 245, 245, 1);
+  font-family: $--font-family;
+}
+input::-webkit-input-placeholder,
+input::-moz-placeholder,
+input:-ms-input-placeholder,
+input:-moz-placeholder {
+  font-size: 12px;
+  font-weight: bold;
+  color: $--color-text-gray-4;
+}
+button,
+textarea {
+  font-family: $--font-family;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: 100%;
+}
+fieldset,
+img {
+  border: 0;
+}
+abbr {
+  border: 0;
+  font-variant: normal;
+}
+a {
+  text-decoration: none;
+  color: inherit;
+  *color: $--color-text-gray-3;
+}
+img {
+  vertical-align: middle;
+}
+
+/* common-style */
+input:-webkit-autofill {
+  box-shadow: 0 0 0 1000px white inset;
+}
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="number"]:focus,
+textarea:focus {
+  box-shadow: 0 0 0 1000px white inset;
+}
+
+/* browse style */
+::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+  background: transparent;
+}
+::-webkit-scrollbar-button {
+  display: none;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+::-webkit-scrollbar-thumb {
+  border-radius: 8px;
+  background: #666;
+}
+::-webkit-scrollbar-corner {
+  background: transparent;
+}
+::-webkit-scrollbar-resizer {
+  background: transparent;
+}
+
+body {
+  font-family: $--font-family;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-size: $--font-size-base;
+  color: $--color-text-dark-1;
+}
+
+/* part */
+.part-box {
+  margin-bottom: 20px;
+  background-color: #fff;
+  border-radius: $--border-radius;
+
+  &-border {
+    border: 1px solid $--color-border;
+  }
+  &-border-bold {
+    border: 1px solid $--color-border-bold;
+  }
+  &-pad {
+    padding: 20px;
+  }
+
+  &-filter {
+    padding: 20px 20px 5px 20px;
+
+    .el-form-item {
+      margin-bottom: 15px;
+    }
+    .el-form-item__label {
+      display: none;
+    }
+  }
+  &-gray {
+    background-color: $--color-text-gray-7;
+  }
+
+  &-flex {
+    display: flex;
+    align-items: stretch;
+    justify-content: space-between;
+  }
+
+  &-action {
+    padding-bottom: 15px;
+    white-space: nowrap;
+    display: flex;
+    align-items: flex-end;
+  }
+  &-tips {
+    font-size: 16px;
+    line-height: 25px;
+    color: $--color-text-dark-1;
+    margin-bottom: 15px;
+  }
+
+  &-head {
+    display: flex;
+    align-items: stretch;
+    justify-content: space-between;
+    min-height: 30px;
+    margin: -10px 0 10px -10px;
+    color: $--color-text-dark;
+
+    > h3 {
+      font-size: 17px;
+    }
+    .el-icon-question {
+      margin-left: 10px;
+      font-size: 16px;
+      color: $--color-text-gray-5;
+      cursor: pointer;
+
+      &:hover {
+        color: #fe8652;
+      }
+    }
+  }
+}
+.part-title {
+  font-size: 16px;
+  font-weight: bold;
+  padding: 15px 20px;
+  line-height: 30px;
+  overflow: hidden;
+
+  h2 {
+    float: left;
+  }
+  &-infos {
+    float: right;
+  }
+}
+.part-body {
+  padding: 25px;
+}
+.part-page {
+  margin-top: 15px;
+  text-align: right;
+}
+.part-none {
+  padding: 100px;
+  font-size: 20px;
+  color: $--color-text-gray-3;
+  text-align: center;
+}
+// box-justify
+.box-justify {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+// page-head
+.page-head {
+  margin-bottom: 20px;
+  color: $--color-text-dark;
+  &-flex {
+    display: flex;
+    align-items: stretch;
+    justify-content: space-between;
+  }
+
+  > h2 {
+    font-size: 20px;
+  }
+  .el-icon-question {
+    margin-left: 10px;
+    font-size: 16px;
+    color: $--color-text-gray-5;
+    cursor: pointer;
+
+    &:hover {
+      color: #fe8652;
+    }
+  }
+}
+
+/* table */
+.table {
+  width: 100%;
+  border-spacing: 0;
+  border-collapse: collapse;
+  text-align: left;
+
+  &.table-white {
+    background-color: #fff;
+  }
+
+  th {
+    padding: 12px;
+    line-height: 1.2;
+    letter-spacing: 1px;
+    color: $--color-text-gray-2;
+    border: 1px solid $--color-border;
+  }
+  td {
+    padding: 14px;
+    line-height: 1.2;
+    color: $--color-text-dark;
+    border: 1px solid $--color-border;
+
+    &.td-link {
+      span {
+        cursor: pointer;
+        &:hover {
+          color: $--color-text-gray;
+        }
+      }
+    }
+  }
+  .td-th {
+    font-weight: 600;
+    color: $--color-text-gray;
+  }
+
+  &--border {
+    border: 1px solid $--color-border;
+    border-radius: 10px;
+    th {
+      background-color: #fcfcfd;
+      border: none;
+      border-bottom: 1px solid $--color-border;
+    }
+    td {
+      border: none;
+      border-bottom: 1px solid $--color-border;
+    }
+  }
+}
+
+// color
+.color-primary {
+  color: $--color-primary !important;
+}
+.color-success {
+  color: $--color-success;
+}
+.color-warning {
+  color: $--color-warning;
+}
+.color-danger {
+  color: $--color-danger;
+}
+.color-info {
+  color: $--color-text-gray-1;
+}
+.color-dark {
+  color: $--color-dark;
+}
+.color-gray {
+  color: $--color-text-gray;
+}
+.color-gray-2 {
+  color: $--color-text-gray-2;
+}
+.color-white {
+  color: #fff;
+}
+
+// text
+.text-center {
+  text-align: center;
+}
+.text-left {
+  text-align: left;
+}
+.text-right {
+  text-align: right;
+}
+.text-prewrap {
+  white-space: pre-wrap;
+}
+
+// other
+.btn-danger {
+  &.el-button--text:not(.is-disabled) {
+    color: $--color-danger !important;
+
+    &:hover {
+      font-weight: 600;
+      color: mix(#000, $--color-danger, 20%) !important;
+    }
+  }
+  &.is-disabled {
+    color: $--color-text-gray-4;
+  }
+}
+.btn-primary {
+  &.el-button--text:not(.is-disabled) {
+    color: $--color-text-dark-1 !important;
+    &:hover {
+      font-weight: 600;
+      color: $--color-primary !important;
+    }
+  }
+}
+
+.btn-white {
+  background-color: #fff !important;
+  color: #999 !important;
+}
+.font-bold {
+  font-weight: bold;
+}
+.table-head-bg {
+  th {
+    background-color: #f6f6f6;
+    color: $--color-text-gray;
+  }
+}
+
+.tab-btns {
+  .el-button {
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+
+    &:first-child {
+      border-bottom-left-radius: 8px;
+    }
+
+    &:last-child {
+      border-bottom-right-radius: 8px;
+    }
+  }
+
+  .el-button + .el-button {
+    margin-left: 10px;
+  }
+}
+
+.cont-link {
+  color: $--color-primary;
+  cursor: pointer;
+  &:hover {
+    opacity: 0.8;
+  }
+}
+.ml-1 {
+  margin-left: 5px;
+}
+.ml-2 {
+  margin-left: 10px;
+}
+.mr-1 {
+  margin-right: 5px;
+}
+.mr-2 {
+  margin-right: 10px;
+}
+.mr-4 {
+  margin-right: 20px;
+}
+.mb-0 {
+  margin-bottom: 0;
+}
+.mb-1 {
+  margin-bottom: 5px;
+}
+.mb-2 {
+  margin-bottom: 10px;
+}
+.mb-4 {
+  margin-bottom: 20px;
+}
+.mlr-1 {
+  margin-left: 5px;
+  margin-right: 5px;
+}
+.width-full {
+  width: 100%;
+}
+.width-400 {
+  width: 400px;
+}
+.width-80 {
+  width: 80px;
+}
+.width-200 {
+  width: 200px;
+}
+
+// other
+.tips-info {
+  font-size: 14px;
+  line-height: 20px;
+  color: $--color-text-gray-2;
+}
+.tips-dark {
+  color: $--color-text-gray;
+}
+.tips-error {
+  color: $--color-danger;
+}
+.tips-icon {
+  display: inline-block;
+  vertical-align: middle;
+  color: $--color-text-gray-3;
+  font-size: 18px;
+  margin: 0 10px;
+  cursor: pointer;
+}
+.form-item-content {
+  color: $--color-text-gray-2;
+}
+.inline-block {
+  display: inline-block;
+  vertical-align: top;
+}
+.custom-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 12px;
+  padding-right: 8px;
+}

+ 664 - 0
src/assets/styles/element-ui-costom.scss

@@ -0,0 +1,664 @@
+/*
+* element-ui不管是自行构建的主题还是动态设置的主题,
+* 产生的css文件中存在近乎1/3的冗余样式,过于累赘,不如直接覆盖样式简洁。
+*/
+// dialog
+.el-dialog {
+  border-radius: 8px;
+  overflow: hidden;
+  border: 1px solid #c8c8ca;
+  box-shadow: 5px 5px 4px 0px rgba(0, 0, 0, 0.1);
+
+  &.is-fullscreen {
+    border-radius: 0;
+
+    .el-dialog__header {
+      width: 100%;
+      position: fixed;
+      z-index: 9;
+      background-color: #fff;
+      border-bottom: 1px solid $--color-border;
+    }
+    .el-dialog__body {
+      padding-top: 90px;
+    }
+  }
+}
+.el-dialog__header {
+  padding: 15px 20px;
+  .el-dialog__title {
+    color: $--color-text-dark;
+    font-size: 16px;
+    line-height: 19px;
+  }
+  .el-dialog__headerbtn {
+    top: 15px;
+    width: 16px;
+    height: 16px;
+    background-image: url(../images/icon-close.png);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+
+    &:hover {
+      background-image: url(../images/icon-close-act.png);
+    }
+
+    .el-dialog__close {
+      display: none;
+    }
+  }
+}
+.el-dialog__body {
+  padding: 30px 40px;
+  position: relative;
+  border-top: 1px solid $--color-border;
+  color: $--color-text-dark-1;
+
+  .el-form-item__label {
+    padding-right: 2px;
+  }
+  .el-input-tips {
+    color: rgba(187, 187, 187, 1);
+    margin-left: 13px;
+  }
+}
+.el-dialog__footer {
+  overflow: hidden;
+  .el-button {
+    width: 100px;
+    border-radius: 8px;
+    float: right;
+    margin-left: 10px;
+  }
+}
+//
+.page-dialog {
+  .el-dialog.is-fullscreen {
+    background: $--color-background;
+    .el-dialog__body {
+      padding: 70px 20px 20px;
+    }
+  }
+  .el-dialog {
+    background: $--color-background;
+    .el-dialog__body {
+      padding: 20px;
+    }
+    .el-dialog__header {
+      background-color: #fff;
+    }
+  }
+}
+
+// .opacity-dialog
+.opacity-dialog {
+  .el-dialog {
+    background-color: transparent;
+  }
+  .el-dialog__header,
+  .el-dialog__footer {
+    display: none;
+  }
+  .el-dialog__body {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    padding: 0;
+    background-color: transparent;
+  }
+}
+
+// form
+.el-form {
+  &-item {
+    &__error {
+      font-size: 12px;
+      color: rgba(254, 108, 105, 1);
+    }
+    &__content {
+      .el-table {
+        line-height: 1;
+      }
+    }
+  }
+  // form-info
+  &.form-info {
+    .el-form-item {
+      margin-bottom: 0;
+
+      .el-form-item__label {
+        color: $--color-text-gray-2;
+      }
+    }
+  }
+  &--label-top {
+    .el-form-item__label {
+      line-height: 20px;
+      padding-bottom: 5px;
+      font-size: 12px;
+    }
+  }
+}
+// input
+.el-input {
+  &.is-focus {
+    .el-input__inner {
+      border-color: $--color-primary !important;
+    }
+  }
+  &.is-disabled {
+    .el-input__inner {
+      color: $--color-text-gray-2;
+    }
+  }
+  .el-input__inner {
+    border-radius: 8px;
+    border-color: #ddd;
+    background-color: #fff;
+  }
+  // .el-input__suffix {
+  //   right: 0;
+  //   border-left: 1px solid #ddd;
+  // }
+}
+// textarea
+.el-textarea {
+  &.is-disabled {
+    .el-textarea__inner {
+      color: $--color-text-gray-2;
+    }
+  }
+}
+.el-select {
+  .el-input__suffix {
+    right: 0;
+    border-left: 1px solid #ddd;
+  }
+  .el-input {
+    .el-select__caret {
+      width: 30px;
+    }
+    .el-icon-arrow-up:before {
+      font-size: 12px;
+      content: "\e78f";
+    }
+  }
+}
+.el-select-dropdown {
+  &.popper-filter {
+    .el-scrollbar {
+      display: block !important;
+      padding-top: 52px;
+    }
+    .el-select-filter {
+      padding: 0 10px;
+      position: absolute;
+      width: 100%;
+      top: 10px;
+      left: 0;
+      z-index: 9;
+    }
+  }
+}
+// upload
+.el-upload,
+.el-upload-dragger {
+  width: 100%;
+}
+.el-upload-dragger {
+  &:hover {
+    border-color: $--color-primary;
+  }
+  .el-icon-upload {
+    color: $--color-primary;
+  }
+  .el-upload__text em {
+    color: $--color-primary;
+  }
+}
+// radio
+.el-radio-button {
+  &:not(.is-disabled):hover {
+    .el-radio-button__inner {
+      color: $--color-primary;
+    }
+  }
+
+  &.is-active {
+    .el-radio-button__inner {
+      color: $--color-white !important;
+      border-color: $--color-primary !important;
+      background: $--color-primary !important;
+    }
+  }
+}
+// button
+.el-button {
+  border-radius: $--border-radius;
+
+  > .icon {
+    margin-right: 5px;
+  }
+  > span {
+    display: inline-block;
+  }
+  &.is-disabled:not(.el-button--text) {
+    color: $--color-text-gray-3 !important;
+    background: $--color-background !important;
+    border: 1px solid $--color-border !important;
+  }
+}
+
+.el-button + .popover-button,
+.popover-button + .el-button {
+  margin-left: 10px;
+}
+.el-button + .el-button {
+  margin-left: 10px;
+}
+.el-button--text + .el-button--text {
+  margin-left: 5px;
+}
+.el-button--small {
+  padding-top: 8px;
+  padding-bottom: 8px;
+  font-size: 12px;
+}
+.el-button--text {
+  color: $--color-text-gray-2;
+
+  & + .el-button--text {
+    margin-left: 10px;
+  }
+}
+.el-button--info {
+  background-color: $--color-cyan;
+  border-color: $--color-cyan;
+
+  &:hover,
+  &:focus {
+    background-color: $--color-cyan-light;
+    border-color: $--color-cyan-light;
+  }
+}
+.el-button--primary {
+  background-color: $--color-primary;
+  border-color: $--color-primary;
+
+  &:hover,
+  &:focus {
+    background-color: $--color-primary-light;
+    border-color: $--color-primary-light;
+  }
+}
+.el-button--success {
+  background-color: $--color-success;
+  border-color: $--color-success;
+
+  &:hover,
+  &:focus {
+    background-color: $--color-success-light;
+    border-color: $--color-success-light;
+  }
+}
+
+// table
+.el-table {
+  color: $--color-text-dark-1;
+
+  thead th {
+    color: $--color-text-gray-2;
+  }
+
+  thead.is-group th {
+    background-color: $--color-white;
+  }
+
+  tr.el-table__row {
+    color: $--color-text-dark;
+  }
+  td,
+  th {
+    border-color: $--color-border !important;
+    padding: 14px 0;
+    font-weight: 500;
+  }
+  .el-table__row.row-danger {
+    color: $--color-danger;
+  }
+  &.el-table--noback {
+    tr.el-table__row {
+      background-color: $--color-white;
+    }
+  }
+
+  .cell-head {
+    display: inline-block;
+    vertical-align: middle;
+    line-height: 1.3;
+  }
+  // caret-wrapper
+  .caret-wrapper {
+    width: 20px;
+    height: 20px;
+    top: -1px;
+    .sort-caret {
+      &.ascending {
+        top: -1px;
+      }
+      &.descending {
+        bottom: -1px;
+      }
+    }
+  }
+  // action-column
+  td.action-column {
+    padding-left: 10px;
+    padding-right: 10px;
+    .cell {
+      padding: 0;
+      margin: 0 -5px;
+    }
+    .el-button--text {
+      padding: 0;
+      margin: 0 5px;
+      border: none !important;
+      outline: none !important;
+      &:not(.is-disabled):hover {
+        transform: scale(1.1);
+      }
+    }
+  }
+}
+.el-table--border {
+  border-radius: 10px;
+  th {
+    padding: 12px 0;
+    background-color: #fcfcfd;
+    border-right: none;
+  }
+  td {
+    border-right: none;
+  }
+}
+// el-checkbox
+.el-checkbox {
+  .el-checkbox__label {
+    color: $--color-text-gray-2 !important;
+  }
+  .el-checkbox__inner::after {
+    border-width: 2px;
+  }
+}
+.el-checkbox__input.is-checked .el-checkbox__inner {
+  background-color: $--color-white;
+  border-color: $--color-primary;
+
+  &::after {
+    border-color: $--color-primary;
+  }
+}
+.el-checkbox__input.is-indeterminate .el-checkbox__inner {
+  background-color: $--color-white;
+  border-color: $--color-primary;
+  &::before {
+    background-color: $--color-primary;
+  }
+}
+
+.el-radio {
+  .el-radio__label {
+    color: $--color-text-gray-2 !important;
+  }
+}
+.el-radio__input.is-checked .el-radio__inner {
+  background-color: $--color-white;
+  border-color: $--color-primary;
+  &::after {
+    width: 6px;
+    height: 6px;
+    background-color: $--color-primary;
+  }
+}
+
+// el-switch
+.el-switch {
+  &.is-checked {
+    .el-switch__core {
+      background-color: $--color-primary;
+      border-color: $--color-primary;
+    }
+  }
+}
+
+// el-pagination
+.el-pagination-li {
+  min-width: 32px;
+  height: 32px;
+  border-radius: 8px;
+  overflow: hidden;
+  background-color: $--color-white;
+  border: 1px solid #e1e3eb;
+}
+.el-pagination {
+  padding: 0;
+  .el-pagination__total {
+    float: left;
+  }
+  span:not([class*="suffix"]) {
+    line-height: 32px;
+    height: 32px;
+  }
+  &.is-background {
+    .btn-prev,
+    .btn-next {
+      color: $--color-text-gray-2;
+      margin: 0 5px;
+      @extend .el-pagination-li;
+    }
+    .btn-prev:disabled,
+    .btn-next:disabled {
+      opacity: 0.7;
+    }
+
+    .el-pager li {
+      color: $--color-text-gray-2;
+      margin: 0 5px;
+      padding: 0 8px;
+      line-height: 32px;
+
+      @extend .el-pagination-li;
+      &:not(.disabled).active {
+        color: #fff;
+        background-color: $--color-primary;
+      }
+    }
+  }
+}
+// el-message-box
+.el-message-box {
+  width: 320px;
+  background-color: #f6f6f6;
+  border-radius: 10px;
+  &__title {
+    display: none;
+  }
+  &__headerbtn {
+    display: none;
+  }
+  &__content {
+    text-align: center;
+
+    .el-message-box__status {
+      position: relative;
+      top: 0;
+      height: 48px;
+      width: 48px;
+      transform: none;
+      margin-bottom: 10px;
+
+      &.el-icon-warning {
+        border-radius: 50%;
+        &::before {
+          content: "";
+          position: absolute;
+          top: 0;
+          left: 0;
+          width: 100%;
+          height: 100%;
+          background-image: url(../images/icon-doubt.png);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+    }
+    .el-message-box__message {
+      padding: 0;
+    }
+  }
+  &__btns {
+    height: 75px;
+    padding: 30px 20px 10px;
+    text-align: center;
+
+    > .el-button {
+      width: 100px;
+    }
+  }
+}
+
+.alert-message {
+  .el-message-box__btns {
+    text-align: center;
+    > .el-button {
+      position: relative;
+      left: auto;
+      top: 0;
+      margin: 0;
+    }
+  }
+}
+// .el-message
+.el-message {
+  .el-message__content {
+    word-wrap: break-word;
+    max-width: 600px;
+    line-height: 1.4;
+  }
+}
+.el-message-loading {
+  border-color: mix($--color-white, $--color-success, 80%);
+  background-color: mix($--color-white, $--color-success, 90%);
+}
+// el-date-editor
+.el-date-editor {
+  border-radius: 8px;
+  .el-range-separator {
+    width: auto;
+  }
+  .el-range-input {
+    background-color: transparent;
+  }
+}
+
+// el-step
+.el-step {
+  &__title.is-success,
+  &__description.is-success,
+  &__title.is-process,
+  &__description.is-process {
+    color: $--color-success;
+  }
+  &__title.is-process {
+    font-weight: normal;
+  }
+  &__head.is-success {
+    .el-step__line {
+      background-color: $--color-success;
+    }
+    .el-step__icon.is-text {
+      color: $--color-white;
+      border-color: $--color-success;
+      background-color: $--color-success;
+    }
+  }
+  &__head.is-process {
+    .el-step__icon.is-text {
+      color: $--color-success;
+      border-color: $--color-success;
+    }
+  }
+
+  &__title.is-wait,
+  &__description.is-wait {
+    color: $--color-text-gray-2;
+  }
+  &__head.is-wait {
+    .el-step__icon.is-text {
+      color: $--color-text-gray-2;
+      border-color: #e1e3eb;
+      background-color: #e1e3eb;
+    }
+  }
+}
+// el-popover
+.el-popper-dark {
+  background-color: $--color-text-dark-1;
+  color: #fff;
+  font-size: 12px;
+  line-height: 18px;
+  padding: 16px;
+  border: none;
+}
+.el-popper-dark {
+  box-shadow: 0px 10px 10px 0px rgba(54, 61, 89, 0.2);
+}
+.el-popper-dark[x-placement^="right"] .popper__arrow {
+  border-right-color: $--color-text-dark-1;
+
+  &::after {
+    border-right-color: $--color-text-dark-1;
+  }
+}
+.el-popper-dark[x-placement^="top"] .popper__arrow {
+  border-top-color: $--color-text-dark-1;
+
+  &::after {
+    border-top-color: $--color-text-dark-1;
+  }
+}
+.el-popper-dark[x-placement^="bottom"] .popper__arrow {
+  border-bottom-color: $--color-text-dark-1;
+
+  &::after {
+    border-bottom-color: $--color-text-dark-1;
+  }
+}
+.el-popper-dark[x-placement^="left"] .popper__arrow {
+  border-left-color: $--color-text-dark-1;
+
+  &::after {
+    border-left-color: $--color-text-dark-1;
+  }
+}
+// popper-list
+.popper-list {
+  min-width: auto;
+
+  .el-button {
+    display: block;
+    width: 100%;
+    margin: 0;
+    &:not(:last-child) {
+      margin-bottom: 5px;
+    }
+  }
+}
+// .el-tag
+.el-tag {
+  &.tag-spin {
+    margin: 3px;
+  }
+}

+ 134 - 0
src/assets/styles/home.scss

@@ -0,0 +1,134 @@
+/* home */
+.home {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  z-index: auto;
+}
+
+.home-body {
+  position: absolute;
+  left: 0;
+  top: 50px;
+  right: 0;
+  bottom: 0;
+  overflow: auto;
+  background: $--color-background;
+  z-index: 98;
+}
+
+/* head */
+.home-header {
+  position: absolute;
+  width: 100%;
+  height: 50px;
+  top: 0;
+  left: 0;
+  z-index: 99;
+  color: #fff;
+  padding-left: 220px;
+  background-color: $--color-text-dark;
+
+  display: flex;
+  align-items: stretch;
+  justify-content: space-between;
+  -webkit-app-region: drag;
+
+  .menu-list {
+    li {
+      position: relative;
+      display: inline-block;
+      vertical-align: top;
+      padding: 10px;
+      height: 50px;
+      line-height: 30px;
+      opacity: 0.4;
+      font-size: 16px;
+      position: relative;
+      text-align: center;
+      cursor: pointer;
+
+      &:hover {
+        opacity: 1;
+      }
+
+      &.menu-item-act {
+        opacity: 1;
+
+        &::after {
+          content: "";
+          display: block;
+          position: absolute;
+          left: 5px;
+          right: 5px;
+          bottom: 0;
+          border-top: 5px solid #fff;
+          border-radius: 5px;
+          z-index: 8;
+        }
+      }
+
+      span {
+        display: inline-block;
+        vertical-align: top;
+      }
+
+      .icon {
+        margin-top: -3px;
+        margin-right: 6px;
+      }
+
+      > i {
+        margin-right: 6px;
+      }
+    }
+  }
+
+  .head-menu {
+    flex-grow: 1;
+    overflow-y: hidden;
+    overflow-x: auto;
+
+    ul {
+      white-space: nowrap;
+    }
+  }
+
+  .head-user {
+    flex-grow: 0;
+    flex-shrink: 0;
+
+    .menu-item-account {
+      white-space: nowrap;
+      padding: 10px;
+
+      span {
+        max-width: 156px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+
+  // .head-menu-btn
+  .head-menu-btn {
+    display: none;
+    float: right;
+    line-height: 36px;
+    padding: 12px 15px;
+    text-align: center;
+
+    > span {
+      display: block;
+      height: 36px;
+      width: 36px;
+      border-radius: 5px;
+      background-color: rgba($color: #fff, $alpha: 0.3);
+    }
+
+    i {
+      font-size: 22px;
+      vertical-align: middle;
+    }
+  }
+}

+ 8 - 0
src/assets/styles/index.scss

@@ -0,0 +1,8 @@
+@import "./variables.scss";
+@import "./base.scss";
+@import "./home.scss";
+
+@import "./login.scss";
+@import "./pages.scss";
+
+@import "./element-ui-costom.scss";

+ 118 - 0
src/assets/styles/login.scss

@@ -0,0 +1,118 @@
+/* login */
+.login-home {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  z-index: 8;
+  background-image: url(../images/login-back.png);
+  background-repeat: no-repeat;
+  background-size: cover;
+  overflow: auto;
+}
+.login-footer {
+  position: absolute;
+  width: 100%;
+  bottom: 0;
+  padding: 10px;
+  color: $--color-text-gray;
+  text-align: center;
+
+  a {
+    margin: 0 5px;
+
+    &:hover {
+      color: $--color-primary;
+    }
+  }
+}
+
+.login-box {
+  position: absolute;
+  width: 860px;
+  height: 514px;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  border-radius: 20px;
+  background-color: #fff;
+  overflow: hidden;
+}
+.login-theme {
+  width: 420px;
+  height: 100%;
+  border-radius: 20px;
+  background-color: #3858e0;
+  background-image: url(../images/login-theme.png);
+  background-size: 100% 100%;
+  float: left;
+  position: relative;
+
+  > h2 {
+    position: absolute;
+    width: 100%;
+    top: 60px;
+    left: 0;
+    text-align: center;
+    font-size: 25px;
+    color: #fff;
+  }
+}
+.login-body {
+  margin-left: 420px;
+  height: 100%;
+  overflow: hidden;
+  padding: 80px 75px;
+}
+.login-title {
+  text-align: center;
+  margin-bottom: 40px;
+  h1 {
+    font-size: 21px;
+    font-weight: bold;
+  }
+
+  img {
+    display: block;
+    max-width: 160px;
+    height: 40px;
+    margin: 0 auto;
+  }
+}
+.login-form {
+  .login-submit-btn {
+    width: 100%;
+    height: 48px;
+    border-radius: 24px;
+    font-size: 18px;
+  }
+  .vlcode-right {
+    width: 100px;
+  }
+  .vlcode-left {
+    margin-right: 105px;
+  }
+  .el-form-item__content {
+    border-bottom: 1px solid #e1e3eb;
+    padding-bottom: 2px;
+  }
+  .el-form-item:last-child {
+    margin-bottom: 0;
+    .el-form-item__content {
+      border: none;
+    }
+  }
+  .el-form-item:nth-last-of-type(2) {
+    .el-form-item__content {
+      border: none;
+    }
+  }
+  .el-input__inner {
+    border: none;
+    border-radius: 0 !important;
+  }
+  .el-input__prefix {
+    left: 9px;
+  }
+}

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


+ 43 - 0
src/assets/styles/variables.scss

@@ -0,0 +1,43 @@
+// color ------------------->
+$--color-text-dark: #1f2230 !default;
+$--color-text-dark-1: #434656 !default;
+$--color-text-gray: #6f7482 !default;
+$--color-text-gray-1: #7a7c87 !default;
+$--color-text-gray-2: #8b8fa1 !default;
+$--color-text-gray-3: #aaa !default;
+$--color-text-gray-4: #ccc !default;
+$--color-text-gray-5: #d3d5e0 !default;
+$--color-text-gray-6: #e0e1eb !default;
+$--color-text-gray-7: #f2f4fa !default;
+$--color-border: #eff0f5;
+$--color-border-bold: #e6e6e6;
+$--color-background: #eff0f5;
+// status
+$--color-primary: #3a5ae5 !default;
+$--color-primary-light: mix(#fff, $--color-primary, 20%) !default;
+$--color-success: #3fcb98 !default;
+$--color-success-light: #32cf8a !default;
+$--color-warning: #ff9427 !default;
+$--color-danger: #fe5d4e !default;
+$--color-cyan: #2abcff !default;
+$--color-cyan-light: #5fc9fa !default;
+$--color-blue: #556dff !default;
+$--color-blue-white: #4f79ff !default;
+$--color-blue-dark: #172666 !default;
+$--color-purple: #9877ff !default;
+$--color-white: #ffffff;
+$--color-dark: #1f2230;
+
+// shadow
+$--shadow-light: 0 0 1px rgba(0, 0, 0, 0.15) !default;
+
+// size ------------------->
+$--font-size-base: 14px !default;
+$--font-size-medium: 16px !default;
+$--font-size-large: 18px !default;
+$--border-radius: 8px;
+$--border-radius-large: 12px;
+$--border-radius-huge: 20px;
+// font-family
+$--font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
+  "Microsoft YaHei", Arial, sans-serif;

+ 136 - 0
src/background.js

@@ -0,0 +1,136 @@
+"use strict";
+
+import { app, protocol, BrowserWindow, Menu, ipcMain } from "electron";
+import {
+  createProtocol
+  /* installVueDevtools */
+} from "vue-cli-plugin-electron-builder/lib";
+import { closeEncryptCheck } from "./plugins/encryptProcess";
+import { stopConnect } from "./plugins/hardwareCheck";
+const isDevelopment = process.env.NODE_ENV !== "production";
+
+// Keep a global reference of the window object, if you don't, the window will
+// be closed automatically when the JavaScript object is garbage collected.
+let win;
+
+// Scheme must be registered before the app is ready
+protocol.registerSchemesAsPrivileged([
+  { scheme: "app", privileges: { secure: true, standard: true } }
+]);
+
+function createWindow() {
+  // Create the browser window.
+  win = new BrowserWindow({
+    width: isDevelopment ? 1428 : 1024,
+    height: 700,
+    minWidth: 1024,
+    minHeight: 600,
+    frame: isDevelopment,
+    useContentSize: true,
+    webPreferences: {
+      nodeIntegration: true,
+      webSecurity: false
+    }
+  });
+
+  if (process.env.WEBPACK_DEV_SERVER_URL) {
+    // Load the url of the dev server if in development mode
+    win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
+    if (!process.env.IS_TEST) win.webContents.openDevTools();
+  } else {
+    createProtocol("app");
+    // Load the index.html when not in development
+    win.loadURL("app://./index.html");
+  }
+
+  win.on("closed", () => {
+    win = null;
+  });
+}
+
+ipcMain.on("close-window", async () => {
+  await closeEncryptCheck();
+  stopConnect();
+  app.quit();
+});
+
+ipcMain.on("minimize-window", () => {
+  win.minimize();
+});
+ipcMain.on("maximize-window", () => {
+  if (win.isMaximized()) {
+    win.unmaximize();
+  } else {
+    win.maximize();
+  }
+});
+
+// Quit when all windows are closed.
+app.on("window-all-closed", () => {
+  // On macOS it is common for applications and their menu bar
+  // to stay active until the user quits explicitly with Cmd + Q
+  if (process.platform !== "darwin") {
+    app.quit();
+  }
+});
+
+app.on("activate", () => {
+  // On macOS it's common to re-create a window in the app when the
+  // dock icon is clicked and there are no other windows open.
+  if (win === null) {
+    createWindow();
+  }
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+// Some APIs can only be used after this event occurs.
+app.on("ready", async () => {
+  if (isDevelopment && !process.env.IS_TEST) {
+    // Install Vue Devtools
+    // Devtools extensions are broken in Electron 6.0.0 and greater
+    // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info
+    // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode
+    // If you are not using Windows 10 dark mode, you may uncomment these lines
+    // In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines
+    try {
+      // 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"
+      //   )
+      // );
+    } catch (e) {
+      console.error("Vue Devtools failed to install:", e.toString());
+    }
+  }
+
+  if (!isDevelopment) {
+    Menu.setApplicationMenu(null);
+  }
+  createWindow();
+});
+
+// Exit cleanly on request from parent process in development mode.
+if (isDevelopment) {
+  if (process.platform === "win32") {
+    process.on("message", data => {
+      if (data === "graceful-exit") {
+        app.quit();
+      }
+    });
+  } else {
+    process.on("SIGTERM", () => {
+      app.quit();
+    });
+  }
+}

+ 110 - 0
src/components/ImageFlexContain.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="image-flex-contain">
+    <div class="image-flex-image" :style="styles">
+      <img
+        :src="image.url"
+        :alt="image.title"
+        @load="resizeImage(initRotate)"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "image-flex-contain",
+  props: {
+    image: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    initRotate: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+      styles: { width: "", height: "", top: "", left: "", transform: "" },
+      deg: this.initRotate
+    };
+  },
+  mounted() {
+    this.registWinResize();
+  },
+  beforeDestroy() {
+    window.removeEventListener("resize", () => {
+      this.resizeImage(this.deg);
+    });
+  },
+  methods: {
+    registWinResize() {
+      window.addEventListener("resize", () => {
+        this.resizeImage(this.deg);
+      });
+    },
+    resizeImage(deg = 0) {
+      this.deg = deg;
+      const box = this.$el;
+      const imgDom = box.firstChild.firstChild;
+      const { naturalWidth, naturalHeight } = imgDom;
+      const imageSize = this.getImageSizePos({
+        win: {
+          width: box.clientWidth,
+          height: box.clientHeight
+        },
+        img: {
+          width: naturalWidth,
+          height: naturalHeight
+        },
+        rotate: deg
+      });
+      this.styles = Object.assign(this.styles, {
+        width: imageSize.width + "px",
+        height: imageSize.height + "px",
+        top: imageSize.top + "px",
+        left: imageSize.left + "px",
+        transform: `rotate(${deg}deg)`
+      });
+    },
+    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;
+    }
+  }
+};
+</script>

+ 12 - 0
src/components/MoveBar.vue

@@ -0,0 +1,12 @@
+<template>
+  <div class="move-bar"></div>
+</template>
+
+<script>
+export default {
+  name: "move-bar",
+  data() {
+    return {};
+  }
+};
+</script>

+ 296 - 0
src/components/SimpleImagePreview.vue

@@ -0,0 +1,296 @@
+<template>
+  <Modal
+    :class="prefixCls"
+    v-model="modalIsShow"
+    title="图片预览"
+    fullscreen
+    footer-hide
+    @on-visible-change="visibleChange"
+  >
+    <div slot="header"></div>
+    <div :class="[`${prefixCls}-close`]" @click="cancel">
+      <i class="el-icon-circle-close"></i>
+      <Icon type="ios-close" />
+    </div>
+
+    <div :class="[`${prefixCls}-body`]" ref="ReviewBody">
+      <div
+        :class="[`${prefixCls}-guide`, `${prefixCls}-guide-prev`]"
+        @click.stop="showPrev"
+      >
+        <Icon type="ios-arrow-back" />
+      </div>
+      <div
+        :class="[`${prefixCls}-guide`, `${prefixCls}-guide-next`]"
+        @click.stop="showNext"
+      >
+        <Icon type="ios-arrow-forward" />
+      </div>
+      <div
+        :class="[
+          `${prefixCls}-imgs`,
+          { [`${prefixCls}-imgs-nosition`]: nosition }
+        ]"
+        :style="styles"
+        v-move-ele.prevent.stop="{ mouseMove, click: cancel }"
+        v-if="modalIsShow"
+      >
+        <img
+          :key="curImage.url"
+          :src="curImage.url"
+          :alt="curImage.name"
+          ref="PreviewImgDetail"
+          @load="reizeImage"
+        />
+      </div>
+      <div :class="[`${prefixCls}-none`]" v-if="!curImage.url">
+        <Icon type="md-image" />
+        <p>暂无数据</p>
+      </div>
+
+      <div :class="[`${prefixCls}-loading`]" v-show="loading">
+        <Icon class="ivu-load-loop" type="ios-loading" />
+      </div>
+    </div>
+
+    <div :class="[`${prefixCls}-footer`]">
+      <ul>
+        <li title="合适大小" @click="toOrigin">
+          <Icon type="md-expand" />
+        </li>
+        <li
+          title="放大"
+          @click="toMagnify"
+          :class="{
+            'li-disabled': transform.scale === maxScale
+          }"
+        >
+          <Icon type="md-add-circle" />
+        </li>
+        <li
+          title="缩小"
+          @click="toShrink"
+          :class="{
+            'li-disabled': transform.scale === minScale
+          }"
+        >
+          <Icon type="md-remove-circle" />
+        </li>
+        <li title="旋转" @click.stop="toRotate">
+          <Icon type="ios-refresh-circle" />
+        </li>
+      </ul>
+    </div>
+  </Modal>
+</template>
+
+<script>
+import MoveEle from "./move-ele";
+const prefixCls = "cc-image-preview";
+
+export default {
+  name: "simple-image-preview",
+  props: {
+    curImage: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  directives: { MoveEle },
+  data() {
+    return {
+      prefixCls,
+      modalIsShow: false,
+      styles: { width: "", height: "", top: "", left: "", transform: "" },
+      initWidth: 500,
+      minScale: 0.2,
+      maxScale: 3,
+      transform: {
+        scale: 1,
+        rotate: 0
+      },
+      loading: false,
+      loadingSetT: null,
+      nosition: false
+    };
+  },
+  mounted() {
+    this.registWheelHandle();
+  },
+  methods: {
+    visibleChange(visible) {
+      if (!visible) return;
+      // this.loading = true;
+      // this.$nextTick(() => {
+      //   this.registfileLoad();
+      // });
+    },
+    // registfileLoad() {
+    //   const imgDom = this.$refs.PreviewImgDetail;
+    //   imgDom.onload = () => {
+    //     this.reizeImage(imgDom);
+    //   };
+    // },
+    reizeImage() {
+      if (this.loadingSetT) clearTimeout(this.loadingSetT);
+
+      const imgDom = this.$refs.PreviewImgDetail;
+      const { naturalWidth, naturalHeight } = imgDom;
+      const imageSize = this.getImageSizePos({
+        win: {
+          width: this.$refs.ReviewBody.clientWidth,
+          height: this.$refs.ReviewBody.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;
+    },
+    cancel() {
+      this.modalIsShow = false;
+      this.$emit("on-close");
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    showPrev() {
+      this.$emit("on-prev");
+      // this.initData();
+    },
+    showNext() {
+      this.$emit("on-next");
+      // this.initData();
+    },
+    // dome-move
+    registWheelHandle() {
+      this.$refs.ReviewBody.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.reizeImage();
+    },
+    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.$refs.ReviewBody.clientWidth,
+          height: this.$refs.ReviewBody.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>

+ 15 - 0
src/components/ViewFooter.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="view-footer home-footer">
+    <p>
+      Copyright © 2018-2020
+      <a href="http:\\www.qmth.com" target="_blank">启明泰和</a> , All Rights
+      Reserved.
+    </p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "view-footer"
+};
+</script>

+ 42 - 0
src/components/ViewHeader.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="view-header">
+    <div class="head-logo">
+      <slot name="logo">
+        <!-- big logo 160*40 -->
+        <h1>LOGO</h1>
+      </slot>
+    </div>
+    <div class="head-user">
+      <span class="user-paper">查看试题</span>
+      <span class="user-name"
+        ><Icon type="md-person" size="16" /> {{ username }}</span
+      >
+      <span class="user-logout" @click="logout">
+        <Icon type="md-power" size="20" />
+      </span>
+    </div>
+    <div class="head-info">
+      <slot name="info"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "view-header",
+  data() {
+    return {};
+  },
+  computed: {
+    username() {
+      return this.$store.state.user.name;
+    }
+  },
+  methods: {
+    logout() {
+      this.$ls.clear();
+      this.$router.push({ name: "Login" });
+    }
+  }
+};
+</script>

+ 56 - 0
src/components/move-ele.js

@@ -0,0 +1,56 @@
+export default {
+  inserted(el, { value, modifiers }, vnode) {
+    let [_x, _y] = [0, 0];
+    // 当前拖动事务开始前元素的left,top
+    let [oleft, otop] = [0, 0];
+    // 元素移动后的left,top
+    let [left, top] = [0, 0];
+    let isDrag = false;
+
+    let moveHandle = function(e) {
+      isDrag = true;
+      if (modifiers.prevent) {
+        e.preventDefault();
+      }
+      left = oleft + e.pageX - _x;
+      top = otop + e.pageY - _y;
+
+      if (value && value.mouseMove) {
+        value.mouseMove({ left, top });
+      } else {
+        el.style.left = left + "px";
+        el.style.top = top + "px";
+      }
+    };
+
+    let upHandle = function(e) {
+      if (modifiers.prevent) {
+        e.preventDefault();
+      }
+      oleft = left;
+      otop = top;
+
+      if (value && value.mouseUp) value.mouseUp({ oleft, otop });
+      if (value && value.click && !isDrag) value.click();
+
+      document.removeEventListener("mousemove", moveHandle);
+      document.removeEventListener("mouseup", upHandle);
+
+      isDrag = false;
+    };
+
+    el.addEventListener("mousedown", function(e) {
+      if (modifiers.prevent) {
+        e.preventDefault();
+      }
+      _x = e.pageX;
+      _y = e.pageY;
+      oleft = el.offsetLeft;
+      otop = el.offsetTop;
+      if (value && value.mouseDown) value.mouseDown({ oleft, otop });
+
+      document.addEventListener("mousemove", moveHandle);
+      document.addEventListener("mouseup", upHandle);
+    });
+  }
+};

+ 4 - 0
src/config.js

@@ -0,0 +1,4 @@
+export default {
+  domain: process.env.VUE_APP_DOMAIN,
+  input: "",
+};

+ 15 - 0
src/constants/enumerate.js

@@ -0,0 +1,15 @@
+// 启用/禁用
+export const ABLE_TYPE = {
+  DISABLE: "禁用",
+  ENABLE: "启用"
+};
+
+// image-type
+export const IMAGE_TYPE = {
+  1: "原图",
+  2: "裁切图"
+};
+export const BOOLEAN_TYPE = {
+  0: "否",
+  1: "是"
+};

+ 119 - 0
src/main.js

@@ -0,0 +1,119 @@
+import Vue from "vue";
+import axios from "axios";
+
+import App from "./App.vue";
+import router from "./router";
+import store from "./store";
+import globalVuePlugins from "./plugins/globalVuePlugins";
+// 系统初始采集配置
+import CONFIG from "./config";
+
+// https://github.com/RobinCK/vue-ls
+import VueLocalStorage from "vue-ls";
+import ElementUI from "element-ui";
+import "element-ui/lib/theme-chalk/index.css";
+import "./assets/styles/index.scss";
+
+import { initConfigData } from "@/plugins/env";
+import log4js from "./plugins/logger";
+const logger = log4js.getLogger("request");
+
+Vue.use(ElementUI, { size: "small" });
+Vue.use(VueLocalStorage);
+Vue.use(globalVuePlugins);
+
+// 加载采集配置
+const GLOBAL = initConfigData(CONFIG);
+Vue.prototype.GLOBAL = GLOBAL;
+
+Vue.config.productionTip = false;
+
+// logger
+const addLog = (datas, type) => {
+  if (type === "start") {
+    const msg = `${datas.url},开始请求`;
+    logger.info(msg);
+  } else if (type === "success") {
+    const msg = `${datas.config.url},请求成功`;
+    logger.info(msg);
+  } else {
+    const msg = `${datas.config.url},请求错误,错误信息:${datas.response.data.status} - ${datas.response.data.message}`;
+    logger.error(msg);
+  }
+};
+
+// axios interceptors
+var load = "";
+// 同一时间有多个请求时,会形成队列。在第一个请求创建loading,在最后一个响应关闭loading
+var queue = [];
+axios.interceptors.request.use(
+  config => {
+    // 显示loading提示
+    if (!queue.length && !config["showTinyTips"] && !config["silentRequest"]) {
+      load = ViewUI.Message.loading({
+        content: "Loading...",
+        duration: 0
+      });
+    }
+
+    queue.push(1);
+
+    // 为请求地址添加全局domain
+    if (config.url.indexOf("http://") < 0) {
+      config.url = GLOBAL.domain + config.url;
+    }
+
+    // 为请求头添加信息
+    let organizationId = Vue.ls.get("organizationId");
+    if (organizationId) {
+      config.headers["organizationId"] = organizationId;
+    }
+    let user = Vue.ls.get("user", { userId: "", examId: "" });
+    config.headers["userId"] = user.userId;
+    config.headers["examId"] = user.examId;
+
+    // 设置延迟时效
+    config.timeout = 10 * 60 * 1000;
+
+    addLog(config, "start");
+    return config;
+  },
+  error => {
+    // console.log(error);
+    // logger.error("");
+    // 关闭loading提示
+    // 串联并发请求,延时处理是为防止多个loading实例闪屏。
+    setTimeout(() => {
+      queue.shift();
+      if (!queue.length) load && load();
+    }, 100);
+    return Promise.reject(error);
+  }
+);
+axios.interceptors.response.use(
+  response => {
+    addLog(response, "success");
+    // 关闭loading提示
+    setTimeout(() => {
+      queue.shift();
+      if (!queue.length) load && load();
+    }, 100);
+    return response;
+  },
+  error => {
+    addLog(error, "error");
+
+    // 关闭loading提示
+    setTimeout(() => {
+      queue.shift();
+      if (!queue.length) load && load();
+    }, 100);
+    return Promise.reject(error);
+  }
+);
+
+new Vue({
+  router,
+  store,
+  render: h => h(App)
+}).$mount("#app");

+ 20 - 0
src/mixins/authUnvalidMixin.js

@@ -0,0 +1,20 @@
+export default {
+  computed: {
+    showToLoginModel() {
+      return this.$store.state.showToLoginModel;
+    }
+  },
+  watch: {
+    showToLoginModel(val) {
+      if (val) {
+        this.$Modal.confirm({
+          content: "身份验证失效,是否重新登陆?",
+          onOk: () => {
+            this.$store.commit("setShowToLoginModel", false);
+            this.$router.push({ name: "Login" });
+          }
+        });
+      }
+    }
+  }
+};

+ 40 - 0
src/mixins/initStoreMixin.js

@@ -0,0 +1,40 @@
+import db from "../plugins/db";
+
+export default {
+  methods: {
+    // initDb(name) {
+    //   db.init(name);
+    // },
+    getLastLoginUser() {
+      return db.getDict("lastLoginUser", "");
+    },
+    async setLastLoginUser(user) {
+      await db.setDict("lastLoginUser", JSON.stringify(user));
+    },
+    async initStartCountTime(subjects) {
+      const storeStartCountTime = await db.getDict("startCountTime", "");
+
+      let startCountTime = {};
+      if (storeStartCountTime) {
+        startCountTime = JSON.parse(storeStartCountTime);
+        this.$store.commit("client/setStartCountTime", startCountTime);
+      } else {
+        subjects.map(subject => {
+          startCountTime[subject.id] = 0;
+        });
+        this.$store.dispatch("client/updateStartCountTime", startCountTime);
+      }
+    },
+    async initStore() {
+      const storeDict = await db.getAllDict();
+      const needInitDict = ["startCountTime", "lastLoginUser"];
+      for (let key in needInitDict) {
+        if (
+          !Object.prototype.hasOwnProperty.call(storeDict, needInitDict[key])
+        ) {
+          await db.initDict(needInitDict[key]);
+        }
+      }
+    }
+  }
+};

+ 17 - 0
src/mixins/setTimeMixins.js

@@ -0,0 +1,17 @@
+export default {
+  data() {
+    return {
+      setTs: []
+    };
+  },
+  methods: {
+    addSetTime(action, time = 1 * 1000) {
+      this.setTs.push(setTimeout(action, time));
+    },
+    clearSetTs() {
+      if (!this.setTs.length) return;
+      this.setTs.forEach(t => clearTimeout(t));
+      this.setTs = [];
+    }
+  }
+};

+ 65 - 0
src/mixins/uploadTaskMixin.js

@@ -0,0 +1,65 @@
+import db from "../plugins/db";
+import UploadTask from "../plugins/imageUpload";
+// import { getLocalDate } from "../plugins/utils";
+
+/**
+ * 上传流程:
+ * 1、先取数据库中所有未上传的记录,生成上传任务列表,开启当前上传周期。
+ * 2、当前上传周期内新采集的图片追加进当前上传周期。
+ * 3、当前上传周期内上传失败的图片进入下次上传周期。
+ * 4、当前上传周期结束之后,定时开启下次上传周期。
+ */
+export default {
+  data() {
+    return {
+      uploadTask: null,
+      taskSetTs: []
+    };
+  },
+  methods: {
+    async addUploadTask(task) {
+      await db.saveUploadInfo(task).catch(err => {
+        console.log(err);
+      });
+    },
+    async uploadSuccessCallback(curUploadTask) {
+      await db.updateUploadState(curUploadTask.id);
+    },
+    async initUploadTask() {
+      this.clearTaskSetTs();
+      const unuploadList = await db.searchUploadList({
+        isUpload: 0
+      });
+      // 创建上传任务
+      this.uploadTask = new UploadTask({
+        taskList: unuploadList,
+        uploadSuccessCallback: curUploadTask => {
+          this.uploadSuccessCallback(curUploadTask);
+        },
+        uploadTaskOverCallback: () => {
+          if (!this.uploadTask) return;
+          this.taskSetTs.push(
+            setTimeout(() => {
+              this.initUploadTask();
+            }, 0.5 * 1000)
+          );
+        },
+        uploadErrorCallBack: curUploadTask => {
+          const content = `考生:${curUploadTask.studentName},准考证:${curUploadTask.examNumber},科目:${curUploadTask.subjectName},图片上传失败!`;
+          console.log(content);
+          return;
+        }
+      });
+    },
+    stopUpload() {
+      this.clearTaskSetTs();
+      this.uploadTask && this.uploadTask.stopUploadTask();
+      this.uploadTask = null;
+    },
+    clearTaskSetTs() {
+      if (!this.taskSetTs.length) return;
+      this.taskSetTs.forEach(t => clearTimeout(t));
+      this.taskSetTs = [];
+    }
+  }
+};

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

@@ -0,0 +1,35 @@
+import { $post, $get } from "@/plugins/axios";
+
+export const uploadFormalImage = (options, datas, config) => {
+  const pathInfo = options.imageEncrypt ? "image/uploadsheet" : "ms-sheet";
+  return $post(
+    `/api/file/${pathInfo}/${options.examId}/${options.subjectId}/${options.examNumber}`,
+    datas,
+    { ...config, showTinyTips: true }
+  );
+};
+export const uploadSliceImage = (options, datas, config) => {
+  const pathInfo = options.imageEncrypt ? "image/upload" : "ms-slice";
+  return $post(
+    `/api/file/${pathInfo}/${options.examId}/${options.subjectId}/${options.examNumber}`,
+    datas,
+    { ...config, showTinyTips: true }
+  );
+};
+export const getStudentGroupByExamNumber = examNumber => {
+  return $get(`/api/exam/listStudents/${examNumber}`);
+};
+export const getStudentByExamNumber = examNumber => {
+  return $get(`/api/exam/getStudent/${examNumber}`);
+};
+export const uploadStudent = datas => {
+  return $post(`/api/upload/student/${datas.subjectId}`, datas, {
+    showTinyTips: true
+  });
+};
+export const saveCollectLog = datas => {
+  return $post(`/api/marklog/saveCollectLog`, datas, { showTinyTips: true });
+};
+export const getLevelList = examId => {
+  return $get(`/api/level/${examId}`);
+};

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

@@ -0,0 +1,10 @@
+import TaskManage from "./views/TaskManage.vue";
+
+
+export default [
+  {
+    path: "/task-manage",
+    name: "TaskManage",
+    component: TaskManage
+  },
+];

+ 38 - 0
src/modules/client/store.js

@@ -0,0 +1,38 @@
+import db from "../../plugins/db";
+
+const state = {
+  scanNo: 0, // 已采集数量(从当前设置的计数时间算起)
+  unuploadNo: 0, // 未上传数量(从当前设置的计数时间算起),
+  startCountTime: null, // 计数时间,默认:当前登录时间
+  showToLoginModel: false,
+};
+
+const mutations = {
+  setStartCountTime(state, startCountTime) {
+    state.startCountTime = startCountTime;
+  },
+  setShowToLoginModel(state, showToLoginModel) {
+    state.showToLoginModel = showToLoginModel;
+  },
+  setScanNo(state, scanNo) {
+    state.scanNo = scanNo;
+  },
+  setUnuploadNo(state, unuploadNo) {
+    state.unuploadNo = unuploadNo;
+  }
+};
+
+const actions = {
+  async updateStartCountTime({ commit }, data) {
+    const startCountTime = Object.assign({}, state.startCountTime, data);
+    commit("setStartCountTime", startCountTime);
+    await db.setDict("startCountTime", JSON.stringify(startCountTime));
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+};

+ 16 - 0
src/modules/client/views/TaskManage.vue

@@ -0,0 +1,16 @@
+<template>
+  <div class="task-manage">
+    task-manage
+  </div>
+</template>
+
+<script>
+export default {
+  name:"task-manage",
+  data () {
+    return {
+    }
+  },
+  methods:{}
+}
+</script>

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


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

@@ -0,0 +1,226 @@
+<template>
+  <div class="login" @keyup.enter="submit">
+    <div class="login-header home-header">
+      <div class="head-actions">
+        <span class="action-icon" title="最小化" @click="minWin"
+          ><Icon type="md-remove" size="16"
+        /></span>
+        <span class="action-icon" title="最大化" @click="maxWin"
+          ><Icon type="md-browsers" size="16"
+        /></span>
+        <span class="action-icon" title="关闭" @click="close"
+          ><Icon type="md-close" size="16"
+        /></span>
+      </div>
+    </div>
+    <div class="login-body">
+      <div class="login-title">
+        <h1>美术阅卷系统登录</h1>
+      </div>
+      <div class="login-form">
+        <Form ref="loginForm" :model="loginModel" :rules="loginRules">
+          <FormItem prop="loginname">
+            <Input
+              size="large"
+              v-model.trim="loginModel.loginname"
+              prefix="md-person"
+              placeholder="请输入用户名"
+              clearable
+            ></Input>
+          </FormItem>
+          <FormItem prop="password">
+            <Input
+              size="large"
+              type="password"
+              v-model.trim="loginModel.password"
+              prefix="md-lock"
+              placeholder="请输入密码"
+              clearable
+            ></Input>
+          </FormItem>
+          <FormItem style="margin-top:54px;">
+            <Button
+              size="large"
+              type="primary"
+              :disabled="isSubmit || loading || !encryptValid"
+              @click="submit"
+              style="width: 120px"
+              >登录</Button
+            >
+          </FormItem>
+        </Form>
+      </div>
+    </div>
+
+    <div class="login-tips" v-if="tips">{{ tips }}</div>
+  </div>
+</template>
+
+<script>
+import { password } from "@/plugins/formRules";
+import initStoreMixin from "../../../mixins/initStoreMixin";
+import { formatDate } from "../../../plugins/utils";
+import { $get } from "@/plugins/axios";
+import { mapState, mapActions } from "vuex";
+const { ipcRenderer } = require("electron");
+
+export default {
+  name: "login",
+  mixins: [initStoreMixin],
+  data() {
+    return {
+      loginModel: {
+        loginname: "scan01",
+        password: "123456"
+      },
+      loginRules: {
+        loginname: [
+          {
+            required: true,
+            pattern: /^[a-zA-Z0-9_-]{2,40}$/,
+            message: "用户名只能包含字母、数字、下划线以及短横线",
+            trigger: "change"
+          }
+        ],
+        password
+      },
+      isSubmit: false,
+      encryptValid: true,
+      loading: false,
+      tips: "",
+      exceptionIsConfirm: true
+    };
+  },
+  computed: {
+    ...mapState("client", ["encryptResult"])
+  },
+  watch: {
+    encryptResult(val, oldval) {
+      if (val.success) {
+        this.encryptSuccess();
+      } else {
+        this.encryptError(val);
+      }
+    }
+  },
+  mounted() {
+    this.closeHardware();
+    this.$ls.clear();
+    this.initData();
+  },
+  methods: {
+    ...mapActions("client", ["checkEncrypt"]),
+    minWin() {
+      ipcRenderer.send("minimize-window");
+    },
+    maxWin() {
+      ipcRenderer.send("maximize-window");
+    },
+    close() {
+      ipcRenderer.send("close-window");
+    },
+    async initData() {
+      await this.initStore();
+      const lastLoginUser = await this.getLastLoginUser();
+
+      if (lastLoginUser) {
+        this.loginModel = { ...JSON.parse(lastLoginUser) };
+      }
+    },
+    async submit(name) {
+      const valid = await this.$refs.loginForm.validate();
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await $get(
+        "/api/user/login",
+        this.loginModel
+      ).catch(() => {});
+      this.isSubmit = false;
+      if (!data) return;
+
+      data.loginTime = formatDate();
+      data.name = this.loginModel.loginname;
+
+      await this.setLastLoginUser(this.loginModel);
+      await this.initScanArea(data.subjects);
+      await this.initStartCountTime(data.subjects);
+
+      this.$ls.set("user", data);
+      this.$ls.set("organizationId", data.organizationId);
+      this.$store.commit("setUser", data);
+      const paramSet = data.paramSetting.collectConfig
+        ? JSON.parse(data.paramSetting.collectConfig)
+        : data.paramSetting;
+      this.$store.commit("client/setClientConfig", {
+        imageEncrypt: paramSet.imageEncrypt,
+        packageScan: paramSet.packageScan,
+        paperStage: paramSet.paperStage
+      });
+
+      if (data.roleCode === "ADMIN") {
+        this.$router.push({
+          name: "PaperExport"
+        });
+        return;
+      }
+
+      this.toCheckEncrypt();
+    },
+    // 软硬件绑定
+    checkHardware() {
+      return this.$store.dispatch("checkHardware");
+    },
+    closeHardware() {
+      return this.$store.dispatch("stopHardware");
+    },
+    // 加密狗
+    toCheckEncrypt() {
+      this.exceptionIsConfirm = true;
+      this.loading = true;
+      this.tips = "正在检测加密狗,请稍后……";
+      this.checkEncrypt();
+    },
+    encryptSuccess() {
+      if (!this.exceptionIsConfirm) return;
+      this.exceptionIsConfirm = false;
+
+      this.loading = false;
+      this.tips = "";
+      this.encryptValid = true;
+      this.$Modal.success({
+        content: "加密狗检测成功!",
+        onOk: () => {
+          this.exceptionIsConfirm = true;
+          this.$router.push({
+            name: "ActionType"
+          });
+        }
+      });
+      this.checkHardware();
+    },
+    encryptError(error) {
+      if (!this.exceptionIsConfirm) return;
+      this.exceptionIsConfirm = false;
+
+      this.loading = false;
+      this.encryptValid = false;
+      this.tips = "校验失败!";
+      this.$Modal.confirm({
+        content: error.msg,
+        okText: "关闭程序",
+        cancelText: "重新检测",
+        onOk: () => {
+          ipcRenderer.send("close-window");
+        },
+        onCancel: () => {
+          setTimeout(() => {
+            this.toCheckEncrypt();
+          });
+        }
+      });
+    }
+  }
+};
+</script>

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

@@ -0,0 +1,16 @@
+<template>
+  <div class="login-home">
+    login-home
+  </div>
+</template>
+
+<script>
+export default {
+  name:"login-home",
+  data () {
+    return {
+    }
+  },
+  methods:{}
+}
+</script>

+ 207 - 0
src/plugins/axios.js

@@ -0,0 +1,207 @@
+import axios from "axios";
+import qs from "qs";
+import ViewUI from "view-design";
+import { objTypeOf } from "./utils";
+// import router from "../router";
+// import Vue from "vue";
+let tinyTipsIsShow = false;
+
+/**
+ * errorCallback 请求失败的回调
+ * @param {Object} error 请求失败时的错误信息
+ */
+const errorCallback = (error, config = {}) => {
+  const showTinyTips = config && config["showTinyTips"];
+  const slientRequest = config && config["silentRequest"];
+
+  let content = "";
+  if (error.response) {
+    content =
+      (error.response.data && error.response.data.message) || "服务错误";
+  } else if (error.request) {
+    content = "请求错误";
+    if (error.message.indexOf("timeout") > -1) {
+      content = "请求超时";
+    }
+  } else {
+    return error;
+  }
+
+  if (slientRequest) {
+    return error;
+  }
+
+  if (showTinyTips) {
+    if (!tinyTipsIsShow) {
+      ViewUI.Message.error({
+        content: "上传异常",
+        duration: 5,
+        onClose() {
+          tinyTipsIsShow = false;
+        }
+      });
+      tinyTipsIsShow = true;
+    }
+  } else {
+    content = content.indexOf("###") !== -1 ? "参数错误" : content;
+    ViewUI.Notice.error({ title: "错误提示", desc: content });
+  }
+  return error;
+};
+
+/**
+ * errorDataCallback 请求成功,结果有误的回调
+ * @param {Object} error Response中的data信息
+ */
+// const errorDataCallback = error => {
+//   let content = error.message || "数据错误";
+//   content = content.indexOf("###") !== -1 ? "参数错误" : content;
+
+//   // TODO:自定义处理逻辑,以下为epcc实例
+//   if (error.code === -100) {
+//     content = "身份验证失效,请重新登录";
+//     ViewUI.Modal.confirm({
+//       title: "重新登陆?",
+//       content,
+//       onOk: () => {
+//         Vue.ls.clear();
+//         router.push({ name: "Login" });
+//       }
+//     });
+//   } else {
+//     ViewUI.Notice.error({ title: "错误提示", desc: content });
+//   }
+//   return error;
+// };
+
+/**
+ * response format
+ *  {
+      config, header, data, request, status, statusText
+    }
+ * 
+ */
+
+/**
+ * successCallback 请求成功的回调
+ * @param {Object} data Response中的data信息
+ */
+const successCallback = data => {
+  return data;
+  // if (data.code === 0) {
+  //   return data.data;
+  // } else {
+  //   throw new Error(errorDataCallback(data));
+  // }
+};
+
+/**
+ * get请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $get = (url, datas) => {
+  let sqDatas = "";
+  if (datas) {
+    sqDatas = qs.stringify(datas, {
+      arrayFormat: "brackets"
+    });
+    url += "?" + sqDatas;
+  }
+  return axios
+    .get(url)
+    .then(rep => {
+      return successCallback(rep.data);
+    })
+    .catch(error => {
+      throw new Error(errorCallback(error));
+    });
+};
+
+/**
+ * post请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $post = (url, datas, config = {}) => {
+  let sqDatas = "";
+  if (objTypeOf(datas) === "object" || objTypeOf(datas) === "array") {
+    sqDatas = qs.stringify(datas, { allowDots: true });
+  } else {
+    sqDatas = datas;
+  }
+  return axios
+    .post(url, sqDatas, config)
+    .then(rep => {
+      return successCallback(rep.data);
+    })
+    .catch(error => {
+      throw new Error(errorCallback(error, config));
+    });
+};
+
+/**
+ * delete请求
+ * @param {String} url
+ * @param {Object} datas
+ */
+const $del = (url, datas) => {
+  let sqDatas = "";
+  if (datas) {
+    sqDatas = qs.stringify(datas, { arrayFormat: "brackets" });
+    url += "?" + sqDatas;
+  }
+  return axios
+    .delete(url)
+    .then(rep => {
+      return rep.data;
+    })
+    .catch(error => {
+      throw new Error(errorCallback(error));
+    });
+};
+
+/**
+ * put 请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $put = (url, datas) => {
+  return axios
+    .put(url, datas)
+    .then(rep => {
+      return rep.data;
+    })
+    .catch(error => {
+      throw new Error(errorCallback(error));
+    });
+};
+
+/**
+ * patch请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $patch = (url, datas) => {
+  return axios
+    .patch(url, datas)
+    .then(rep => {
+      return rep.data;
+    })
+    .catch(error => {
+      throw new Error(errorCallback(error));
+    });
+};
+
+const $directGet = (url, datas) => {
+  let sqDatas = "";
+  if (datas) {
+    sqDatas = qs.stringify(datas, {
+      arrayFormat: "brackets"
+    });
+    url += "?" + sqDatas;
+  }
+  return axios.get(url, { silentRequest: true });
+};
+
+export { $get, $post, $del, $put, $patch, $directGet };

+ 816 - 0
src/plugins/db.js

@@ -0,0 +1,816 @@
+import { getDatabaseDir } from "./env";
+import { formatDate } from "./utils";
+const path = require("path");
+const fs = require("fs");
+const sqlite = require("sqlite3").verbose();
+
+let db = undefined;
+
+init();
+
+function init(sqlName = "client") {
+  if (db) return;
+  const databasePath = getDatabaseDir();
+  var orgDb = path.join(databasePath, "org.rdb");
+  var clientDb = path.join(databasePath, sqlName + ".rdb");
+
+  if (!fs.existsSync(clientDb)) {
+    fs.copyFileSync(orgDb, clientDb);
+  }
+  db = new sqlite.Database(clientDb, sqlite.OPEN_READWRITE);
+}
+
+function serializeWhere(params) {
+  const where = Object.keys(params)
+    .map(key => {
+      if (key.indexOf("Time") !== -1) {
+        return `${key} LIKE $${key}`;
+      } else {
+        return `${key} = $${key}`;
+      }
+    })
+    .join(" AND ");
+  const whereData = {};
+  Object.entries(params).map(([key, val]) => {
+    whereData[`$${key}`] = key.indexOf("Time") !== -1 ? `${val}%` : val;
+  });
+
+  return {
+    where,
+    whereData
+  };
+}
+
+function serializeUpdate(params) {
+  const template = Object.keys(params)
+    .map(key => {
+      return `${key}=$${key}`;
+    })
+    .join(",");
+  const templateData = {};
+  Object.entries(params).map(([key, val]) => {
+    templateData[`$${key}`] = val;
+  });
+
+  return {
+    template,
+    templateData
+  };
+}
+
+// scan
+function saveUploadInfo(params) {
+  const sql = `INSERT INTO scan (examId, examName, subjectId, subjectName, examNumber, studentName, siteCode, roomCode, originImgPath,formalImgPath, sliceImgPath,compressRate,isManual,imageEncrypt,level,clientUserId, clientUsername, clientUserLoginTime, isUpload,createdTime, finishTime) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
+  const datas = [
+    params.examId,
+    params.examName,
+    params.subjectId,
+    params.subjectName,
+    params.examNumber,
+    params.studentName,
+    params.siteCode,
+    params.roomCode,
+    params.originImgPath || "",
+    params.formalImgPath,
+    params.sliceImgPath,
+    params.compressRate || 100,
+    params.isManual ? 1 : 0,
+    params.imageEncrypt,
+    params.level,
+    params.clientUserId,
+    params.clientUsername,
+    params.clientUserLoginTime,
+    0, // isUpload
+    formatDate(), // createdTime
+    null
+  ];
+  return new Promise((resolve, reject) => {
+    db.serialize(() => {
+      db.run(sql, datas, function(err) {
+        if (err) reject(err);
+        resolve(this.lastID);
+      });
+    });
+  });
+}
+
+function searchUploadList(params) {
+  const { where, whereData } = serializeWhere(params);
+
+  const sql = `SELECT * FROM scan WHERE ${where}`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, whereData, (err, rows) => {
+      if (err) reject("search info fail!");
+      resolve(rows);
+    });
+  });
+}
+
+function countScanList(params) {
+  const { where, whereData } = serializeWhere(params);
+  const sql = `SELECT COUNT(1) AS count FROM scan WHERE ${where}`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, whereData, (err, rows) => {
+      if (err) reject("count list fail!");
+
+      resolve(rows[0].count);
+    });
+  });
+}
+
+function getUploadCount(limitTime) {
+  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) => {
+      if (err) reject("count list fail!");
+
+      resolve(rows[0].count);
+    });
+  });
+}
+
+function getScanCount(limitTime, subjectId) {
+  const sql = `SELECT COUNT(DISTINCT examNumber) AS count FROM scan WHERE strftime('%s',createdTime, 'utc') >= '${limitTime}' AND subjectId = '${subjectId}'`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("count list fail!");
+
+      resolve(rows[0].count);
+    });
+  });
+}
+
+function getHistory(limitTime, subjectId) {
+  const sql = `SELECT max(id) id, examNumber,studentName name,formalImgPath FROM scan WHERE strftime('%s',createdTime, 'utc') >= '${limitTime}' AND subjectId = '${subjectId}' GROUP BY examNumber ORDER BY id DESC LIMIT 30`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("get list fail!");
+
+      resolve(rows);
+    });
+  });
+}
+
+function updateUploadState(id) {
+  const sql = `UPDATE scan SET isUpload=$isUpload,finishTime=$finishTime WHERE id=$id`;
+  const datas = {
+    $isUpload: 1,
+    $finishTime: formatDate(),
+    $id: id
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update upload info fail!");
+
+      resolve();
+    });
+  });
+}
+
+function updateLocalPaperId(id, paperId) {
+  const sql = `UPDATE scan SET paperId=$paperId WHERE id=$id`;
+  const datas = {
+    $paperId: paperId,
+    $id: id
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update paperId info fail!");
+
+      resolve();
+    });
+  });
+}
+
+function deleteScanById(id) {
+  const sql = `DELETE FROM scan WHERE id=$id`;
+  const datas = {
+    $id: id
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("delete scan fail!");
+
+      resolve(true);
+    });
+  });
+}
+
+// dict
+function getAllDict() {
+  const sql = `SELECT * FROM dict`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("get all dict fail!");
+
+      const storeDict = {};
+      rows.map(item => {
+        storeDict[item.key] = item.val;
+      });
+      resolve(storeDict);
+    });
+  });
+}
+
+function initDict(key, val = "") {
+  const sql = `INSERT INTO dict (key, val) VALUES (?,?)`;
+  const datas = [key, val];
+
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject(`init dict ${key} fail!`);
+
+      resolve();
+    });
+  });
+}
+
+function getDict(key, defaultVal = "") {
+  const sql = `SELECT * FROM dict WHERE key=?`;
+  return new Promise((resolve, reject) => {
+    db.get(sql, key, (err, row) => {
+      if (err) reject(`get dict ${key} fail!`);
+      resolve((row && row.val) || defaultVal);
+    });
+  });
+}
+
+function setDict(key, val) {
+  const sql = `UPDATE dict SET val=? WHERE key=?`;
+  return new Promise((resolve, reject) => {
+    db.run(sql, [val, key], err => {
+      if (err) reject(`update dict ${key} fail!`);
+
+      resolve();
+    });
+  });
+}
+
+// paper-manage
+function getAreas(subjectId) {
+  const sql = `SELECT DISTINCT siteCode from scan WHERE subjectId = '${subjectId}'`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("get list fail!");
+      resolve(rows.map(item => item.siteCode));
+    });
+  });
+}
+
+function getPaperList({
+  examId,
+  subjectId,
+  areaCode,
+  isManual,
+  missing,
+  sortBy,
+  pageNumber,
+  pageSize
+}) {
+  const orderBy = sortBy === "1" ? "id DESC" : "examNumber ASC";
+
+  let options = [];
+  if (examId) {
+    options.push(`examId = '${examId}'`);
+  }
+  if (subjectId) {
+    options.push(`subjectId = '${subjectId}'`);
+  }
+  if (areaCode) {
+    options.push(`siteCode = '${areaCode}'`);
+  }
+  if (isManual !== null) {
+    options.push(`isManual = ${isManual}`);
+  }
+  if (missing !== null) {
+    options.push(`missing = ${missing}`);
+  }
+  const optionStr = options.length ? "WHERE " + options.join(" and ") : "";
+
+  const condition = `${optionStr} GROUP BY examNumber ORDER BY ${orderBy}`;
+  const countSql = `SELECT count(id) count FROM ( SELECT max(id) id FROM scan ${condition})`;
+
+  // console.log(countSql);
+
+  return new Promise((resolve, reject) => {
+    db.all(countSql, (err, rows) => {
+      if (err) reject("count list fail!");
+
+      // console.log(rows);
+
+      const total = rows[0].count;
+      const offset = (pageNumber - 1) * pageSize;
+      const sql = `SELECT max(id) pid, * FROM scan ${condition} LIMIT ${pageSize} OFFSET ${offset}`;
+
+      // console.log(sql);
+
+      db.all(sql, (err, rows) => {
+        if (err) reject("get list fail!");
+        resolve({
+          datas: rows,
+          pageNumber,
+          pageSize,
+          total
+        });
+      });
+    });
+  });
+}
+
+function absentLocalPaper(id, missing) {
+  const sql = `UPDATE scan SET missing=$missing WHERE id=$id`;
+  const datas = {
+    $missing: missing,
+    $id: id
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update absent info fail!");
+
+      resolve();
+    });
+  });
+}
+
+function saveRotateHistory(params) {
+  const sql = `INSERT INTO rotate_history (examId,subjectId,paperId, studentName, examNumber,imageEncrypt,filePath, isUpload,createdTime, finishTime) VALUES (?,?,?,?,?,?,?,?,?,?)`;
+  const datas = [
+    params.examId,
+    params.subjectId,
+    params.paperId,
+    params.studentName,
+    params.examNumber,
+    params.imageEncrypt,
+    params.filePath,
+    0, // isUpload
+    formatDate(), // createdTime
+    null
+  ];
+  return new Promise((resolve, reject) => {
+    db.serialize(() => {
+      db.run(sql, datas, function(err) {
+        if (err) reject(err);
+        resolve(this.lastID);
+      });
+    });
+  });
+}
+
+function getUnfinishRotateHistoryList() {
+  const sql = `SELECT * FROM rotate_history WHERE isUpload = 0`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("get history list fail!");
+
+      resolve(rows);
+    });
+  });
+}
+
+function getUnfinishRotateHistoryCount() {
+  const sql = `SELECT COUNT(1) AS count FROM rotate_history WHERE isUpload = 0`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, (err, rows) => {
+      if (err) reject("count history list fail!");
+
+      resolve(rows[0].count);
+    });
+  });
+}
+
+function finishRotateHistory(id) {
+  const sql = `UPDATE rotate_history SET isUpload=$isUpload,finishTime=$finishTime WHERE id=$id`;
+  const datas = {
+    $id: id,
+    $isUpload: 1,
+    $finishTime: formatDate()
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update download task info fail!");
+
+      resolve(true);
+    });
+  });
+}
+
+// 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) {
+  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);
+    });
+  });
+}
+
+// cropper-task
+export function getCropperTaskList(params) {
+  const { where, whereData } = serializeWhere(params);
+  let sql = `SELECT * FROM cropper_task`;
+  if (where) {
+    sql += ` WHERE ${where}`;
+  }
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, whereData, (err, rows) => {
+      if (err) reject("search task fail!");
+      resolve(rows);
+    });
+  });
+}
+
+export function deleteCropperTaskById(id) {
+  const sql = `DELETE FROM cropper_task WHERE id=$id`;
+  const datas = {
+    $id: id
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("delete cropper-task fail!");
+      resolve(true);
+    });
+  });
+}
+
+export function deleteCropperTaskDetail(cropperTaskId) {
+  const sql = `DELETE FROM cropper_task_detail WHERE cropperTaskId=$cropperTaskId`;
+  const datas = {
+    $cropperTaskId: cropperTaskId
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("delete cropper-task-detail fail!");
+      resolve(true);
+    });
+  });
+}
+
+export function addCropperTask(params) {
+  const sql = `INSERT INTO cropper_task (name,inputFile,taskCount,finishedCount,createTime,updateTime) VALUES (?,?,?,?,?,?)`;
+  const datas = [
+    params.name,
+    params.inputFile,
+    params.taskCount,
+    0,
+    formatDate(), // createTime
+    ""
+  ];
+
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, function(err) {
+      if (err) reject(err);
+      resolve(this.lastID);
+    });
+  });
+}
+
+export function updateCropperTask(params) {
+  const sql = `UPDATE cropper_task SET name=$name,inputFile=$inputFile,taskCount=$taskCount,finishedCount=$finishedCount,updateTime=$updateTime WHERE id=$id`;
+  const datas = {
+    $id: params.id,
+    $name: params.name,
+    $inputFile: params.inputFile,
+    $taskCount: params.taskCount,
+    $finishedCount: 0,
+    $updateTime: formatDate()
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update cropper task fail!");
+
+      resolve(true);
+    });
+  });
+}
+
+export function updateCropperTaskFinishedCount(params) {
+  const sql = `UPDATE cropper_task SET finishedCount=$finishedCount,updateTime=$updateTime WHERE id=$id`;
+  const datas = {
+    $id: params.id,
+    $finishedCount: params.finishedCount,
+    $updateTime: formatDate()
+  };
+  return new Promise((resolve, reject) => {
+    db.run(sql, datas, err => {
+      if (err) reject("update cropper task count fail!");
+
+      resolve(true);
+    });
+  });
+}
+
+export function getCropperTaskFinishCount(cropperTaskId) {
+  const { where, whereData } = serializeWhere({
+    cropperTaskId: cropperTaskId,
+    isFinished: 1
+  });
+  const sql = `SELECT COUNT(1) AS count FROM cropper_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);
+    });
+  });
+}
+
+/**
+ * 
+CREATE TABLE "cropper_task" (
+  "id" INTEGER NOT NULL,
+  "name" TEXT NOT NULL,
+  "inputFile" TEXT NOT NULL,
+  "taskCount" integer NOT NULL,
+  "finishedCount" integer NOT NULL DEFAULT 0,
+  "createTime" TEXT NOT NULL,
+  "updateTime" TEXT NOT NULL DEFAULT '',
+  PRIMARY KEY ("id")
+);
+ * 
+CREATE TABLE "cropper_task_detail" (
+  "id" INTEGER NOT NULL,
+  "cropperTaskId" INTEGER NOT NULL,
+  "examId" text NOT NULL,
+  "paperId" text,
+  "subjectId" text NOT NULL,
+  "subject" text NOT NULL,
+  "subjectName" TEXT,
+  "examNumber" text NOT NULL,
+  "studentName" TEXT NOT NULL,
+  "originImgPath" TEXT,
+  "formalImgPath" TEXT,
+  "sliceImgPath" TEXT,
+  "compressRate" integer NOT NULL DEFAULT 100,
+  "imageEncrypt" integer NOT NULL DEFAULT 1,
+  "isUpload" integer NOT NULL DEFAULT 0,
+  "cropperSet" TEXT,
+  "isFinished" integer NOT NULL DEFAULT 0,
+  "createTime" TEXT NOT NULL DEFAULT '',
+  "updateTime" TEXT NOT NULL DEFAULT '',
+  PRIMARY KEY ("id")
+);
+ */
+
+export function getCropperTaskDetailList(params, fields = []) {
+  const { where, whereData } = serializeWhere(params);
+  const fieldCont = fields.length ? fields.join(",") : "*";
+  const sql = `SELECT ${fieldCont} FROM cropper_task_detail WHERE ${where}`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, whereData, (err, rows) => {
+      if (err) reject("search task detail list fail!");
+      resolve(rows);
+    });
+  });
+}
+
+/**
+ *
+ * @param {Array} paramList 参数列表
+ */
+export function addCropperTaskDetail(paramList) {
+  const listVals = paramList.map(params => {
+    const datas = [
+      params.cropperTaskId,
+      params.examId,
+      params.paperId || "",
+      params.subjectId,
+      params.subject,
+      params.subjectName,
+      params.examNumber,
+      params.studentName,
+      params.originImgPath,
+      params.formalImgPath,
+      params.sliceImgPath,
+      params.imageEncrypt,
+      0, // isUpload
+      params.cropperSet,
+      0, // isFinished
+      formatDate(), // createTime
+      ""
+    ];
+    const nitem = datas.map(val =>
+      typeof val === "string" ? `'${val}'` : val
+    );
+    return `(${nitem.join(",")})`;
+  });
+  const vals = listVals.join(",");
+
+  const sql = `INSERT INTO cropper_task_detail (cropperTaskId,examId,paperId, subjectId,subject,subjectName,examNumber,studentName,originImgPath, formalImgPath,sliceImgPath,imageEncrypt,isUpload,cropperSet,isFinished,createTime,updateTime) VALUES ${vals}`;
+  // console.log(sql);
+
+  return new Promise((resolve, reject) => {
+    db.run(sql, function(err) {
+      if (err) reject(err);
+      resolve(true);
+    });
+  });
+}
+
+export function updateCropperTaskDetail(params) {
+  if (!params.id) return Promise.reject("id is required");
+
+  const id = params.id;
+  let paramData = { ...params };
+  delete paramData.id;
+  const { template, templateData } = serializeUpdate(paramData);
+  const sql = `UPDATE cropper_task_detail SET ${template} WHERE id=${id}`;
+
+  return new Promise((resolve, reject) => {
+    db.run(sql, templateData, err => {
+      if (err) reject("update cropper task detail fail!");
+
+      resolve(true);
+    });
+  });
+}
+
+export function getCropperTaskDetail(cropperTaskDetailId) {
+  const { where, whereData } = serializeWhere({
+    id: cropperTaskDetailId
+  });
+  const sql = `SELECT * FROM cropper_task_detail WHERE ${where}`;
+
+  return new Promise((resolve, reject) => {
+    db.all(sql, whereData, (err, rows) => {
+      if (err) reject("search task detail fail!");
+      resolve(rows[0]);
+    });
+  });
+}
+
+export default {
+  init,
+  // scan
+  saveUploadInfo,
+  searchUploadList,
+  countScanList,
+  getUploadCount,
+  getScanCount,
+  getHistory,
+  updateUploadState,
+  updateLocalPaperId,
+  deleteScanById,
+  // dict
+  getDict,
+  setDict,
+  getAllDict,
+  initDict,
+  // paper-manage
+  getAreas,
+  getPaperList,
+  absentLocalPaper,
+  saveRotateHistory,
+  getUnfinishRotateHistoryList,
+  finishRotateHistory,
+  getUnfinishRotateHistoryCount,
+  // export data
+  addExportTask,
+  addExportTaskDetail,
+  getUnfinishTask,
+  getDownloadTaskList,
+  getDownloadTaskCount,
+  updateDownloadTaskDownload,
+  updateTaskFinish
+};

+ 43 - 0
src/plugins/db.sql

@@ -0,0 +1,43 @@
+/*
+ Date: 2022/10/09 15:32:08
+ */
+PRAGMA foreign_keys = false;
+
+-- ----------------------------
+-- Table structure for dict
+-- ----------------------------
+DROP TABLE IF EXISTS "dict";
+
+CREATE TABLE "dict" ("key" TEXT NOT NULL, "val" TEXT);
+
+-- ----------------------------
+-- Table structure for scan
+-- ----------------------------
+DROP TABLE IF EXISTS "scan";
+
+CREATE TABLE "scan" (
+  "id" INTEGER NOT NULL,
+  "taskId" text NOT NULL,
+  "taskName" TEXT,
+  "courseCode" text NOT NULL,
+  "courseName" TEXT NOT NULL,
+  "teachingClassName" text NOT NULL,
+  "frontOriginImgPath" TEXT,
+  "versoOriginImgPath" TEXT,
+  "isUpload" integer NOT NULL DEFAULT 0,
+  "clientUserId" text NOT NULL,
+  "clientUsername" text NOT NULL,
+  "clientUserLoginTime" text NOT NULL,
+  "createdTime" text NOT NULL,
+  "finishTime" text,
+  PRIMARY KEY ("id")
+);
+
+-- ----------------------------
+-- Table structure for sqlite_sequence
+-- ----------------------------
+DROP TABLE IF EXISTS "sqlite_sequence";
+
+CREATE TABLE "sqlite_sequence" ("name", "seq");
+
+PRAGMA foreign_keys = true;

+ 130 - 0
src/plugins/env.js

@@ -0,0 +1,130 @@
+const path = require("path");
+const fs = require("fs");
+const homePath = path.dirname(process.execPath);
+const storePath = path.join(homePath, "stores");
+const extraPath =
+  process.env.NODE_ENV === "production"
+    ? path.join(homePath, "extra")
+    : path.join(__static, "../extra");
+
+initPath();
+function initPath() {
+  const paths = [
+    storePath,
+    getInputDir(),
+    getStoresDir("out"),
+    getOutputDir("formal"),
+    getOutputDir("slice"),
+    getTmpDir(),
+    // cropper
+    getStoresDir("cropper"),
+    path.join(getStoresDir("cropper"), "origin"),
+    path.join(getStoresDir("cropper"), "formal"),
+    path.join(getStoresDir("cropper"), "slice")
+  ];
+  paths.forEach(path => {
+    if (!fs.existsSync(path)) fs.mkdirSync(path);
+  });
+}
+// base
+function getHomeDir(name) {
+  return path.join(homePath, name);
+}
+
+function getStoresDir(name) {
+  return path.join(storePath, name);
+}
+
+function getExtraDir(name) {
+  return path.join(extraPath, name);
+}
+
+// stores
+function getInputDir() {
+  return getStoresDir("in");
+}
+function getOutputDir(type) {
+  return path.join(getStoresDir("out"), type);
+}
+function getTmpDir() {
+  return getStoresDir("tmp");
+}
+
+// extra
+function getDatabaseDir() {
+  return getExtraDir("database");
+}
+
+function getImgDecodeTool() {
+  return path.join(getExtraDir("zxingA"), "zxing.exe");
+}
+
+function getFontPath() {
+  // 文件名必须是英文,否则会报错
+  return path.join(getExtraDir("font"), "simhei-subfont.ttf");
+}
+
+function getEncryptPath() {
+  return path.join(getExtraDir("encrypt"), "msyjencrypt.exe");
+}
+
+function getHardwareCheckPath() {
+  return path.join(getExtraDir("artControl"), "ArtControl.exe");
+}
+
+/**
+ *
+ * @param {String} pathContent 目录路径
+ */
+function makeDirSync(pathContent) {
+  let mkPathList = [];
+  let curPath = pathContent;
+
+  while (!fs.existsSync(curPath)) {
+    mkPathList.unshift(curPath);
+    curPath = path.dirname(curPath);
+  }
+
+  mkPathList.forEach(path => {
+    fs.mkdirSync(path);
+  });
+}
+
+function formatNum(num, [min, max]) {
+  return !num || num > max || num < min ? max : num;
+}
+
+function initConfigData(data) {
+  let configData = { ...data };
+  if (!configData.input) configData.input = getInputDir();
+
+  if (process.env.NODE_ENV === "development") return configData;
+
+  const configPath = path.join(homePath, "config.json");
+  if (fs.existsSync(configPath)) {
+    configData = JSON.parse(fs.readFileSync(configPath, "utf8"));
+    configData.compressRate = formatNum(configData.compressRate, [0, 100]);
+    if (!configData.input) configData.input = getInputDir();
+  } else {
+    fs.writeFileSync(configPath, JSON.stringify(configData), "utf8");
+  }
+
+  return configData;
+}
+
+export {
+  initPath,
+  getHomeDir,
+  getStoresDir,
+  getExtraDir,
+  getInputDir,
+  getOutputDir,
+  getTmpDir,
+  makeDirSync,
+  getDatabaseDir,
+  getImgDecodeTool,
+  getFontPath,
+  getEncryptPath,
+  getHardwareCheckPath,
+  initConfigData
+};

+ 84 - 0
src/plugins/formRules.js

@@ -0,0 +1,84 @@
+// async-validator rules
+// to view at https://github.com/yiminghe/async-validator
+
+const username = [
+  {
+    required: true,
+    pattern: /^[a-zA-Z0-9_-]{2,19}$/,
+    message: "用户名只能包含字母、数字、下划线以及短横线,长度3-20位",
+    trigger: "change"
+  }
+];
+
+const commonCode = ({ prop, min = 3, max = 20 }) => {
+  return [
+    {
+      required: true,
+      pattern: new RegExp(`^[a-zA-Z0-9_]{${min},${max}}$`),
+      message: `${prop}只能由数字、字母和下划线组成,长度${min}-${max}个字符`,
+      trigger: "change"
+    }
+  ];
+};
+
+const email = [
+  {
+    required: true,
+    type: "email",
+    message: "邮箱格式不正确",
+    trigger: "change"
+  }
+];
+
+const password = [
+  {
+    required: true,
+    pattern: /^[a-zA-Z0-9_]{6,20}$/,
+    message: "密码只能由数字、字母和下划线组成,长度6-20个字符",
+    trigger: "change"
+  }
+];
+
+const phone = [
+  {
+    required: true,
+    pattern: /^1\d{10}$/,
+    message: "请输入合适的手机号码",
+    trigger: "change"
+  }
+];
+
+const smscode = [
+  {
+    required: true,
+    pattern: /^[a-zA-Z0-9]{4}$/,
+    message: "请输入4位短信验证码",
+    trigger: "change"
+  }
+];
+
+const numberValidator = message => {
+  return [
+    {
+      required: true,
+      validator: (rule, value, callback) => {
+        if (!value && value !== 0) {
+          callback(new Error(message));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ];
+};
+
+export {
+  username,
+  commonCode,
+  email,
+  password,
+  phone,
+  smscode,
+  numberValidator
+};

+ 32 - 0
src/plugins/globalVuePlugins.js

@@ -0,0 +1,32 @@
+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";
+import MoveBar from "@/components/MoveBar.vue";
+
+const components = {
+  ViewFooter,
+  ViewHeader,
+  MoveBar
+};
+
+export default {
+  install: function(Vue) {
+    // 实例方法
+    Vue.prototype.$tableAction = tableAction;
+    Vue.prototype.$objAssign = objAssign;
+    Vue.prototype.$randomCode = randomCode;
+
+    // 注册全局组件
+    Object.keys(components).forEach(key => {
+      Vue.component(key, components[key]);
+    });
+
+    //全局 mixins
+    Vue.mixin(globalMixins);
+
+    // 全局 directive
+    Vue.directive("CodeInput", CodeInput);
+  }
+};

+ 86 - 0
src/plugins/imageOcr.js

@@ -0,0 +1,86 @@
+import {
+  getInputDir,
+  getOutputDir,
+  makeDirSync
+} from "./env";
+import { randomCode } from "./utils";
+const fs = require("fs");
+const path = require("path");
+
+
+/**
+ * 旋转图片,并保存为正式文件
+ * @param {*} imgPath 图片路径
+ * @param {String} paperInfo 保持文件名称
+ * @param {Object} collectConfig 裁剪区域
+ */
+async function saveOutputImage(imgPath, paperInfo, collectConfig) {
+  // console.log(collectConfig);
+  const outputOriginPath = saveOriginImage(imgPath, paperInfo);
+  const outputFormalPath = await saveFormalImage(
+    imgPath,
+    paperInfo,
+    collectConfig
+  );
+  const outputSlicelPath = await saveSliceImage(
+    imgPath,
+    paperInfo,
+    collectConfig
+  ).catch(() => {});
+
+  if (outputSlicelPath && outputFormalPath)
+    return { outputSlicelPath, outputFormalPath, outputOriginPath };
+  return Promise.reject("试卷保存失败");
+}
+
+function saveOriginImage(imgPath, paperInfo) {
+  if (paperInfo.compressRate === 100) return "";
+
+  const outputOriginPath = getOutputImagePath(paperInfo, "origin");
+  fs.copyFileSync(imgPath, outputOriginPath);
+  return outputOriginPath;
+}
+
+
+
+
+function getOutputImagePath(paperInfo, type) {
+  const outputDir = path.join(
+    getOutputDir(type),
+    paperInfo.examId + "",
+    paperInfo.subjectId + ""
+  );
+
+  if (!fs.existsSync(outputDir)) makeDirSync(outputDir);
+  return path.join(outputDir, `${paperInfo.examNumber}-${randomCode()}.jpg`);
+}
+
+/**
+ * 获取最早添加的文件
+ * @param {String} dir 图片目录
+ */
+function getEarliestFile(dir) {
+  const ddir = dir || getInputDir();
+  const files = fs
+    .readdirSync(ddir)
+    .filter(fileName => fileName.toLowerCase().match(/\.(jpg|png|jpeg)/))
+    .map(fileName => {
+      return {
+        name: fileName,
+        time: fs.statSync(path.join(ddir, fileName)).birthtimeMs
+      };
+    })
+    .sort((a, b) => a.time - b.time);
+
+  if (!files.length) return { url: "", name: "" };
+
+  return {
+    url: path.join(ddir, files[0].name),
+    name: files[0].name
+  };
+}
+
+export {
+  saveOutputImage,
+  getEarliestFile,
+};

+ 155 - 0
src/plugins/imageUpload.js

@@ -0,0 +1,155 @@
+const fs = require("fs");
+const crypto = require("crypto");
+import { formatDate } from "./utils";
+import {
+  uploadSliceImage,
+  uploadFormalImage,
+  uploadStudent,
+  saveCollectLog
+} from "../modules/client/api";
+import db from "@/plugins/db";
+
+/**
+ * 文件上传
+ * @param {Object} options 上传配置信息
+ * @param {String} type 上传类型:formal=>原图,slice=>剪切图
+ */
+function toUploadImg(options, type) {
+  const formData = new FormData();
+  const filePath =
+    type === "formal" ? options.formalImgPath : options.sliceImgPath;
+
+  const buffer = fs.readFileSync(filePath);
+  let fsHash = crypto.createHash("md5");
+  fsHash.update(buffer);
+  let md5 = fsHash.digest("hex");
+  formData.append("md5", md5);
+
+  const file = new File([buffer], options.examNumber + ".jpg");
+  formData.append("file", file);
+  formData.append("level", options.level);
+  formData.append("scanUserId", options.clientUserId);
+
+  return type === "formal"
+    ? uploadFormalImage(options, formData, { headers: { md5 } })
+    : uploadSliceImage(options, formData, { headers: { md5 } });
+}
+
+function toUploadStudent(options) {
+  const datas = {
+    subjectId: options.subjectId,
+    examNumber: options.examNumber,
+  };
+  return uploadStudent(datas);
+}
+
+function toSaveCollectLog(options) {
+  const datas = {
+    workId: options.examId,
+    subjectId: options.subjectId,
+    examNumber: options.examNumber,
+    clientUsername: options.clientUsername,
+    clientUserId: options.clientUserId,
+    clientUserLoginTime: options.clientUserLoginTime,
+    time: formatDate(),
+    level: options.level,
+    name: options.studentName,
+    manual: options.isManual + ""
+  };
+  return saveCollectLog(datas);
+}
+
+/**
+ * 获取文件的MD5
+ * @param {String} source 文件路径
+ */
+// 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,
+    uploadErrorCallBack
+  }) {
+    this.taskList = taskList;
+    this.setT = "";
+    this.taskRunning = false;
+    this.uploadSuccessCallback = uploadSuccessCallback;
+    this.uploadTaskOverCallback = uploadTaskOverCallback;
+    this.uploadErrorCallBack = uploadErrorCallBack;
+
+    this.startUploadTask();
+  }
+
+  addUploadTask(data) {
+    this.taskList.push(data);
+  }
+
+  getCurTask() {
+    return this.taskList.shift();
+  }
+
+  async startUploadTask() {
+    this.taskRunning = true;
+    setTimeout(() => {
+      this.runUploadTask();
+    }, 10);
+  }
+
+  async runUploadTask() {
+    if (!this.taskList.length || !this.taskRunning) {
+      this.overUploadTask();
+      return;
+    }
+    const curTask = this.getCurTask();
+
+    const uploadAll = [
+      toUploadImg(curTask, "formal"),
+      toUploadImg(curTask, "slice")
+    ];
+    let uploadResult = true;
+    await Promise.all(uploadAll).catch(() => {
+      uploadResult = false;
+    });
+
+    let updateStdRes, saveLogRes;
+    if (uploadResult) {
+      updateStdRes = await toUploadStudent(curTask).catch(() => {});
+      saveLogRes = await toSaveCollectLog(curTask).catch(() => {});
+
+      if (updateStdRes && saveLogRes) {
+        // 更新paperId
+        await db.updateLocalPaperId(curTask.id, updateStdRes.paperId);
+        this.uploadSuccessCallback(curTask);
+      }
+    }
+    // 待定:只要有一个接口出错,则提示上传失败!
+    if (!uploadResult || !updateStdRes || !saveLogRes) {
+      this.uploadErrorCallBack(curTask);
+    }
+
+    this.setT = setTimeout(() => {
+      this.runUploadTask();
+    }, 10);
+  }
+
+  overUploadTask() {
+    this.taskRunning = false;
+    if (this.setT) clearTimeout(this.setT);
+    this.uploadTaskOverCallback && this.uploadTaskOverCallback();
+  }
+  stopUploadTask() {
+    this.taskRunning = false;
+    this.taskList = [];
+  }
+}
+
+export default UploadTask;
+
+export { toUploadImg };

+ 21 - 0
src/plugins/logger.js

@@ -0,0 +1,21 @@
+import { getHomeDir } from "./env";
+const log4js = require("log4js");
+const path = require("path");
+
+const MAX_SIZE = 10 * 1024 * 1024;
+
+const logDir = getHomeDir("logs");
+const logsFilePath = path.join(logDir, "scan.log");
+
+log4js.configure({
+  appenders: {
+    scan: {
+      type: "file",
+      filename: logsFilePath,
+      maxLogSize: MAX_SIZE
+    }
+  },
+  categories: { default: { appenders: ["scan"], level: "debug" } }
+});
+
+export default log4js;

+ 18 - 0
src/plugins/mixins.js

@@ -0,0 +1,18 @@
+export default {
+  methods: {
+    deletePageLastItem() {
+      let page = this.current || 1;
+      if (this.$refs.TableList.data.length === 1) {
+        page = page > 1 ? page - 1 : 1;
+      }
+      this.toPage && this.toPage(page);
+    },
+    goback() {
+      window.history.back();
+    },
+    getRouterPath(location) {
+      const { href } = this.$router.resolve(location);
+      return href;
+    }
+  }
+};

+ 163 - 0
src/plugins/utils.js

@@ -0,0 +1,163 @@
+const deepmerge = require("deepmerge");
+
+/**
+ * 判断对象类型
+ * @param {*} obj 对象
+ */
+function objTypeOf(obj) {
+  const toString = Object.prototype.toString;
+  const map = {
+    "[object Boolean]": "boolean",
+    "[object Number]": "number",
+    "[object String]": "string",
+    "[object Function]": "function",
+    "[object Array]": "array",
+    "[object Date]": "date",
+    "[object RegExp]": "regExp",
+    "[object Undefined]": "undefined",
+    "[object Null]": "null",
+    "[object Object]": "object",
+    "[object FormData]": "formData"
+  };
+  return map[toString.call(obj)];
+}
+
+/**
+ * 深拷贝
+ * @param {Object/Array} data 需要拷贝的数据
+ */
+function deepCopy(data, options) {
+  const defObj = objTypeOf(data) === "array" ? [] : {};
+  return deepmerge(defObj, data, options || {});
+}
+
+/**
+ * 将目标对象中有的属性值与源对象中的属性值合并
+ * @param {Object} target 目标对象
+ * @param {Object} sources 源对象
+ */
+function objAssign(target, sources) {
+  let targ = { ...target };
+  for (let k in targ) {
+    targ[k] = Object.prototype.hasOwnProperty.call(sources, k)
+      ? sources[k]
+      : targ[k];
+  }
+  return targ;
+}
+
+/**
+ * 获取随机code,默认获取16位
+ * @param {Number} len 推荐8的倍数
+ *
+ */
+function randomCode(len = 16) {
+  if (len <= 0) return;
+  let steps = Math.ceil(len / 8);
+  let stepNums = [];
+  for (let i = 0; i < steps; i++) {
+    let ranNum = Math.random()
+      .toString(32)
+      .slice(-8);
+    stepNums.push(ranNum);
+  }
+
+  return stepNums.join("");
+}
+
+/**
+ * 序列化参数
+ * @param {Object} params 参数对象
+ */
+function qsParams(params) {
+  return Object.entries(params)
+    .map(([key, val]) => `${key}=${val}`)
+    .join("&");
+}
+
+/**
+ *
+ * @param {String} format 时间格式
+ * @param {Date} date 需要格式化的时间对象
+ */
+function formatDate(format = "YYYY-MM-DD HH:mm:ss", date = new Date()) {
+  if (objTypeOf(date) !== "date") return;
+  const options = {
+    "Y+": date.getFullYear(),
+    "M+": date.getMonth() + 1,
+    "D+": date.getDate(),
+    "H+": date.getHours(),
+    "m+": date.getMinutes(),
+    "s+": date.getSeconds()
+  };
+  Object.entries(options).map(([key, val]) => {
+    if (new RegExp("(" + key + ")").test(format)) {
+      const zeros = key === "Y+" ? "0000" : "00";
+      const value = (zeros + val).substr(("" + val).length);
+      format = format.replace(RegExp.$1, value);
+    }
+  });
+  return format;
+}
+
+/**
+ * 获取本地日期,格式YYYY-M-D
+ */
+function getLocalDate() {
+  return formatDate("YYYY-MM-DD");
+}
+
+/**
+ * 清除html标签
+ * @param {String} str html字符串
+ */
+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,
+  objAssign,
+  randomCode,
+  qsParams,
+  formatDate,
+  removeHtmlTag,
+  getLocalDate,
+  timeNumberToText
+};

+ 54 - 0
src/router.js

@@ -0,0 +1,54 @@
+import Vue from "vue";
+import Router from "vue-router";
+
+import Home from "./views/Home";
+import Layout from "./views/Layout";
+import Login from "./views/Login";
+// modules
+import client from "./modules/client/router";
+import manage from "./modules/manage/router";
+import cropperTask from "./modules/cropper-task/router";
+
+Vue.use(Router);
+/**
+ * 权限控制:
+ * 1、不同角色构建不同模块。
+ * 2、登录成功之后根据角色注册相应模块。
+ * 3、不同模块打包不同文件。(webpackChunkName)
+ */
+
+export default new Router({
+  routes: [
+    {
+      path: "/",
+      name: "Index",
+      redirect: { name: "Login" }
+    },
+    {
+      path: "/login",
+      name: "Login",
+      component: Login
+    },
+    {
+      path: "/home",
+      name: "Home",
+      component: Home,
+      children: [...client, ...manage]
+    },
+    {
+      path: "",
+      name: "CropperHome",
+      component: Layout,
+      children: [...cropperTask]
+    }
+    // [lazy-loaded] route level code-splitting
+    // {
+    //   path: "/about",
+    //   name: "about",
+    //   // this generates a separate chunk (about.[hash].js) for this route
+    //   // which is lazy-loaded when the route is visited.
+    //   component: () =>
+    //     import(/* webpackChunkName: "about" */ "./views/About.vue")
+    // }
+  ]
+});

+ 25 - 0
src/store.js

@@ -0,0 +1,25 @@
+import Vue from "vue";
+import Vuex from "vuex";
+
+Vue.use(Vuex);
+
+// modules
+import client from "./modules/client/store";
+
+export default new Vuex.Store({
+  state: {
+    user: {},
+    // 硬件检测结果
+    hardwareCheckResult: {}
+  },
+  mutations: {
+    setUser(state, user) {
+      state.user = user;
+    },
+  },
+  actions: {
+  },
+  modules: {
+    client
+  }
+});

+ 70 - 0
src/views/Home.vue

@@ -0,0 +1,70 @@
+<template>
+  <Layout :back-handle="toBack">
+  </Layout>
+</template>
+
+<script>
+import Layout from "./Layout.vue";
+import uploadTaskMixin from "../mixins/uploadTaskMixin";
+import setTimeMixins from "../mixins/setTimeMixins";
+import { mapState, mapMutations } from "vuex";
+import db from "../plugins/db";
+import log4js from "@/plugins/logger";
+const logger = log4js.getLogger("scan");
+
+export default {
+  name: "home",
+  mixins: [uploadTaskMixin, setTimeMixins],
+  components: { Layout },
+  data() {
+    return {
+      showProgress: false,
+    };
+  },
+  computed: {
+    ...mapState("client", ["curSubject", "clientConfig"])
+  },
+  created() {
+    this.examName = this.$ls.get("user", { examName: "" }).examName;
+    const scanBackRouter = this.clientConfig.paperStage
+      ? "CheckInfo"
+      : "ScanWait";
+    this.backRouters.GroupScan = scanBackRouter;
+    this.backRouters.LineScan = scanBackRouter;
+    this.initUploadTask();
+    this.setCurSubject({});
+    this.updateUnuploadCount();
+  },
+  methods: {
+    ...mapMutations("client", ["setCurSubject"]),
+    // unupload count
+    async updateUnuploadCount() {
+      this.clearSetTs();
+      const unuploadNo = await db.countScanList({ isUpload: 0 });
+      this.$store.commit("client/setUnuploadNo", unuploadNo);
+      this.addSetTime(() => {
+        this.updateUnuploadCount();
+      }, 2 * 1000);
+    },
+    toBack() {
+      const curRouteName = this.$route.name;
+      const backRouter = this.backRouters[curRouteName];
+      if (curRouteName === "GroupScan" || curRouteName === "LineScan") {
+        this.$Modal.confirm({
+          content: "当前正处于采集状态,确定要退出吗?",
+          onOk: () => {
+            this.$router.push({ name: backRouter });
+            logger.warn("退出采集页面");
+          }
+        });
+      } else {
+        this.$router.push({ name: backRouter });
+      }
+    }
+  },
+  beforeDestroy() {
+    this.stopUpload();
+    this.clearSetTs();
+  }
+};
+</script>

+ 79 - 0
src/views/Layout.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="home">
+    <div class="home-header">
+      <div class="head-title">
+      </div>
+      <div class="head-actions">
+        <span class="action-icon" title="返回" @click="toBack">
+          <i class="el-icon-arrow-left"></i>
+        </span>
+        <span class="action-icon" title="最小化" @click="minWin">
+          <i class="el-icon-minus"></i>
+        </span>
+        <span class="action-icon" title="最大化" @click="maxWin">
+          <i class="el-icon-files"></i>
+        </span>
+        <span class="action-icon" title="关闭" @click="close">
+          <i class="el-icon-close"></i>
+        </span>
+        <span
+          v-if="isDev"
+          class="action-icon action-logout"
+          @click="logout"
+          title="退出"
+        >
+          <i class="el-icon-switch-button"></i>
+        </span>
+      </div>
+ 
+    </div>
+    <div class="home-body">
+      <router-view />
+    </div>
+  </div>
+</template>
+
+<script>
+const { ipcRenderer } = require("electron");
+
+export default {
+  name: "layout",
+  props: {
+    backHandle: {
+      type: Function,
+    },
+  },
+  data() {
+    return {
+      isDev: process.env.NODE_ENV === "development",
+    };
+  },
+  methods: {
+    logout() {
+      this.$ls.clear();
+      this.$router.push({ name: "Login" });
+    },
+    toBack() {
+      if (this.backHandle && typeof this.backHandle === "function") {
+        this.backHandle();
+      } else {
+        window.history.go(-1);
+      }
+    },
+    minWin() {
+      ipcRenderer.send("minimize-window");
+    },
+    maxWin() {
+      ipcRenderer.send("maximize-window");
+    },
+    close() {
+      this.$Modal.confirm({
+        content: "请确保当前窗口没有任务正在进行,确认要关闭当前窗口吗?",
+        onOk: () => {
+          ipcRenderer.send("close-window");
+        },
+      });
+    },
+  },
+};
+</script>

+ 67 - 0
vue.config.js

@@ -0,0 +1,67 @@
+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
+// electron-bulder配置:https://www.electron.build/configuration/contents#extrafiles
+var config = {
+  // publicPath: './',
+  devServer: {
+    port: 8066
+  },
+  pluginOptions: {
+    electronBuilder: {
+      externals: ["node-xlsx"],
+      builderOptions: {
+        extraFiles: [
+          "extra/encrypt/**",
+          "extra/font/**",
+          "extra/imagemagick/**",
+          "extra/zxing/**",
+          "extra/zxingA/**",
+          "extra/artControl/**",
+          "extra/database/org.rdb",
+          "config.sample.json",
+          "sense_shield_installer_pub_2.2.0.46331.exe"
+        ],
+        win: {
+          target: "portable",
+          signAndEditExecutable: false
+        }
+      }
+    }
+  }
+};
+
+// 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;