lrf vor 1 Jahr
Commit
bb641a04a3

+ 4 - 0
.env

@@ -0,0 +1,4 @@
+VUE_APP_AXIOS_BASE_URL = 'http://127.0.0.1'
+VUE_APP_ROUTER="exam"
+VUE_APP_VIEW_LEAVE_MAIN_EVENT="leaveMainEvent"
+VUE_APP_VIEW_LEAVE_VIEW_EVENT="leaveViewEvent"

+ 1 - 0
.env.production

@@ -0,0 +1 @@
+VUE_APP_AXIOS_BASE_URL = 'http://baoan.fwedzgc.com:8090'

+ 33 - 0
.eslintrc.js

@@ -0,0 +1,33 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+  },
+  extends: ['plugin:vue/essential', '@vue/prettier'],
+  plugins: ['vue'],
+  rules: {
+    'max-len': [
+      'warn',
+      {
+        code: 10000,
+      },
+    ],
+    'no-unused-vars': 'off',
+    'no-console': 'off',
+    'prettier/prettier': [
+      'warn',
+      {
+        singleQuote: true,
+        trailingComma: 'es5',
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 160,
+      },
+    ],
+  },
+  parserOptions: {
+    parser: 'babel-eslint',
+  },
+};

+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+.DS_Store
+node_modules
+/dist
+/electron_bulid
+electron依赖.zip
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+#Electron-builder output
+/dist_electron

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# exam
+
+## Project setup
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+```
+npm run serve
+```
+
+### Compiles and minifies for production
+```
+npm run build
+```
+
+### Lints and fixes files
+```
+npm run lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 3 - 0
babel.config.js

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

Datei-Diff unterdrückt, da er zu groß ist
+ 35131 - 0
package-lock.json


+ 67 - 0
package.json

@@ -0,0 +1,67 @@
+{
+  "name": "exam",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint",
+    "electron:build": "vue-cli-service electron:build",
+    "electron:serve": "vue-cli-service electron:serve",
+    "postinstall": "electron-builder install-app-deps",
+    "postuninstall": "electron-builder install-app-deps"
+  },
+  "main": "background.js",
+  "dependencies": {
+    "axios": "^0.25.0",
+    "core-js": "^3.6.5",
+    "element-ui": "^2.15.6",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.1",
+    "naf-core": "^0.1.2",
+    "vue": "^2.6.11",
+    "vue-meta": "^2.4.0",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "babel-eslint": "^10.1.0",
+    "electron": "^13.0.0",
+    "electron-chromedriver": "^16.0.0",
+    "electron-devtools-installer": "^3.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.3.1",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^3.0.4",
+    "less-loader": "^5.0.0",
+    "prettier": "^2.2.1",
+    "vue-cli-plugin-electron-builder": "~2.1.1",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended",
+      "@vue/prettier"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


+ 40 - 0
public/index.html

@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="">
+  <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">
+    <script type="text/javascript" src="./js/jquery.js"></script>
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= htmlWebpackPlugin.options.title %></title>
+    <style>
+      /*修改滚动条样式*/
+      div::-webkit-scrollbar{
+        width:10px;
+        height:10px;
+        /**/
+      }
+      div::-webkit-scrollbar-track{
+        background: rgb(239, 239, 239);
+        border-radius:2px;
+      }
+      div::-webkit-scrollbar-thumb{
+        background: #bfbfbf;
+        border-radius:10px;
+      }
+      div::-webkit-scrollbar-thumb:hover{
+        background: #333;
+      }
+      div::-webkit-scrollbar-corner{
+        background: #179a16;
+      }
+    </style>
+  </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>

Datei-Diff unterdrückt, da er zu groß ist
+ 3444 - 0
public/js/jquery.js


BIN
public/logo.ico


+ 55 - 0
src/App.vue

@@ -0,0 +1,55 @@
+<template>
+  <div id="app">
+    <el-row>
+      <el-col :span="24">
+        <router-view @toScan="toScan" ref="view" />
+      </el-col>
+    </el-row>
+    <div>
+      <gpy ref="gpy"></gpy>
+    </div>
+  </div>
+</template>
+<script>
+const _ = require('lodash');
+import gpy from './views/parts/gpy.vue';
+export default {
+  components: { gpy },
+  mounted() {},
+  methods: {
+    async toScan() {
+      const idCardInfo = await this.$refs.gpy.getIdCard();
+      const cardID = _.get(idCardInfo, 'cardID');
+      if (cardID) {
+        this.$message.success('身份证采集成功!');
+        this.$set(this.$refs.view.form, `card`, cardID);
+      } else this.$message.error('身份证采集失败!');
+    },
+  },
+};
+</script>
+<style>
+body {
+  margin: 0;
+}
+.w_1200 {
+  width: 1200px;
+  margin: 0 auto;
+}
+p {
+  margin: 0;
+  padding: 0;
+}
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.capture {
+  position: absolute;
+  top: 0px;
+  right: 0px;
+  z-index: 999;
+  background: blueviolet;
+}
+</style>

BIN
src/assets/bg.jpg


BIN
src/assets/img/img.jpg


BIN
src/assets/img/login-bg.jpg


BIN
src/assets/logo.png


+ 93 - 0
src/background.js

@@ -0,0 +1,93 @@
+'use strict';
+import { app, protocol, BrowserWindow, ipcMain } from 'electron';
+import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
+import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
+const isDevelopment = process.env.NODE_ENV !== 'production';
+
+// Scheme must be registered before the app is ready
+protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }]);
+let mainWindow;
+async function createWindow() {
+  // Create the browser window.
+  const win = new BrowserWindow({
+    alwaysOnTop: true,
+    frame: false,
+    titleBarStyle: 'hidden',
+    fullscreen: true,
+    // width: 1440,
+    // height: 920,
+    webPreferences: {
+      // Use pluginOptions.nodeIntegration, leave this alone
+      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
+      nodeIntegration: true,
+      enableRemoteModule: true,
+      contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
+      webSecurity: false,
+    },
+  });
+  // ipcMain.on(process.env.VUE_APP_VIEW_LEAVE_MAIN_EVENT, (e, args) => {
+  //   console.log('主进程接收信息');
+  //   e.reply(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT, true);
+  // });
+  if (process.env.WEBPACK_DEV_SERVER_URL) {
+    // Load the url of the dev server if in development mode
+    await 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');
+  }
+  mainWindow = win;
+}
+
+// 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 (BrowserWindow.getAllWindows().length === 0) 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
+    try {
+      await installExtension(VUEJS_DEVTOOLS);
+    } catch (e) {
+      console.error('Vue Devtools failed to install:', e.toString());
+    }
+  }
+  createWindow();
+});
+
+app.on('browser-window-blur', () => {
+  console.log('焦点离开窗口');
+  mainWindow.webContents.send(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT, true);
+  app.focus();
+});
+
+// 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();
+    });
+  }
+}

+ 17 - 0
src/main.js

@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import App from './App.vue';
+import router from './router';
+import store from './store';
+import '@/plugins/element.js';
+import '@/plugins/axios';
+import '@/plugins/check-res';
+import '@/plugins/meta';
+import electron from 'electron';
+Vue.prototype.$electron = electron;
+Vue.config.productionTip = false;
+
+new Vue({
+  router,
+  store,
+  render: (h) => h(App),
+}).$mount('#app');

+ 20 - 0
src/plugins/axios.js

@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import AxiosWrapper from '@/util/axios-wrapper';
+
+const Plugin = {
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$axios) {
+          this.$store.$axios = this.$axios;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    vue.prototype.$axios = new AxiosWrapper(options);
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });
+// process.env.NODE_ENV === 'development' ? process.env.VUE_APP_AXIOS_BASE_URL : process.env.VUE_APP_HOST

+ 39 - 0
src/plugins/check-res.js

@@ -0,0 +1,39 @@
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-unused-vars */
+/* eslint-disable no-shadow */
+import Vue from 'vue';
+import _ from 'lodash';
+import { Message } from 'element-ui';
+
+const vm = new Vue({});
+const Plugin = {
+  install(Vue, options) {
+    // 4. 添加实例方法
+    Vue.prototype.$checkRes = (res, okText, errText) => {
+      let _okText = okText;
+      let _errText = errText;
+      if (!_.isFunction(okText) && _.isObject(okText) && okText != null) {
+        ({ okText: _okText, errText: _errText } = okText);
+      }
+      const { errcode = 0, errmsg } = res || {};
+      if (errcode === 0) {
+        if (_.isFunction(_okText)) {
+          return _okText();
+        }
+        if (_okText) {
+          Message.success(_okText);
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message.error(_errText || errmsg);
+      // Message({ message: _errText || errmsg, duration: 60000 });
+      return false;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 16 - 0
src/plugins/components.js

@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import dataTable from '@common/src/components/frame/filter-page-table.vue';
+import dataForm from '@common/src/components/frame/form.vue';
+import eUpload from '@common/src/components/frame/e-upload.vue';
+import sUpload from '@common/src/components/frame/s-upload.vue';
+import eDateRange from '@common/src/components/frame/e-dateRange.vue';
+const Plugin = (vue) => {
+  vue.prototype.$dev_mode = process.env.NODE_ENV === 'development';
+  vue.component('data-table', dataTable);
+  vue.component('data-form', dataForm);
+  vue.component('eUpload', eUpload);
+  vue.component('sUpload', sUpload);
+  vue.component('eDateRange', eDateRange);
+};
+
+Vue.use(Plugin);

+ 5 - 0
src/plugins/element.js

@@ -0,0 +1,5 @@
+import Vue from 'vue';
+import Element from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+Vue.use(Element, { size: 'small' });

+ 6 - 0
src/plugins/filters.js

@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import filters from '@/util/filters';
+
+for (const method in filters) {
+  Vue.filter(method, filters[method]);
+}

+ 27 - 0
src/plugins/loading.js

@@ -0,0 +1,27 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+
+const Plugin = {
+  // eslint-disable-next-line no-unused-vars
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        // eslint-disable-next-line no-underscore-dangle
+        const isRoot = this.constructor === Vue;
+        // console.log(`rootId:${rootVue_uid}; thisId:${this._uid}`);
+        // if (rootVue_uid !== 3) {
+        //   console.log(this);
+        // }
+        if (isRoot) {
+          const el = document.getElementById('loading');
+          if (el) el.style.display = 'none';
+        }
+      },
+    });
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });

+ 4 - 0
src/plugins/meta.js

@@ -0,0 +1,4 @@
+import Vue from 'vue';
+import Meta from 'vue-meta';
+
+Vue.use(Meta);

+ 33 - 0
src/plugins/methods.js

@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import _ from 'lodash';
+const Plugin = {
+  install(Vue, options) {
+    // 3. 注入组件
+    Vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$toUndefined) {
+          this.$store.$toUndefined = this.$toUndefined;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    Vue.prototype.$toUndefined = (object) => {
+      let keys = Object.keys(object);
+      keys.map((item) => {
+        object[item] = object[item] === '' ? (object[item] = undefined) : object[item];
+      });
+      return object;
+    };
+    Vue.prototype.$turnTo = (item) => {
+      if (item.info_type == 1) {
+        window.open(item.url);
+      } else {
+        let router = window.vm.$router;
+        let route = window.vm.$route.path;
+        router.push({ path: `/info/detail?id=${item.id}` });
+      }
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 21 - 0
src/plugins/setting.js

@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+Vue.config.weixin = {
+  // baseUrl: process.env.BASE_URL + 'weixin',
+  baseUrl: `http://${location.host}`,
+};
+
+Vue.config.stomp = {
+  // brokerURL: 'ws://192.168.1.118/ws',
+  brokerURL: '/ws', // ws://${location.host}/ws
+  // brokerURL: 'ws://127.0.0.1:8000/ws',
+  connectHeaders: {
+    host: 'platform',
+    login: 'visit', //visit
+    passcode: 'visit', //visit123
+  },
+  // debug: true,
+  reconnectDelay: 5000,
+  heartbeatIncoming: 4000,
+  heartbeatOutgoing: 4000,
+};

