zhangjie преди 3 години
родител
ревизия
0b36d69fff

+ 68 - 67
package.json

@@ -1,67 +1,68 @@
-{
-  "name": "ecs-web-admin",
-  "version": "0.1.0",
-  "private": true,
-  "scripts": {
-    "start": "vue-cli-service serve --port 7002",
-    "serve": "vue-cli-service serve",
-    "build:dev": "vue-cli-service build",
-    "build:test": "vue-cli-service build",
-    "prebuild:prod": "node prebuild",
-    "build:prod": "vue-cli-service build",
-    "postbuild:prod": "IS_PROD=true node postbuild",
-    "lint": "vue-cli-service lint",
-    "test:unit": "vue-cli-service test:unit"
-  },
-  "dependencies": {
-    "axios": "^0.21.1",
-    "axios-progress-bar": "^1.2.0",
-    "bootstrap": "^4.6.0",
-    "echarts": "^4.9.0",
-    "element-ui": "^2.15.0",
-    "lodash": "^4.17.15",
-    "moment": "^2.29.1",
-    "print-js": "^1.0.61",
-    "randomcolor": "^0.6.2",
-    "rasterizehtml": "^1.3.0",
-    "register-service-worker": "^1.7.2",
-    "spark-md5": "^3.0.1",
-    "viewerjs": "^1.9.0",
-    "vue": "^2.6.12",
-    "vue-awesome": "^4.1.0",
-    "vue-echarts": "^4.1.0",
-    "vue-router": "^3.5.1",
-    "vuex": "^3.6.2"
-  },
-  "devDependencies": {
-    "@vue/cli-plugin-babel": "~4.5.11",
-    "@vue/cli-plugin-eslint": "~4.5.11",
-    "@vue/cli-plugin-pwa": "~4.5.11",
-    "@vue/cli-plugin-router": "~4.5.11",
-    "@vue/cli-plugin-unit-jest": "~4.5.11",
-    "@vue/cli-plugin-vuex": "~4.5.11",
-    "@vue/cli-service": "~4.5.11",
-    "@vue/eslint-config-prettier": "^6.0.0",
-    "@vue/test-utils": "^1.0.3",
-    "babel-eslint": "^10.1.0",
-    "eslint": "^7.15.0",
-    "eslint-plugin-prettier": "^3.3.0",
-    "eslint-plugin-vue": "^7.3.0",
-    "lint-staged": "^10.5.3",
-    "prettier": "^2.2.1",
-    "sass": "^1.30.0",
-    "sass-loader": "^10.1.0",
-    "vue-cli-plugin-axios": "0.0.4",
-    "vue-cli-plugin-element": "^1.0.1",
-    "vue-template-compiler": "^2.6.12"
-  },
-  "gitHooks": {
-    "pre-commit": "lint-staged"
-  },
-  "lint-staged": {
-    "*.{js,jsx,vue}": [
-      "vue-cli-service lint",
-      "git add"
-    ]
-  }
-}
+{
+  "name": "ecs-web-admin",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "start": "vue-cli-service serve --port 7002",
+    "serve": "vue-cli-service serve",
+    "build:dev": "vue-cli-service build",
+    "build:test": "vue-cli-service build",
+    "prebuild:prod": "node prebuild",
+    "build:prod": "vue-cli-service build",
+    "postbuild:prod": "IS_PROD=true node postbuild",
+    "lint": "vue-cli-service lint",
+    "test:unit": "vue-cli-service test:unit"
+  },
+  "dependencies": {
+    "axios": "^0.21.1",
+    "axios-progress-bar": "^1.2.0",
+    "bootstrap": "^4.6.0",
+    "echarts": "^4.9.0",
+    "element-ui": "^2.15.0",
+    "js-md5": "^0.7.3",
+    "lodash": "^4.17.15",
+    "moment": "^2.29.1",
+    "print-js": "^1.0.61",
+    "randomcolor": "^0.6.2",
+    "rasterizehtml": "^1.3.0",
+    "register-service-worker": "^1.7.2",
+    "spark-md5": "^3.0.1",
+    "viewerjs": "^1.9.0",
+    "vue": "^2.6.12",
+    "vue-awesome": "^4.1.0",
+    "vue-echarts": "^4.1.0",
+    "vue-router": "^3.5.1",
+    "vuex": "^3.6.2"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.11",
+    "@vue/cli-plugin-eslint": "~4.5.11",
+    "@vue/cli-plugin-pwa": "~4.5.11",
+    "@vue/cli-plugin-router": "~4.5.11",
+    "@vue/cli-plugin-unit-jest": "~4.5.11",
+    "@vue/cli-plugin-vuex": "~4.5.11",
+    "@vue/cli-service": "~4.5.11",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "@vue/test-utils": "^1.0.3",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^7.15.0",
+    "eslint-plugin-prettier": "^3.3.0",
+    "eslint-plugin-vue": "^7.3.0",
+    "lint-staged": "^10.5.3",
+    "prettier": "^2.2.1",
+    "sass": "^1.30.0",
+    "sass-loader": "^10.1.0",
+    "vue-cli-plugin-axios": "0.0.4",
+    "vue-cli-plugin-element": "^1.0.1",
+    "vue-template-compiler": "^2.6.12"
+  },
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.{js,jsx,vue}": [
+      "vue-cli-service lint",
+      "git add"
+    ]
+  }
+}

+ 347 - 336
src/assets/styles/base.scss