+ 65 - 0
src/plugins/stomp.js

@@ -0,0 +1,65 @@
+/**
+ * 基于WebStomp的消息处理插件
+ */
+
+import Vue from 'vue';
+import _ from 'lodash';
+import assert from 'assert';
+import { Client } from '@stomp/stompjs/esm5/client';
+
+const Plugin = {
+  install(Vue, options) {
+    assert(_.isObject(options));
+    if (options.debug && !_.isFunction(options.debug)) {
+      options.debug = (str) => {
+        console.log(str);
+      };
+    }
+    assert(_.isString(options.brokerURL));
+    if (!options.brokerURL.startsWith('ws://')) {
+      options.brokerURL = `ws://${location.host}${options.brokerURL}`;
+    }
+
+    // 3. 注入组件
+    Vue.mixin({
+      beforeDestroy: function () {
+        if (this.$stompClient) {
+          this.$stompClient.deactivate();
+          delete this.$stompClient;
+        }
+      },
+    });
+
+    // 4. 添加实例方法
+    Vue.prototype.$stomp = function (subscribes = {}) {
+      // connect to mq
+      const client = new Client(options);
+      client.onConnect = (frame) => {
+        // Do something, all subscribes must be done is this callback
+        // This is needed because this will be executed after a (re)connect
+        console.log('[stomp] connected');
+        Object.keys(subscribes)
+          .filter((p) => _.isFunction(subscribes[p]))
+          .forEach((key) => {
+            client.subscribe(key, subscribes[key]);
+          });
+      };
+
+      client.onStompError = (frame) => {
+        // Will be invoked in case of error encountered at Broker
+        // Bad login/passcode typically will cause an error
+        // Complaint brokers will set `message` header with a brief message. Body may contain details.
+        // Compliant brokers will terminate the connection after any error
+        console.log('Broker reported error: ' + frame.headers['message']);
+        console.log('Additional details: ' + frame.body);
+      };
+
+      client.activate();
+
+      this.$stompClient = client;
+    };
+  },
+};
+export default () => {
+  Vue.use(Plugin, Vue.config.stomp);
+};

+ 25 - 0
src/plugins/var.js

@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import _ from 'lodash';
+
+const getSiteId = () => {
+  let host = `${window.location.hostname}`; //`999991.smart.jilinjobswx.cn ${window.location.hostname}`
+  let schId;
+  host = host.replace('http://', '');
+  let arr = host.split('.');
+  if (arr.length > 0) {
+    schId = arr[0];
+    if (schId === 'smart') schId = 'master';
+    else `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? (schId = '99991') : '';
+    sessionStorage.setItem('schId', `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? '99991' : schId);
+  }
+  return schId;
+};
+const Plugin = {
+  install(vue, options) {
+    // 4. 添加实例方法
+    vue.prototype.$limit = 10;
+    vue.prototype.$site = getSiteId();
+  },
+};
+
+Vue.use(Plugin);

+ 52 - 0
src/router/index.js