@@ -1,336 +1,347 @@
-/* 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-placeholder;
-}
-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-secondary;
-}
-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-primary;
-  background-color: $--color-background;
-}
-
-/* part */
-.part-box {
-  margin-bottom: 20px;
-  background-color: $--color-white;
-  border-radius: 20px;
-  padding: 30px;
-}
-.part-box-border {
-  border: 1px solid $--color-border;
-}
-
-.el-form.part-filter-form {
-  padding: 20px 0 5px;
-
-  /* element-ui*/
-  .el-form-item {
-    margin-bottom: 15px;
-    border: 1px solid $--color-border-dark;
-    border-radius: 5px;
-    display: inline-flex;
-    overflow: hidden;
-    justify-content: space-between;
-    align-items: center;
-    width: 220px;
-
-    &:last-child {
-      border: none;
-      width: auto;
-    }
-  }
-  .el-form-item__label {
-    margin: 0;
-    padding: 0 16px 0 12px;
-    color: $--color-text-secondary;
-    position: relative;
-    white-space: nowrap;
-
-    &::after {
-      content: "";
-      position: absolute;
-      height: 16px;
-      width: 4px;
-      right: 0;
-      top: 50%;
-      margin-top: -8px;
-      background-image: url(../images/icon-split.png);
-      background-size: 100% 100%;
-    }
-  }
-  .el-form-item__content {
-    flex-grow: 2;
-  }
-  .el-input__inner {
-    border: none;
-    padding-left: 9px;
-  }
-  .el-input.is-disabled {
-    .el-input__inner {
-      background-color: $--color-white;
-    }
-  }
-  .el-input-number--medium {
-    width: 100%;
-    .el-input__inner {
-      padding-left: 43px;
-    }
-  }
-}
-
-.part-box-title {
-  font-size: 20px;
-  line-height: 1;
-  padding-bottom: 20px;
-  border-bottom: 1px solid $--color-border;
-  margin: 0;
-  font-weight: 500;
-}
-.part-box-header {
-  padding-bottom: 20px;
-  border-bottom: 1px solid $--color-border;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  .part-box-title {
-    font-size: 20px;
-    line-height: 1;
-    margin: 0;
-    padding: 0;
-    border: none;
-  }
-}
-.part-box-action {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  border-top: 1px solid $--color-border;
-  padding-top: 20px;
-}
-.part-page {
-  margin-top: 15px;
-  text-align: right;
-}
-.part-none {
-  padding: 100px;
-  font-size: 20px;
-  color: #aaa;
-  text-align: center;
-}
-// ckeditor
-.ckeditor {
-  line-height: 24px;
-  .cke_textarea_inline {
-    min-height: 36px;
-    border: 1px solid $--color-border-dark;
-    border-radius: 5px;
-    padding: 5px 10px;
-    outline: none;
-    overflow: auto;
-  }
-  .cke_focus {
-    border-color: $--color-text-secondary;
-  }
-  .cke_button__image {
-    display: none !important;
-  }
-  p {
-    margin: 0;
-  }
-}
-.area-ckeditor {
-  .cke_textarea_inline {
-    min-height: 200px;
-  }
-}
-// other
-.box-justify {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-}
-.body-content {
-  margin: 15px;
-}
-.padding-tb-20 {
-  padding: 20px 0;
-}
-.padding-top-6 {
-  padding-top: 6px;
-}
-.padding-none {
-  padding: 0 !important;
-}
-.margin-right-5 {
-  margin-right: 5px;
-}
-.margin-right-10 {
-  margin-right: 10px;
-}
-.margin-left-10 {
-  margin-left: 10px;
-}
-.margin-bottom-15 {
-  margin-bottom: 15px;
-}
-.margin-tb-20 {
-  margin: 20px 0;
-}
-.line-seperator {
-  border-bottom: 1px solid $--color-border;
-  margin: 15px 0;
-}
-.tiny-btn {
-  padding: 0;
-  height: 28px;
-  width: 28px;
-  line-height: 28px;
-  border-radius: 6px;
-  border: none;
-  background-color: $--color-border-dark;
-  font-weight: 600;
-  font-size: 14px;
-}
-.tips-info {
-  color: $--color-text-secondary;
-  height: 25px;
-  line-height: 25px;
-}
-.dialog-input-width {
-  width: 200px;
-}
-.pull_length {
-  width: 180px;
-}
-.select_width {
-  width: 150px;
-}
-.search_width {
-  width: 150px;
-}
-.search_width_80px {
-  width: 80px;
-}
-.search_width_120px {
-  width: 120px;
-}
-.form_width {
-  width: 200px;
-}
-
-.margin_top_10 {
-  margin-top: 10px;
-}
-.margin_left_10 {
-  margin-left: 10px;
-}
+/* 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-placeholder;
+}
+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-secondary;
+}
+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-primary;
+  background-color: $--color-background;
+}
+
+/* part */
+.part-box {
+  margin-bottom: 20px;
+  background-color: $--color-white;
+  border-radius: 20px;
+  padding: 30px;
+}
+.part-box-border {
+  border: 1px solid $--color-border;
+}
+
+.el-form.part-filter-form {
+  padding: 20px 0 5px;
+
+  /* element-ui*/
+  .el-form-item {
+    margin-bottom: 15px;
+    border: 1px solid $--color-border-dark;
+    border-radius: 5px;
+    display: inline-flex;
+    overflow: hidden;
+    justify-content: space-between;
+    align-items: center;
+    width: 220px;
+
+    &:last-child {
+      border: none;
+      width: auto;
+    }
+  }
+  .el-form-item__label {
+    margin: 0;
+    padding: 0 16px 0 12px;
+    color: $--color-text-secondary;
+    position: relative;
+    white-space: nowrap;
+
+    &::after {
+      content: "";
+      position: absolute;
+      height: 16px;
+      width: 4px;
+      right: 0;
+      top: 50%;
+      margin-top: -8px;
+      background-image: url(../images/icon-split.png);
+      background-size: 100% 100%;
+    }
+  }
+  .el-form-item__content {
+    flex-grow: 2;
+  }
+  .el-input__inner {
+    border: none;
+    padding-left: 9px;
+  }
+  .el-input.is-disabled {
+    .el-input__inner {
+      background-color: $--color-white;
+    }
+  }
+  .el-input-number--medium {
+    width: 100%;
+    .el-input__inner {
+      padding-left: 43px;
+    }
+  }
+}
+
+.part-box-title {
+  font-size: 20px;
+  line-height: 1;
+  padding-bottom: 20px;
+  border-bottom: 1px solid $--color-border;
+  margin: 0;
+  font-weight: 500;
+}
+.part-box-header {
+  padding-bottom: 20px;
+  border-bottom: 1px solid $--color-border;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  .part-box-title {
+    font-size: 20px;
+    line-height: 1;
+    margin: 0;
+    padding: 0;
+    border: none;
+  }
+}
+.part-box-action {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border-top: 1px solid $--color-border;
+  padding-top: 20px;
+}
+.part-page {
+  margin-top: 15px;
+  text-align: right;
+}
+.part-none {
+  padding: 100px;
+  font-size: 20px;
+  color: #aaa;
+  text-align: center;
+}
+// ckeditor
+.ckeditor {
+  line-height: 24px;
+  .cke_textarea_inline {
+    min-height: 36px;
+    border: 1px solid $--color-border-dark;
+    border-radius: 5px;
+    padding: 5px 10px;
+    outline: none;
+    overflow: auto;
+  }
+  .cke_focus {
+    border-color: $--color-text-secondary;
+  }
+  .cke_button__image {
+    display: none !important;
+  }
+  p {
+    margin: 0;
+  }
+}
+.area-ckeditor {
+  .cke_textarea_inline {
+    min-height: 200px;
+  }
+}
+// other
+.box-justify {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.body-content {
+  margin: 15px;
+}
+.padding-tb-20 {
+  padding: 20px 0;
+}
+.padding-top-6 {
+  padding-top: 6px;
+}
+.padding-none {
+  padding: 0 !important;
+}
+.margin-right-5 {
+  margin-right: 5px;
+}
+.margin-right-10 {
+  margin-right: 10px;
+}
+.margin-left-10 {
+  margin-left: 10px;
+}
+.margin-top-20 {
+  margin-top: 20px;
+}
+.margin-bottom-15 {
+  margin-bottom: 15px;
+}
+.margin-tb-20 {
+  margin: 20px 0;
+}
+.line-seperator {
+  border-bottom: 1px solid $--color-border;
+  margin: 15px 0;
+}
+.text-center {
+  text-align: center;
+}
+.tiny-btn {
+  padding: 0;
+  height: 28px;
+  width: 28px;
+  line-height: 28px;
+  border-radius: 6px;
+  border: none;
+  background-color: $--color-border-dark;
+  font-weight: 600;
+  font-size: 14px;
+}
+.tips-info {
+  color: $--color-text-secondary;
+  height: 25px;
+  line-height: 25px;
+}
+.tips-error {
+  color: $--color-danger;
+}
+.dialog-input-width {
+  width: 200px;
+}
+.pull_length {
+  width: 180px;
+}
+.select_width {
+  width: 150px;
+}
+.search_width {
+  width: 150px;
+}
+.search_width_80px {
+  width: 80px;
+}
+.search_width_120px {
+  width: 120px;
+}
+.form_width {
+  width: 200px;
+}
+.width-full {
+  width: 100%;
+}
+.margin_top_10 {
+  margin-top: 10px;
+}
+.margin_left_10 {
+  margin-left: 10px;
+}

+ 241 - 0
src/components/ImportFileDialog.vue

@@ -0,0 +1,241 @@
+<template>
+  <el-dialog
+    custom-class="side-dialog"
+    :visible.sync="modalIsShow"
+    :title="dialogTitle"
+    width="520px"
+    :modal="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-upload
+      ref="UploadComp"
+      :action="uploadUrl"
+      :headers="headers"
+      :max-size="maxSize"
+      :accept="accept"
+      :format="format"
+      :data="uploadDataDict"
+      :on-error="handleError"
+      :on-success="handleSuccess"
+      :on-change="fileChange"
+      :http-request="upload"
+      :show-file-list="false"
+      :disabled="loading"
+      :auto-upload="false"
+    >
+      <el-button
+        slot="trigger"
+        size="small"
+        type="primary"
+        icon="icon icon-search-white"
+        :disabled="loading"
+      >
+        选择文件
+      </el-button>
+      <el-button
+        size="small"
+        type="primary"
+        icon="icon icon-save-white"
+        :loading="loading"
+        :disabled="!fileValid"
+        @click="submitUpload"
+      >
+        确认上传
+      </el-button>
+      <el-button
+        size="small"
+        type="primary"
+        icon="icon icon-delete-white"
+        :disabled="loading"
+        @click="removeFile"
+      >
+        清空文件
+      </el-button>
+      <el-button
+        v-if="templateUrl"
+        size="small"
+        type="primary"
+        icon="icon icon-export-white"
+        @click="exportFile"
+      >
+        下载模板
+      </el-button>
+    </el-upload>
+    <p v-if="filename" class="tips-info">{{ filename }}</p>
+    <p v-if="!res.success" class="tips-info tips-error">{{ res.message }}</p>
+  </el-dialog>
+</template>
+
+<script>
+import { fileMD5 } from "@/plugins/md5";
+import { $httpWithMsg } from "@/plugins/axios";
+
+export default {
+  name: "ImportFileDialog",
+  props: {
+    dialogTitle: {
+      type: String,
+      default: "导入文件",
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["xlsx", "xls"];
+      },
+    },
+    accept: {
+      type: String,
+      default: null,
+    },
+    uploadUrl: {
+      type: String,
+      required: true,
+    },
+    uploadData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024,
+    },
+    addFilenameParam: {
+      type: String,
+      default: "filename",
+    },
+    templateUrl: {
+      type: String,
+      default: "",
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      headers: {
+        md5: "",
+      },
+      res: {},
+      loading: false,
+      uploadDataDict: {},
+      filename: "",
+      fileValid: false,
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.res = {};
+      this.loading = false;
+      this.uploadDataDict = {};
+      this.filename = "";
+      this.fileValid = false;
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    checkFileFormat(fileType) {
+      const _file_format = fileType.split(".").pop().toLowerCase();
+      return this.format.length
+        ? this.format.some((item) => item.toLowerCase() === _file_format)
+        : true;
+    },
+    fileChange(fileObj) {
+      if (fileObj.status === "ready") {
+        this.handleBeforeUpload(fileObj.raw).catch(() => {});
+      }
+    },
+    async handleBeforeUpload(file) {
+      this.res = {};
+      this.filename = file.name;
+      this.uploadDataDict = {
+        ...this.uploadData,
+      };
+      this.uploadDataDict[this.addFilenameParam] = file.name;
+
+      if (!this.checkFileFormat(file.name)) {
+        this.handleFormatError();
+        this.fileValid = false;
+        return Promise.reject();
+      }
+
+      if (file.size > this.maxSize) {
+        this.handleExceededSize();
+        this.fileValid = false;
+        return Promise.reject();
+      }
+
+      const md5 = await fileMD5(file);
+      this.headers["md5"] = md5;
+      this.fileValid = true;
+
+      return true;
+    },
+    upload(options) {
+      if (!options.file) return Promise.reject("文件丢失");
+
+      let formData = new FormData();
+      Object.entries(options.data).forEach(([k, v]) => {
+        formData.append(k, v);
+      });
+      formData.append("file", options.file);
+
+      return $httpWithMsg.post(options.action, formData, {
+        headers: options.headers,
+      });
+    },
+    handleError(error) {
+      this.loading = false;
+      this.res = {
+        success: false,
+        message: error.message,
+      };
+    },
+    handleSuccess() {
+      this.loading = false;
+      this.res = {
+        success: true,
+        message: "导入成功!",
+      };
+      this.cancel();
+      this.$emit("uploaded");
+    },
+    handleFormatError() {
+      const content = "只支持文件格式为" + this.format.join("/");
+      this.res = {
+        success: false,
+        message: content,
+      };
+    },
+    handleExceededSize() {
+      const content =
+        "文件大小不能超过" + Math.floor(this.maxSize / (1024 * 1024)) + "M";
+      this.res = {
+        success: false,
+        message: content,
+      };
+    },
+    // action
+    submitUpload() {
+      if (this.loading) return;
+      this.$refs.UploadComp.submit();
+      this.loading = true;
+    },
+    removeFile() {
+      if (this.loading) return;
+      this.uploadDataDict = {};
+      this.filename = "";
+      this.fileValid = false;
+      this.res = {};
+      this.$refs.UploadComp.clearFiles();
+    },
+    exportFile() {
+      window.location.href = this.templateUrl;
+    },
+  },
+};
+</script>