@@ -0,0 +1,52 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+const originalPush = VueRouter.prototype.push;
+VueRouter.prototype.push = function push(location) {
+  return originalPush.call(this, location).catch((err) => err);
+};
+
+Vue.use(VueRouter);
+
+const routes = [
+  {
+    path: '/',
+    redirect: '/login',
+  },
+  {
+    path: '/exam',
+    name: 'exam',
+    meta: { title: '考试' },
+    component: () => import(/* webpackChunkName: "exam" */ '../views/exam.vue'),
+  },
+  {
+    path: '/login',
+    name: 'login',
+    meta: { title: '登陆' },
+    component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'),
+  },
+  {
+    path: '/test',
+    name: 'test',
+    meta: { title: '测试' },
+    component: () => import(/* webpackChunkName: "test" */ '../views/test.vue'),
+  },
+];
+
+const router = new VueRouter({
+  mode: 'hash',
+  // base: process.env.VUE_APP_ROUTER,
+  routes,
+});
+router.beforeEach((to, from, next) => {
+  document.title = `${to.meta.title} `;
+  const noCheck = _.get(to, 'meta.noCheck', false);
+  if (noCheck) next();
+  else {
+    const token = localStorage.getItem('token');
+    // if (!token) next('/login');
+    // else next();
+    next();
+  }
+});
+
+export default router;

+ 42 - 0
src/store/index.js

@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+Vue.use(Vuex);
+import exam_answer from '@common/src/store/exam/answer';
+import examination_examinee from '@common/src/store/examination_examinee';
+import security_guard_collect from '@common/src/store/security_guard_collect';
+export default new Vuex.Store({
+  state: {
+    checkResult: false,
+    isExaming: false,
+    timer: undefined,
+    resultList: [],
+  },
+  mutations: {
+    setCheckResult(state, payload) {
+      state.checkResult = payload;
+    },
+    setExaming(state, payload) {
+      state.isExaming = payload;
+    },
+    setTimer(state, payload) {
+      state.timer = payload;
+    },
+    setRecord(state, payload) {
+      state.resultList.push(payload);
+    },
+    resetVar(state) {
+      state.checkResult = false;
+      state.isExaming = false;
+      state.resultList = [];
+      if (state.timer) clearInterval(state.timer);
+      state.timer = undefined;
+    },
+  },
+  actions: {},
+  modules: {
+    exam_answer,
+    examination_examinee,
+    security_guard_collect,
+  },
+});

+ 140 - 0
src/util/axios-wrapper.js

@@ -0,0 +1,140 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+// import { Indicator } from 'mint-ui';
+import util from './user-util';
+
+const { trimData, isNullOrUndefined } = Util;
+const { ErrorCode } = Error;
+
+let currentRequests = 0;
+
+export default class AxiosWrapper {
+  constructor({ baseUrl = '', unwrap = true } = {}) {
+    this.baseUrl = baseUrl;
+    this.unwrap = unwrap;
+  }
+
+  // 替换uri中的参数变量
+  static merge(uri, query = {}) {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/gi;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach((key) => {
+      console.log(query[key]);
+      if (!isNullOrUndefined(query[key])) {
+        uri = uri.replace(`:${key}`, query[key]);
+      }
+    });
+    return uri;
+  }
+
+  //检查query范围参数
+  static checkRangeQuery(query) {
+    for (const key in query) {
+      if (query[key] === '') query[key] = null;
+    }
+    for (const key in query) {
+      if (_.isObject(query[key])) {
+        // 处理,将{start,end}转换成key@start/end
+        if (_.get(query[key], 'start') && _.get(query[key], 'end')) {
+          query[`${key}@start`] = _.get(query[key], 'start');
+          query[`${key}@end`] = _.get(query[key], 'end');
+          delete query[key];
+        }
+      }
+    }
+
+    return query;
+  }
+
+  $get(uri, query, options) {
+    return this.$request(uri, null, query, options);
+  }
+
+  $post(uri, data = {}, query, options) {
+    return this.$request(uri, data, query, options);
+  }
+  $delete(uri, data = {}, router, query, options = {}) {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options, router);
+  }
+  async $request(uri, data, query, options) {
+    query = AxiosWrapper.checkRangeQuery(query);
+    // TODO: 合并query和options
+    if (_.isObject(query) && _.isObject(options)) {
+      options = { ...options, params: query, method: 'get' };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    // 处理范围参数
+    if (!options) options = {};
+    if (options.params) options.params = trimData(options.params);
+    const url = AxiosWrapper.merge(uri, options.params);
+    currentRequests += 1;
+    // Indicator.open({
+    //   spinnerType: 'fading-circle',
+    // });
+
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      const token = localStorage.getItem('token');
+      if (token) axios.defaults.headers.common.Authorization = token;
+      let res = await axios.request({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options,
+      });
+      res = res.data;
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return res;
+      }
+      // unwrap data
+      if (this.unwrap) {
+        res = _.omit(res, ['errmsg', 'details']);
+        const keys = Object.keys(res);
+        if (keys.length === 1 && keys.includes('data')) {
+          res = res.data;
+        }
+      }
+      return res;
+    } catch (err) {
+      let errmsg = '接口请求失败,请稍后重试';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+      }
+      console.error(
+        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
+        ${err.message}`
+      );
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message };
+    } finally {
+      /* eslint-disable */
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+        // Indicator.close();
+      }
+    }
+  }
+}

+ 10 - 0
src/util/filters.js

@@ -0,0 +1,10 @@
+import _ from 'lodash';
+
+const filters = {
+  getName(object) {
+    const { data, searchItem } = object;
+    return _.get(data, searchItem) === undefined ? '' : _.get(data, searchItem);
+  },
+};
+
+export default filters;

+ 50 - 0
src/util/methods-util.js

@@ -0,0 +1,50 @@
+import { Util } from 'naf-core';
+
+const { isNullOrUndefined } = Util;
+
+export default {
+  //判断信息是否过期
+  isDateOff(dataDate) {
+    const now = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
+    dataDate = new Date(dataDate);
+    return now.getTime() <= dataDate.getTime();
+  },
+  //判断企业是否可以执行此动作/显示
+  checkCorp(data) {
+    const { role, unit, selfUnit, status, displayType, userid } = data;
+    if (!isNullOrUndefined(selfUnit) && !isNullOrUndefined(status)) {
+      return role === 'corp' && selfUnit === unit && status === '0';
+    } else if (!isNullOrUndefined(displayType)) {
+      if (role === 'corp') {
+        return role === displayType;
+      } else {
+        return role === displayType && !isNullOrUndefined(userid);
+      }
+    }
+  },
+  //获取url的参数params
+  getParams() {
+    let str = location.href;
+    let num = str.indexOf('?');
+    const param = {};
+    str = str.substr(num + 1);
+    let num2 = str.indexOf('#');
+    let str2 = '';
+    if (num2 > 0) {
+      str2 = str.substr(0, num2);
+    } else {
+      num2 = str.indexOf('/');
+      str2 = str.substr(0, num2);
+    }
+    const arr = str2.split('&');
+    for (let i = 0; i < arr.length; i++) {
+      num = arr[i].indexOf('=');
+      if (num > 0) {
+        const name = arr[i].substring(0, num);
+        const value = arr[i].substr(num + 1);
+        param[name] = decodeURI(value);
+      }
+    }
+    return param;
+  },
+};

+ 135 - 0
src/util/print.js

@@ -0,0 +1,135 @@
+// 打印类属性、方法定义
+/* eslint-disable */
+const Print = function (dom, options) {
+  if (!(this instanceof Print)) return new Print(dom, options);
+
+  this.options = this.extend({
+    'noPrint': '.no-print'
+  }, options);
+
+  if ((typeof dom) === "string") {
+    this.dom = document.querySelector(dom);
+  } else {
+    this.isDOM(dom)
+    this.dom = this.isDOM(dom) ? dom : dom.$el;
+  }
+
+  this.init();
+};
+Print.prototype = {
+  init: function () {
+    var content = this.getStyle() + this.getHtml();
+    this.writeIframe(content);
+  },
+  extend: function (obj, obj2) {
+    for (var k in obj2) {
+      obj[k] = obj2[k];
+    }
+    return obj;
+  },
+
+  getStyle: function () {
+    var str = "",
+      styles = document.querySelectorAll('style,link');
+    for (var i = 0; i < styles.length; i++) {
+      str += styles[i].outerHTML;
+    }
+    str += "<style>" + (this.options.noPrint ? this.options.noPrint : '.no-print') + "{display:none;}</style>";
+
+    return str;
+  },
+
+  getHtml: function () {
+    var inputs = document.querySelectorAll('input');
+    var textareas = document.querySelectorAll('textarea');
+    var selects = document.querySelectorAll('select');
+
+    for (var k = 0; k < inputs.length; k++) {
+      if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
+        if (inputs[k].checked == true) {
+          inputs[k].setAttribute('checked', "checked")
+        } else {
+          inputs[k].removeAttribute('checked')
+        }
+      } else if (inputs[k].type == "text") {
+        inputs[k].setAttribute('value', inputs[k].value)
+      } else {
+        inputs[k].setAttribute('value', inputs[k].value)
+      }
+    }
+
+    for (var k2 = 0; k2 < textareas.length; k2++) {
+      if (textareas[k2].type == 'textarea') {
+        textareas[k2].innerHTML = textareas[k2].value
+      }
+    }
+
+    for (var k3 = 0; k3 < selects.length; k3++) {
+      if (selects[k3].type == 'select-one') {
+        var child = selects[k3].children;
+        for (var i in child) {
+          if (child[i].tagName == 'OPTION') {
+            if (child[i].selected == true) {
+              child[i].setAttribute('selected', "selected")
+            } else {
+              child[i].removeAttribute('selected')
+            }
+          }
+        }
+      }
+    }
+
+    return this.dom.outerHTML;
+  },
+
+  writeIframe: function (content) {
+    var w, doc, iframe = document.createElement('iframe'),
+      f = document.body.appendChild(iframe);
+    iframe.id = "myIframe";
+    //iframe.style = "position:absolute;width:0;height:0;top:-10px;left:-10px;";
+    iframe.setAttribute('style', 'position:absolute;width:0;height:0;top:-10px;left:-10px;');
+    w = f.contentWindow || f.contentDocument;
+    doc = f.contentDocument || f.contentWindow.document;
+    doc.open();
+    doc.write(content);
+    doc.close();
+    var _this = this
+    iframe.onload = function(){
+      _this.toPrint(w);
+      setTimeout(function () {
+        document.body.removeChild(iframe)
+      }, 100)
+    }
+  },
+
+  toPrint: function (frameWindow) {
+    try {
+      setTimeout(function () {
+        frameWindow.focus();
+        try {
+          if (!frameWindow.document.execCommand('print', false, null)) {
+            frameWindow.print();
+          }
+        } catch (e) {
+          frameWindow.print();
+        }
+        frameWindow.close();
+      }, 10);
+    } catch (err) {
+      console.log('err', err);
+    }
+  },
+  isDOM: (typeof HTMLElement === 'object') ?
+    function (obj) {
+      return obj instanceof HTMLElement;
+    } :
+    function (obj) {
+      return obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string';
+    }
+};
+const MyPlugin = {}
+MyPlugin.install = function (Vue, options) {
+  // 4. 添加实例方法
+  Vue.prototype.$print = Print
+}
+export default MyPlugin

+ 69 - 0
src/util/user-util.js

@@ -0,0 +1,69 @@
+/* eslint-disable no-console */
+export default {
+  get user() {
+    const val = sessionStorage.getItem('user');
+    try {
+      if (val) return JSON.parse(val);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  },
+  set user(userinfo) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+  },
+  get token() {
+    return sessionStorage.getItem('token');
+  },
+  set token(token) {
+    sessionStorage.setItem('token', token);
+  },
+  get openid() {
+    return sessionStorage.getItem('openid');
+  },
+  set openid(openid) {
+    sessionStorage.setItem('openid', openid);
+  },
+  get isGuest() {
+    return !this.user || this.user.role === 'guest';
+  },
+  save({ userinfo, token }) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+    sessionStorage.setItem('token', token);
+  },
+
+  get corpInfo() {
+    const val = sessionStorage.getItem('corpInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set corpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+  saveCorpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+
+  get unit() {
+    const val = sessionStorage.getItem('unit');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set unit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  saveUnit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  get userInfo() {
+    const val = sessionStorage.getItem('userInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set userInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+  saveUserInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+};

+ 232 - 0
src/views/exam.vue

@@ -0,0 +1,232 @@
+<template>
+  <div id="exam">
+    <!-- style="position: fixed; right: 0; top: 0" -->
+    <el-form :disabled="!checkResult && !isExaming">
+      <el-row>
+        <el-col :span="24" class="main">
+          <el-col :span="24" class="one"> 保安考试 </el-col>
+          <el-col :span="24" class="two">
+            <el-col :span="24" class="two_1">
+              <template v-for="(q, qi) in question.questions">
+                <el-row :key="`qt${qi}`" class="list">
+                  <el-col :span="24" class="index">{{ qi + 1 }}.{{ q.title }}</el-col>
+                  <template v-if="getQuestType(q) === 'judge'">
+                    <el-radio-group v-model="q.answer">
+                      <el-col :span="24" v-for="(s, si) in q.selects" :key="`selects${qi}-${si}`">
+                        <el-radio :key="`selects${qi}-${si}`" :label="s.title" :value="s.title"> </el-radio>
+                      </el-col>
+                    </el-radio-group>
+                  </template>
+                  <template v-if="getQuestType(q) === 'radio'">
+                    <el-radio-group v-model="q.answer">
+                      <el-col :span="24" v-for="(s, si) in q.selects" :key="`selects${qi}-${si}`">
+                        <el-radio :key="`selects${qi}-${si}`" :label="s.title" :value="s.title"> </el-radio>
+                      </el-col>
+                    </el-radio-group>
+                  </template>
+                  <template v-if="getQuestType(q) === 'checkbox'">
+                    <el-checkbox-group v-model="q.answer">
+                      <el-col :span="24" v-for="(s, si) in q.selects" :key="`checkbox${qi}-${si}`">
+                        <el-checkbox :key="`checkbox${qi}-${si}`" :label="s.title"> {{ s.title }}</el-checkbox>
+                      </el-col>
+                    </el-checkbox-group>
+                  </template>
+                </el-row>
+              </template>
+            </el-col>
+            <el-col :span="24" class="two_2">
+              <el-button @click="computedScore" type="primary">提交</el-button>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { mapState, mapMutations, createNamespacedHelpers } from 'vuex';
+const { mapActions: exam_answer } = createNamespacedHelpers('exam_answer');
+const _ = require('lodash');
+export default {
+  name: 'exam',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      question: {
+        questions: {},
+      },
+      isComplete: false,
+    };
+  },
+  created() {
+    this.initListener();
+    this.toExam();
+  },
+  methods: {
+    ...mapMutations(['resetVar', 'setExaming']),
+    ...exam_answer(['getExam', 'update']),
+    async toExam() {
+      let user = sessionStorage.getItem('user');
+      if (!user) {
+        this.$confirm('未找到您的登陆信息,请重新登陆', '考生未登录', {
+          confirmButtonText: '确定',
+          showCancelButton: false,
+          type: 'error',
+        }).then(() => {
+          this.$router.push('/login');
+        });
+        return;
+      }
+      user = JSON.parse(user);
+      const exam_info = _.omit(user, ['room_id', 'security_guard_id', 'name', 'card', 'img', 'is_money']);
+      const obj = {
+        exam_place_id: user.room_id,
+        user_id: user.security_guard_id,
+        user_name: user.name,
+        user_card: user.card,
+        exam: true,
+        exam_info,
+      };
+      let res = await this.getExam(obj);
+      if (this.$checkRes(res)) {
+        this.$set(this, `question`, res.data);
+        // 开始考试
+        this.setExaming(true);
+      }
+    },
+    getQuestType(quest) {
+      return _.get(quest.type, 'type');
+    },
+    async computedScore() {
+      if (this.isComplete) {
+        console.log('已经交卷,不需要重复提交');
+        return;
+      }
+      let dup = _.cloneDeep(this.question);
+      let score = 0;
+      let rights = [];
+      for (const q of dup.questions) {
+        const { selects, answer } = q;
+        const questType = this.getQuestType(q);
+        const questScore = _.get(q.type, 'score');
+        const right = selects.filter((f) => f.isRight);
+        let answerRight = false;
+        if (questType === 'radio') {
+          answerRight = right.find((f) => f.title === answer);
+        } else if (questType === 'checkbox') {
+          if (answer.length <= 0) continue;
+          answerRight = answer.every((e) => right.find((f) => f.title === e));
+        }
+        if (answerRight) {
+          rights.push(q);
+          score += questScore;
+        }
+      }
+      dup.score = score;
+      // 考试的情况下,返回的数据会带有数据id,需要走保存
+      if (dup._id) {
+        const res = await this.update(dup);
+        if (this.$checkRes(res)) {
+          this.isComplete = true;
+          this.$confirm(`交卷成功:您本次考试的分数为:${score}分`, '交卷提示', {
+            confirmButtonText: '确定',
+            showCancelButton: false,
+            type: 'success',
+          }).then(() => {
+            this.$router.push('/login');
+          });
+        }
+      } else
+        this.$confirm(`交卷成功:您本次练习的分数为:${score}分`, '交卷提示', {
+          confirmButtonText: '确定',
+          showCancelButton: false,
+          type: 'success',
+        }).then(() => {
+          this.$router.push('/login');
+        });
+    },
+    // 初始化失焦程序监听
+    initListener() {
+      // 先销毁(理论上不用,但是销毁也没错)
+      this.$electron.ipcRenderer.removeAllListeners(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT);
+      // 建立离开的监听
+      const ipcRenderer = this.$electron.ipcRenderer;
+      ipcRenderer.on(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT, (e, args) => {
+        // 接着去干别的事,或停止考试,或累加次数'
+        this.$message.warning('您已离开考试的程序,考试结束');
+        this.computedScore();
+        // TODO:人脸对比监控,如果出现问题,终止考试
+      });
+    },
+  },
+  computed: {
+    ...mapState(['checkResult', 'isExaming']),
+  },
+  watch: {
+    checkResult: {
+      handler(val) {
+        if (!val) {
+          this.$message.error('因考生和当前身份证人像不匹配,禁止答卷');
+          this.computedScore();
+        }
+      },
+    },
+  },
+  beforeDestroy() {
+    // 销毁离开的监听
+    this.$electron.ipcRenderer.removeAllListeners(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT);
+  },
+  beforeRouteLeave(to, form, next) {
+    // 离开,重置所有的变量
+    this.resetVar();
+    next();
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.main {
+  width: 100%;
+  height: 100vh;
+  overflow: hidden;
+  background-image: url('~@/assets/bg.jpg');
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  .one {
+    text-align: center;
+    margin: 30px 0;
+    font-size: 40px;
+    font-family: cursive;
+    font-weight: bold;
+    color: #33dac1;
+    text-shadow: 1px 1px 3px #666;
+  }
+  .two {
+    background-color: #ffffff9f;
+    height: 750px;
+    overflow-y: auto;
+    margin: 0px 20%;
+    width: 60%;
+    padding: 15px;
+    border-radius: 10px;
+    box-shadow: 0 0 10px #33dac1;
+    .two_1 {
+      .list {
+        border: 1px dashed #f1f1f1;
+        margin: 0 0 10px 0;
+        border-radius: 10px;
+        padding: 10px;
+        .index {
+          margin: 0 0 5px 0;
+          font-weight: bold;
+        }
+      }
+    }
+    .two_2 {
+      text-align: center;
+    }
+  }
+}
+</style>

+ 103 - 0
src/views/login copy.vue

@@ -0,0 +1,103 @@
+<template>
+  <div id="login">
+    <el-form :model="form" label-position="left" label-width="80px" style="padding: 20px" v-if="view !== 'isLogin'">
+      <el-form-item label="身份证号" prop="card" :required="true">
+        <el-input v-model="form.card" :readonly="true" placeholder="请扫描身份证号"></el-input>
+      </el-form-item>
+      <el-form-item label="准考证号" prop="exam_num" :required="true">
+        <el-input v-model="form.exam_num" placeholder="请输入准考证号"></el-input>
+      </el-form-item>
+      <el-form-item label="考场编号" prop="testsite_num" :required="true">
+        <el-input v-model="form.testsite_num" placeholder="请输入准考证号"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="toSave()">登陆</el-button>
+      </el-form-item>
+    </el-form>
+    <div style="padding: 20px" v-else>
+      <el-descriptions title="核对信息" border :column="3">
+        <el-descriptions-item label="人像" :span="3">
+          <img :src="info.img" style="height: 128px; width: 128px" />
+        </el-descriptions-item>
+        <el-descriptions-item label="姓名">{{ info.name }}</el-descriptions-item>
+        <el-descriptions-item label="准考证号">{{ info.exam_num }}</el-descriptions-item>
+        <el-descriptions-item label="考场编号">{{ info.testsite_num }}</el-descriptions-item>
+        <el-descriptions-item label="性别">{{ info.gender }}</el-descriptions-item>
+        <el-descriptions-item label="联系电话">{{ info.phone }}</el-descriptions-item>
+        <el-descriptions-item label="考试日期">{{ info.exam_date }}</el-descriptions-item>
+        <el-descriptions-item label="考试时间">{{ info.exam_time }}</el-descriptions-item>
+        <el-descriptions-item label="考试地址">{{ info.exam_addr }}</el-descriptions-item>
+        <el-descriptions-item label="座位号">{{ info.seat_num }}</el-descriptions-item>
+        <el-descriptions-item label="考试等级">{{ info.exam_grade }}</el-descriptions-item>
+        <el-descriptions-item label="考试类型">{{ info.exam_type }}</el-descriptions-item>
+        <el-descriptions-item label="缴费状况">{{ info.is_money }}</el-descriptions-item>
+      </el-descriptions>
+      <el-row type="flex" justify="space-around" style="padding: 20px">
+        <el-col :span="4">
+          <el-button type="primary" @click="toStart">开始考试</el-button>
+        </el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions } = createNamespacedHelpers('examination_examinee');
+const { mapActions: security_guard_collect } = createNamespacedHelpers('security_guard_collect');
+
+export default {
+  name: 'login',
+  components: {},
+  data: function () {
+    return {
+      view: 'login',
+      form: {
+        // card: '220182199603257019',
+        // exam_num: '2201002021002001',
+        // testsite_num: '2201002021002',
+      },
+      rules: {},
+      info: {},
+      fullscreen: false,
+    };
+  },
+  created() {},
+  mounted() {},
+  methods: {
+    ...mapActions(['fetch']),
+    ...security_guard_collect(['query']),
+    async toSave() {
+      // 查询出这个人的信息;以及考场信息,后三位是座位号.剩下的就是考场号
+      let dup = _.cloneDeep(this.form);
+      const res = await this.fetch({ ...dup, status: '2' });
+      if (this.$checkRes(res, null, '查询失败,请稍后尝试!')) {
+        if (!res.data) {
+          this.$message.error('未找到考生信息,请重新核对信息!');
+          return;
+        }
+        this.$set(this, `info`, res.data);
+        const binfo = await this.query({ security_guard_id: res.data.security_guard_id, type: '人像' });
+        if (this.$checkRes(binfo)) {
+          if (_.isArray(binfo.data)) {
+            const head = _.head(binfo.data);
+            if (head) {
+              this.info.img = `${process.env.VUE_APP_AXIOS_BASE_URL}${head.data}`;
+            }
+          }
+        }
+        this.view = 'isLogin';
+        sessionStorage.setItem('user', JSON.stringify(_.cloneDeep(this.info)));
+      }
+    },
+    toStart() {
+      this.$router.push('/exam');
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 175 - 0
src/views/login.vue

@@ -0,0 +1,175 @@
+<template>
+  <div id="login">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="one" v-if="view !== 'isLogin'">
+          <el-col :span="24" class="one_1">长春市保安服务监管系统-考试系统</el-col>
+          <el-col :span="24" class="one_2">
+            <el-form ref="form" :model="form" :rules="rules" label-position="left" label-width="80px" style="padding: 20px" v-if="view !== 'isLogin'">
+              <el-form-item label="身份证号" prop="card" :required="true">
+                <el-input v-model="form.card" :readonly="true" placeholder="请扫描身份证号">
+                  <template #append>
+                    <el-button @click="toScanIdCard()">点击扫描身份证</el-button>
+                  </template>
+                </el-input>
+              </el-form-item>
+              <el-form-item label="准考证号" prop="exam_num" :required="true">
+                <el-input v-model="form.exam_num" placeholder="请输入准考证号"></el-input>
+              </el-form-item>
+              <el-form-item label="考点编号" prop="testsite_num" :required="true">
+                <el-input v-model="form.testsite_num" placeholder="请输入准考证号"></el-input>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" size="small" @click="toSave()">登陆</el-button>
+              </el-form-item>
+            </el-form>
+          </el-col>
+        </el-col>
+        <el-col :span="24" class="two" v-else>
+          <el-col :span="24" class="two_1">核对信息</el-col>
+          <el-col :span="24" class="two_2">
+            <el-descriptions title="" border :column="3">
+              <el-descriptions-item label="人像" :span="3">
+                <img :src="info.img" style="height: 192px; width: 192px" />
+              </el-descriptions-item>
+              <el-descriptions-item label="姓名">{{ info.name }}</el-descriptions-item>
+              <el-descriptions-item label="准考证号">{{ info.exam_num }}</el-descriptions-item>
+              <el-descriptions-item label="考点编号">{{ info.testsite_num }}</el-descriptions-item>
+              <el-descriptions-item label="性别">{{ info.gender }}</el-descriptions-item>
+              <el-descriptions-item label="联系电话">{{ info.phone }}</el-descriptions-item>
+              <el-descriptions-item label="考试日期">{{ info.exam_date }}</el-descriptions-item>
+              <el-descriptions-item label="考试时间">{{ info.exam_time }}</el-descriptions-item>
+              <el-descriptions-item label="考试地址">{{ info.exam_addr }}</el-descriptions-item>
+              <el-descriptions-item label="座位号">{{ info.seat_num }}</el-descriptions-item>
+              <el-descriptions-item label="考试等级">{{ info.exam_grade }}</el-descriptions-item>
+              <el-descriptions-item label="考试类型">{{ info.exam_type }}</el-descriptions-item>
+              <el-descriptions-item label="缴费状况">{{ info.is_money }}</el-descriptions-item>
+            </el-descriptions>
+            <el-row type="flex" justify="space-around" style="padding: 20px">
+              <el-col :span="4">
+                <el-button type="primary" @click="toStart" :disabled="!checkResult">开始考试</el-button>
+              </el-col>
+            </el-row>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions } = createNamespacedHelpers('examination_examinee');
+const { mapActions: security_guard_collect } = createNamespacedHelpers('security_guard_collect');
+export default {
+  name: 'login',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      view: 'login',
+      form: {
+        // card: '22010319950601161X',
+        // exam_num: '2201002022001001',
+        // testsite_num: '2201002022001',
+      },
+      rules: {
+        card: [{ required: true, message: '请扫描身份证号', trigger: 'blur' }],
+        exam_num: [{ required: true, message: '请输入准考证号', trigger: 'blur' }],
+        testsite_num: [{ required: true, message: '请输入准考证号', trigger: 'blur' }],
+      },
+      info: {},
+    };
+  },
+  created() {},
+  mounted() {},
+  methods: {
+    ...mapActions(['fetch']),
+    ...security_guard_collect(['query']),
+    toSave() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          this.save();
+        } else {
+          return false;
+        }
+      });
+    },
+    async save() {
+      // 查询出这个人的信息;以及考点信息,后三位是座位号.剩下的就是考点号
+      let dup = _.cloneDeep(this.form);
+      const res = await this.fetch({ ...dup, status: '2' });
+      if (this.$checkRes(res, null, '查询失败,请稍后尝试!')) {
+        if (!res.data) {
+          this.$message.error('未找到考生信息,请重新核对信息!');
+          return;
+        }
+        this.$set(this, `info`, res.data);
+        const binfo = await this.query({ security_guard_id: res.data.security_guard_id, type: '人像' });
+        if (this.$checkRes(binfo)) {
+          if (_.isArray(binfo.data)) {
+            const head = _.head(binfo.data);
+            if (head) {
+              this.info.img = `${process.env.VUE_APP_AXIOS_BASE_URL}${head.data}`;
+            }
+          }
+          this.view = 'isLogin';
+          sessionStorage.setItem('user', JSON.stringify(_.cloneDeep(this.info)));
+        }
+      }
+    },
+
+    toScanIdCard() {
+      this.$emit('toScan');
+    },
+
+    toStart() {
+      this.$router.push('/exam');
+    },
+  },
+  computed: {
+    ...mapState(['checkResult']),
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.main {
+  background: url('~@/assets/img/login-bg.jpg');
+  height: 100vh;
+  background-repeat: no-repeat;
+  background-size: cover;
+  .one {
+    padding: 10% 37%;
+    .one_1 {
+      text-align: center;
+      font-size: 20px;
+      font-weight: bold;
+      margin: 30px 0;
+    }
+    .one_2 {
+      width: 500px;
+      height: 300px;
+      border: 1px solid #409eff;
+      border-radius: 10px;
+    }
+  }
+  .two {
+    padding: 10% 20%;
+    .two_1 {
+      text-align: center;
+      font-size: 20px;
+      font-weight: bold;
+      margin: 30px 0;
+    }
+    .two_2 {
+      padding: 10px;
+      border: 1px solid #409eff;
+      border-radius: 10px;
+    }
+  }
+}
+</style>

+ 271 - 0
src/views/parts/face.vue

@@ -0,0 +1,271 @@
+<template>
+  <div class="webrtc_face_recognition">
+    <div class="option">
+      <div>
+        <label>面板操作:</label>
+        <button @click="fnOpen">启动摄像头视频媒体</button>
+        <button @click="fnClose">结束摄像头视频媒体</button>
+        相识度:{{ distance }}
+      </div>
+    </div>
+    <div class="see">
+      <video id="myVideo" muted loop playsinline @loadedmetadata="fnRun"></video>
+      <canvas id="myCanvas" />
+      <div id="catch"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as faceapi from 'face-api.js';
+export default {
+  name: 'WebRTCFaceRecognition',
+  data() {
+    return {
+      nets: 'tinyFaceDetector', // 模型
+      options: null, // 模型参数
+      withBoxes: true, // 框or轮廓
+      detectFace: 'detectSingleFace', // 单or多人脸
+      detection: 'landmark',
+      videoEl: null,
+      canvasEl: null,
+      timeout: 0,
+      // 视频媒体参数配置
+      constraints: {
+        audio: false,
+        video: {
+          // ideal(应用最理想的)
+          width: {
+            min: 320,
+            ideal: 1280,
+            max: 1920,
+          },
+          height: {
+            min: 240,
+            ideal: 720,
+            max: 1080,
+          },
+          // frameRate受限带宽传输时,低帧率可能更适宜
+          frameRate: {
+            min: 15,
+            ideal: 30,
+            max: 60,
+          },
+          // 显示模式前置后置
+          facingMode: 'environment',
+        },
+      },
+      faceList: [],
+      distance: 0,
+    };
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.fnInit();
+    });
+  },
+  methods: {
+    // 初始化模型加载
+    async fnInit() {
+      faceapi.env.monkeyPatch({
+        Canvas: HTMLCanvasElement,
+        Image: HTMLImageElement,
+        ImageData: ImageData,
+        Video: HTMLVideoElement,
+        createCanvasElement: () => document.createElement('canvas'),
+        createImageElement: () => document.createElement('img'),
+      });
+      await faceapi.nets[this.nets].loadFromUri('/models'); // 算法模型
+      await faceapi.loadFaceLandmarkModel('/models'); // 轮廓模型
+      await faceapi.loadFaceRecognitionModel('/models'); // 人脸对比
+      // 根据算法模型参数识别调整结果
+      switch (this.nets) {
+        case 'ssdMobilenetv1':
+          this.options = new faceapi.SsdMobilenetv1Options({
+            minConfidence: 0.5, // 0.1 ~ 0.9
+          });
+          break;
+        case 'tinyFaceDetector':
+          this.options = new faceapi.TinyFaceDetectorOptions({
+            inputSize: 512, // 160 224 320 416 512 608
+            scoreThreshold: 0.5, // 0.1 ~ 0.9
+          });
+          break;
+        case 'mtcnn':
+          this.options = new faceapi.MtcnnOptions({
+            minFaceSize: 20, // 0.1 ~ 0.9
+            scaleFactor: 0.709, // 0.1 ~ 0.9
+          });
+          break;
+      }
+      // 节点属性化
+      this.videoEl = document.getElementById('myVideo');
+      this.canvasEl = document.getElementById('myCanvas');
+    },
+    // 人脸面部勘探轮廓识别绘制
+    async fnRunFaceLandmark() {
+      if (this.videoEl.paused) return clearTimeout(this.timeout);
+      // 识别绘制人脸信息
+      const result = await faceapi[this.detectFace](this.videoEl, this.options).withFaceLandmarks();
+      if (result && !this.videoEl.paused) {
+        const dims = faceapi.matchDimensions(this.canvasEl, this.videoEl, true);
+        const resizeResult = faceapi.resizeResults(result, dims);
+        this.withBoxes ? faceapi.draw.drawDetections(this.canvasEl, resizeResult) : faceapi.draw.drawFaceLandmarks(this.canvasEl, resizeResult);
+        this.takePhoto();
+      } else {
+        this.canvasEl.getContext('2d').clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+      }
+      this.timeout = setTimeout(() => this.fnRunFaceLandmark());
+    },
+    async takePhoto() {
+      //获得Canvas对象
+      let canvas = document.createElement('canvas');
+      let ctx = canvas.getContext('2d');
+      let video = this.videoEl;
+      ctx.drawImage(video, 0, 0, 400, 400);
+      let imgsrc = canvas.toDataURL(this.picType);
+      const res = this.translateBase64ImgToFile(imgsrc, `temp-${new Date().getTime()}`, this.picType);
+      let afterDeal = await faceapi.bufferToImage(res);
+      const face = await this.getFace(afterDeal);
+      // const face = canvas;
+      if (this.faceList.length === 2 && face) {
+        this.faceList = [];
+        // document.getElementById('catch').innerHTML = '';
+      }
+      if (face) this.faceList.push(face);
+      this.getResult();
+      // this.closeVideo();
+    },
+    /** 提取人脸 */
+    async getFace(c) {
+      let canvasBoxEl = document.getElementById('catch');
+      // this.canvasEl.appendChild(c);
+      const detections = await faceapi.detectAllFaces(c, this.options);
+      console.log(detections);
+      const faceImages = await faceapi.extractFaces(c, detections);
+      console.log(faceImages);
+      if (detections.length <= 0 || faceImages.length <= 0) return;
+      faceImages.forEach((canvas) => canvasBoxEl.appendChild(canvas));
+      canvasBoxEl.appendChild(document.createElement('HR'));
+      return _.head(faceImages);
+    },
+    /**
+     * 识别度
+     */
+    async getResult() {
+      if (this.faceList.length !== 2) return;
+      const list = [await faceapi.computeFaceDescriptor(this.faceList[0]), await faceapi.computeFaceDescriptor(this.faceList[1])];
+      // 图片误差值,越小越精确
+      this.distance = faceapi.euclideanDistance(list[0], list[1]).toFixed(2);
+    },
+    /**
+     * Base64转File
+     * @param base64 String base64格式字符串
+     * @param contentType String file对象的文件类型,如:"image/png"
+     * @param filename String 文件名称或者文件路径
+     */
+    translateBase64ImgToFile(base64, filename, contentType) {
+      let arr = base64.split(','); //去掉base64格式图片的头部
+      let bstr = atob(arr[1]); //atob()方法将数据解码
+      let leng = bstr.length;
+      let u8arr = new Uint8Array(leng);
+      while (leng--) {
+        u8arr[leng] = bstr.charCodeAt(leng); //返回指定位置的字符的 Unicode 编码
+      }
+      return new File([u8arr], filename, { type: contentType });
+    },
+    // 执行检测识别类型
+    fnRun() {
+      if (this.detection === 'landmark') {
+        this.fnRunFaceLandmark();
+        return;
+      }
+      if (this.detection === 'expression') {
+        this.$message.error('未加载该模块');
+        return;
+      }
+      if (this.detection === 'age_gender') {
+        this.$message.error('未加载该模块');
+        return;
+      }
+    },
+    // 启动摄像头视频媒体
+    fnOpen() {
+      if (typeof window.stream === 'object') return;
+      clearTimeout(this.timeout);
+      this.timeout = setTimeout(() => {
+        clearTimeout(this.timeout);
+        navigator.mediaDevices.getUserMedia(this.constraints).then(this.fnSuccess).catch(this.fnError);
+      }, 300);
+    },
+    // 成功启动视频媒体流
+    fnSuccess(stream) {
+      window.stream = stream; // 使流对浏览器控制台可用
+      this.videoEl.srcObject = stream;
+      this.videoEl.play();
+    },
+    // 失败启动视频媒体流
+    fnError(error) {
+      console.log(error);
+      alert('视频媒体流获取错误' + error);
+    },
+    // 结束摄像头视频媒体
+    fnClose() {
+      this.canvasEl.getContext('2d').clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+      this.videoEl.pause();
+      clearTimeout(this.timeout);
+      if (typeof window.stream === 'object') {
+        window.stream.getTracks().forEach((track) => track.stop());
+        window.stream = '';
+        this.videoEl.srcObject = null;
+      }
+    },
+  },
+  beforeDestroy() {
+    this.fnClose();
+  },
+  watch: {
+    nets(val) {
+      this.nets = val;
+      this.fnInit();
+    },
+    detection(val) {
+      this.detection = val;
+      this.videoEl.pause();
+      setTimeout(() => {
+        this.videoEl.play();
+        setTimeout(() => this.fnRun(), 300);
+      }, 300);
+    },
+  },
+};
+</script>
+
+<style scoped>
+button {
+  height: 30px;
+  border: 2px #42b983 solid;
+  border-radius: 4px;
+  background: #42b983;
+  color: white;
+  margin: 10px;
+}
+.see {
+  position: relative;
+}
+.see canvas {
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+.option {
+  padding-bottom: 20px;
+}
+.option div {
+  padding: 10px;
+  border-bottom: 2px #42b983 solid;
+}
+.option div label {
+  margin-right: 20px;
+}
+</style>

+ 161 - 0
src/views/parts/gpy.vue

@@ -0,0 +1,161 @@
+<template>
+  <div id="gpy">
+    <div class="videoFrame">
+      <img id="video" class="videoElement" />
+    </div>
+    <!-- <el-button type="primary" @click="openVideo()">开人像摄像头</el-button>
+    <el-button type="primary" @click="closeVideo()">关人像摄像头</el-button> -->
+    <!-- <el-button type="primary" @click="getIdCard()">身份证采集</el-button>
+    <el-button type="primary" @click="getImg()">采集图像</el-button>
+    <el-button type="primary" :disabled="!canCompare()" @click="toCompare()">人证比对</el-button> -->
+  </div>
+</template>
+
+<script>
+const _ = require('lodash');
+const moment = require('moment');
+import { mapState, mapMutations } from 'vuex';
+export default {
+  name: 'gpy',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      base64Prefix: 'data:image/png;base64,',
+      idCard: {},
+      catchPic: '',
+      loopTime: 5000, // 单位:ms
+      trueLimit: 0.7, // 70%
+      computedTimes: 10, //10次抓取对比,得一次结论
+    };
+  },
+  created() {},
+  methods: {
+    ...mapMutations(['setCheckResult', 'setTimer', 'setRecord']),
+    // 获取身份证信息
+    getIdCard() {
+      return new Promise((resolve, reject) => {
+        $.ajax({
+          url: `http://127.0.0.1:38088/card=idcard`,
+          type: 'post',
+          dataType: 'json',
+          data: {},
+          success: (res) => {
+            if (res.code != 0) {
+              resolve();
+            } else {
+              this.$set(this, `idCard`, res.IDCardInfo);
+              resolve(res.IDCardInfo);
+              // 打开摄像头,开始抓人脸,然后根据规则,决定是不是同一人
+              this.checkPersonLoop();
+            }
+          },
+        });
+      });
+    },
+    // 循环抓人像事件注册
+    checkPersonLoop() {
+      this.getImg(true);
+      if (this.timer) clearInterval(this.timer);
+      let timer = setInterval(() => {
+        // 不在考试时,就不抓了
+        if (!this.isExaming) return;
+        this.getImg();
+        const total = this.resultList.length;
+        if (total % this.computedTimes === 0) this.setResult();
+      }, this.loopTime);
+      this.setTimer(timer);
+    },
+    // 抓人像
+    getImg(setResult = false) {
+      // 确保摄像头开启着
+      this.openVideo();
+      $.ajax({
+        url: `http://127.0.0.1:38088/video=grabimage`,
+        type: 'post',
+        dataType: 'json',
+        data: '{"filepath":"base64","camidx":"1"}',
+        success: (res) => {
+          if (res.code != 0) {
+          } else {
+            this.$set(this, `catchPic`, res.photoBase64);
+            if (this.canCompare()) this.toCompare(setResult);
+          }
+        },
+      });
+    },
+    //开人像摄像头
+    openVideo() {
+      document.getElementById('video').src = 'http://127.0.0.1:38088/video=stream&camidx=1';
+    },
+    // 关人像摄像头
+    closeVideo() {
+      $.ajax({
+        url: `http://127.0.0.1:38088/video=close`,
+        type: 'post',
+        dataType: 'json',
+        data: '{"camidx":"1"}',
+        success: (res) => {
+          document.getElementById('video').src = '';
+        },
+      });
+    },
+    // 检查变量,确定是否可以进行比对
+    canCompare() {
+      const idPhoto = _.get(this.idCard, 'photoBase64');
+      const catchPhoto = this.catchPic;
+      return idPhoto && catchPhoto;
+    },
+    // 认证比对
+    toCompare(setResult = false) {
+      $.ajax({
+        url: `http://127.0.0.1:38088/comparison=imgdata`,
+        type: 'post',
+        dataType: 'json',
+        data: '{"FaceOne":"' + this.catchPic + '","FaceTwo":"' + _.get(this.idCard, 'photoBase64') + '"}',
+        success: (res) => {
+          if (res.code != 0) {
+          } else {
+            const obj = { time: moment().format('YYYY-MM-DD HH:mm:ss'), num: res.data };
+            if (parseInt(res.data) > 50) obj.result = true;
+            else obj.result = false;
+            this.setRecord(obj);
+            // TODO,记录结果,照片
+            if (setResult) this.setResult();
+          }
+        },
+      });
+    },
+    // 设置检查结果
+    setResult() {
+      // 检查检测成功次数 是否占 总检查次数的 ${trueLimit} 及其以上:
+      // 若高于基准线,则检查成功;反之失败
+      const trueList = this.resultList.filter((f) => f.result);
+      const trueNum = trueList.length;
+      const total = this.resultList.length;
+      const truePercent = _.divide(trueNum, total);
+      let res = true;
+      if (truePercent <= this.trueLimit) res = false;
+      console.log(this.resultList);
+      console.log(this.trueList);
+      console.log(res);
+      this.setCheckResult(res);
+    },
+  },
+  computed: {
+    ...mapState(['isExaming', 'timer', 'resultList']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.videoFrame {
+  position: fixed;
+  top: 0;
+  right: 0;
+  .videoElement {
+    width: 12.5rem;
+    height: 12.5rem;
+  }
+}
+</style>

+ 35 - 0
src/views/test.vue

@@ -0,0 +1,35 @@
+<template>
+  <div id="test">
+    <p>test</p>
+    <el-button @click="$router.push('/')">返回</el-button>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'test',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {
+    this.$electron.ipcRenderer.removeAllListeners(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT);
+    // 建立离开的监听
+    const ipcRenderer = this.$electron.ipcRenderer;
+    ipcRenderer.on(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT, (e, args) => {
+      console.log('进程接收');
+      // TODO,接着去干别的事,或停止考试,或累加次数
+    });
+  },
+  methods: {},
+  computed: {},
+  beforeDestroy() {
+    // 销毁离开的监听
+    this.$electron.ipcRenderer.removeAllListeners(process.env.VUE_APP_VIEW_LEAVE_VIEW_EVENT);
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 64 - 0
vue.config.js

@@ -0,0 +1,64 @@
+const path = require('path');
+const common = path.resolve(__dirname, '../baoan-common');
+module.exports = {
+  // publicPath: `/${process.env.VUE_APP_ROUTER}`,
+  publicPath: `/`,
+  outputDir: process.env.VUE_APP_ROUTER,
+  productionSourceMap: false,
+  configureWebpack: (config) => {
+    Object.assign(config, {
+      resolve: {
+        alias: {
+          '@': path.resolve(__dirname, './src'),
+          '@c': path.resolve(__dirname, './src/components'),
+          '@a': path.resolve(__dirname, './src/assets'),
+          '@common': common,
+        },
+      },
+    });
+  },
+  pluginOptions: {
+    electronBuilder: {
+      nodeIntegration: true,
+      outputDir: 'electron_bulid', //打包后输出的目录名
+      builderOptions: {
+        productName: '保安考试', //包名
+        appId: 'com.baoan.exam', //项目id
+        files: ['**/*'],
+        extraFiles: [
+          {
+            from: './resource/*', // 项目资源
+            to: './resource', // 打包后输出到的按照目录资源
+          },
+        ],
+        win: {
+          icon: './public/logo.ico',
+        },
+      },
+    },
+  },
+
+  devServer: {
+    port: '11203',
+    // proxy: {
+    //   '/files': {
+    //     target: 'http://106.12.161.200', //http://baoan.fwedzgc.com:8090
+    //   },
+    //   '/api/position': {
+    //     target: 'http://106.12.161.200',
+    //     changeOrigin: true,
+    //     ws: false,
+    //   },
+    //   '/api/exam': {
+    //     target: 'http://127.0.0.1:6104',
+    //     changeOrigin: true,
+    //     ws: false,
+    //   },
+    //   '/api': {
+    //     target: 'http://127.0.0.1:6100',
+    //     changeOrigin: true,
+    //     ws: false,
+    //   },
+    // },
+  },
+};