+ 3 - 0
src/main.js

@@ -12,6 +12,9 @@ import "./directives/directives.js";
 import "./filters/filters.js";
 import "./assets/styles/index.scss";
 
+import globalVuePlugins from "./plugins/globalVuePlugins";
+Vue.use(globalVuePlugins);
+
 Vue.config.productionTip = process.env.NODE_ENV !== "production";
 
 if (

+ 23 - 0
src/mixins/common.js

@@ -0,0 +1,23 @@
+export default {
+  methods: {
+    deletePageLastItem(len = 1) {
+      let page = this.currentPage || 1;
+      if (this.$refs.table.data.length === len) {
+        page = page > 1 ? page - 1 : 1;
+      }
+      this.handleCurrentChange && this.handleCurrentChange(page);
+    },
+    goback() {
+      this.$router.go(-1);
+    },
+    getRouterPath(location) {
+      const { href } = this.$router.resolve(location);
+      return href;
+    },
+    indexMethod(index) {
+      const currentPage = this.currentPage || 1;
+      const size = this.pageSize || 10;
+      return (currentPage - 1) * size + index + 1;
+    },
+  },
+};

+ 17 - 0
src/mixins/timeMixin.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 = [];
+    },
+  },
+};

+ 56 - 2
src/modules/card/api.js

@@ -1,6 +1,60 @@
-import { randomCode } from "./plugins/utils";
+import { $httpWithMsg } from "../../plugins/axios";
 import Vue from "vue";
+import { QUESTION_API } from "@/constants/constants.js";
 
+export const questionTeacherQueryApi = () => {
+  return $httpWithMsg.get(QUESTION_API + "/course/query");
+};
+export const courseQueryApi = (name) => {
+  return $httpWithMsg.get(QUESTION_API + "/course/query", {
+    params: { name },
+  });
+};
+// card-mamage
+export const cardListApi = (datas, { pageNo, pageSize }) => {
+  return $httpWithMsg.get(`${QUESTION_API}/card-list/${pageNo}/${pageSize}`, {
+    params: datas,
+  });
+};
+export const cardDeleteApi = (cardId) => {
+  return $httpWithMsg.get(QUESTION_API + "/card-list", {
+    params: { id: cardId },
+  });
+};
+export const cardEnableApi = ({ id, enable }) => {
+  return $httpWithMsg.get(QUESTION_API + "/card-list", {
+    params: { id, enable },
+  });
+};
+export const cardUpdateApi = (datas) => {
+  return $httpWithMsg.post(QUESTION_API + "/card-list", datas);
+};
+// card-head-manage
+export const cardHeadListApi = (datas, { pageNo, pageSize }) => {
+  return $httpWithMsg.get(`${QUESTION_API}/card-list/${pageNo}/${pageSize}`, {
+    params: datas,
+  });
+};
+export const cardHeadDeleteApi = (cardHeadId) => {
+  return $httpWithMsg.get(QUESTION_API + "/card-list", {
+    params: { id: cardHeadId },
+  });
+};
+export const cardHeadDetailApi = (cardHeadId) => {
+  return $httpWithMsg.get(QUESTION_API + "/card-list", {
+    params: { id: cardHeadId },
+  });
+};
+export const cardHeadUpdateApi = (datas) => {
+  return $httpWithMsg.post(QUESTION_API + "/card-list", datas);
+};
+export const cardHeadEnableApi = ({ id, enable }) => {
+  return $httpWithMsg.get(QUESTION_API + "/card-list", {
+    params: { id, enable },
+  });
+};
+
+// card-edit
 export const cardConfigInfos = () => {
   return Promise.resolve({
     id: "173438690998091776",
@@ -36,5 +90,5 @@ export const cardDetail = () => {
 
 export const saveCard = (datas) => {
   Vue.ls.set("cardData", datas);
-  return Promise.resolve(randomCode());
+  return Promise.resolve("11");
 };

+ 36 - 0
src/modules/card/assets/styles/card-page.scss

@@ -0,0 +1,36 @@
+// card-head-edit
+.card-head-edit {
+  .template-list {
+    font-size: 0;
+  }
+  .template-item {
+    display: inline-block;
+    vertical-align: top;
+    margin: 0 10px 10px 0;
+    &-body {
+      width: 240px;
+      height: 160px;
+      border-radius: 15px;
+      border: 1px solid $--color-border-dark;
+      overflow: hidden;
+      position: relative;
+
+      > img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+    }
+    &-action {
+      position: absolute;
+      bottom: 10px;
+      left: 0;
+      width: 100%;
+      text-align: center;
+    }
+    &-title {
+      margin-top: 10px;
+      text-align: center;
+    }
+  }
+}

+ 129 - 0
src/modules/card/components/ModifyCard.vue

@@ -0,0 +1,129 @@
+<template>
+  <el-dialog
+    custom-class="side-dialog"
+    :visible.sync="modalIsShow"
+    title="编辑"
+    width="550px"
+    :modal="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :key="modalForm.id"
+      :model="modalForm"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item label="所属课程:">
+        <el-input v-model.trim="modalForm.courseNamesStr" readonly></el-input>
+      </el-form-item>
+      <el-form-item label="创建人:">
+        <el-input v-model.trim="modalForm.creatorName" readonly></el-input>
+      </el-form-item>
+      <el-form-item prop="teacherId" label="命题老师:">
+        <el-select
+          v-model="modalForm.teacherId"
+          class="width-full"
+          placeholder="请选择"
+          clearable
+          filterable
+        >
+          <el-option
+            v-for="item in teachers"
+            :key="item.id"
+            :value="item.id"
+            :label="item.name"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确认</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { cardUpdateApi, questionTeacherQueryApi } from "../api";
+
+const initModalForm = {
+  id: null,
+  courseNamesStr: "",
+  creatorName: "",
+  teacherId: "",
+};
+
+export default {
+  name: "ModifyCard",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      teachers: [],
+      rules: {
+        teacherId: [
+          {
+            required: true,
+            message: "请选择命题老师",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  mounted() {
+    this.getTeachers();
+  },
+  methods: {
+    async getTeachers() {
+      const data = await questionTeacherQueryApi();
+      this.teachers = data || [];
+    },
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+      this.isSubmit = false;
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      let datas = {
+        id: this.modalForm.id,
+        teacherId: this.modalForm.teacherId,
+      };
+      const data = await cardUpdateApi(datas).catch(() => {
+        this.isSubmit = false;
+      });
+
+      if (!data) return;
+
+      this.isSubmit = false;
+      this.$message.success("编辑成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 3 - 3
src/modules/card/components/UploadButton.vue

@@ -20,7 +20,7 @@
 </template>
 
 <script>
-import { fileMD5 } from "../plugins/md5";
+// import { fileMD5 } from "../plugins/md5";
 import ajax from "../plugins/ajax";
 
 export default {
@@ -97,8 +97,8 @@ export default {
         return Promise.reject();
       }
 
-      const md5 = await fileMD5(file);
-      this.headers["md5"] = md5;
+      // const md5 = await fileMD5(file);
+      // this.headers["md5"] = md5;
       this.loading = true;
 
       return true;

+ 17 - 32
src/modules/card/router/index.js

@@ -1,50 +1,39 @@
 import Vue from "vue";
 import VueRouter from "vue-router";
-import Home from "../views/Home.vue";
 
 Vue.use(VueRouter);
 
 const routes = [
   {
-    path: "/",
-    name: "Home",
-    component: Home,
-  },
-  {
-    path: "/card/edit/:cardId?",
-    name: "CardEdit",
+    path: "/card/card-manage",
+    name: "CardManage",
     component: () =>
-      import(/* webpackChunkName: "CardEdit" */ "../views/CardEdit.vue"),
+      import(/* webpackChunkName: "card" */ "../views/CardManage.vue"),
   },
   {
-    path: "/card/free-edit/:cardId?",
-    name: "CardFreeEdit",
+    path: "/card/card-head-manege",
+    name: "CardHeadManage",
     component: () =>
-      import(/* webpackChunkName: "CardEdit" */ "../views/CardFreeEdit.vue"),
+      import(/* webpackChunkName: "card" */ "../views/CardHeadManage.vue"),
   },
   {
-    // viewType::: view:预览,print:打印,frame:iframe嵌套
-    path: "/card/preview/:cardId/:viewType",
-    name: "CardPreview",
+    path: "/card/card-head-edit/:cardHeadId?",
+    name: "CardHeadEdit",
     component: () =>
-      import(/* webpackChunkName: "CardPreview" */ "../views/CardPreview.vue"),
+      import(/* webpackChunkName: "card" */ "../views/CardHeadEdit.vue"),
   },
   {
-    // viewType::: view:预览,print:打印,frame:iframe嵌套
-    path: "/card/free-preview/:cardId/:viewType",
-    name: "CardFreePreview",
+    path: "/card/edit/:cardId?",
+    name: "CardEdit",
     component: () =>
-      import(
-        /* webpackChunkName: "CardPreview" */ "../views/CardFreePreview.vue"
-      ),
+      import(/* webpackChunkName: "card" */ "../views/CardEdit.vue"),
   },
   {
-    path: "/card/card-rule/preview/:cardRuleId",
-    name: "CardRulePreview",
+    // viewType::: view:预览,print:打印,frame:iframe嵌套
+    path: "/card/preview/:cardId/:viewType",
+    name: "CardPreview",
     component: () =>
-      import(
-        /* webpackChunkName: "CardRulePreview" */ "../views/CardRulePreview.vue"
-      ),
+      import(/* webpackChunkName: "card" */ "../views/CardPreview.vue"),
   },
   // {
   //   path: "/about",
@@ -57,8 +46,4 @@ const routes = [
   // }
 ];
 
-const router = new VueRouter({
-  routes,
-});
-
-export default router;
+export default routes;

+ 1 - 13
src/modules/card/views/CardFreePreview.vue

@@ -14,7 +14,6 @@
 import CardFreeView from "../modules/free/components/CardFreeView";
 import { cardDetail } from "../api";
 import { deepCopy } from "../plugins/utils";
-const JsBarcode = require("jsbarcode");
 
 export default {
   name: "CardFreePreview",
@@ -94,18 +93,7 @@ export default {
       return fieldInfos;
     },
     getBase64Barcode(str) {
-      const canvas = document.createElement("CANVAS");
-      JsBarcode(canvas, str, {
-        width: 2,
-        height: 30,
-        displayValue: false,
-        marginLeft: 20,
-        marginRight: 20,
-        marginTop: 0,
-        marginBottom: 0,
-      });
-
-      return canvas.toDataURL();
+      return str;
     },
     appendFieldInfo(pages, fieldInfos) {
       const VALID_ELEMENTS_FOR_EXTERNAL = ["BARCODE", "FILL_FIELD"];

+ 231 - 0
src/modules/card/views/CardHeadEdit.vue

@@ -0,0 +1,231 @@
+<template>
+  <div class="card-head-edit">
+    <div class="part-box">
+      <div class="part-box-header">
+        <h2 class="part-box-title">版头模板预选设置</h2>
+        <el-button type="danger" plain icon="icon icon-back" @click="goBack"
+          >返回</el-button
+        >
+      </div>
+      <el-form
+        ref="modalFormComp"
+        class="padding-tb-20"
+        label-width="140px"
+        :rules="rules"
+        :model="modalForm"
+      >
+        <el-form-item prop="name" label="版头名称:">
+          <el-input
+            v-model.trim="modalForm.name"
+            placeholder="请输入版头名称"
+            style="width: 100%"
+            clearable
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="cardTitle" label="题卡标题:">
+          <el-input
+            v-model="modalForm.cardTitle"
+            type="textarea"
+            :rows="2"
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="attention" label="注意事项:">
+          <el-input
+            v-model="modalForm.attention"
+            type="textarea"
+            :rows="4"
+          ></el-input>
+          <p class="tips-info">
+            提示:换行之后,题卡注意事项会展示为多条内容,内容序号会被自动添加。
+          </p>
+        </el-form-item>
+        <el-form-item prop="objectiveAttention" label="客观题注意事项:">
+          <el-input
+            v-model="modalForm.objectiveAttention"
+            placeholder="请输入"
+            clearable
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="subjectiveAttention" label="主观题注意事项:">
+          <el-input
+            v-model="modalForm.subjectiveAttention"
+            placeholder="请输入"
+            clearable
+          ></el-input>
+        </el-form-item>
+        <el-form-item prop="templateId" label="模板选择">
+          <div class="template-list">
+            <div class="template-item">
+              <div class="template-item-body">
+                <img src="" alt="" />
+                <div class="template-item-action">
+                  <el-button type="primary" plain>预览</el-button>
+                  <el-button type="primary">使用</el-button>
+                </div>
+              </div>
+              <p class="template-item-title">111</p>
+            </div>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <div class="margin-top-20 text-center">
+        <el-button type="primary" :loading="isSubmit" @click="submit"
+          >保存</el-button
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { cardHeadDetailApi, cardHeadUpdateApi } from "../api";
+const initModalForm = {
+  id: null,
+  name: "",
+  cardTitle: "",
+  attention: "",
+  objectiveAttention: "",
+  subjectiveAttention: "",
+  templateId: null,
+};
+
+export default {
+  name: "CardHeadEdit",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      isSubmit: false,
+      loading: false,
+      cardHeadId: this.$route.params.cardHeadId,
+      modalForm: { ...initModalForm },
+      rules: {
+        name: [
+          {
+            required: true,
+            message: "请输入版头名称",
+            trigger: "change",
+          },
+          {
+            message: "版头名称不能超过50个字",
+            max: 50,
+            trigger: "change",
+          },
+        ],
+        cardTitle: [
+          {
+            required: true,
+            message: "请输入题卡标题",
+            trigger: "change",
+          },
+          {
+            validator: (rule, value, callback) => {
+              const vals = value.split(/\n/);
+              if (vals.length > 2) {
+                callback(new Error("题卡标题只能换行一次"));
+              } else {
+                callback();
+              }
+            },
+            trigger: "change",
+          },
+        ],
+        attention: [
+          {
+            required: true,
+            message: "请输入注意事项",
+            trigger: "change",
+          },
+          {
+            validator: (rule, value, callback) => {
+              const val = value.replace(/\n/g, "");
+              if (val.length > 200) {
+                callback(new Error("注意事项最多只能输入200个字符"));
+              } else {
+                callback();
+              }
+            },
+            trigger: "change",
+          },
+        ],
+        objectiveAttention: [
+          {
+            required: true,
+            message: "请输入客观题注意事项",
+            trigger: "change",
+          },
+          {
+            max: 26,
+            message: "客观题注意事项最多只能输入26个汉字",
+            trigger: "change",
+          },
+        ],
+        subjectiveAttention: [
+          {
+            required: true,
+            message: "请输入主观题注意事项",
+            trigger: "change",
+          },
+          {
+            max: 26,
+            message: "主观题注意事项最多只能输入26个汉字",
+            trigger: "change",
+          },
+        ],
+        templateId: [
+          {
+            required: true,
+            message: "请选择模板",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  mounted() {
+    // this.initData();
+  },
+  methods: {
+    async initData() {
+      if (this.cardHeadId) {
+        this.loading = true;
+        const res = await cardHeadDetailApi(this.cardHeadId);
+        this.modalForm = { ...res.data };
+      } else {
+        this.modalForm = { ...initModalForm };
+      }
+    },
+    goBack() {
+      this.$router.push({
+        name: "CardHeadManage",
+        params: {
+          isClear: sessionStorage.getItem("card_head_manage") == "true" ? 0 : 1,
+        },
+      });
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      let datas = {
+        ...this.modalForm,
+      };
+      const data = await cardHeadUpdateApi(datas).catch(() => {});
+      this.isSubmit = false;
+      if (!data) return;
+
+      this.$message.success("保存成功!");
+      this.goBack();
+    },
+  },
+};
+</script>

+ 274 - 0
src/modules/card/views/CardHeadManage.vue

@@ -0,0 +1,274 @@
+<template>
+  <div
+    v-loading.fullscreen="loading"
+    class="card-head-manage content"
+    element-loading-text="请稍后..."
+  >
+    <div class="part-box">
+      <div class="part-box-header">
+        <h2 class="part-box-title">题卡版头管理</h2>
+        <el-button type="danger" plain icon="icon icon-back" @click="goBack"
+          >返回</el-button
+        >
+      </div>
+      <!-- 搜索 -->
+      <el-form class="part-filter-form" inline :model="searchForm">
+        <el-form-item label="版头名称">
+          <el-input
+            v-model="searchForm.name"
+            placeholder="请输入版头名称"
+            maxlength="50"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="danger" @click="handleCurrentChange(1)">
+            查询
+          </el-button>
+        </el-form-item>
+      </el-form>
+
+      <div class="part-box-action">
+        <div>
+          <el-button
+            type="danger"
+            plain
+            icon="icon icon-delete"
+            @click="toBatchDelete"
+            >删除
+          </el-button>
+        </div>
+        <div>
+          <el-button type="primary" icon="icon icon-plus-white" @click="toAdd"
+            >新增
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <div class="part-box">
+      <!-- 页面列表 -->
+      <el-table
+        ref="table"
+        :data="tableData"
+        resizable
+        @selection-change="selectChange"
+      >
+        <el-table-column
+          type="selection"
+          width="50"
+          align="center"
+        ></el-table-column>
+        <el-table-column prop="loginName" label="版头名称"></el-table-column>
+        <el-table-column width="170" label="创建时间"></el-table-column>
+        <el-table-column prop="name" label="创建人"></el-table-column>
+        <el-table-column width="50" label="状态">
+          <template slot-scope="scope">
+            <span v-if="scope.row.enable">
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="启用"
+                placement="left"
+              >
+                <i class="icon icon-right"></i>
+              </el-tooltip>
+            </span>
+            <span v-else>
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="禁用"
+                placement="left"
+              >
+                <i class="icon icon-error"></i>
+              </el-tooltip>
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column width="170" label="操作">
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="primary"
+              plain
+              @click="toEdit(scope.row)"
+              >编辑
+            </el-button>
+            <el-button
+              size="mini"
+              :type="scope.row.enable ? 'danger' : 'primary'"
+              plain
+              @click="toEnable(scope.row)"
+            >
+              {{ scope.row.enable ? "禁用" : "启用" }}
+            </el-button>
+            <el-button
+              v-if="false"
+              size="mini"
+              type="danger"
+              plain
+              @click="toDelete(scope.row)"
+              >删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          :current-page="currentPage"
+          :page-size="10"
+          :page-sizes="[10, 20, 50, 100, 200, 300]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { cardHeadListApi, cardHeadDeleteApi, cardHeadEnableApi } from "../api";
+
+export default {
+  name: "CardHeadManage",
+  data() {
+    return {
+      loading: false,
+      searchForm: {
+        name: "",
+      },
+      selectedIds: [],
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+    };
+  },
+  mounted() {
+    const cacheInfo = window.sessionStorage.getItem("card-head-manage");
+    if (cacheInfo) {
+      const { currentPage, pageSize, searchForm } = JSON.parse(cacheInfo);
+      this.searchForm = { ...searchForm };
+      this.currentPage = currentPage;
+      this.pageSize = pageSize;
+      this.handleCurrentChange(this.currentPage);
+      window.sessionStorage.removeItem("card-head-manage");
+    } else {
+      this.handleCurrentChange(1);
+    }
+  },
+  methods: {
+    async search() {
+      if (this.loading) return;
+      this.loading = true;
+
+      const res = await cardHeadListApi(this.searchForm, {
+        pageNo: this.currentPage,
+        pageSize: this.pageSize,
+      }).catch(() => {});
+
+      this.loading = false;
+      if (!res) return;
+
+      this.tableData = res.data.list;
+      this.total = res.data.total;
+    },
+    handleCurrentChange(val) {
+      this.selectedIds = [];
+      this.currentPage = val;
+      this.search();
+    },
+    handleSizeChange(val) {
+      this.selectedIds = [];
+      this.currentPage = 1;
+      this.pageSize = val;
+      this.search();
+    },
+    selectChange(selections) {
+      this.selectedIds = selections.map((item) => item.id);
+    },
+    async toBatchDelete() {
+      if (!this.selectedIds.length) {
+        this.$message.error("请选择数据");
+        return;
+      }
+
+      const confirm = await this.$confirm(
+        `确定要删除选择中的这些数据吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await cardHeadDeleteApi(this.selectedIds);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem(this.selectedIds.length);
+    },
+    toAdd() {
+      this.cacheSearchInfo();
+      this.$router.push({
+        name: "CardHeadEdit",
+      });
+    },
+    async toEnable(row) {
+      const action = row.enable ? "禁用" : "启用";
+      const confirm = await this.$confirm(
+        `确定要${action}该题卡版头吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      const enable = !row.enable;
+      await cardHeadEnableApi({
+        id: row.id,
+        enable,
+      });
+      this.$message.success("操作成功!");
+      this.search();
+    },
+    toEdit(row) {
+      this.cacheSearchInfo();
+      this.$router.push({
+        name: "CardHeadEdit",
+        params: {
+          cardHeadId: row.id,
+        },
+      });
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `确定要删除版头【${row.name}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await cardHeadDeleteApi([row.id]);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem();
+    },
+    goBack() {
+      window.history.go(-1);
+    },
+    cacheSearchInfo() {
+      window.sessionStorage.setItem(
+        "card-head-mamange",
+        JSON.stringify({
+          searchForm: this.searchForm,
+          currentPage: this.currentPage,
+          pageSize: this.pageSize,
+        })
+      );
+    },
+  },
+};
+</script>

+ 397 - 0
src/modules/card/views/CardManage.vue

@@ -0,0 +1,397 @@
+<template>
+  <div
+    v-loading.fullscreen="loading"
+    class="card-manage content"
+    element-loading-text="请稍后..."
+  >
+    <div class="part-box">
+      <h2 class="part-box-title">题卡管理</h2>
+      <!-- 搜索 -->
+      <el-form class="part-filter-form" inline :model="searchForm">
+        <el-form-item label="课程">
+          <el-select
+            v-model="searchForm.courseId"
+            filterable
+            clearable
+            placeholder="请选择"
+          >
+            <el-option
+              v-for="item in courseList"
+              :key="item.id"
+              :label="item.name + ' - ' + item.code"
+              :value="item.id"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="命题老师">
+          <el-input
+            v-model="searchForm.teacherName"
+            placeholder="请输入命题老师"
+            maxlength="20"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="danger" @click="handleCurrentChange(1)">
+            查询
+          </el-button>
+        </el-form-item>
+      </el-form>
+
+      <div class="part-box-action">
+        <div>
+          <el-button
+            type="primary"
+            plain
+            icon="icon icon-edit"
+            @click="toCardHead"
+            >题卡版头管理
+          </el-button>
+          <el-button
+            type="primary"
+            plain
+            icon="icon icon-edit"
+            @click="toExportPaperStruct"
+            >导入试卷结构
+          </el-button>
+          <el-button
+            type="danger"
+            plain
+            icon="icon icon-delete"
+            @click="toBatchDelete"
+            >批量删除
+          </el-button>
+        </div>
+        <div>
+          <el-button
+            type="primary"
+            icon="icon icon-export-white"
+            @click="toBatchDownload"
+            >批量下载
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <div class="part-box">
+      <!-- 页面列表 -->
+      <el-table
+        ref="table"
+        :data="tableData"
+        resizable
+        @selection-change="selectChange"
+      >
+        <el-table-column
+          type="selection"
+          width="50"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="所属课程">
+          <span
+            slot-scope="scope"
+            v-html="
+              scope.row.courseNamesStr &&
+              scope.row.courseNamesStr.replace(/,/g, '<br />')
+            "
+          >
+          </span>
+        </el-table-column>
+        <el-table-column width="170" label="创建时间"></el-table-column>
+        <el-table-column prop="name" label="创建人"></el-table-column>
+        <el-table-column width="170" label="提交时间"></el-table-column>
+        <el-table-column prop="teacherName" label="命题老师"></el-table-column>
+        <el-table-column width="50" label="状态">
+          <template slot-scope="scope">
+            <span v-if="scope.row.enable">
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="启用"
+                placement="left"
+              >
+                <i class="icon icon-right"></i>
+              </el-tooltip>
+            </span>
+            <span v-else>
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="禁用"
+                placement="left"
+              >
+                <i class="icon icon-error"></i>
+              </el-tooltip>
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column width="170" label="操作">
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              :type="scope.row.enable ? 'danger' : 'primary'"
+              plain
+              @click="toEnable(scope.row)"
+            >
+              {{ scope.row.enable ? "禁用" : "启用" }}
+            </el-button>
+            <el-dropdown>
+              <el-button type="primary" plain size="mini">
+                更多<i class="el-icon-more el-icon--right"></i>
+              </el-button>
+              <el-dropdown-menu slot="dropdown" class="action-dropdown">
+                <el-dropdown-item>
+                  <el-button
+                    size="mini"
+                    type="primary"
+                    plain
+                    @click="toEdit(scope.row)"
+                    >编辑
+                  </el-button>
+                </el-dropdown-item>
+                <el-dropdown-item>
+                  <el-button
+                    size="mini"
+                    type="primary"
+                    plain
+                    @click="toView(scope.row)"
+                    >查看
+                  </el-button>
+                </el-dropdown-item>
+                <el-dropdown-item>
+                  <el-button
+                    v-if="false"
+                    size="mini"
+                    type="danger"
+                    plain
+                    @click="toDelete(scope.row)"
+                    >删除
+                  </el-button>
+                </el-dropdown-item>
+                <el-dropdown-item>
+                  <el-button
+                    size="mini"
+                    type="danger"
+                    plain
+                    @click="toDownload(scope.row)"
+                  >
+                    下载
+                  </el-button>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          :current-page="currentPage"
+          :page-size="pageSize"
+          :page-sizes="[10, 20, 50, 100, 200, 300]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </div>
+
+    <!-- ImportFileDialog -->
+    <import-file-dialog
+      ref="ImportFileDialog"
+      dialog-title="导入试卷结构"
+      :upload-url="uploadUrl"
+      :template-url="templateUrl"
+    ></import-file-dialog>
+    <!-- ModifyCard -->
+    <modify-card
+      ref="ModifyCard"
+      :instance="curRow"
+      @modified="search"
+    ></modify-card>
+  </div>
+</template>
+
+<script>
+import {
+  cardListApi,
+  courseQueryApi,
+  cardDeleteApi,
+  cardEnableApi,
+} from "../api";
+import ImportFileDialog from "@/components/ImportFileDialog.vue";
+import ModifyCard from "../components/ModifyCard.vue";
+import { QUESTION_API } from "@/constants/constants.js";
+import { mapState } from "vuex";
+
+export default {
+  name: "CardManage",
+  components: { ImportFileDialog, ModifyCard },
+  data() {
+    return {
+      loading: false,
+      courseList: [],
+      searchForm: {
+        courseId: "",
+        teacherName: "",
+      },
+      selectedIds: [],
+      tableData: [
+        {
+          id: "1",
+          courseNamesStr: "123",
+          teacherName: "2332",
+        },
+      ],
+      curRow: {},
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      // upload
+      uploadUrl: `${QUESTION_API}/upload`,
+      templateUrl: "",
+    };
+  },
+  computed: {
+    ...mapState({
+      user: (state) => state.user,
+    }),
+  },
+  mounted() {
+    const cacheInfo = window.sessionStorage.getItem("card-manage");
+    if (cacheInfo) {
+      const { currentPage, pageSize, searchForm } = JSON.parse(cacheInfo);
+      this.searchForm = { ...searchForm };
+      this.currentPage = currentPage;
+      this.pageSize = pageSize;
+      this.handleCurrentChange(this.currentPage);
+      window.sessionStorage.removeItem("card-manage");
+    } else {
+      // this.handleCurrentChange(1);
+    }
+    this.getCoursesList();
+    this.templateUrl = `${QUESTION_API}/course/importTemplate?$key=${this.user.key}&$token=${this.user.token}`;
+  },
+  methods: {
+    async getCoursesList() {
+      const res = await courseQueryApi();
+      this.courseList = res.data || [];
+    },
+    async search() {
+      if (this.loading) return;
+      this.loading = true;
+
+      const res = await cardListApi(this.searchForm, {
+        pageNo: this.currentPage,
+        pageSize: this.pageSize,
+      }).catch(() => {});
+
+      this.loading = false;
+      if (!res) return;
+
+      this.tableData = res.data.list;
+      this.total = res.data.total;
+    },
+    handleCurrentChange(val) {
+      this.selectedIds = [];
+      this.currentPage = val;
+      this.search();
+    },
+    handleSizeChange(val) {
+      this.selectedIds = [];
+      this.currentPage = 1;
+      this.pageSize = val;
+      this.search();
+    },
+    selectChange(selections) {
+      this.selectedIds = selections.map((item) => item.id);
+    },
+    toCardHead() {
+      this.cacheSearchInfo();
+      this.$router.push({ name: "CardHeadManage" });
+    },
+    toExportPaperStruct() {
+      this.$refs.ImportFileDialog.open();
+    },
+    toBatchDownload() {
+      if (!this.selectedIds.length) {
+        this.$message.error("请选择数据");
+        return;
+      }
+    },
+    async toBatchDelete() {
+      if (!this.selectedIds.length) {
+        this.$message.error("请选择数据");
+        return;
+      }
+
+      const confirm = await this.$confirm(
+        `确定要删除选中的这些数据吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await cardDeleteApi(this.selectedIds);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem(this.selectedIds.length);
+    },
+    async toEnable(row) {
+      const action = row.enable ? "禁用" : "启用";
+      const confirm = await this.$confirm(`确定要${action}该题卡吗?`, "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      const enable = !row.enable;
+      await cardEnableApi({
+        id: row.id,
+        enable,
+      });
+      row.enable = enable;
+      this.$message.success("操作成功!");
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyCard.open();
+    },
+    toView(row) {
+      console.log(row);
+      const url = this.getRouterPath({
+        name: "CardPreview",
+        params: { cardId: row.cardId },
+      });
+      window.open(url);
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `确定要删除题卡【${row.name}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await cardDeleteApi([row.id]);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem();
+    },
+    toDownload(row) {
+      // TODO:
+      console.log(row);
+      // 只下载题卡PDF
+    },
+    cacheSearchInfo() {
+      window.sessionStorage.setItem(
+        "card-mamange",
+        JSON.stringify({
+          searchForm: this.searchForm,
+          currentPage: this.currentPage,
+          pageSize: this.pageSize,
+        })
+      );
+    },
+  },
+};
+</script>

+ 1 - 13
src/modules/card/views/CardPreview.vue

@@ -15,7 +15,6 @@
 import CardView from "../components/CardView";
 import { cardDetail } from "../api";
 import { deepCopy } from "../plugins/utils";
-const JsBarcode = require("jsbarcode");
 
 export default {
   name: "CardPreview",
@@ -141,18 +140,7 @@ export default {
       return fieldInfos;
     },
     getBase64Barcode(str) {
-      const canvas = document.createElement("CANVAS");
-      JsBarcode(canvas, str, {
-        width: 2,
-        height: 30,
-        displayValue: false,
-        marginLeft: 20,
-        marginRight: 20,
-        marginTop: 0,
-        marginBottom: 0,
-      });
-
-      return canvas.toDataURL();
+      return str;
     },
     appendFieldInfo(pages, fieldInfos) {
       pages.forEach((page, pageNo) => {

+ 0 - 124
src/modules/card/views/Home.vue

@@ -1,124 +0,0 @@
-<template>
-  <div class="home">
-    <div class="part-box part-box-pad part-box-flex">
-      <div></div>
-      <div>
-        <el-button type="primary" icon="el-icon-refresh" @click="toAdd"
-          >新增标准题卡</el-button
-        >
-        <el-button type="success" icon="el-icon-refresh" @click="toFreeAdd"
-          >新增自由题卡</el-button
-        >
-      </div>
-    </div>
-    <div class="part-box part-box-pad">
-      <el-table ref="TableList" :data="cards">
-        <el-table-column type="index" label="序号" width="70"></el-table-column>
-        <el-table-column prop="name" label="名称"></el-table-column>
-        <el-table-column class-name="action-column" label="操作" width="200px">
-          <template slot-scope="scope">
-            <el-button
-              class="btn-primary"
-              type="text"
-              @click="toEdit(scope.row)"
-              >编辑</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-      <div class="part-page">
-        <el-pagination
-          background
-          layout="total,prev, pager, next"
-          :current-page="current"
-          :total="total"
-          :page-size="size"
-          @current-change="toPage"
-        >
-        </el-pagination>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: "Home",
-  data() {
-    return {
-      current: 1,
-      size: 10,
-      total: 10,
-      cards: [],
-    };
-  },
-  mounted() {
-    this.getList();
-  },
-  methods: {
-    getList() {
-      this.cards = [
-        {
-          id: "1111",
-          name: "题卡001",
-        },
-        {
-          id: "2222",
-          name: "题卡002",
-        },
-      ];
-      this.total = this.cards.length;
-    },
-    toPage(page) {
-      this.current = page;
-      this.getList();
-    },
-    toEdit(row) {
-      this.$ls.set("prepareTcPCard", {
-        examTaskId: "1111",
-        courseCode: "数学",
-        courseName: "sx001",
-        makeMethod: "SELF",
-        cardRuleId: "1",
-      });
-      this.$router.push({
-        name: "CardFreeEdit",
-        params: {
-          cardId: row.id,
-        },
-      });
-    },
-    toAdd() {
-      this.$ls.set("prepareTcPCard", {
-        examTaskId: "1111",
-        courseCode: "数学",
-        courseName: "sx001",
-        makeMethod: "SELF",
-        cardRuleId: "1",
-      });
-      this.$router.push({
-        name: "CardEdit",
-      });
-    },
-    toFreeAdd() {
-      this.$ls.set("prepareTcPCard", {
-        examTaskId: "1111",
-        courseCode: "数学",
-        courseName: "sx001",
-        makeMethod: "SELF",
-        cardRuleId: "1",
-      });
-      this.$router.push({
-        name: "CardFreeEdit",
-      });
-    },
-  },
-};
-</script>
-
-<style scoped>
-.home {
-  background: #eff0f5;
-  padding: 20px;
-}
-</style>

+ 15 - 11
src/modules/portal/views/home/Home.vue

@@ -166,9 +166,12 @@ import { USER_SIGNOUT, USER_SIGNIN } from "../../store/user";
 import { QUESTION_API } from "@/constants/constants";
 import HomeSide from "./HomeSide.vue";
 import LinkTitles from "./LinkTitles.vue";
+import timeMixin from "../../../../mixins/timeMixin";
+
 export default {
   name: "Home",
   components: { HomeSide, LinkTitles },
+  mixins: [timeMixin],
   data() {
     var validatePass = (rule, value, callback) => {
       if (value === "") {
@@ -246,20 +249,21 @@ export default {
     }
     this.onlineSignal();
   },
+  beforeDestroy() {
+    this.clearSetTs();
+  },
   methods: {
     ...mapActions([USER_SIGNOUT, USER_SIGNIN]),
     async onlineSignal() {
-      try {
-        this.$httpWithoutBar
-          .post(QUESTION_API + "/user/online/signal")
-          .then(() => {
-            setTimeout(() => {
-              this.onlineSignal();
-            }, 5000);
-          });
-      } catch (error) {
-        console.log("tag", error);
-      }
+      await this.$httpWithoutBar
+        .post(QUESTION_API + "/user/online/signal")
+        .catch((error) => {
+          console.log("tag", error);
+        });
+
+      this.addSetTime(() => {
+        this.onlineSignal();
+      }, 5000);
     },
     openMessageHome() {
       this.$router.push({ path: "/home/site-message" });

+ 3 - 1
src/modules/portal/views/home/HomeSide.vue

@@ -93,6 +93,7 @@ const MENU_ICONS = {
 import { mapMutations } from "vuex";
 import { UPDATE_CURRENT_PATHS } from "../../store/currentPaths";
 import { UPDATE_MENU_LIST } from "../../store/menuList";
+import menus from "./menus";
 
 export default {
   name: "HomeSide",
@@ -122,7 +123,8 @@ export default {
   },
   async created() {
     this.group = routesToMenu.find((v) => this.$route.path.startsWith(v.path));
-    this.menuList = await this.getUserPrivileges();
+    // this.menuList = await this.getUserPrivileges();
+    this.menuList = menus;
     this.UPDATE_MENU_LIST(this.menuList);
     this.updatePath();
   },

+ 442 - 0
src/modules/portal/views/home/menus.js

@@ -0,0 +1,442 @@
+export default [
+  {
+    id: 1,
+    code: "base_info",
+    name: "系统管理",
+    parentId: null,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 1,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "",
+    nodeName: "系统管理",
+    parentNodeId: null,
+    nodeCode: "base_info",
+    nodeId: "1",
+  },
+  {
+    id: 6,
+    code: "index_course",
+    name: "课程列表",
+    parentId: 5,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 1,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/course",
+    nodeName: "课程列表",
+    parentNodeId: "5",
+    nodeCode: "index_course",
+    nodeId: "6",
+  },
+  {
+    id: 9,
+    code: "ques_struct_prepare",
+    name: "精确结构预设",
+    parentId: 8,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 1,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/paper_structure/0",
+    nodeName: "精确结构预设",
+    parentNodeId: "8",
+    nodeCode: "ques_struct_prepare",
+    nodeId: "9",
+  },
+  {
+    id: 12,
+    code: "import_test_paper",
+    name: "题库列表",
+    parentId: 11,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 1,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/import_paper/0",
+    nodeName: "题库列表",
+    parentNodeId: "11",
+    nodeCode: "import_test_paper",
+    nodeId: "12",
+  },
+  {
+    id: 14,
+    code: "exam_paper_manager",
+    name: "卷库列表",
+    parentId: 13,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 1,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/gen_paper/0",
+    nodeName: "卷库列表",
+    parentNodeId: "13",
+    nodeCode: "exam_paper_manager",
+    nodeId: "14",
+  },
+  {
+    id: 16,
+    code: "question_list",
+    name: "试题列表",
+    parentId: 15,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 1,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/question_list/0",
+    nodeName: "试题列表",
+    parentNodeId: "15",
+    nodeCode: "question_list",
+    nodeId: "16",
+  },
+  {
+    id: 18,
+    code: "user_data_rule_setting",
+    name: "关联课程",
+    parentId: 3,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 1,
+    ext1: "button",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "",
+    nodeName: "关联课程",
+    parentNodeId: "3",
+    nodeCode: "user_data_rule_setting",
+    nodeId: "18",
+  },
+  {
+    id: 3,
+    code: "index_user",
+    name: "用户管理",
+    parentId: 1,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 2,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/user",
+    nodeName: "用户管理",
+    parentNodeId: "1",
+    nodeCode: "index_user",
+    nodeId: "3",
+  },
+  {
+    id: 101,
+    code: "card_mamange",
+    name: "题卡管理",
+    parentId: 1,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 4,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/card/card-manage",
+    nodeName: "题卡管理",
+    parentNodeId: "1",
+    nodeCode: "card_mamange",
+    nodeId: "101",
+  },
+  {
+    id: 5,
+    code: "course_manage",
+    name: "课程管理",
+    parentId: null,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 2,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "",
+    nodeName: "课程管理",
+    parentNodeId: null,
+    nodeCode: "course_manage",
+    nodeId: "5",
+  },
+  {
+    id: 7,
+    code: "lesson_property",
+    name: "课程属性",
+    parentId: 5,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 2,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/course_property/0",
+    nodeName: "课程属性",
+    parentNodeId: "5",
+    nodeCode: "lesson_property",
+    nodeId: "7",
+  },
+  {
+    id: 10,
+    code: "bluepoint",
+    name: "蓝图试卷结构",
+    parentId: 8,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 2,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/blue_paper_structure/0",
+    nodeName: "蓝图试卷结构",
+    parentNodeId: "8",
+    nodeCode: "bluepoint",
+    nodeId: "10",
+  },
+  {
+    id: 17,
+    code: "single_question_import",
+    name: "单题录入",
+    parentId: 15,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 2,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/insert_paper_title",
+    nodeName: "单题录入",
+    parentNodeId: "15",
+    nodeCode: "single_question_import",
+    nodeId: "17",
+  },
+  {
+    id: 19,
+    code: "exam_paper_storage",
+    name: "试卷仓库",
+    parentId: 13,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 2,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/paper_storage/0",
+    nodeName: "试卷仓库",
+    parentNodeId: "13",
+    nodeCode: "exam_paper_storage",
+    nodeId: "19",
+  },
+  {
+    id: 22,
+    code: "check_duplicate_list",
+    name: "题库查重",
+    parentId: 11,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 2,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/check_duplicate_list/0",
+    nodeName: "题库查重",
+    parentNodeId: "11",
+    nodeCode: "check_duplicate_list",
+    nodeId: "22",
+  },
+  {
+    id: 4,
+    code: "export_template",
+    name: "模板管理",
+    parentId: 1,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 3,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/export_template",
+    nodeName: "模板管理",
+    parentNodeId: "1",
+    nodeCode: "export_template",
+    nodeId: "4",
+  },
+  {
+    id: 8,
+    code: "structure_manage",
+    name: "组卷结构预设",
+    parentId: null,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 3,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "",
+    nodeName: "组卷结构预设",
+    parentNodeId: null,
+    nodeCode: "structure_manage",
+    nodeId: "8",
+  },
+  {
+    id: 20,
+    code: "paper_pending_trial",
+    name: "题库待审",
+    parentId: 11,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 3,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/paper_pending_trial/0",
+    nodeName: "题库待审",
+    parentNodeId: "11",
+    nodeCode: "paper_pending_trial",
+    nodeId: "20",
+  },
+  {
+    id: 21,
+    code: "exam_paper_pending_trial",
+    name: "考卷待审",
+    parentId: 13,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 3,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "/questions/exam_paper_pending_trial/0",
+    nodeName: "考卷待审",
+    parentNodeId: "13",
+    nodeCode: "exam_paper_pending_trial",
+    nodeId: "21",
+  },
+  {
+    id: 11,
+    code: "question_bank",
+    name: "题库管理",
+    parentId: null,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 4,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "",
+    nodeName: "题库管理",
+    parentNodeId: null,
+    nodeCode: "question_bank",
+    nodeId: "11",
+  },
+  {
+    id: 13,
+    code: "paper_manage",
+    name: "卷库管理",
+    parentId: null,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 5,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "",
+    nodeName: "卷库管理",
+    parentNodeId: null,
+    nodeCode: "paper_manage",
+    nodeId: "13",
+  },
+  {
+    id: 15,
+    code: "ques_manage",
+    name: "试题管理",
+    parentId: null,
+    hasPrivilege: true,
+    description: "",
+    updateTime: "2021-09-08 11:31:29",
+    creationTime: "2021-09-08 11:31:29",
+    seq: 6,
+    ext1: "menu",
+    ext2: "",
+    ext3: "",
+    ext4: "",
+    ext5: "",
+    nodeName: "试题管理",
+    parentNodeId: null,
+    nodeCode: "ques_manage",
+    nodeId: "15",
+  },
+];

+ 3 - 0
src/modules/questions/routes/routes.js

@@ -33,6 +33,8 @@ import ExamPaperPendingTrial from "../views/ExamPaperPendingTrial.vue";
 import CheckDuplicateList from "../views/CheckDuplicateList.vue";
 import CheckDuplicateInfo from "../views/CheckDuplicateInfo.vue";
 
+import CardRoutes from "../../card/router";
+
 export default [
   {
     path: "/questions", //首页
@@ -167,6 +169,7 @@ export default [
         path: "paper_storage/:isClear",
         component: PaperStorage,
       },
+      ...CardRoutes,
     ],
   },
   {

+ 1 - 0
src/plugins/axios.js

@@ -470,3 +470,4 @@ loadProgressBar({}, Vue.$httpWithoutAuth);
 
 import "axios-progress-bar/dist/nprogress.css";
 export default Plugin;
+export const $httpWithMsg = _$httpWith500Msg;

+ 13 - 0
src/plugins/globalVuePlugins.js

@@ -0,0 +1,13 @@
+import { objAssign } from "@/plugins/utils";
+// mixins
+import commonMixins from "../mixins/common";
+
+export default {
+  install: function (Vue) {
+    // 实例方法
+    Vue.prototype.$objAssign = objAssign;
+
+    //全局 mixins
+    Vue.mixin(commonMixins);
+  },
+};

+ 23 - 0
src/plugins/md5.js

@@ -0,0 +1,23 @@
+const md5 = require("js-md5");
+
+/**
+ *
+ * @param {any} str 字符串
+ */
+export const MD5 = (content) => {
+  return md5(content);
+};
+
+export const fileMD5 = (file) => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onloadend = function () {
+      const arrayBuffer = reader.result;
+      resolve(md5(arrayBuffer));
+    };
+    reader.onerror = function (err) {
+      reject(err);
+    };
+    reader.readAsArrayBuffer(file);
+  });
+};

+ 270 - 0
src/plugins/utils.js

@@ -0,0 +1,270 @@
+/**
+ * 判断对象类型
+ * @param {*} obj 对象
+ */
+export 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 Blob]": "blob",
+  };
+  return map[toString.call(obj)];
+}
+
+/**
+ * 深拷贝
+ * @param {Object/Array} data 需要拷贝的数据
+ */
+export function deepCopy(data) {
+  return JSON.parse(JSON.stringify(data));
+}
+
+/**
+ * 将目标对象中有的属性值与源对象中的属性值合并
+ * @param {Object} target 目标对象
+ * @param {Object} sources 源对象
+ */
+export 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;
+}
+
+/**
+ * 文件流下载
+ * @param {Object} option 文件下载设置
+ */
+export function download(option) {
+  let defOpt = {
+    type: "get",
+    url: "",
+    data: "",
+    fileName: "",
+    header: "",
+  };
+  let opt = objAssign(defOpt, option);
+
+  return new Promise((resolve, reject) => {
+    let xhr = new XMLHttpRequest();
+    xhr.open(opt.type.toUpperCase(), opt.url, true);
+    xhr.responseType = "blob";
+
+    // header set
+    if (opt.header && objTypeOf(opt.header) === "object") {
+      for (let key in opt.header) {
+        xhr.setRequestHeader(key, opt.header[key]);
+      }
+    }
+
+    xhr.onload = function () {
+      if (this.readyState === 4 && this.status === 200) {
+        if (this.response.size < 1024) {
+          reject("文件不存在!");
+          return;
+        }
+
+        var blob = this.response;
+        let pdfUrl = "";
+        let uRl = window.URL || window.webkitURL;
+        if (uRl && uRl.createObjectURL) {
+          pdfUrl = uRl.createObjectURL(blob);
+        } else {
+          reject("浏览器不兼容!");
+        }
+        let a = document.createElement("a");
+        a.download = opt.fileName;
+        a.href = pdfUrl;
+        document.body.appendChild(a);
+        a.click();
+        a.parentNode.removeChild(a);
+        resolve(true);
+      } else {
+        reject("请求错误!");
+      }
+    };
+
+    if (opt.type.toUpperCase() === "POST") {
+      let fromData = new FormData();
+      for (let key in opt.data) {
+        fromData.append(key, opt.data[key]);
+      }
+      xhr.send(fromData);
+    } else {
+      xhr.send();
+    }
+  });
+}
+
+/**
+ * 获取随机code,默认获取16位
+ * @param {Number} len 推荐8的倍数
+ *
+ */
+export 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 {String} format 时间格式
+ * @param {Date} date 需要格式化的时间对象
+ */
+export 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;
+}
+
+export function parseTimeRangeDateAndTime(startTime, endTime) {
+  if (!startTime || !endTime)
+    return {
+      date: "",
+      time: "",
+    };
+
+  const st = formatDate("YYYY-MM-DD HH:mm", new Date(startTime)).split(" ");
+  const et = formatDate("YYYY-MM-DD HH:mm", new Date(endTime)).split(" ");
+
+  return {
+    date: st[0],
+    time: `${st[1]}-${et[1]}`,
+  };
+}
+
+/**
+ * 获取本地时间,格式:年月日时分秒
+ */
+export function localNowDateTime() {
+  return formatDate("YYYY年MM月DD日HH时mm分ss秒");
+}
+
+/**
+ *
+ * @param {Number} time 时间戳
+ */
+export function getTimeDatestamp(time) {
+  const date = formatDate("YYYY-MM-DD HH:mm", new Date(time)).split(" ")[0];
+  return new Date(`${date} 00:00:00`).getTime();
+}
+
+/**
+ * 清除html标签
+ * @param {String} str html字符串
+ */
+export function removeHtmlTag(str) {
+  return str.replace(/<[^>]+>/g, "");
+}
+
+/**
+ * 计算总数
+ * @param {Array} dataList 需要统计的数组
+ */
+export function calcSum(dataList) {
+  if (!dataList.length) return 0;
+  return dataList.reduce(function (total, item) {
+    return total + item;
+  }, 0);
+}
+
+/** 获取数组最大数 */
+export function maxNum(dataList) {
+  return Math.max.apply(null, dataList);
+}
+
+export function isEmptyObject(obj) {
+  return !Object.keys(obj).length;
+}
+
+export function humpToLowLine(a) {
+  return a
+    .replace(/([A-Z])/g, "-$1")
+    .toLowerCase()
+    .slice(1);
+}
+
+export function pickByNotNull(params) {
+  let nData = {};
+  Object.entries(params).forEach(([key, val]) => {
+    if (val === null || val === "null" || val === "") return;
+    nData[key] = val;
+  });
+  return nData;
+}
+
+export function autoSubmitForm(url, params) {
+  const form = document.createElement("form");
+  form.action = url;
+  form.method = "post";
+
+  Object.entries(params).forEach(([key, val]) => {
+    const input = document.createElement("input");
+    input.type = "hidden";
+    input.name = key;
+    input.value = val;
+    form.appendChild(input);
+  });
+  document.body.appendChild(form);
+  form.submit();
+}
+
+export function blobToText(blob) {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.readAsText(blob, "utf-8");
+    reader.onload = function () {
+      resolve(reader.result);
+    };
+    reader.onerror = function () {
+      reject();
+    };
+  });
+}
+
+export function parseHrefParam(href, paramName = null) {
+  if (!href) return;
+  const paramStr = href.split("?")[1];
+  if (!paramStr) return;
+
+  let params = {};
+  paramStr.split("&").forEach((item) => {
+    const con = item.split("=");
+    params[con[0]] = con[1];
+  });
+
+  return paramName ? params[paramName] : params;
+}