zhangjie 3 лет назад
Сommit
efbd9f34ce
100 измененных файлов с 8242 добавлено и 0 удалено
  1. 3 0
      .browserslistrc
  2. 6 0
      .env
  3. 5 0
      .env.production
  4. 3 0
      .eslintignore
  5. 25 0
      .eslintrc.js
  6. 23 0
      .gitignore
  7. 5 0
      .prettierrc
  8. 5 0
      CHANGE.md
  9. 103 0
      README.md
  10. 3 0
      babel.config.js
  11. 9 0
      dev-proxy.copy.js
  12. 52 0
      package.json
  13. 5 0
      postcss.config.js
  14. BIN
      public/favicon.ico
  15. 25 0
      public/index.html
  16. 5 0
      src/App.vue
  17. BIN
      src/assets/images/icon-account.png
  18. BIN
      src/assets/images/icon-base.png
  19. BIN
      src/assets/images/icon-book.png
  20. BIN
      src/assets/images/icon-checkcode.png
  21. BIN
      src/assets/images/icon-close-act.png
  22. BIN
      src/assets/images/icon-close.png
  23. BIN
      src/assets/images/icon-customer.png
  24. BIN
      src/assets/images/icon-date.png
  25. BIN
      src/assets/images/icon-doubt.png
  26. BIN
      src/assets/images/icon-download.png
  27. BIN
      src/assets/images/icon-error.png
  28. BIN
      src/assets/images/icon-exam.png
  29. BIN
      src/assets/images/icon-files-act.png
  30. BIN
      src/assets/images/icon-files.png
  31. BIN
      src/assets/images/icon-location.png
  32. BIN
      src/assets/images/icon-logout.png
  33. BIN
      src/assets/images/icon-password.png
  34. BIN
      src/assets/images/icon-phone.png
  35. BIN
      src/assets/images/icon-report.png
  36. BIN
      src/assets/images/icon-search.png
  37. BIN
      src/assets/images/icon-shut.png
  38. BIN
      src/assets/images/icon-workspace.png
  39. BIN
      src/assets/images/login-back.png
  40. BIN
      src/assets/images/login-theme.png
  41. 44 0
      src/assets/styles/account.scss
  42. 26 0
      src/assets/styles/adaptive.scss
  43. 507 0
      src/assets/styles/base.scss
  44. 436 0
      src/assets/styles/common-comp.scss
  45. 644 0
      src/assets/styles/element-ui-costom.scss
  46. 341 0
      src/assets/styles/home.scss
  47. 92 0
      src/assets/styles/icons.scss
  48. 11 0
      src/assets/styles/index.scss
  49. 118 0
      src/assets/styles/login.scss
  50. 1074 0
      src/assets/styles/pages.scss
  51. 82 0
      src/assets/styles/paper-approve.css
  52. 42 0
      src/assets/styles/variables.scss
  53. 60 0
      src/components/MoreText.vue
  54. 247 0
      src/components/SimpleImagePreview.vue
  55. 164 0
      src/components/UploadButton.vue
  56. 130 0
      src/components/UploadFetchFile.vue
  57. 105 0
      src/components/UploadFileDialog.vue
  58. 202 0
      src/components/UploadFileView.vue
  59. 15 0
      src/components/ViewFooter.vue
  60. 63 0
      src/components/base/CampusSelect.vue
  61. 116 0
      src/components/base/ClazzSelect.vue
  62. 88 0
      src/components/base/CollegeSelect.vue
  63. 84 0
      src/components/base/CourseSelect.vue
  64. 75 0
      src/components/base/ExamSelect.vue
  65. 79 0
      src/components/base/MajorSelect.vue
  66. 71 0
      src/components/base/RoomSelect.vue
  67. 63 0
      src/components/base/SchoolSelect.vue
  68. 65 0
      src/components/base/SemesterSelect.vue
  69. 47 0
      src/components/common/CascaderSplit/CascaderSplit.vue
  70. 90 0
      src/components/common/CascaderSplit/LevelOne.vue
  71. 102 0
      src/components/common/CascaderSplit/LevelTwo.vue
  72. 18 0
      src/components/common/CascaderSplit/com-func.js
  73. 101 0
      src/components/common/DimensionTreeTable/DimensionTreeTable.vue
  74. 2 0
      src/components/common/DimensionTreeTable/index.js
  75. 92 0
      src/components/common/DimensionTreeTable/intro.md
  76. 418 0
      src/components/common/ImageEditUpload/ImageEditUpload.vue
  77. 2 0
      src/components/common/ImageEditUpload/index.js
  78. 55 0
      src/components/common/ImageEditUpload/intro.md
  79. BIN
      src/components/common/ImageEditUpload/temp-img.png
  80. 322 0
      src/components/common/ImageListUpload/ImageListUpload.vue
  81. 2 0
      src/components/common/ImageListUpload/index.js
  82. 47 0
      src/components/common/ImageListUpload/intro.md
  83. 312 0
      src/components/common/ImagePreview/ImagePreview.vue
  84. 2 0
      src/components/common/ImagePreview/index.js
  85. 51 0
      src/components/common/ImagePreview/move-ele.js
  86. 190 0
      src/components/common/ImportFile/ImportFile.vue
  87. 2 0
      src/components/common/ImportFile/index.js
  88. 74 0
      src/components/common/ImportFile/intro.md
  89. 72 0
      src/components/common/LabelFilter/LabelFilter.vue
  90. 205 0
      src/components/common/LabelFilter/LabelSelect.vue
  91. 283 0
      src/components/common/RichTextEditor/RichTextEditor.vue
  92. 2 0
      src/components/common/RichTextEditor/index.js
  93. 104 0
      src/components/common/RichTextEditor/intro.md
  94. 28 0
      src/components/common/StepsProgress/StepFour.vue
  95. 28 0
      src/components/common/StepsProgress/StepOne.vue
  96. 28 0
      src/components/common/StepsProgress/StepThree.vue
  97. 28 0
      src/components/common/StepsProgress/StepTwo.vue
  98. 126 0
      src/components/common/StepsProgress/StepsProgress.vue
  99. 82 0
      src/components/common/utils/ajax.js
  100. 6 0
      src/config.js

+ 3 - 0
.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not ie <= 8

+ 6 - 0
.env

@@ -0,0 +1,6 @@
+NODE_ENV=development
+VUE_APP_DOMAIN=
+VUE_APP_TIMEOUT=600000
+VUE_APP_PAGE_SIZE=10
+VUE_APP_AUTH_TIMEOUT=72000000
+VUE_APP_SELF_DEFINE_DOMAIN=true

+ 5 - 0
.env.production

@@ -0,0 +1,5 @@
+NODE_ENV=production
+VUE_APP_DOMAIN=
+VUE_APP_TIMEOUT=600000
+VUE_APP_PAGE_SIZE=10
+VUE_APP_AUTH_TIMEOUT=7200000

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+
+public
+vue.config.js

+ 25 - 0
.eslintrc.js

@@ -0,0 +1,25 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  extends: ['plugin:vue/essential', '@vue/prettier'],
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'no-unused-vars': [
+      'error',
+      { vars: 'all', args: 'none', ignoreRestSiblings: false }
+    ],
+    'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }],
+    'vue/no-use-v-if-with-v-for': [
+      'error',
+      {
+        allowUsingIterationVar: true
+      }
+    ]
+  },
+  parserOptions: {
+    parser: 'babel-eslint'
+  }
+}

+ 23 - 0
.gitignore

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

+ 5 - 0
.prettierrc

@@ -0,0 +1,5 @@
+{
+  "semi": true,
+  "singleQuote": false,
+  "jsxBracketSameLine": false
+}

+ 5 - 0
CHANGE.md

@@ -0,0 +1,5 @@
+
+
+# 1.0.0
+
+- 系统1.0.0

+ 103 - 0
README.md

@@ -0,0 +1,103 @@
+# eds-web
+
+**教务数据对接系统**
+
+## 项目操作
+
+#### 项目安装
+
+```
+yarn install
+```
+
+#### 开发模式
+
+```
+yarn run serve
+```
+
+#### 项目打包
+
+```
+yarn run build
+```
+
+#### lint 项目文件,并修正格式
+
+```
+yarn run lint
+```
+
+#### 自定义配置
+
+查看 [配置文档](https://cli.vuejs.org/config/).
+
+## 项目技术
+
+- ui 框架:
+  - [elementUi-2.13.1](https://element.eleme.cn/#/zh-CN/component/installation)
+- vue 技术:
+
+  - [vue-2.6.10](https://cn.vuejs.org/v2/guide/)
+  - [router-3.0.3](https://router.vuejs.org/zh/)
+  - [vuex-3.0.1](https://vuex.vuejs.org/zh/guide/)
+  - [axios-0.18.0](https://github.com/axios/axios)
+  - [vue-ls-3.2.1](https://github.com/RobinCK/vue-ls)
+
+## 格式化
+
+- 项目采用 prettier-eslint 规范项目代码
+- 推荐使用 vscode 作为项目编辑器
+- 提交代码前(即 commit 前), 项目会自检规范格式。
+
+## vscode 推荐配置
+
+#### 推荐插件
+
+插件安装在 vscode 界面左上角最下面那图标中
+
+- ESLint
+- One Dark Pro
+- Prettier - Code Formatter
+- Atom Keymap
+- Vetur
+
+## 目录介绍
+
+```bash
+- public                    # 打包静态文件目录
+- src                       # 项目主文件夹
+  - assets                  # 静态资源存放地址
+  - compontents             # 通用组件
+  - constants               # 静态数据存放地址
+    - enumerate.js          # 配置静态数据
+    - navs.js               # 全局左侧导航栏信息
+  - modules                 # 模块目录
+  - plugins                 # 插件文档
+    - axios.js              # 全局axios封装
+    - formRules.js          # 表单验证规则
+    - globalVuePlugins.js   # 全局vue插件注册文件
+    - mixins.js             # mixins配置
+    - utils.js              # 工具箱
+  - router                  # router配置
+  - store                   # store配置
+  - views                   # 路由组件
+  - App.vue                 # 主组件入口
+  - config.js               # 全局配置文件
+  - main.js                 # 主程序入口
+- .eslintrc.js              # eslint配置文件
+- .prettierrc               # prettier配置文件
+- dev-proxy.copy.js         # 开发代理配置副本
+- vue.config.js             # 项目开发环境配置文件
+```
+
+## 第一次创建项目
+
+- dev 模式推荐使用 proxy
+
+  - 具体配置请查看`dev-proxy.copy.js`
+
+## TODO
+
+- 暂无
+-

+ 3 - 0
babel.config.js

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

+ 9 - 0
dev-proxy.copy.js

@@ -0,0 +1,9 @@
+module.exports = {
+  "/api/": {
+    target: "http://192.168.10.239:9888",
+    changeOrigin: true,
+    pathRewrite: {
+      "^/api": "/"
+    }
+  }
+};

+ 52 - 0
package.json

@@ -0,0 +1,52 @@
+{
+  "name": "eds-web",
+  "version": "1.0.0",
+  "scripts": {
+    "start": "npm run serve",
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint",
+    "test:unit": "vue-cli-service test:unit"
+  },
+  "dependencies": {
+    "core-js": "^3.6.5",
+    "deepmerge": "^4.2.2",
+    "element-ui": "^2.14.1",
+    "js-md5": "^0.7.3",
+    "jsbarcode": "^3.11.3",
+    "vue": "^2.6.11",
+    "vue-ls": "^3.2.2",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0",
+    "axios": "^0.18.0",
+    "cropperjs": "^1.5.1",
+    "crypto-js": "^4.0.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",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.1.3",
+    "eslint-plugin-vue": "^6.2.2",
+    "lint-staged": "^9.5.0",
+    "prettier": "^1.19.1",
+    "sass": "^1.26.5",
+    "sass-loader": "^8.0.2",
+    "vue-template-compiler": "^2.6.11",
+    "terser-webpack-plugin": "^1.2.3"
+  },
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.{js,vue}": [
+      "vue-cli-service lint",
+      "git add"
+    ]
+  }
+}

+ 5 - 0
postcss.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  plugins: {
+    autoprefixer: {}
+  }
+};

BIN
public/favicon.ico


+ 25 - 0
public/index.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0,
+    maximum-scale=1.0,minimum-scale=1.0, user-scalable=no"" />
+    <meta name=" renderer" content="webkit|ie-comp|ie-stand" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
+    <title>教务数据对接系统</title>
+  </head>
+
+  <body>
+    <noscript>
+      <strong
+        >We're sorry but 教务数据对接系统 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>

+ 5 - 0
src/App.vue

@@ -0,0 +1,5 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


BIN
src/assets/images/login-theme.png


+ 44 - 0
src/assets/styles/account.scss

@@ -0,0 +1,44 @@
+.user-info {
+  text-align: center;
+}
+.user-info-avatar {
+  margin: 50px auto 10px;
+  width: 100px;
+  height: 100px;
+  background-color: #e0e0e0;
+  border-radius: 50%;
+  overflow: hidden;
+  /* linshi */
+  line-height: 100px;
+  font-size: 50px;
+}
+.user-info-name {
+  margin: 10px 0;
+  font-size: 18px;
+}
+.user-info-email {
+  font-size: 14px;
+  margin-bottom: 20px;
+}
+.user-setting h3 {
+  font-size: 16px;
+  text-align: center;
+}
+.user-table {
+  min-width: 700px;
+  max-width: 1000px;
+  margin: 30px auto;
+}
+
+// reset-title
+.reset-title {
+  height: 30px;
+  line-height: 30px;
+  font-size: 16px;
+  text-align: center;
+  margin-bottom: 30px;
+}
+.reset-form {
+  width: 600px;
+  margin: 0 auto;
+}

+ 26 - 0
src/assets/styles/adaptive.scss

@@ -0,0 +1,26 @@
+@media screen and (min-width: 1440px) {
+  // home
+  .home-breadcrumb {
+    font-size: $--font-size-base;
+
+    .el-breadcrumb {
+      font-size: $--font-size-base;
+    }
+  }
+  .custom-tree-node {
+    font-size: $--font-size-base;
+  }
+  // element-ui
+  .el-form {
+    &--label-top {
+      .el-form-item__label {
+        padding-bottom: 8px;
+        font-size: $--font-size-base;
+      }
+    }
+  }
+  .el-table,
+  .el-button--small.el-button--text {
+    font-size: $--font-size-base;
+  }
+}

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

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

+ 436 - 0
src/assets/styles/common-comp.scss

@@ -0,0 +1,436 @@
+// tips
+.cc-tips-error {
+  color: $--color-danger;
+}
+.cc-tips-success {
+  color: $--color-success;
+}
+
+// image-edit-upload
+.image-edit-upload {
+  position: relative;
+  height: 400px;
+  background: #fff;
+  padding-bottom: 30px;
+  box-sizing: content-box;
+}
+.cc-edit-upload {
+  &-main {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    z-index: 8;
+
+    .el-upload-dragger {
+      height: auto;
+    }
+    .el-upload-input {
+      display: none;
+    }
+  }
+  &-cover {
+    height: 400px;
+    > img {
+      display: block;
+      height: 100%;
+      width: auto;
+      margin: 0 auto;
+    }
+  }
+  &-dcover {
+    padding: 109px 0;
+    > i {
+      font-size: 160px;
+      color: #999;
+    }
+  }
+  &-edit {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    z-index: 9;
+  }
+
+  &-edit-box {
+    height: 350px;
+    position: relative;
+    .img-progress {
+      position: absolute;
+      height: 10px;
+      width: 100%;
+      bottom: -8px;
+      left: 0;
+      z-index: 7;
+    }
+  }
+  &-btns {
+    height: 50px;
+    padding-top: 10px;
+    text-align: center;
+    button {
+      margin: 0 10px;
+    }
+  }
+  &-tips {
+    position: absolute;
+    top: 405px;
+    left: 0;
+    font-size: 14px;
+    z-index: 9;
+  }
+}
+
+// image-list-upload
+$--cc-list-upload-pre: cc-list-upload;
+
+.cc-list-upload {
+  &-list {
+    font-size: 0;
+  }
+  &-item {
+    font-size: 14px;
+    display: inline-block;
+    vertical-align: top;
+    margin: 20px 10px;
+    border: 1px solid $--color-text-gray-6;
+    height: 150px;
+    width: 150px;
+    position: relative;
+
+    &-unvalid,
+    &-error {
+      border-color: $--color-danger;
+    }
+    &-success {
+      border-color: $--color-success;
+    }
+    &-ready .#{$--cc-list-upload-pre}-action,
+    &-uploading .#{$--cc-list-upload-pre}-action {
+      display: none !important;
+    }
+    &-ready .#{$--cc-list-upload-pre}-progress,
+    &-uploading .#{$--cc-list-upload-pre}-progress {
+      display: block;
+    }
+  }
+  &-box {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    z-index: auto;
+  }
+  &-info {
+    z-index: 7;
+    > img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+  }
+  &-input {
+    visibility: hidden;
+  }
+  &-progress {
+    display: none;
+    z-index: 10;
+    background: rgba(0, 0, 0, 0.3);
+    padding: 5px;
+    text-align: center;
+    color: #000;
+  }
+  &-action {
+    display: none;
+    z-index: 8;
+    background: rgba(0, 0, 0, 0.7);
+    padding: 60px 5px 0;
+    line-height: 30px;
+    text-align: center;
+    > i {
+      font-size: 40px;
+      color: #c0c0c0;
+      cursor: pointer;
+    }
+    > i:hover {
+      color: #fff;
+    }
+  }
+  &-item:hover .#{$--cc-list-upload-pre}-action {
+    display: block;
+  }
+
+  &-tips {
+    position: absolute;
+    width: 100%;
+    bottom: -30px;
+    text-align: center;
+    line-height: 30px;
+    &-error {
+      color: $--color-danger;
+    }
+    &-success {
+      color: $--color-success;
+    }
+  }
+
+  &-add {
+    cursor: pointer;
+    > i {
+      display: block;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      font-size: 60px;
+      transform: translate(-50%, -50%);
+      color: #999;
+    }
+  }
+}
+
+// image-preview
+.cc-image-preview {
+  &-header {
+    position: absolute;
+    width: 100%;
+    padding: 15px;
+    top: 0;
+    left: 0;
+    line-height: 20px;
+    text-align: center;
+    font-size: 16px;
+    z-index: 99;
+    color: #f0f0f0;
+    h3 {
+      position: absolute;
+      left: 15px;
+      z-index: auto;
+    }
+  }
+  &-close {
+    position: absolute;
+    width: 30px;
+    height: 30px;
+    top: 5px;
+    right: 15px;
+    z-index: 100;
+    line-height: 30px;
+    text-align: center;
+    font-size: 40px;
+    color: #fff;
+    text-shadow: 0 0 2px #333;
+    cursor: pointer;
+
+    &:hover {
+      color: $--color-danger;
+    }
+  }
+  &-body {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: auto;
+    overflow: hidden;
+  }
+  &-imgs {
+    position: absolute;
+    top: 0;
+    left: 50%;
+    width: 600px;
+    margin-left: -300px;
+    // box-shadow: 0px 24px 36px 0px rgba(0, 0, 0, 0.3);
+    transition: width, height, transform 0.2s linear;
+    z-index: 8;
+    &-nosition {
+      transition: none;
+    }
+    &-move {
+      cursor: move;
+    }
+    > img {
+      display: block;
+      width: 100%;
+    }
+  }
+  &-guide {
+    position: absolute;
+    width: 80px;
+    height: 80px;
+    line-height: 80px;
+    top: 50%;
+    margin-top: -80px;
+    text-align: center;
+    color: #d0d0d0;
+    z-index: 9;
+    font-size: 60px;
+    text-shadow: 0 0 2px #333;
+    cursor: pointer;
+
+    &:hover {
+      color: #eee;
+    }
+    > i {
+      margin-top: -5px;
+    }
+    &-prev {
+      left: 0;
+    }
+    &-next {
+      right: 0;
+    }
+  }
+  &-footer {
+    position: absolute;
+    height: 60px;
+    bottom: 0;
+    right: 0;
+    padding: 10px;
+    font-size: 30px;
+    color: #d0d0d0;
+    z-index: 99;
+    li {
+      display: inline-block;
+      vertical-align: top;
+      height: 40px;
+      width: 40px;
+      line-height: 40px;
+      margin: 0 5px;
+      text-align: center;
+      cursor: pointer;
+      transition: transform 0.2s linear;
+    }
+    li:hover {
+      // color: #000;
+      transform: scale(1.1, 1.1);
+    }
+    li.li-disabled {
+      cursor: not-allowed;
+      color: #d0d0d0 !important;
+      transform: none !important;
+    }
+  }
+
+  &-loading {
+    position: absolute;
+    width: 60px;
+    height: 60px;
+    top: 0;
+    left: 50%;
+    margin: 0 0 0 -30px;
+    color: $--color-border;
+    font-size: 50px;
+    text-align: center;
+    line-height: 60px;
+    z-index: 99;
+  }
+
+  &-preload {
+    position: absolute;
+    z-index: 9;
+    width: 100px;
+    height: 100px;
+    overflow: hidden;
+    top: -1000px;
+    left: -1000px;
+  }
+
+  &-none {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    color: #e0e0e0;
+    text-align: center;
+    font-size: 20px;
+    > i {
+      font-size: 30px;
+    }
+  }
+
+  // view-ui
+  .ivu-modal-content {
+    background: transparent;
+  }
+  .ivu-modal-header {
+    display: none;
+  }
+  .ivu-modal-body {
+    top: 0;
+  }
+  .ivu-modal-close {
+    display: none;
+  }
+  .ivu-modal-mask {
+    background: rgba(55, 55, 55, 0.8);
+  }
+}
+
+// import-file
+.cc-import-file {
+  &-tips {
+    height: 20px;
+    line-height: 20px;
+  }
+  &-footer {
+    padding: 10px 0;
+    font-size: 14px;
+    a:hover {
+      color: $--color-primary;
+    }
+  }
+}
+
+// rich-editor
+.cc-editor {
+  .ql-editor {
+    max-height: 500px !important;
+    height: 300px;
+    overflow: auto;
+  }
+}
+
+// label-select
+$--cc-labels-pre: cc-labels;
+.cc-labels {
+  margin-bottom: 8px;
+
+  &-label {
+    display: block;
+    float: left;
+    height: 24px;
+    line-height: 24px;
+    font-size: 14px;
+    padding-right: 20px;
+    font-weight: 600;
+  }
+  &-options {
+    margin-left: 60px;
+    li {
+      display: inline-block;
+      vertical-align: top;
+      margin: 0 5px 5px 0;
+      height: 24px;
+      line-height: 24px;
+      padding: 0 10px;
+      border-radius: 3px;
+      cursor: pointer;
+    }
+    li:hover {
+      color: #409eff;
+    }
+    li.#{$--cc-labels-pre}-selected {
+      background: #409eff;
+      color: #fff;
+    }
+    li.#{$--cc-labels-pre}-disabled {
+      color: #909399;
+      cursor: not-allowed;
+    }
+  }
+}

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

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

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

@@ -0,0 +1,341 @@
+/* home */
+.home {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  z-index: auto;
+}
+.home-body {
+  position: absolute;
+  left: 0;
+  top: 50px;
+  right: 0;
+  bottom: 0;
+  overflow: auto;
+  background: $--color-background;
+  z-index: 98;
+}
+.home-main {
+  position: relative;
+  padding: 20px 30px 50px 250px;
+  min-height: 100%;
+}
+
+/* navs */
+.home-navs {
+  position: absolute;
+  width: 220px;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  z-index: 100;
+  overflow: auto;
+  font-size: 14px;
+  background: $--color-white;
+  border-top-right-radius: $--border-radius-huge;
+  border-bottom-right-radius: $--border-radius-huge;
+
+  &::before {
+    content: "";
+    display: block;
+    position: absolute;
+    height: 100%;
+    width: 1px;
+    right: 0;
+    bottom: 0;
+    z-index: 9;
+    background: rgba(229, 229, 229, 1);
+  }
+
+  .head-logo {
+    padding: 0 40px;
+    font-size: 20px;
+    line-height: 40px;
+    text-align: center;
+    &-content {
+      display: block;
+      padding: 30px 0;
+      border-bottom: 1px solid #eff0f5;
+    }
+    img {
+      display: block;
+      max-width: 160px;
+      height: 40px;
+    }
+  }
+
+  .nav-part {
+    padding: 20px 0;
+    border-top: 1px solid $--color-border;
+  }
+
+  .nav-head {
+    padding: 10px 0;
+    color: $--color-text-gray-2;
+    font-size: $--font-size-base;
+    line-height: 20px;
+    position: relative;
+    font-weight: 500;
+    > span {
+      display: inline-block;
+      vertical-align: top;
+      font-weight: 600;
+    }
+    &-right-icon {
+      position: absolute;
+      right: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      color: #d3d5e0;
+      font-size: 12px;
+    }
+  }
+
+  // .nav-list {
+  //   padding: 0 0 0 23px;
+  // }
+  .nav-item {
+    overflow: hidden;
+    color: $--color-text-dark-1;
+    &-main {
+      padding: 10px 0;
+      line-height: 20px;
+      position: relative;
+      font-weight: 500;
+      cursor: pointer;
+      &-act,
+      &:hover {
+        font-weight: 600;
+        color: $--color-primary;
+      }
+    }
+    &-icon {
+      display: block;
+      position: absolute;
+      width: 20px;
+      height: 20px;
+      top: 50%;
+      margin-top: -10px;
+      text-align: center;
+      line-height: 20px;
+    }
+    &-icon-right {
+      right: 5px;
+    }
+    &-info {
+      display: block;
+      position: absolute;
+      padding: 0 3px;
+      min-width: 16px;
+      height: 16px;
+      font-size: 12px;
+      line-height: 16px;
+      top: 12px;
+      right: 40px;
+      background-color: $--color-warning;
+      color: #fff;
+      text-align: center;
+      border-radius: 3px;
+    }
+  }
+}
+.el-menu-home {
+  padding-top: 20px;
+  .el-submenu {
+    margin-bottom: 20px;
+  }
+  .el-submenu__title {
+    padding: 0 40px !important;
+    height: 50px;
+    line-height: 50px;
+    font-weight: 600;
+
+    > .icon {
+      margin-right: 12px;
+    }
+  }
+  .el-menu-item {
+    height: auto;
+    min-height: 40px;
+    line-height: 20px;
+    padding: 10px 40px !important;
+    white-space: normal;
+  }
+  .el-menu-item.is-active {
+    font-weight: 600;
+  }
+  .el-submenu__icon-arrow {
+    right: 40px;
+  }
+}
+
+/* head */
+.home-header {
+  position: absolute;
+  width: 100%;
+  height: 50px;
+  top: 0;
+  left: 0;
+  z-index: 99;
+  color: #fff;
+  padding-left: 220px;
+  background-color: $--color-text-dark;
+  overflow: hidden;
+
+  .menu-list {
+    li {
+      display: inline-block;
+      vertical-align: top;
+      padding: 10px 25px;
+      height: 50px;
+      line-height: 30px;
+      opacity: 0.4;
+      font-size: 16px;
+      position: relative;
+      text-align: center;
+      cursor: pointer;
+
+      &:hover {
+        opacity: 1;
+      }
+
+      &.menu-item-act {
+        opacity: 1;
+      }
+
+      span {
+        display: inline-block;
+        vertical-align: top;
+        margin-left: 8px;
+      }
+      .icon {
+        margin-top: -3px;
+      }
+    }
+  }
+  .head-menu {
+    float: left;
+  }
+  .head-user {
+    float: right;
+    padding-right: 10px;
+    li {
+      padding: 10px;
+    }
+    .menu-item-account {
+      white-space: nowrap;
+      padding: 10px;
+      span {
+        max-width: 156px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+  // .head-menu-btn
+  .head-menu-btn {
+    display: none;
+    float: right;
+    line-height: 36px;
+    padding: 12px 15px;
+    text-align: center;
+    > span {
+      display: block;
+      height: 36px;
+      width: 36px;
+      border-radius: 5px;
+      background-color: rgba($color: #fff, $alpha: 0.3);
+    }
+    i {
+      font-size: 22px;
+      vertical-align: middle;
+    }
+  }
+}
+// menu-dialog
+.menu-dialog {
+  .el-dialog.is-fullscreen {
+    border-radius: 0;
+    box-shadow: none;
+
+    .el-dialog__body {
+      padding: 10px;
+      &::after {
+        display: none;
+      }
+    }
+  }
+
+  .menu-logout {
+    padding: 10px;
+    width: 52px;
+    height: 52px;
+    margin: 0 auto;
+    border: 1px solid $--color-text-gray-3;
+    border-radius: 50%;
+    font-size: 30px;
+    text-align: center;
+    color: $--color-text-gray-3;
+    cursor: pointer;
+
+    &:hover {
+      border-color: $--color-danger;
+      color: $--color-danger;
+    }
+  }
+}
+
+// home-breadcrumb
+.home-breadcrumb {
+  margin-bottom: 18px;
+  font-size: 12px;
+  font-weight: 500;
+  color: $--color-text-gray-2;
+
+  .breadcrumb-tips {
+    display: inline-block;
+    vertical-align: middle;
+    > i {
+      margin-top: -2px;
+      margin-right: 8px;
+    }
+  }
+  .el-breadcrumb {
+    line-height: 16px;
+    display: inline-block;
+    vertical-align: middle;
+    font-size: 12px;
+
+    .el-breadcrumb__item {
+      .el-breadcrumb__inner {
+        color: $--color-text-gray-2;
+      }
+    }
+    .el-breadcrumb__separator {
+      margin: 0 5px;
+    }
+  }
+}
+
+// home-view
+
+/* view-footer */
+.home-footer {
+  position: absolute;
+  width: 100%;
+  height: 60px;
+  bottom: 0;
+  left: 0;
+  z-index: auto;
+  padding: 20px 0;
+  line-height: 20px;
+  color: $--color-text-gray-3;
+  text-align: center;
+  font-size: 13px;
+  a {
+    color: $--color-text-gray-3;
+  }
+  a:hover {
+    color: $--color-text-gray;
+  }
+}

+ 92 - 0
src/assets/styles/icons.scss

@@ -0,0 +1,92 @@
+// icon
+.icon {
+  display: inline-block;
+  vertical-align: middle;
+  width: 16px;
+  height: 16px;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+
+  // home
+  &-location {
+    background-image: url(../images/icon-location.png);
+    width: 10px;
+    height: 10px;
+  }
+  // navs
+  &-exam {
+    background-image: url(../images/icon-exam.png);
+  }
+  &-report {
+    background-image: url(../images/icon-report.png);
+  }
+  &-base {
+    width: 14px;
+    background-image: url(../images/icon-base.png);
+  }
+  &-customer {
+    background-image: url(../images/icon-customer.png);
+  }
+  &-workspace {
+    background-image: url(../images/icon-workspace.png);
+  }
+  &-book {
+    background-image: url(../images/icon-book.png);
+  }
+
+  &-account {
+    background-image: url(../images/icon-account.png);
+  }
+  &-logout {
+    background-image: url(../images/icon-logout.png);
+  }
+
+  // login
+  &-phone {
+    background-image: url(../images/icon-phone.png);
+    width: 14px;
+    height: 16px;
+  }
+  &-password {
+    background-image: url(../images/icon-password.png);
+    width: 14px;
+    height: 14px;
+  }
+  &-checkcode {
+    background-image: url(../images/icon-checkcode.png);
+    width: 14px;
+    height: 16px;
+  }
+  // other
+  &-error {
+    background-image: url(../images/icon-error.png);
+    width: 16px;
+    height: 16px;
+  }
+  &-search {
+    background-image: url(../images/icon-search.png);
+    width: 14px;
+    height: 14px;
+  }
+  &-download {
+    background-image: url(../images/icon-download.png);
+    width: 14px;
+    height: 14px;
+  }
+  &-close {
+    background-image: url(../images/icon-close.png);
+  }
+  &-close-act {
+    background-image: url(../images/icon-close-act.png);
+  }
+  &-files {
+    background-image: url(../images/icon-files.png);
+    width: 14px;
+    height: 12px;
+  }
+  &-files-act {
+    background-image: url(../images/icon-files-act.png);
+    width: 14px;
+    height: 12px;
+  }
+}

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

@@ -0,0 +1,11 @@
+@import "./variables.scss";
+@import "./base.scss";
+@import "./icons.scss";
+@import "./home.scss";
+
+@import "./login.scss";
+@import "./pages.scss";
+
+@import "./element-ui-costom.scss";
+@import "./common-comp.scss";
+@import "./adaptive.scss";

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

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

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

@@ -0,0 +1,1074 @@
+.task-apply-steps {
+  min-height: 90px;
+  margin-bottom: 20px;
+  margin-top: -32px;
+  .el-step__title {
+    font-size: 14px;
+    line-height: 30px;
+  }
+}
+.task-exam-room {
+  background-color: #fff;
+  border-radius: $--border-radius;
+  margin-bottom: 10px;
+}
+// task-detail
+.task-detail {
+  .table {
+    border-radius: $--border-radius;
+    color: $--color-text-dark-1;
+    border: 1px solid $--color-border;
+    border-bottom: 0;
+    border-right: 0;
+    border-collapse: separate;
+    overflow: hidden;
+
+    th {
+      border-left: 0;
+      border-top: 0;
+      color: $--color-text-gray-2;
+      padding: 15px;
+    }
+    td {
+      border-left: 0;
+      border-top: 0;
+      padding: 10px 15px;
+
+      i {
+        display: inline-block;
+        vertical-align: middle;
+      }
+    }
+  }
+  .icon {
+    margin-right: 8px;
+  }
+
+  .task-action {
+    margin-top: 50px;
+    text-align: center;
+  }
+  .task-audit {
+    padding-top: 15px;
+    border-top: 2px dashed $--color-border;
+    margin-top: 20px;
+  }
+  // .task-audit-history {
+  // }
+
+  .image-item {
+    display: inline-block;
+    vertical-align: top;
+    width: 80px;
+    height: 80px;
+    margin: 0 10px 10px 0;
+    border: 1px solid $--color-text-gray-4;
+    position: relative;
+    border-radius: 5px;
+    overflow: hidden;
+
+    img {
+      position: absolute;
+      margin: auto;
+      max-width: 100%;
+      max-height: 100%;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: auto;
+      cursor: pointer;
+    }
+
+    .image-delete {
+      display: none;
+      position: absolute;
+      z-index: 99;
+      width: 100%;
+      height: 30px;
+      line-height: 30px;
+      text-align: center;
+      font-size: 20px;
+      bottom: 0;
+      left: 0;
+      background-color: rgba($color: #000000, $alpha: 0.3);
+      color: $--color-danger;
+      cursor: pointer;
+    }
+
+    &:hover {
+      .image-delete {
+        display: block;
+      }
+    }
+  }
+  .image-add {
+    font-size: 40px;
+    line-height: 1;
+    padding: 19px;
+    text-align: center;
+    border-radius: 5px;
+    border-style: dashed;
+    overflow: hidden;
+    cursor: pointer;
+    color: $--color-text-gray-5;
+
+    &:hover {
+      color: $--color-text-gray-3;
+    }
+  }
+  .image-list-none {
+    color: $--color-text-gray-2;
+    margin-bottom: 10px;
+  }
+}
+// apply-audit-history
+.apply-audit-history {
+  .audit-result {
+    margin: 10px 0;
+  }
+}
+// task-list
+.task-head {
+  font-size: 16px;
+
+  i {
+    cursor: pointer;
+
+    &:hover {
+      color: $--color-primary;
+    }
+  }
+}
+.task-list {
+  .task-item {
+    margin-bottom: 15px;
+    padding: 15px;
+    border-radius: $--border-radius;
+    background-color: $--color-background;
+
+    p {
+      line-height: 1.5;
+      margin-bottom: 5px;
+      > span:first-child {
+        color: $--color-text-dark-1;
+      }
+    }
+  }
+  .task-action {
+    cursor: pointer;
+    &:hover {
+      color: $--color-primary;
+    }
+  }
+}
+// create-exam-and-print-task
+.create-exam-and-print-task {
+  .apply-body {
+    width: 1000px;
+    margin: 0 auto 30px;
+  }
+  .apply-part {
+    padding: 20px 30px;
+    border-radius: 20px;
+    border: 1px solid $--color-text-gray-5;
+    margin-bottom: 20px;
+
+    &-title {
+      font-weight: 600;
+      font-size: 18px;
+      line-height: 1;
+      margin-bottom: 30px;
+    }
+  }
+}
+// flow-timeline
+.flow-timeline {
+  margin-top: 10px;
+  padding: 15px;
+  border: 1px solid $--color-text-gray-6;
+  border-radius: 10px;
+  background-color: $--color-text-gray-7;
+  .el-timeline {
+    padding: 0 10px;
+    &-item__tail {
+      border-color: #ddd;
+    }
+  }
+  .timeline-item-stop {
+    .el-timeline-item__tail {
+      border-left-style: dashed;
+    }
+  }
+
+  .el-timeline-item__node--success {
+    background-color: $--color-success;
+  }
+
+  .flow-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    &-time {
+      color: #909399;
+      line-height: 1;
+      font-size: 13px;
+      margin-bottom: 8px;
+      padding-top: 4px;
+    }
+    &-title {
+      font-size: 16px;
+      line-height: 1;
+      margin-bottom: 5px;
+    }
+    &-desc {
+      color: $--color-text-gray;
+
+      > span:not(:first-child) {
+        margin-left: 10px;
+      }
+    }
+
+    &-attachment {
+      .btn-primary {
+        &:hover {
+          font-weight: normal;
+        }
+
+        & + .btn-primary {
+          position: relative;
+          margin-left: 20px;
+
+          &::before {
+            content: "";
+            display: block;
+            position: absolute;
+            width: 0;
+            height: 14px;
+            border-left: 1px solid $--color-text-gray-4;
+            left: -10px;
+            top: 50%;
+            margin-top: -7px;
+          }
+        }
+      }
+    }
+
+    &-action {
+      text-align: center;
+    }
+
+    // action
+    .user-select {
+      width: 40px;
+      height: 40px;
+      padding: 10px;
+      font-size: 18px;
+      line-height: 1;
+    }
+  }
+}
+
+// wait-task
+.wait-task {
+  .wait-module {
+    height: 100%;
+  }
+}
+// card-title-rule-edit
+.card-title-rule-edit {
+  .field-item {
+    display: inline-block;
+    vertical-align: top;
+    margin: 0 10px 10px 0;
+    padding: 8px 10px;
+    border-radius: 4px;
+    line-height: 1;
+    background-color: $--color-background;
+    cursor: pointer;
+
+    &:hover {
+      color: $--color-primary;
+    }
+    &-act {
+      background-color: $--color-primary;
+      color: #fff !important;
+    }
+    &-disabled {
+      cursor: not-allowed;
+
+      &:hover {
+        color: $--color-text-dark;
+      }
+    }
+  }
+  .field-textarea {
+    border-radius: $--border-radius;
+    border: 1px solid $--color-text-gray-4;
+    min-height: 60px;
+    padding: 2px;
+    overflow: hidden;
+
+    &:focus {
+      border-color: $--color-primary;
+    }
+
+    span.var-field {
+      display: inline-block;
+      vertical-align: middle;
+      padding: 3px 5px;
+      background-color: $--color-primary;
+      color: $--color-white;
+      line-height: 1;
+      border-radius: 3px;
+    }
+  }
+}
+
+// page-preview
+.preview-frame {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 99;
+}
+// label-edit
+.label-edit {
+  min-height: 60px;
+  .label-item {
+    display: inline-block;
+    vertical-align: top;
+    border: 1px solid $--color-text-gray-6;
+    border-radius: $--border-radius;
+    padding: 5px 40px 5px 10px;
+    position: relative;
+    margin: 0 10px 10px 0;
+    line-height: 21px;
+  }
+  .label-item-content {
+    margin: 0;
+    line-height: 21px;
+    vertical-align: middle;
+  }
+  .label-item-delete {
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    z-index: 99;
+    font-size: 16px;
+    color: $--color-text-gray-5;
+    cursor: pointer;
+    &:hover {
+      color: $--color-danger;
+    }
+  }
+  .label-add {
+    display: inline-block;
+    vertical-align: top;
+    border: 1px solid $--color-text-gray-6;
+    border-radius: $--border-radius;
+    padding: 0 10px;
+    color: $--color-text-gray-2;
+    cursor: pointer;
+    &:hover {
+      border-color: $--color-primary;
+      color: $--color-primary;
+    }
+  }
+}
+
+// rule-exam
+
+// select-orgs
+.select-orgs {
+  position: relative;
+
+  &-disabled {
+    &::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: 99;
+      background-color: #f5f7fa;
+      opacity: 0.3;
+      cursor: not-allowed;
+    }
+  }
+
+  .el-checkbox.is-disabled + span.el-tree-node__label {
+    color: $--color-text-gray-4;
+  }
+}
+// privilege-set
+.privilege-set {
+  .cell-check-list {
+    text-align: left;
+    padding-left: 20px;
+  }
+}
+// organization-manage
+.organization-manage {
+  .org-tree-head {
+    height: 40px;
+    line-height: 40px;
+    color: $--color-text-gray-2;
+    border-bottom: 1px solid $--color-border;
+    padding: 0 8px;
+
+    > div {
+      float: right;
+      font-weight: 500;
+      &:first-child {
+        float: left;
+      }
+
+      &:nth-of-type(2) {
+        width: 200px;
+        text-align: right;
+      }
+      &:nth-of-type(3) {
+        width: 100px;
+      }
+    }
+  }
+  .el-tree-node__content {
+    height: auto;
+    padding: 4px 0;
+    border-bottom: 1px solid $--color-border;
+  }
+  .org-edit {
+    .org-type {
+      width: 100px;
+      display: inline-block;
+    }
+    .org-actions {
+      width: 200px;
+      display: inline-block;
+      text-align: right;
+    }
+    .el-button--text:hover {
+      transform: scale(1.1);
+    }
+  }
+}
+// modify-print-plan
+.modify-print-plan {
+  .part-box {
+    margin-bottom: 40px;
+  }
+}
+
+// paper-track-preview-dialog
+.paper-track-preview-dialog {
+  .paper-preview {
+    overflow: auto;
+    canvas {
+      display: block;
+      margin: 0 auto;
+    }
+  }
+  .preview-loading {
+    position: absolute;
+    top: 100px;
+    width: 100%;
+    z-index: auto;
+    font-size: 100px;
+    text-align: center;
+    color: #aaa;
+  }
+  .el-dialog__body {
+    padding: 70px 20px 80px !important;
+  }
+  .el-dialog__footer {
+    position: fixed;
+    width: 100%;
+    left: 0;
+    bottom: 0;
+    z-index: 9;
+    padding: 15px 20px;
+    border-top: 1px solid #eff0f5;
+    background-color: #fff;
+    .el-button {
+      float: none;
+    }
+  }
+}
+
+// marker-login
+.marker-login {
+  .part-box {
+    padding: 100px 50px;
+    text-align: center;
+  }
+  .auth-item {
+    padding: 80px;
+    margin: 0 20px;
+    font-size: 40px;
+  }
+}
+
+// modify-exam
+.modify-exam {
+  .tips-info {
+    font-size: 14px;
+  }
+  .el-radio-v {
+    display: block;
+    margin-bottom: 8px;
+  }
+}
+// modify-exam-config
+
+// modify-flow-detail
+.modify-flow-detail {
+  .flow-box {
+    display: flex;
+    justify-content: space-between;
+    align-items: stretch;
+  }
+  .flow-property {
+    width: 300px;
+    min-height: 400px;
+    flex-grow: 0;
+    flex-shrink: 0;
+    border: 1px solid $--color-text-gray-5;
+    border-radius: 10px;
+    padding: 15px;
+    background-color: #fff;
+
+    &-title {
+      font-size: 16px;
+      line-height: 1;
+      padding-bottom: 10px;
+      margin-bottom: 10px;
+      border-bottom: 1px solid $--color-text-gray-5;
+    }
+  }
+  .property-part {
+    margin-bottom: 15px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid $--color-text-gray-5;
+
+    &-title {
+      font-size: 14px;
+      line-height: 1;
+      margin-bottom: 10px;
+    }
+  }
+  .property-desc {
+    margin-bottom: 10px;
+  }
+  .flow-radio-v {
+    .el-radio {
+      display: block;
+      margin-bottom: 8px;
+    }
+  }
+  .flow-users {
+    margin-top: 10px;
+  }
+  .user-list {
+    margin-top: 10px;
+
+    .el-tag {
+      margin: 3px;
+    }
+  }
+  .user-clear {
+    padding: 5px 10px;
+    width: 68px;
+    margin: 3px;
+  }
+
+  .flow-content {
+    margin-right: 20px;
+    flex-grow: 1;
+    border: 1px solid $--color-text-gray-5;
+    border-radius: 10px;
+    padding: 10px;
+    position: relative;
+    background-color: #fff;
+  }
+  .flow-main {
+    width: 200px;
+    margin: 30px auto 0;
+  }
+  .flow-node {
+    position: relative;
+    box-shadow: 0 0 0 1px #ccc;
+    border-radius: 10px;
+    margin-bottom: 60px;
+    cursor: pointer;
+
+    &:hover {
+      opacity: 0.9;
+    }
+    &.is-active {
+      opacity: 1;
+      box-shadow: 0 0 0 2px $--color-blue;
+    }
+
+    &-title {
+      padding: 8px 10px;
+      border-bottom: 1px solid #ccc;
+      background-color: $--color-blue;
+      color: #fff;
+      border-top-left-radius: 10px;
+      border-top-right-radius: 10px;
+    }
+    &-content {
+      padding: 10px;
+      min-height: 40px;
+    }
+  }
+  .node-start {
+    cursor: default;
+    .flow-node-content {
+      background-color: mix(#fff, $--color-success, 20%);
+      text-align: center;
+      font-size: 18px;
+      border-radius: 10px;
+      color: #fff;
+    }
+  }
+  .node-end {
+    cursor: default;
+
+    .flow-node-content {
+      background-color: mix(#fff, $--color-danger, 20%);
+      text-align: center;
+      font-size: 18px;
+      border-radius: 10px;
+      color: #fff;
+    }
+  }
+  .flow-link {
+    position: absolute;
+    width: 30px;
+    left: 50%;
+    margin-left: -15px;
+    height: 52px;
+    bottom: -56px;
+
+    &::before {
+      content: "";
+      display: block;
+      position: absolute;
+      border-left: 2px solid $--color-text-gray-2;
+      top: 0;
+      bottom: 2px;
+      left: 50%;
+      margin-left: -1px;
+      z-index: 8;
+    }
+    &::after {
+      content: "";
+      display: block;
+      position: absolute;
+      width: 0;
+      height: 0;
+      border-width: 8px;
+      border-style: solid;
+      border-color: $--color-text-gray-2 transparent transparent transparent;
+      left: 50%;
+      margin-left: -8px;
+      bottom: -8px;
+      z-index: 9;
+    }
+
+    .node-add {
+      position: absolute;
+      width: 24px;
+      height: 24px;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      border-radius: 50%;
+      z-index: 99;
+      background-color: $--color-primary;
+      color: #fff;
+      font-size: 16px;
+      line-height: 25px;
+      text-align: center;
+      cursor: pointer;
+
+      &:hover {
+        background-color: mix(#000, $--color-primary, 10%);
+      }
+    }
+  }
+}
+.select-user-dialog {
+  .user-search {
+    margin-bottom: 5px;
+  }
+  .user-types {
+    font-size: 0;
+    margin-bottom: 5px;
+  }
+  .user-type {
+    display: inline-block;
+    vertical-align: top;
+    font-size: 14px;
+    height: 28px;
+    width: 50%;
+    padding: 0 10px;
+    line-height: 26px;
+    border: 1px solid #e0e0e0;
+    text-align: center;
+    cursor: pointer;
+    &:hover {
+      border-color: $--color-primary;
+      color: $--color-primary;
+    }
+
+    &.is-active {
+      background-color: $--color-primary;
+      border-color: $--color-primary;
+      color: #fff;
+    }
+  }
+  .user-tree {
+    padding: 5px;
+    border: 1px solid #e0e0e0;
+    height: 300px;
+    overflow: auto;
+  }
+  .user-part-title {
+    height: 28px;
+    line-height: 26px;
+    border-radius: 5px;
+    background-color: #f0f0f0;
+    border: 1px solid #e0e0e0;
+    text-align: center;
+    margin-bottom: 5px;
+  }
+  .user-list {
+    border: 1px solid #e0e0e0;
+    padding: 5px;
+    height: 333px;
+    overflow: auto;
+  }
+  .user-item {
+    margin: 3px 0;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: #f0f0f0;
+    border-radius: 3px;
+  }
+  .user-cont {
+    line-height: 20px;
+    padding: 4px 6px;
+  }
+  .user-delete {
+    padding: 0;
+    color: $--color-danger;
+
+    &:hover {
+      color: mix(#000, $--color-danger, 10%);
+    }
+  }
+}
+
+// card-manage
+.card-create-type {
+  .card-types {
+    font-size: 0;
+    text-align: center;
+  }
+  .card-type {
+    display: inline-block;
+    vertical-align: top;
+    font-size: 20px;
+    margin: 0 5px;
+    height: 70px;
+    width: 150px;
+    line-height: 1;
+    padding: 25px 0;
+    border-radius: $--border-radius;
+    color: #fff;
+    cursor: pointer;
+
+    &:hover {
+      opacity: 0.8;
+    }
+
+    &:nth-of-type(1) {
+      background-color: $--color-primary;
+    }
+    &:nth-of-type(2) {
+      background-color: $--color-success;
+    }
+    &:nth-of-type(3) {
+      background-color: $--color-cyan;
+    }
+  }
+}
+// modify-card
+.modify-card {
+  .el-dialog.is-fullscreen {
+    border: none;
+    .el-dialog__body {
+      border: none;
+      padding: 0;
+    }
+  }
+}
+// modify-mark-params
+.modify-mark-params {
+  .mark-body {
+    margin: 40px 0;
+  }
+  .structure-desc {
+    margin-bottom: 10px;
+  }
+  .expand-btn {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    line-height: 19px;
+    font-size: 13px;
+    border: 1px solid #6f7482;
+    border-radius: 3px;
+    text-align: center;
+    cursor: pointer;
+
+    &:hover {
+      color: $--color-primary;
+      border-color: $--color-primary;
+    }
+
+    &-unexpand {
+      background-color: #ebeffc;
+    }
+  }
+  .total-info {
+    text-align: right;
+    padding-top: 10px;
+    padding-right: 210px;
+    > span {
+      color: $--color-danger;
+      font-weight: 600;
+      margin: 0 2px;
+    }
+  }
+  .row-unexpand-sub {
+    display: none;
+  }
+  .marker-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+}
+// modify-marker-question
+.modify-marker-question {
+  .el-dialog {
+    width: 900px;
+    margin: 0 auto;
+
+    &__header,
+    &__footer {
+      display: none;
+    }
+    &__body {
+      padding: 20px;
+    }
+  }
+
+  .marker-box {
+    height: 600px;
+    background-color: $--color-background;
+    border-radius: $--border-radius;
+    padding: 15px;
+    display: flex;
+    flex-direction: column;
+
+    &-uq {
+      display: block;
+      height: 100%;
+      overflow: auto;
+      background-color: #fff;
+      border: 1px solid $--color-background;
+
+      .el-tag {
+        margin: 3px;
+      }
+    }
+  }
+  .user-title {
+    margin-bottom: 10px;
+    flex-grow: 0;
+  }
+  .user-search {
+    margin-bottom: 10px;
+    flex-grow: 0;
+  }
+  .user-tree {
+    padding: 10px;
+    border-radius: 4px;
+    background-color: #fff;
+    overflow: auto;
+    flex-grow: 2;
+  }
+
+  .marker-footer {
+    margin-top: 15px;
+    text-align: center;
+  }
+}
+// modify-student-simple
+.modify-student-simple {
+  .tab-body {
+    min-height: 300px;
+  }
+}
+// modify-mark-area
+.modify-mark-area {
+  .el-dialog__body {
+    background-color: $--color-background;
+    position: relative;
+    z-index: 3;
+  }
+  .el-dialog__footer {
+    display: none;
+  }
+}
+.area-cropper {
+  position: relative;
+  border: 1px solid #e0e0e0;
+  background-color: #fff;
+  margin: 10px 0;
+
+  .cropper-img img {
+    display: block;
+    width: 100%;
+    height: auto;
+  }
+  .cropper-areas {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 9;
+  }
+
+  .area-selection {
+    position: absolute;
+    z-index: 999;
+    border: 1px solid $--color-blue;
+    background-color: rgba($color: #000000, $alpha: 0.2);
+  }
+
+  .element-item-body {
+    position: absolute;
+    background-color: rgba($color: #000000, $alpha: 0.2);
+  }
+  .element-delete {
+    position: absolute;
+    height: 20px;
+    width: 20px;
+    top: 0;
+    right: -20px;
+    line-height: 20px;
+    text-align: center;
+    font-size: 18px;
+    display: none;
+    z-index: 9;
+    color: $--color-danger;
+    cursor: pointer;
+    &:hover {
+      color: mix(#fff, $--color-danger, 20%);
+    }
+  }
+
+  .element-resize {
+    background-color: transparent;
+    > .resize-control {
+      > .control-point,
+      > .control-line {
+        display: none;
+      }
+    }
+
+    &:hover {
+      > .resize-control {
+        > .control-line {
+          display: block;
+        }
+      }
+    }
+
+    &-act {
+      > .resize-control {
+        > .control-point,
+        > .control-line {
+          display: block;
+        }
+      }
+      .element-delete {
+        display: block;
+      }
+    }
+  }
+  .element-resize-compact {
+    > .resize-control {
+      > .control-line {
+        display: block;
+      }
+    }
+
+    &:hover {
+      > .resize-control {
+        > .control-line {
+          border-color: $--color-primary;
+        }
+      }
+    }
+
+    &.element-resize-act {
+      > .resize-control {
+        > .control-line {
+          border-color: $--color-primary;
+          &-left,
+          &-right {
+            border-left-style: solid;
+          }
+          &-top,
+          &-bottom {
+            border-top-style: solid;
+          }
+        }
+      }
+    }
+  }
+}
+// answer-popover
+.answer-popover {
+  line-height: 24px;
+  padding: 15px;
+
+  .answer-divider {
+    margin: 10px 0;
+    height: 1px;
+    border-bottom: 1px dashed $--color-text-gray-5;
+  }
+  .el-checkbox__label,
+  .el-radio__label {
+    width: 24px;
+    text-align: center;
+  }
+  .el-radio__label {
+    display: inline-block;
+  }
+}

+ 82 - 0
src/assets/styles/paper-approve.css

@@ -0,0 +1,82 @@
+body,
+div,
+h1,
+p,
+tr,
+th,
+td,
+span {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+}
+
+body {
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
+    "Microsoft YaHei", Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-size: 14px;
+  color: #1f2230;
+}
+
+.paper-main {
+  width: 210mm;
+  margin: 0 auto;
+  color: #1f2230;
+}
+.paper-a4 {
+  width: 210mm;
+  height: 297mm;
+  padding: 40px;
+  overflow: hidden;
+  page-break-after: always;
+}
+.paper-h1 {
+  font-size: 24px;
+  text-align: center;
+  line-height: 1;
+  padding-top: 20px;
+  padding-bottom: 30px;
+}
+.paper-base {
+  margin-bottom: 30px;
+}
+.paper-row {
+  margin: 0 -5px 10px;
+}
+.paper-item {
+  display: inline-block;
+  vertical-align: top;
+  width: 50%;
+  padding: 0 5px;
+}
+.paper-item > span:first-child {
+  float: left;
+  width: 96px;
+  text-align: right;
+}
+.paper-item > span:last-child {
+  display: block;
+  margin-left: 96px;
+}
+
+.paper-table {
+  width: 100%;
+  border-spacing: 0;
+  border-collapse: collapse;
+  margin-bottom: 40px;
+  text-align: center;
+}
+.paper-table th {
+  padding: 8px;
+  line-height: 20px;
+  letter-spacing: 1px;
+  border: 1px solid #1f2230;
+}
+.paper-table td {
+  padding: 8px;
+  line-height: 20px;
+  border: 1px solid #1f2230;
+}

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

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

+ 60 - 0
src/components/MoreText.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="more-text">
+    <span>{{ showContent }}</span>
+    <el-popover
+      v-if="moreContent"
+      placement="top"
+      width="300"
+      trigger="hover"
+      :content="moreContent"
+    >
+      <el-button
+        class="ml-1"
+        type="text"
+        icon="el-icon-more"
+        slot="reference"
+      ></el-button>
+    </el-popover>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "more-text",
+  props: {
+    data: {
+      type: Array,
+      default() {
+        return [];
+      }
+    },
+    showCount: {
+      type: Number,
+      default: 1
+    }
+  },
+  data() {
+    return {
+      showContent: "",
+      moreContent: ""
+    };
+  },
+  watch: {
+    data(val) {
+      this.initData();
+    }
+  },
+  created() {
+    this.initData();
+  },
+  methods: {
+    initData() {
+      this.showContent = this.data.slice(0, this.showCount).join(",");
+      this.moreContent = "";
+      if (this.data.length > this.showCount) {
+        this.moreContent = this.data.join(",");
+      }
+    }
+  }
+};
+</script>

+ 247 - 0
src/components/SimpleImagePreview.vue

@@ -0,0 +1,247 @@
+<template>
+  <el-dialog
+    :class="[prefixCls, 'opacity-dialog']"
+    :visible.sync="modalIsShow"
+    title="图片预览"
+    fullscreen
+    append-to-body
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+  >
+    <div slot="title"></div>
+    <div slot="footer"></div>
+    <div :class="[`${prefixCls}-close`]" @click="cancel">
+      <i class="el-icon-circle-close"></i>
+    </div>
+
+    <div :class="[`${prefixCls}-body`]" ref="ReviewBody">
+      <div
+        v-if="!simple"
+        :class="[`${prefixCls}-guide`, `${prefixCls}-guide-prev`]"
+        @click.stop="showPrev"
+      >
+        <i class="el-icon-arrow-left"></i>
+      </div>
+      <div
+        v-if="!simple"
+        :class="[`${prefixCls}-guide`, `${prefixCls}-guide-next`]"
+        @click.stop="showNext"
+      >
+        <i class="el-icon-arrow-right"></i>
+      </div>
+      <div
+        :class="[
+          `${prefixCls}-imgs`,
+          { [`${prefixCls}-imgs-nosition`]: nosition }
+        ]"
+        :style="styles"
+        v-if="modalIsShow"
+      >
+        <img
+          :key="curImage.url"
+          :src="curImage.url"
+          :alt="curImage.filename"
+          ref="PreviewImgDetail"
+          @load="reizeImage"
+        />
+      </div>
+      <div :class="[`${prefixCls}-none`]" v-if="!curImage.url">
+        <i class="el-icon-picture"></i>
+        <p>暂无数据</p>
+      </div>
+
+      <div :class="[`${prefixCls}-loading`]" v-show="loading">
+        <i class="el-icon-loading"></i>
+      </div>
+    </div>
+
+    <div v-if="!simple" :class="[`${prefixCls}-footer`]">
+      <ul>
+        <li title="旋转" @click.stop="toRotate">
+          <i class="el-icon-refresh-right"></i>
+        </li>
+      </ul>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+const prefixCls = "cc-image-preview";
+
+export default {
+  name: "simple-image-preview",
+  props: {
+    curImage: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    simple: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      prefixCls,
+      modalIsShow: false,
+      styles: { width: "", height: "", top: "", left: "", transform: "" },
+      initWidth: 500,
+      transform: {
+        scale: 1,
+        rotate: 0
+      },
+      loading: false,
+      loadingSetT: null,
+      nosition: false
+    };
+  },
+  watch: {
+    "curImage.url": {
+      handler(val) {
+        if (val) {
+          this.loadingSetT = setTimeout(() => {
+            this.loading = true;
+          }, 300);
+          this.styles = {
+            width: "",
+            height: "",
+            top: "",
+            left: "",
+            transform: ""
+          };
+        }
+      }
+    }
+  },
+  methods: {
+    reizeImage() {
+      if (this.loadingSetT) clearTimeout(this.loadingSetT);
+
+      const imgDom = this.$refs.PreviewImgDetail;
+      const { naturalWidth, naturalHeight } = imgDom;
+      const imageSize = this.getImageSizePos({
+        win: {
+          width: this.$refs.ReviewBody.clientWidth,
+          height: this.$refs.ReviewBody.clientHeight
+        },
+        img: {
+          width: naturalWidth,
+          height: naturalHeight
+        },
+        rotate: 0
+      });
+
+      this.styles = Object.assign(this.styles, {
+        width: imageSize.width + "px",
+        height: imageSize.height + "px",
+        top: imageSize.top + "px",
+        left: imageSize.left + "px",
+        marginLeft: "auto",
+        transform: "none"
+      });
+      this.transform = {
+        scale: 1,
+        rotate: 0
+      };
+      this.loading = false;
+      setTimeout(() => {
+        this.nosition = false;
+      }, 100);
+    },
+    getImageSizePos({ win, img, rotate }) {
+      const imageSize = {
+        width: 0,
+        height: 0,
+        top: 0,
+        left: 0
+      };
+      const isHorizontal = !!(rotate % 180);
+
+      const rateWin = isHorizontal
+        ? win.height / win.width
+        : win.width / win.height;
+      const hwin = isHorizontal
+        ? {
+            width: win.height,
+            height: win.width
+          }
+        : win;
+
+      const rateImg = img.width / img.height;
+
+      if (rateImg <= rateWin) {
+        imageSize.height = Math.min(hwin.height, img.height);
+        imageSize.width = Math.floor(
+          (imageSize.height * img.width) / img.height
+        );
+      } else {
+        imageSize.width = Math.min(hwin.width, img.width);
+        imageSize.height = Math.floor(
+          (imageSize.width * img.height) / img.width
+        );
+      }
+      imageSize.left = (win.width - imageSize.width) / 2;
+      imageSize.top = (win.height - imageSize.height) / 2;
+      return imageSize;
+    },
+    cancel() {
+      this.modalIsShow = false;
+      this.$emit("on-close");
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    showPrev() {
+      this.$emit("on-prev");
+      // this.initData();
+    },
+    showNext() {
+      this.$emit("on-next");
+      // this.initData();
+    },
+    // dome-move
+    setStyleTransform() {
+      const { scale, rotate } = this.transform;
+      this.styles.transform = `scale(${scale}, ${scale}) rotate(${rotate}deg)`;
+    },
+    toRotate() {
+      this.transform.rotate = this.transform.rotate + 90;
+      this.setStyleTransform();
+      // 调整图片尺寸
+      const { naturalWidth, naturalHeight } = this.$refs.PreviewImgDetail;
+      const imageSize = this.getImageSizePos({
+        win: {
+          width: this.$refs.ReviewBody.clientWidth,
+          height: this.$refs.ReviewBody.clientHeight
+        },
+        img: {
+          width: naturalWidth,
+          height: naturalHeight
+        },
+        rotate: this.transform.rotate
+      });
+
+      this.styles = Object.assign(this.styles, {
+        width: imageSize.width + "px",
+        height: imageSize.height + "px",
+        top: imageSize.top + "px",
+        left: imageSize.left + "px"
+      });
+      // 360度无缝切换到0度
+      if (this.transform.rotate >= 360) {
+        setTimeout(() => {
+          this.nosition = true;
+          this.transform.rotate = 0;
+          this.setStyleTransform();
+          setTimeout(() => {
+            this.nosition = false;
+          }, 100);
+        }, 200);
+        // 200ms当次旋转动画持续时间
+      }
+    }
+  }
+};
+</script>

+ 164 - 0
src/components/UploadButton.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-upload
+    :action="uploadUrl"
+    :headers="headers"
+    :max-size="maxSize"
+    :format="format"
+    :accept="accept"
+    :data="uploadDataDict"
+    :before-upload="handleBeforeUpload"
+    :on-error="handleError"
+    :on-success="handleSuccess"
+    :http-request="upload"
+    :disabled="disabled"
+    :show-file-list="false"
+    style="display:inline-block;margin: 0 10px;"
+    ref="UploadComp"
+  >
+    <el-button
+      :type="btnType"
+      :icon="btnIcon"
+      :loading="loading"
+      :disabled="disabled"
+      >{{ btnContent }}</el-button
+    >
+  </el-upload>
+</template>
+
+<script>
+import { fileMD5 } from "../plugins/md5";
+import { $post } from "@/plugins/axios";
+
+export default {
+  name: "upload-button",
+  props: {
+    btnIcon: {
+      type: String
+    },
+    btnType: {
+      type: String,
+      default: "default"
+    },
+    btnContent: {
+      type: String
+    },
+    accept: {
+      type: String
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["jpg", "jpeg", "png"];
+      }
+    },
+    uploadUrl: {
+      type: String,
+      required: true
+    },
+    uploadData: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024
+    },
+    addFilenameParam: {
+      type: String,
+      default: "filename"
+    },
+    disabled: { type: Boolean, default: false }
+  },
+  data() {
+    return {
+      headers: {
+        md5: ""
+      },
+      res: {},
+      loading: false,
+      uploadDataDict: {}
+    };
+  },
+  methods: {
+    checkFileFormat(fileType) {
+      const _file_format = fileType
+        .split(".")
+        .pop()
+        .toLocaleLowerCase();
+      return this.format.length
+        ? this.format.some(item => item.toLocaleLowerCase() === _file_format)
+        : true;
+    },
+    async handleBeforeUpload(file) {
+      this.uploadDataDict = {
+        ...this.uploadData
+      };
+      this.uploadDataDict[this.addFilenameParam] = file.name;
+
+      if (file.size > this.maxSize) {
+        this.handleExceededSize();
+        return Promise.reject();
+      }
+
+      if (!this.checkFileFormat(file.name)) {
+        this.handleFormatError();
+        return Promise.reject();
+      }
+
+      const md5 = await fileMD5(file);
+      this.headers["md5"] = md5;
+      this.loading = true;
+
+      return true;
+    },
+    upload(options) {
+      let formData = new FormData();
+      Object.entries(options.data).forEach(([k, v]) => {
+        formData.append(k, v);
+      });
+      formData.append("file", options.file);
+      this.$emit("uploading");
+
+      return $post(options.action, formData, { headers: options.headers });
+    },
+    handleError(error) {
+      this.loading = false;
+      this.res = {
+        success: false,
+        message: error.message
+      };
+      this.$emit("upload-error", error);
+    },
+    handleSuccess(responseData) {
+      this.loading = false;
+      this.res = {
+        success: true,
+        message: "导入成功!"
+      };
+      this.$emit("upload-success", {
+        data: responseData,
+        filename: this.uploadDataDict[this.addFilenameParam]
+      });
+    },
+    handleFormatError() {
+      const content = "只支持文件格式为" + this.format.join("/");
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.$emit("valid-error", this.res);
+    },
+    handleExceededSize() {
+      const content =
+        "文件大小不能超过" + Math.floor(this.maxSize / (1024 * 1024)) + "M";
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.$emit("valid-error", this.res);
+    }
+  }
+};
+</script>

+ 130 - 0
src/components/UploadFetchFile.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="upload-file-view">
+    <el-input
+      :style="{ width: inputWidth }"
+      v-model.trim="attachmentName"
+      placeholder="文件名称"
+      readonly
+    ></el-input>
+    <el-upload
+      action="upload-url"
+      :on-change="handleFileChange"
+      :show-file-list="false"
+      :auto-upload="false"
+      :disabled="disabled"
+      style="display:inline-block;margin: 0 10px;"
+      ref="UploadComp"
+    >
+      <el-button type="primary" :disabled="disabled" :loading="loading"
+        >选择</el-button
+      >
+    </el-upload>
+  </div>
+</template>
+
+<script>
+import { fileMD5 } from "@/plugins/md5";
+
+export default {
+  name: "upload-file-view",
+  props: {
+    inputWidth: {
+      type: String,
+      default: "200px"
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["xls", "xlsx"];
+      }
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      attachmentName: "",
+      canUpload: false,
+      loading: false,
+      res: {}
+    };
+  },
+  methods: {
+    checkFileFormat(fileType) {
+      const _file_format = fileType
+        .split(".")
+        .pop()
+        .toLocaleLowerCase();
+      return this.format.some(
+        item => item.toLocaleLowerCase() === _file_format
+      );
+    },
+    async handleFileChange(file) {
+      console.log(file);
+      this.attachmentName = file.name;
+      this.canUpload = file.status === "ready";
+
+      if (file.size > this.maxSize) {
+        this.handleExceededSize();
+        return Promise.reject();
+      }
+
+      if (!this.checkFileFormat(file.name)) {
+        this.handleFormatError();
+        return Promise.reject();
+      }
+      this.$emit("valid-change", { success: true });
+
+      const md5 = await fileMD5(file.raw);
+
+      this.$emit("file-change", {
+        md5,
+        filename: file.name,
+        file: file.raw
+      });
+    },
+    // upload(options) {
+    //   let formData = new FormData();
+    //   Object.entries(options.data).forEach(([k, v]) => {
+    //     formData.append(k, v);
+    //   });
+    //   formData.append("file", options.file);
+    //   this.$emit("uploading");
+
+    //   return $post(options.action, formData, { headers: options.headers });
+    // },
+    handleFormatError() {
+      const content = "只支持文件格式为" + this.format.join("/");
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.$emit("valid-change", this.res);
+    },
+    handleExceededSize() {
+      const content =
+        "文件大小不能超过" + Math.floor(this.maxSize / 1024) + "M";
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.$emit("valid-change", this.res);
+    },
+    setAttachmentName(name) {
+      this.attachmentName = name;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.upload-file-view {
+  display: inline-block;
+}
+</style>

+ 105 - 0
src/components/UploadFileDialog.vue

@@ -0,0 +1,105 @@
+<template>
+  <el-dialog
+    class="upload-file-dialog"
+    :visible.sync="modalIsShow"
+    title="上传文件"
+    top="10vh"
+    width="710px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="visibleChange"
+  >
+    <div class="file-upload-body">
+      <upload-file-view
+        input-width="270px"
+        :format="format"
+        :upload-url="uploadUrl"
+        @valid-error="validError"
+        @upload-success="uploadSuccess"
+        ref="UploadFileView"
+      ></upload-file-view>
+      <el-button @click="toPreview" style="margin-left: 10px;">预览</el-button>
+    </div>
+    <div slot="footer" style="text-align: right">
+      <el-button type="primary" @click="confirm">保存</el-button>
+      <el-button type="danger" @click="cancel" plain>返回</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import UploadFileView from "./UploadFileView";
+import { attachmentPreview } from "@/modules/login/api";
+
+export default {
+  name: "upload-file-dialog",
+  components: { UploadFileView },
+  props: {
+    paperAttachment: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["xls", "xlsx"];
+      }
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      attachment: {},
+      // import
+      uploadUrl: "/api/print/basic/sys/saveAttachment"
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.$refs.UploadFileView.setAttachmentName(
+        `${this.paperAttachment.filename || ""}`
+      );
+      this.attachment = { ...this.paperAttachment };
+    },
+    // upload-handler
+    validError(errorData) {
+      this.$message.error(errorData.message);
+    },
+    uploadSuccess(data) {
+      this.$message.success("上传成功!");
+      let infos = {
+        attachmentId: data.id,
+        filename: data.filename
+      };
+      this.attachment = Object.assign(this.attachment, infos);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    confirm() {
+      this.$emit("confirm", this.attachment);
+      this.cancel();
+    },
+    async toPreview() {
+      if (!this.attachment.attachmentId) {
+        this.$message.error("请先上传附件!");
+        return;
+      }
+      const data = await attachmentPreview(this.attachment.attachmentId);
+      window.open(data.url);
+    }
+  }
+};
+</script>
+
+<style lang="css" scoped>
+.file-upload-body {
+  min-height: 150px;
+}
+</style>

+ 202 - 0
src/components/UploadFileView.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="upload-file-view">
+    <el-input
+      :style="{ width: inputWidth }"
+      v-model.trim="attachmentName"
+      placeholder="文件名称"
+      readonly
+    ></el-input>
+    <el-upload
+      :action="uploadUrl"
+      :headers="headers"
+      :max-size="maxSize"
+      :format="format"
+      :data="uploadDataDict"
+      :on-change="handleFileChange"
+      :before-upload="handleBeforeUpload"
+      :on-error="handleError"
+      :on-success="handleSuccess"
+      :on-progress="handleProgress"
+      :http-request="upload"
+      :show-file-list="false"
+      :disabled="disabled"
+      style="display:inline-block;margin: 0 10px;"
+      ref="UploadComp"
+    >
+      <el-button type="primary" :disabled="disabled" :loading="loading"
+        >选择</el-button
+      >
+    </el-upload>
+    <el-button
+      type="primary"
+      @click="startUpload"
+      v-if="canUpload && !autoUpload"
+      :loading="loading"
+      style="margin-right: 10px;"
+      >开始上传</el-button
+    >
+  </div>
+</template>
+
+<script>
+import { fileMD5 } from "@/plugins/md5";
+import { $post } from "@/plugins/axios";
+
+export default {
+  name: "upload-file-view",
+  props: {
+    inputWidth: {
+      type: String,
+      default: "400px"
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["xls", "xlsx"];
+      }
+    },
+    uploadUrl: {
+      type: String,
+      required: true
+    },
+    uploadData: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024
+    },
+    addFilenameParam: {
+      type: String,
+      default: "filename"
+    },
+    autoUpload: {
+      type: Boolean,
+      default: true
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      attachmentName: "",
+      canUpload: false,
+      loading: false,
+      uploadDataDict: {},
+      headers: {
+        md5: ""
+      },
+      res: {}
+    };
+  },
+  methods: {
+    startUpload() {
+      this.loading = true;
+      this.$refs.UploadComp.submit();
+    },
+    checkFileFormat(fileType) {
+      const _file_format = fileType
+        .split(".")
+        .pop()
+        .toLocaleLowerCase();
+      return this.format.some(
+        item => item.toLocaleLowerCase() === _file_format
+      );
+    },
+    handleFileChange(file) {
+      this.attachmentName = file.name;
+      this.canUpload = file.status === "ready";
+    },
+    async handleBeforeUpload(file) {
+      this.uploadDataDict = {
+        ...this.uploadData
+      };
+      this.uploadDataDict[this.addFilenameParam] = file.name;
+
+      if (file.size > this.maxSize) {
+        this.handleExceededSize();
+        return Promise.reject();
+      }
+
+      if (!this.checkFileFormat(file.name)) {
+        this.handleFormatError();
+        return Promise.reject();
+      }
+
+      const md5 = await fileMD5(file);
+      this.headers["md5"] = md5;
+
+      if (this.autoUpload) this.loading = true;
+
+      return this.autoUpload;
+    },
+    upload(options) {
+      let formData = new FormData();
+      Object.entries(options.data).forEach(([k, v]) => {
+        formData.append(k, v);
+      });
+      formData.append("file", options.file);
+      this.$emit("uploading");
+
+      return $post(options.action, formData, { headers: options.headers });
+    },
+    handleError(error) {
+      this.canUpload = false;
+      this.loading = false;
+      this.res = {
+        success: false,
+        message: error.message
+      };
+      this.$emit("upload-error", error);
+    },
+    handleSuccess(responseData) {
+      this.canUpload = false;
+      this.loading = false;
+      this.res = {
+        success: true,
+        message: "导入成功!"
+      };
+      this.$emit("upload-success", {
+        ...responseData,
+        filename: this.uploadDataDict[this.addFilenameParam]
+      });
+    },
+    handleProgress() {
+      this.loading = true;
+    },
+    handleFormatError() {
+      const content = "只支持文件格式为" + this.format.join("/");
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.loading = false;
+      this.$emit("valid-error", this.res);
+    },
+    handleExceededSize() {
+      const content =
+        "文件大小不能超过" + Math.floor(this.maxSize / 1024) + "M";
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.loading = false;
+      this.$emit("valid-error", this.res);
+    },
+    setAttachmentName(name) {
+      this.attachmentName = name;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.upload-file-view {
+  display: inline-block;
+}
+</style>

+ 15 - 0
src/components/ViewFooter.vue

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

+ 63 - 0
src/components/base/CampusSelect.vue

@@ -0,0 +1,63 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="campus-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.campusName"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { campusQuery } from "../../modules/base/api";
+
+export default {
+  name: "campus-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    }
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      const res = await campusQuery();
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find(item => item.id === this.selected)
+      );
+    }
+  }
+};
+</script>

+ 116 - 0
src/components/base/ClazzSelect.vue

@@ -0,0 +1,116 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="clazz-select"
+    popper-class="popper-filter"
+    :placeholder="placeholder"
+    :clearable="clearable"
+    :disabled="disabled"
+    :multiple="multiple"
+    @change="select"
+  >
+    <div class="el-select-filter">
+      <el-input
+        v-model="filterLabel"
+        placeholder="请输入"
+        clearable
+        @input="labelChange"
+      ></el-input>
+    </div>
+    <el-option
+      v-for="item in filterOptionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { clazzQuery } from "../../modules/base/api";
+
+export default {
+  name: "clazz-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String, Array], default: "" },
+    clearable: { type: Boolean, default: true },
+    multiple: { type: Boolean, default: false },
+    campusId: { type: String, default: "" },
+    datas: {
+      type: Array
+    }
+  },
+  data() {
+    return {
+      optionList: [],
+      filterOptionList: [],
+      selected: "",
+      filterLabel: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    },
+    datas: {
+      immediate: true,
+      handler(val) {
+        if (!val) return;
+        this.optionList = val.map(item => {
+          return { ...item };
+        });
+        this.labelChange();
+      }
+    },
+    campusId(val, oldval) {
+      if (val !== oldval) {
+        this.search();
+        this.$emit("input", "");
+        this.$emit("change", null);
+      }
+    }
+  },
+  created() {
+    if (!this.datas) this.search();
+  },
+  methods: {
+    async search() {
+      const res = await clazzQuery({
+        campusId: this.campusId
+      });
+      this.optionList = res || [];
+      this.labelChange();
+    },
+    labelChange() {
+      const escapeRegexpString = (value = "") =>
+        String(value).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+      const reg = new RegExp(escapeRegexpString(this.filterLabel), "i");
+
+      this.filterOptionList = this.optionList.filter(item =>
+        reg.test(item.name)
+      );
+    },
+    select() {
+      this.$emit("input", this.selected);
+
+      if (this.multiple) {
+        this.$emit(
+          "change",
+          this.optionList.filter(item => this.selected.includes(item.id))
+        );
+      } else {
+        this.$emit(
+          "change",
+          this.optionList.find(item => item.id === this.selected)
+        );
+      }
+    }
+  }
+};
+</script>

+ 88 - 0
src/components/base/CollegeSelect.vue

@@ -0,0 +1,88 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="college-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import {
+  unitQueryByType,
+  organizationFindByTypeList
+} from "../../modules/base/api";
+
+export default {
+  name: "college-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    semesterId: { type: String, default: "" },
+    cascader: { type: Boolean, default: false }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    },
+    semesterId(val, oldval) {
+      if (val !== oldval) {
+        this.search("");
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    }
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+      if (this.cascader && !this.semesterId) return;
+
+      if (this.cascader) {
+        const res = await unitQueryByType(
+          {
+            semesterId: this.semesterId
+          },
+          "COLLEGE"
+        );
+        this.optionList = res;
+      } else {
+        const res = await organizationFindByTypeList({ orgType: "COLLEGE" });
+        this.optionList = res;
+      }
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find(item => item.id === this.selected)
+      );
+    }
+  }
+};
+</script>

+ 84 - 0
src/components/base/CourseSelect.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="course-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.code"
+      :value="item.code"
+      :label="`${item.name}(${item.code})`"
+    >
+      <span>{{ `${item.name}(${item.code})` }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { courseQuery } from "../../modules/base/api";
+
+export default {
+  name: "course-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    printPlanId: { type: [String, Array], default: "" },
+    teachingRoomId: { type: [String, Array], default: "" }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    },
+    printPlanId(val, oldval) {
+      if (val !== oldval) {
+        this.search("");
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    },
+    teachingRoomId(val, oldval) {
+      if (val !== oldval) {
+        this.search("");
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    }
+  },
+  async created() {
+    this.search();
+  },
+  methods: {
+    async search(query) {
+      const res = await courseQuery({
+        param: query,
+        printPlanId: this.printPlanId,
+        teachingRoomId: this.teachingRoomId
+      });
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find(item => item.code === this.selected)
+      );
+    }
+  }
+};
+</script>

+ 75 - 0
src/components/base/ExamSelect.vue

@@ -0,0 +1,75 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="exam-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { examQuery } from "../../modules/base/api";
+
+export default {
+  name: "exam-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择考试" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    semesterId: { type: String, default: "" }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    },
+    semesterId(val, oldval) {
+      if (val !== oldval) {
+        this.search();
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    }
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+
+      const res = await examQuery({
+        semesterId: this.semesterId
+      });
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find(item => item.id === this.selected)
+      );
+    }
+  }
+};
+</script>

+ 79 - 0
src/components/base/MajorSelect.vue

@@ -0,0 +1,79 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="major-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { unitQueryByType } from "../../modules/base/api";
+
+export default {
+  name: "major-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    collegeId: { type: String, default: "" },
+    cascader: { type: Boolean, default: false }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    },
+    collegeId(val, oldval) {
+      if (val !== oldval) {
+        this.search("");
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    }
+  },
+  async created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+      if (this.cascader && !this.collegeId) return;
+      const res = await unitQueryByType(
+        {
+          collegeId: this.collegeId
+        },
+        "MAJOR"
+      );
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find(item => item.id === this.selected)
+      );
+    }
+  }
+};
+</script>

+ 71 - 0
src/components/base/RoomSelect.vue

@@ -0,0 +1,71 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="room-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="(item, index) in optionList"
+      :key="index"
+      :value="item"
+      :label="item"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { roomQuery } from "../../modules/print/api";
+
+export default {
+  name: "room-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    printPlanId: { type: [String, Array], default: "" }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    },
+    printPlanId(val, oldval) {
+      if (val !== oldval) {
+        this.search("");
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    }
+  },
+  async created() {
+    this.search();
+  },
+  methods: {
+    async search(query) {
+      const res = await roomQuery({
+        param: query,
+        printPlanId: this.printPlanId
+      });
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit("change", this.selected);
+    }
+  }
+};
+</script>

+ 63 - 0
src/components/base/SchoolSelect.vue

@@ -0,0 +1,63 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="school-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { schoolList } from "../../modules/login/api";
+
+export default {
+  name: "school-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    }
+  },
+  async created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      const res = await schoolList();
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find(item => item.id === this.selected)
+      );
+    }
+  }
+};
+</script>

+ 65 - 0
src/components/base/SemesterSelect.vue

@@ -0,0 +1,65 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="semester-select"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { unitQueryByType } from "../../modules/base/api";
+
+export default {
+  name: "semester-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择学期" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true }
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: ""
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      }
+    }
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+
+      const res = await unitQueryByType({}, "SEMESTER");
+      this.optionList = res;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find(item => item.id === this.selected)
+      );
+    }
+  }
+};
+</script>

+ 47 - 0
src/components/common/CascaderSplit/CascaderSplit.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="cascader-split">
+    <el-form ref="FilterForm" label-position="left" label-width="80px" inline>
+      <el-form-item label="levelOne">
+        <level-one
+          v-model="modalForm.levelOne"
+          set-default
+          @on-change="selectChange"
+        ></level-one>
+      </el-form-item>
+      <el-form-item label="levelTwo">
+        <level-two
+          v-model="modalForm.levelTwo"
+          :level-one-model="modalForm.levelOne"
+          @on-change="selectChange"
+          set-default
+        ></level-two>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import LevelOne from "./LevelOne";
+import LevelTwo from "./LevelTwo";
+
+export default {
+  name: "cascader-split",
+  components: {
+    LevelOne,
+    LevelTwo
+  },
+  data() {
+    return {
+      modalForm: {
+        levelOne: "",
+        levelTwo: ""
+      }
+    };
+  },
+  methods: {
+    selectChange() {
+      console.log(this.modalForm);
+    }
+  }
+};
+</script>

+ 90 - 0
src/components/common/CascaderSplit/LevelOne.vue

@@ -0,0 +1,90 @@
+<template>
+  <el-select
+    class="level-one"
+    v-model="levelModel"
+    style="width: 200px;"
+    @change="selected"
+    :placeholder="placeholder"
+    :clearable="clearable"
+    :disabled="disabled"
+  >
+    <el-option
+      v-for="item in dataList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { getData } from "./com-func";
+
+export default {
+  name: "level-one",
+  props: {
+    value: {
+      type: [String, Number]
+    },
+    setDefault: {
+      type: Boolean,
+      default: false
+    },
+    placeholder: {
+      type: String,
+      default: "请选择选项"
+    },
+    clearable: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      dataList: []
+    };
+  },
+  computed: {
+    levelModel: {
+      get() {
+        return this.value;
+      },
+      set(selectId) {
+        this.$emit("input", selectId);
+      }
+    }
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      const data = await getData("level-one");
+      this.dataList = data.map(item => {
+        return {
+          id: item.id,
+          name: item.name
+        };
+      });
+
+      if (this.setDefault) {
+        this.levelModel = this.dataList[0] && this.dataList[0].id;
+        const val = {
+          value: this.dataList[0].id || "",
+          label: this.dataList[0].name || ""
+        };
+        this.$emit("on-change", val);
+        this.$emit("on-set");
+      }
+    },
+    selected(val) {
+      this.$emit("on-change", val);
+    }
+  }
+};
+</script>

+ 102 - 0
src/components/common/CascaderSplit/LevelTwo.vue

@@ -0,0 +1,102 @@
+<template>
+  <el-select
+    class="level-two"
+    v-model="levelModel"
+    style="width: 200px;"
+    @change="selected"
+    :placeholder="placeholder"
+    :clearable="clearable"
+    :disabled="disabled"
+  >
+    <el-option
+      v-for="item in dataList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { getData } from "./com-func";
+
+export default {
+  name: "level-two",
+  props: {
+    value: {
+      type: [String, Number]
+    },
+    setDefault: {
+      type: Boolean,
+      default: false
+    },
+    levelOneModel: {
+      type: [String, Number],
+      required: true
+    },
+    placeholder: {
+      type: String,
+      default: "请选择考试"
+    },
+    clearable: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      dataList: []
+    };
+  },
+  computed: {
+    levelModel: {
+      get() {
+        return this.value;
+      },
+      set(selectId) {
+        this.$emit("input", selectId);
+      }
+    }
+  },
+  watch: {
+    levelOneModel() {
+      this.search();
+    }
+  },
+  methods: {
+    async search() {
+      if (!this.levelOneModel) {
+        this.dataList = [];
+        this.levelModel = "";
+        return;
+      }
+
+      const data = await getData("level-two");
+      this.dataList = data.map(item => {
+        return {
+          id: item.id,
+          name: item.name
+        };
+      });
+
+      if (this.setDefault) {
+        this.levelModel = this.dataList[0] && this.dataList[0].id;
+        const val = {
+          value: this.dataList[0].id || "",
+          label: this.dataList[0].name || ""
+        };
+        this.$emit("on-change", val);
+        this.$emit("on-set");
+      }
+    },
+    selected(val) {
+      this.$emit("on-change", val);
+    }
+  }
+};
+</script>

+ 18 - 0
src/components/common/CascaderSplit/com-func.js

@@ -0,0 +1,18 @@
+export const getData = (nameKey = "name") => {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      const dataList = "#"
+        .repeat(8)
+        .split("")
+        .map((item, index) => {
+          return {
+            id: index + 1,
+            name: `${nameKey}-${Math.random()
+              .toString(32)
+              .slice(-5)}-${index + 1}`
+          };
+        });
+      resolve(dataList);
+    }, 300);
+  });
+};

+ 101 - 0
src/components/common/DimensionTreeTable/DimensionTreeTable.vue

@@ -0,0 +1,101 @@
+<template>
+  <div class="dimension-tree-table">
+    <table class="table">
+      <tr
+        v-for="(tr, trind) in tableTreeList"
+        :key="trind"
+        :data-names="tr.names"
+        :data-ids="tr.ids"
+      >
+        <td v-for="(td, tdind) in tr.tdList" :key="tdind" :rowspan="td.rowspan">
+          <span>{{ td.name }}</span>
+        </td>
+        <td>
+          <slot :trItem="tr">
+            default slot name
+          </slot>
+        </td>
+      </tr>
+    </table>
+  </div>
+</template>
+
+<script>
+import { deepCopy } from "@/plugins/utils";
+
+export default {
+  name: "dimension-tree-table",
+  props: {
+    data: {
+      type: Array,
+      default() {
+        return [];
+      }
+    }
+  },
+  data() {
+    return {
+      dataList: [],
+      dataListNums: [],
+      tableTreeList: []
+    };
+  },
+  watch: {
+    data: {
+      immediate: true,
+      handler(val) {
+        this.dataList = deepCopy(val);
+        this.getDataListNums();
+        this.serilizeTableList();
+      }
+    }
+  },
+  methods: {
+    getDataListNums() {
+      this.dataListNums = this.dataList.map(item => item.length);
+    },
+    getRowSpan(curIndex) {
+      const listNums = this.dataListNums.slice(curIndex + 1);
+      return listNums.reduce((sum, item) => sum * item, 1);
+    },
+    getNextList(curIndex) {
+      if (curIndex + 1 >= this.dataList.length) return;
+      return this.dataList[curIndex + 1];
+    },
+    serilizeTableList() {
+      if (!this.dataList.length) {
+        this.tableTreeList = [];
+        this.dataListNums = [];
+        return;
+      }
+
+      let tableList = [];
+      const serilize = (parentList, allParentList, curList, curIndex) => {
+        const rowspan = this.getRowSpan(curIndex);
+        const nextList = this.getNextList(curIndex);
+        curList.map((item, index) => {
+          const newParentList = index ? [] : deepCopy(parentList);
+          const curItem = { ...item, rowspan };
+          const newAllParentList = [...deepCopy(allParentList), curItem];
+          const curRow = [...newParentList, curItem];
+          if (nextList) {
+            serilize(curRow, newAllParentList, nextList, curIndex + 1);
+          } else {
+            tableList.push({ tdList: curRow, allList: newAllParentList });
+          }
+        });
+      };
+      serilize([], [], this.getNextList(-1), 0);
+
+      this.tableTreeList = tableList.map(item => {
+        return {
+          ...item,
+          ids: item.allList.map(elem => elem.id).join("_"),
+          names: item.allList.map(elem => elem.name).join("_")
+        };
+      });
+      console.log(this.tableTreeList);
+    }
+  }
+};
+</script>

+ 2 - 0
src/components/common/DimensionTreeTable/index.js

@@ -0,0 +1,2 @@
+import DimensionTreeTable from "./DimensionTreeTable.vue";
+export default DimensionTreeTable;

+ 92 - 0
src/components/common/DimensionTreeTable/intro.md

@@ -0,0 +1,92 @@
+# DimensionTreeTable api
+
+## 实例
+
+```html
+<template>
+  <div class="demo">
+    <dimension-tree-table :data="selectedData" #default="{trItem}">
+      <span>{{ trItem.names }}</span>
+    </dimension-tree-table>
+  </div>
+</template>
+
+<script>
+  import DimensionTreeTable from "@/components/common/DimensionTreeTable";
+
+  export default {
+    name: "demo",
+    components: {
+      DimensionTreeTable
+    },
+    data() {
+      return {
+        selectedData: [[{"id":1,"name":"颜色1"}],[{"id":11,"name":"尺码1"},{"id":12,"name":"尺码2"}]],
+      };
+    },
+    methods: {
+    }
+  };
+</script>
+```
+
+## props
+
+| 属性 | 说明               | 类型  | 默认值 |
+| ---- | ------------------ | ----- | ------ |
+| data | 生成表格的二维数组 | Aarry | []     |
+
+
+## Scoped Slot
+
+| name | 说明                                  |
+| ---- | ------------------------------------- |
+| -    | 自定义表格行的内容,参数为 { trItem } |
+
+**说明:** 实例中采用vue2.6以上slot新语法编写,查看[具体文档](https://cn.vuejs.org/v2/guide/components-slots.html)
+
+
+## trItem说明
+
+| 字段    | 说明               |
+| ------- | ------------------ |
+| tdList  | 表格单行渲染的数组 |
+| allList | 表格单行完整数组   |
+| ids     | 单行条目的id       |
+| names   | 单行条目的名称     |
+
+### example
+```json
+{
+    "tdList": [
+        {
+            "id": 1112,
+            "name": "来源2",
+            "selected": true,
+            "rowspan": 1
+        }
+    ],
+    "allList": [
+        {
+            "id": 11,
+            "name": "尺码1",
+            "selected": true,
+            "rowspan": 4
+        },
+        {
+            "id": 111,
+            "name": "材质1",
+            "selected": true,
+            "rowspan": 2
+        },
+        {
+            "id": 1112,
+            "name": "来源2",
+            "selected": true,
+            "rowspan": 1
+        }
+    ],
+    "ids": "11_111_1112",
+    "names": "尺码1_材质1_来源2"
+}
+```

+ 418 - 0
src/components/common/ImageEditUpload/ImageEditUpload.vue

@@ -0,0 +1,418 @@
+<template>
+  <div class="image-edit-upload">
+    <div :class="[`${prefixCls}-main`]" v-show="!edit">
+      <div :class="elPrefixCls">
+        <div
+          :class="classes"
+          @click="handleClick"
+          @drop.prevent="onDrop"
+          @dragover.prevent="dragOver = true"
+          @dragleave.prevent="dragOver = false"
+        >
+          <input
+            ref="input"
+            type="file"
+            :class="[`${elPrefixCls}-input`]"
+            @change="handleChange"
+            :accept="accept"
+          />
+          <div :class="[`${prefixCls}-cover`]" v-if="imgUrl">
+            <img :src="imgUrl" alt="default" />
+          </div>
+          <div :class="[`${prefixCls}-dcover`]" v-else>
+            <i class="el-icon-user-solid"></i><br />
+            <p>点击或拖拽图片到此处</p>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div :class="[`${prefixCls}-edit`]" v-show="file.url && edit">
+      <div :class="[`${prefixCls}-edit-box`]">
+        <img ref="editImage" :src="file.url" />
+        <transition name="fade" v-if="file && file.showProgress">
+          <div class="img-progress">
+            <el-progress
+              :show-text="false"
+              :stroke-width="4"
+              :percentage="file.percentage"
+              :status="
+                file.status === 'finished' && file.showProgress
+                  ? 'success'
+                  : null
+              "
+            ></el-progress>
+          </div>
+        </transition>
+      </div>
+      <div :class="[`${prefixCls}-edit-btns`]">
+        <el-button type="error" @click="clearEdit">取消</el-button>
+        <el-button type="primary" @click="editSave">确认</el-button>
+      </div>
+    </div>
+    <p
+      :class="[
+        `${prefixCls}-tips`,
+        { 'cc-tips-success': res.success, 'cc-tips-error': !res.success }
+      ]"
+      v-if="res.msg"
+    >
+      {{ res.msg }}
+    </p>
+  </div>
+</template>
+
+<script>
+import ajax from "../utils/ajax";
+import Cropper from "cropperjs";
+import "cropperjs/dist/cropper.min.css";
+
+const elPrefixCls = "el-upload";
+const prefixCls = "cc-edit-upload";
+
+const ratios = {
+  double: 1 / 2,
+  square: 1,
+  rectangle: 3 / 4
+};
+
+export default {
+  name: "image-edit-upload",
+  props: {
+    action: {
+      type: String,
+      required: true
+    },
+    headers: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    defImage: {
+      type: String
+    },
+    data: {
+      type: Object
+    },
+    name: {
+      type: String,
+      default: "file"
+    },
+    withCredentials: {
+      type: Boolean,
+      default: false
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["jpg", "png"];
+      }
+    },
+    accept: {
+      type: String
+    },
+    maxSize: {
+      type: Number,
+      default() {
+        return 2 * 1024;
+      }
+    },
+    ratioType: {
+      type: String,
+      default: "rectangle",
+      validator(val) {
+        return ["square", "rectangle", "double"].indexOf(val) > -1;
+      }
+    },
+    onProgress: {
+      type: Function,
+      default() {
+        return {};
+      }
+    },
+    onSuccess: {
+      type: Function,
+      default() {
+        return {};
+      }
+    },
+    onError: {
+      type: Function,
+      default() {
+        return {};
+      }
+    },
+    onRemove: {
+      type: Function,
+      default() {
+        return {};
+      }
+    },
+    onPreview: {
+      type: Function,
+      default() {
+        return {};
+      }
+    },
+    onExceededSize: {
+      type: Function,
+      default() {
+        return {};
+      }
+    },
+    onFormatError: {
+      type: Function,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      file: {},
+      type: "drag",
+      edit: false,
+      cropper: false,
+      imgUrl: this.defImage,
+      prefixCls,
+      elPrefixCls,
+      dragOver: false,
+      tempIndex: 1,
+      res: {
+        success: true,
+        msg: ""
+      }
+    };
+  },
+  computed: {
+    classes() {
+      return [
+        `${elPrefixCls}`,
+        {
+          [`${elPrefixCls}-dragger`]: this.type === "drag",
+          [`${elPrefixCls}-dragOver`]: this.type === "drag" && this.dragOver
+        }
+      ];
+    },
+    aspectRatio() {
+      return ratios[this.ratioType];
+    },
+    limitSize() {
+      const mn = this.maxSize / 1024;
+      return mn >= 1 ? mn + "M" : this.maxSize + "Kb";
+    }
+    // accept() {
+    //   let formats = this.format.map(item => {
+    //     return "image/" + item;
+    //   });
+    //   return formats.join();
+    // }
+  },
+  watch: {
+    edit(value) {
+      if (value) {
+        this.$nextTick(function() {
+          if (!this.$refs.editImage) {
+            return;
+          }
+          this.cropper = new Cropper(this.$refs.editImage, {
+            aspectRatio: this.aspectRatio,
+            minCropBoxWidth: 120,
+            minCropBoxHeight: 120 / this.aspectRatio,
+            viewMode: 1
+          });
+        });
+      } else {
+        if (this.cropper) {
+          this.cropper.destroy();
+          this.cropper = false;
+        }
+      }
+    },
+    defImage(val) {
+      this.imgUrl = val;
+    }
+  },
+  methods: {
+    isIe() {
+      let userAgent = navigator.userAgent;
+      let isIE =
+        userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1;
+      let isEdge = userAgent.indexOf("Edge") > -1 && !isIE;
+      let isIE11 =
+        userAgent.indexOf("Trident") > -1 && userAgent.indexOf("rv:11.0") > -1;
+
+      return isEdge || isIE11 || isIE;
+    },
+    handleClick() {
+      this.res = { success: true, msg: "" };
+      this.$refs.input.click();
+    },
+    handleChange(e) {
+      const files = e.target.files;
+
+      if (!files) return;
+
+      this.uploadFiles(files);
+      this.$refs.input.value = null;
+    },
+    onDrop(e) {
+      this.res = { success: true, msg: "" };
+      this.dragOver = false;
+      this.uploadFiles(e.dataTransfer.files);
+    },
+    uploadFiles(files) {
+      let postFiles = Array.prototype.slice.call(files);
+
+      if (postFiles.length === 0) return;
+
+      if (this.checkValid(postFiles[0])) {
+        this.openEdit(postFiles[0]);
+      }
+    },
+    checkValid(file) {
+      // check format
+      if (this.format.length) {
+        const _file_format = file.name
+          .split(".")
+          .pop()
+          .toLocaleLowerCase();
+        const checked = this.format.some(
+          item => item.toLocaleLowerCase() === _file_format
+        );
+        if (!checked) {
+          this.res = {
+            success: false,
+            msg: "只支持文件格式为" + this.format.join("/")
+          };
+          this.onFormatError(file);
+          return false;
+        }
+      }
+
+      // check maxSize
+      if (this.maxSize) {
+        if (file.size > this.maxSize * 1024) {
+          this.res = {
+            success: false,
+            msg: "文件大小不能超过" + this.limitSize
+          };
+          this.onExceededSize(file);
+          return false;
+        }
+      }
+
+      this.res = {
+        success: true,
+        msg: ""
+      };
+      return true;
+    },
+    openEdit(file) {
+      this.initFile(file);
+      this.$nextTick(function() {
+        this.edit = true;
+      });
+    },
+    initFile(file) {
+      this.file = {
+        status: "uploading",
+        name: file.name,
+        size: file.size,
+        type: file.type,
+        url: this.createUrl(file),
+        response: {},
+        percentage: 0,
+        showProgress: true
+      };
+    },
+    createUrl(file) {
+      let url = "";
+      let URL = window.URL || window.webkitURL;
+      if (URL && URL.createObjectURL) {
+        url = URL.createObjectURL(file);
+      }
+      return url;
+    },
+    clearEdit() {
+      this.edit = false;
+    },
+    editSave() {
+      const { name, type } = this.file;
+      const cropBoxSize = this.cropper.getCropBoxData();
+      // 配置width,height之后会压缩原始图片
+      const binStr = atob(
+        this.cropper
+          .getCroppedCanvas({
+            width: cropBoxSize.width,
+            height: cropBoxSize.height
+          })
+          .toDataURL(type)
+          .split(",")[1]
+      );
+
+      let arr = new Uint8Array(binStr.length);
+      for (let i = 0; i < binStr.length; i++) {
+        arr[i] = binStr.charCodeAt(i);
+      }
+
+      let file;
+      if (this.isIe()) {
+        file = new Blob([arr], { type });
+      } else {
+        file = new File([arr], name, { type });
+      }
+
+      this.post(file);
+    },
+    post(file) {
+      ajax({
+        headers: this.headers,
+        withCredentials: this.withCredentials,
+        file: file,
+        data: this.data,
+        filename: this.name,
+        action: this.action,
+        onProgress: e => {
+          this.handleProgress(e);
+        },
+        onSuccess: res => {
+          this.edit = false;
+          this.handleSuccess(res, file);
+        },
+        onError: (err, response) => {
+          this.edit = false;
+          this.handleError(err, response);
+        }
+      });
+    },
+    handleProgress(e) {
+      this.onProgress(e, this.file);
+      this.file.percentage = parseFloat(e.percent) || 0;
+    },
+    handleSuccess(res, file) {
+      this.file.status = "finished";
+      this.file.response = res;
+      this.res = {
+        success: true,
+        msg: "图片上传成功!"
+      };
+      this.file.url = this.createUrl(file);
+      this.imgUrl = this.file.url;
+      this.onSuccess(res, this.file);
+
+      setTimeout(() => {
+        this.file.showProgress = false;
+      }, 300);
+    },
+    handleError(err, response) {
+      this.file.status = "fail";
+      this.res = {
+        success: false,
+        msg: `图片上传失败:${response.msg || "请求错误"}`
+      };
+      this.onError(err, response, this.file);
+    }
+  }
+};
+</script>

+ 2 - 0
src/components/common/ImageEditUpload/index.js

@@ -0,0 +1,2 @@
+import ImageEditUpload from "./ImageEditUpload.vue";
+export default ImageEditUpload;

+ 55 - 0
src/components/common/ImageEditUpload/intro.md

@@ -0,0 +1,55 @@
+# ImageEditUpload api
+
+## 实例
+
+```html
+<template>
+  <div class="demo">
+    <image-edit-upload
+      :action="action"
+      :headers="headers"
+      ></image-edit-upload>
+  </div>
+</template>
+
+<script>
+import ImageEditUpload from "@/components/common/ImageEditUpload";
+
+export default {
+  name: "demo",
+  components: {
+    ImageEditUpload
+  },
+  data() {
+    return {
+      action: "/enrolling/enrolling/common/upload/planId/schoolId",
+      headers: {
+        Authorization: '123'
+      }
+    };
+  }
+};
+</script>
+```
+
+## ImageEditUpload props
+
+| 属性             | 说明                                                                                                                                        | 类型     | 默认值         |
+| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- |
+| def-image        | 默认的显示的图片地址                                                                                                                        | String   | -              |
+| action           | 上传的地址,必填                                                                                                                            | String   | -              |
+| headers          | 设置上传的请求头部                                                                                                                          | Object   | {}             |
+| data             | 上传时附带的额外参数                                                                                                                        | Object   | -              |
+| name             | 上传的文件字段名                                                                                                                            | String   | file           |
+| with-credentials | 支持发送 cookie 凭证信息                                                                                                                    | Boolean  | false          |
+| show-upload-list | 是否显示已上传文件列表                                                                                                                      | Boolean  | true           |
+| accept           | 接受上传的[文件类型](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept)                                           | String   | -              |
+| format           | 支持的文件类型,与 accept 不同的是,format 是识别文件的后缀名,accept 为 input 标签原生的 accept 属性,会在选择文件时过滤,可以两者结合使用 | Array    | ["jpg", "png"] |
+| max-size         | 文件大小限制,单位 kb                                                                                                                       | Number   | 2 * 1024       |
+| ratio-type       | 图片裁剪的宽高比例,rectangle:3/4,square:1/1,double:1/2                                                                                    | String   | rectangle      |
+| on-progress      | 文件上传时的钩子,返回字段为 event, file                                                                                                    | Function | -              |
+| on-success       | 文件上传成功时的钩子,返回字段为 response, file                                                                                             | Function | -              |
+| on-error         | 文件上传失败时的钩子,返回字段为 error, file                                                                                                | Function | -              |
+| on-format-error  | 文件格式验证失败时的钩子,返回字段为 file                                                                                                   | Function | -              |
+| on-exceeded-size | 文件超出指定大小限制时的钩子,返回字段为 file                                                                                               | Function | -              |
+

BIN
src/components/common/ImageEditUpload/temp-img.png


+ 322 - 0
src/components/common/ImageListUpload/ImageListUpload.vue

@@ -0,0 +1,322 @@
+<template>
+  <div :class="['image-list-upload', prefixCls]">
+    <!-- message alert -->
+    <el-alert
+      :title="res.msg"
+      :type="res.success ? 'success' : 'error'"
+      v-if="res.msg"
+    >
+    </el-alert>
+    <!-- image-list -->
+    <div :class="[`${prefixCls}-list`]">
+      <div
+        v-for="(item, index) in fileList"
+        :class="[
+          `${prefixCls}-item`,
+          `${prefixCls}-item-${item.status}`,
+          { [`${prefixCls}-item-unvalid`]: !item.valid }
+        ]"
+        :key="index"
+      >
+        <div :class="[`${prefixCls}-box`, `${prefixCls}-info`]">
+          <img :src="item.url" :alt="item.name" />
+        </div>
+        <div :class="[`${prefixCls}-box`, `${prefixCls}-progress`]">
+          <el-progress
+            :percentage="item.percentage"
+            :stroke-width="8"
+            :show-text="false"
+          ></el-progress>
+        </div>
+        <div :class="[`${prefixCls}-box `, `${prefixCls}-action`]">
+          <i
+            :class="['el-icon-delete-solid', `${prefixCls}-del`]"
+            title="删除"
+            @click="handleRemove(index)"
+          ></i>
+        </div>
+        <div
+          :class="[
+            `${prefixCls}-tips`,
+            {
+              [`${prefixCls}-tips-success`]:
+                item.valid && item.status === 'success',
+              [`${prefixCls}-tips-error`]:
+                !item.valid || item.status === 'error'
+            }
+          ]"
+        >
+          <span>{{ item.statusTips }}</span>
+        </div>
+      </div>
+
+      <!-- upload add btn -->
+      <div
+        :class="[`${prefixCls}-item`]"
+        @click="handleClick"
+        title="点击添加图片"
+      >
+        <div :class="[`${prefixCls}-box`, `${prefixCls}-add`]">
+          <i class="el-icon-upload"></i>
+        </div>
+        <input
+          ref="fileInput"
+          type="file"
+          :class="[`${prefixCls}-input`]"
+          @change="handleChange"
+          :multiple="multiple"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import ajax from "../utils/ajax";
+
+/**
+ * 图片上传状态:
+ * init 初始状态
+ * ready 准备上传
+ * uploading 正在上传
+ * sucess 上传成功
+ * error 上传失败
+ *
+ * 图片上传流程:
+ * 1 选中图片
+ * 2 校验合法性:大小,类型
+ * 3 自动上传合法图片
+ *
+ * 说明:
+ * 1 继发上传
+ * 2 上传中不可删除图片,上传结束之后可以删除图片
+ * 3 每一个图片上传都会触发开始上传、上传成功以及上传失败事件
+ * 4 所有合法图片上传完成之后,以及删除图片时,都会触发完成事件。
+ */
+
+const prefixCls = "cc-list-upload";
+
+export default {
+  name: "image-list-upload",
+  props: {
+    action: {
+      type: String,
+      default: "/backend/uploadImage"
+    },
+    multiple: {
+      type: Boolean,
+      default: true
+    },
+    headers: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    withCredentials: {
+      type: Boolean,
+      default: false
+    },
+    data: {
+      type: Object
+    },
+    name: {
+      type: String,
+      default: "file"
+    },
+    maxSize: {
+      type: Number,
+      default: 10 * 1024
+    },
+    rendFileUrl: {
+      type: Function,
+      default(fileInfo) {
+        return fileInfo.url;
+      }
+    }
+  },
+  data() {
+    return {
+      prefixCls,
+      fileList: [],
+      format: ["jpg", "jpeg", "png"],
+      limitNum: 10,
+      res: {
+        success: true,
+        msg: ""
+      },
+      isUploading: false
+    };
+  },
+  computed: {
+    limitSize() {
+      let mn = this.maxSize / 1024;
+      return mn >= 1 ? mn + "M" : this.maxSize + "Kb";
+    }
+  },
+  methods: {
+    initData() {
+      this.fileList = [];
+      this.uploadErrorFiles = [];
+      this.res = { success: true, msg: "" };
+      this.isUploading = false;
+    },
+    handleClick() {
+      if (this.isUploading) return;
+      this.$refs.fileInput.click();
+    },
+    handleChange(e) {
+      this.initFileList(e.target.files);
+    },
+    initFileList(files) {
+      if (!files) return;
+      if (files.length + this.fileList.length > this.limitNum) {
+        this.res = {
+          success: false,
+          msg: "文件数量不能超过" + this.limitNum
+        };
+        return;
+      }
+
+      const postFiles = Array.prototype.slice.call(files);
+
+      let fileList = [];
+      postFiles.forEach((el, i) => {
+        el.uid = Date.now() + i;
+        let { statusTips, valid } = this.checkFileValid(el);
+        fileList[i] = {
+          size: el.size,
+          name: el.name,
+          type: el.type,
+          percentage: 0,
+          uid: el.uid,
+          url: this.getFileUrl(el),
+          fileData: {},
+          files: el,
+          status: "init",
+          statusTips,
+          valid
+        };
+      });
+      this.fileList = [...this.fileList, ...fileList];
+      this.toUpload();
+    },
+    getFileUrl(file) {
+      const URL = window.URL || window.webkitURL;
+      if (URL && URL.createObjectURL) return URL.createObjectURL(file);
+      return "";
+    },
+    getFileItem(uid) {
+      return this.fileList.find(el => el.uid === uid);
+    },
+    clearData() {
+      this.$refs.fileInput.value = null;
+      this.fileList = [];
+    },
+    checkFileValid(fileItem) {
+      let result = {};
+      result = this.checkFileSize(fileItem);
+      if (!result.valid) return result;
+
+      result = this.checkFileFormat(fileItem);
+      if (!result.valid) return result;
+
+      return { valid: true, statusTips: "" };
+    },
+    checkFileSize(fileItem) {
+      const valid = fileItem.size / 1024 <= this.maxSize;
+      return {
+        statusTips: valid ? "" : "文件过大",
+        valid
+      };
+    },
+    checkFileFormat(fileItem) {
+      const formats = this.format.join("|");
+      const reg = `\\.(${formats})$`;
+      const pattern = new RegExp(reg, "i");
+
+      const valid = pattern.test(fileItem.name);
+      return {
+        statusTips: valid ? "" : "格式不对",
+        valid
+      };
+    },
+    handleRemove(index) {
+      this.fileList.splice(index, 1);
+      this.emitUploaded();
+      if (!this.fileList.length) this.clearData();
+    },
+    // action
+    async toUpload() {
+      if (this.isUploading) return;
+      this.isUploading = true;
+
+      this.fileList.map(item => {
+        if (item.status === "init" && item.valid) item.status = "ready";
+      });
+      // 上传队列,继发上传,返回失败文件
+      const queenFunc = async () => {
+        for (let fileItem of this.fileList) {
+          await this.upload(fileItem).catch(() => {});
+        }
+      };
+      // 开启上传
+      await queenFunc().catch(() => {});
+
+      this.isUploading = false;
+      this.emitUploaded();
+    },
+    async upload(fileItem) {
+      if (fileItem.status !== "ready") return;
+      this.$emit("upload-start", fileItem);
+
+      fileItem.status = "uploading";
+      let errorData = "";
+      const resData = await this.post(fileItem).catch(error => {
+        errorData = error;
+      });
+
+      if (resData) {
+        fileItem.status = "success";
+        fileItem.statusTips = "上传成功";
+        this.$emit("upload-success", resData, fileItem);
+      } else {
+        fileItem.status = "error";
+        fileItem.statusTips = "上传失败";
+        this.$emit("upload-error", errorData, fileItem);
+      }
+    },
+    post(fileItem) {
+      return new Promise((resolve, reject) => {
+        ajax({
+          headers: this.headers,
+          withCredentials: this.withCredentials,
+          file: fileItem.files,
+          data: this.data,
+          filename: this.name,
+          action: this.GLOBAL.domain + this.action,
+          onProgress: e => {
+            fileItem.percentage = e.percent || 0;
+          },
+          onSuccess: res => {
+            if (res.code === 0) {
+              resolve(res.data);
+            } else {
+              reject({ res, msg: res.message || "上传失败" });
+            }
+          },
+          onError: (err, res) => {
+            reject({ res, msg: "网络错误" });
+          }
+        });
+      });
+    },
+    emitUploaded() {
+      this.$emit(
+        "uploaded",
+        this.fileList.filter(el => el.status === "success" && el.valid)
+      );
+    }
+  }
+};
+</script>

+ 2 - 0
src/components/common/ImageListUpload/index.js

@@ -0,0 +1,2 @@
+import ImageListUpload from "./ImageListUpload.vue";
+export default ImageListUpload;

+ 47 - 0
src/components/common/ImageListUpload/intro.md

@@ -0,0 +1,47 @@
+# ImageListUpload api
+
+## 实例
+
+```html
+<template>
+  <div class="image-list-upload-instance part-box">
+    <image-list-upload></image-list-upload>
+  </div>
+</template>
+
+<script>
+import ImageListUpload from "@/components/common/ImageListUpload";
+
+export default {
+  name: "image-list-upload-instance",
+  components: { ImageListUpload },
+  data() {
+    return {};
+  },
+  methods: {}
+};
+</script>
+```
+
+## props
+
+| 属性             | 说明                     | 类型     | 默认值              |
+| ---------------- | ------------------------ | -------- | ------------------- |
+| action           | 上传的地址,必填         | String   | -                   |
+| multiple         | 支持多文件上传           | Boolean  | true                |
+| headers          | 设置上传的请求头部       | Object   | {}                  |
+| with-credentials | 支持发送 cookie 凭证信息 | Boolean  | false               |
+| data             | 上传时附带的额外参数     | Object   | -                   |
+| name             | 上传的文件字段名         | String   | file                |
+| max-size         | 文件大小限制,单位 kb    | Number   | 10 * 1024           |
+| rendFileUrl      | 上传完后图片解析路径     | Function | return fileInfo.url |
+
+
+## events
+
+| 事件名         | 说明                           | 返回值                       |
+| -------------- | ------------------------------ | ---------------------------- |
+| upload-start   | 文件开始开始上传时的回调       | fileItem:当前上传文件的对象 |
+| upload-success | 文件开始上传成功时的回调       | res, fileItem                |
+| upload-error   | 文件开始上传失败时的回调       | error, fileItem              |
+| uploaded       | 所有合法文件上传成功之后的回调 | fileList                     |

+ 312 - 0
src/components/common/ImagePreview/ImagePreview.vue

@@ -0,0 +1,312 @@
+<template>
+  <el-dialog
+    :class="prefixCls"
+    :visible.sync="modalIsShow"
+    title="图片预览"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :show-close="false"
+    fullscreen
+    append-to-body
+    @open="visibleChange"
+  >
+    <div slot="footer"></div>
+    <div slot="title"></div>
+
+    <div :class="[`${prefixCls}-close`]" @click="modalIsShow = false">
+      <i class="el-icon-circle-close"></i>
+    </div>
+    <div :class="[`${prefixCls}-header`]" v-if="!headerHide">
+      <h3>{{ curFile.name }}</h3>
+      <div :class="[`${prefixCls}-index`]">
+        {{ this.curIndex }} / {{ this.lastIndex }}
+      </div>
+    </div>
+    <div :class="[`${prefixCls}-body`]" ref="ReviewBody">
+      <div
+        :class="[`${prefixCls}-guide`, `${prefixCls}-guide-prev`]"
+        @click="showPrev"
+      >
+        <i class="el-icon-arrow-left"></i>
+      </div>
+      <div
+        :class="[`${prefixCls}-guide`, `${prefixCls}-guide-next`]"
+        @click="showNext"
+      >
+        <i class="el-icon-arrow-right"></i>
+      </div>
+      <div
+        :class="[
+          `${prefixCls}-imgs`,
+          { [`${prefixCls}-imgs-nosition`]: nosition }
+        ]"
+        :style="styles"
+        v-move-ele.prevent="{ mouseMove }"
+      >
+        <img
+          :src="curFile.url"
+          :alt="curFile.name"
+          v-if="curFile.url"
+          ref="PreviewImgDetail"
+        />
+      </div>
+    </div>
+    <div :class="[`${prefixCls}-footer`]">
+      <ul>
+        <li title="合适大小" @click="toOrigin">
+          <i class="el-icon-full-screen"></i>
+        </li>
+        <li
+          title="放大"
+          @click="toMagnify"
+          :class="{
+            'li-disabled': transform.scale === maxScale
+          }"
+        >
+          <i class="el-icon-zoom-in"></i>
+        </li>
+        <li
+          title="缩小"
+          @click="toShrink"
+          :class="{
+            'li-disabled': transform.scale === minScale
+          }"
+        >
+          <i class="el-icon-zoom-out"></i>
+        </li>
+        <li title="旋转" @click="toRotate">
+          <i class="el-icon-refresh-right"></i>
+        </li>
+      </ul>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import MoveEle from "./move-ele";
+
+const prefixCls = "cc-image-preview";
+
+export default {
+  name: "image-preview",
+  props: {
+    imageList: {
+      type: Array,
+      default() {
+        return [];
+      }
+    },
+    initIndex: {
+      type: Number,
+      default: 0
+    },
+    headerHide: {
+      type: Boolean,
+      default: false
+    }
+  },
+  directives: {
+    MoveEle
+  },
+  data() {
+    return {
+      prefixCls,
+      modalIsShow: false,
+      curFile: { name: "", url: null },
+      curIndex: 1,
+      minScale: 0.2,
+      maxScale: 3,
+      styles: { width: "", height: "", top: "100px", left: "", transform: "" },
+      initWidth: 500,
+      transform: {
+        scale: 1,
+        rotate: 0
+      },
+      nosition: false
+    };
+  },
+  computed: {
+    isFirst() {
+      return this.curIndex === 1;
+    },
+    isLast() {
+      return this.lastIndex === this.curIndex;
+    },
+    lastIndex() {
+      return this.imageList.length;
+    }
+  },
+  watch: {
+    curIndex: {
+      immediate: true,
+      handler(val, oldVal) {
+        this.curFileChange(val);
+      }
+    }
+  },
+  methods: {
+    visibleChange() {
+      if (!this.imageList.length) return;
+      this.curIndex = this.initIndex + 1;
+    },
+    curFileChange(val) {
+      this.nosition = true;
+      this.curFile = this.imageList[val - 1];
+      this.$nextTick(() => {
+        this.fileLoad();
+      });
+    },
+    setCurIndex(index) {
+      this.curIndex = index;
+    },
+    fileLoad() {
+      const imgDom = this.$refs.PreviewImgDetail;
+      if (!imgDom) return;
+
+      imgDom.onload = () => {
+        const { naturalWidth, naturalHeight } = imgDom;
+        const imageSize = this.getImageSizePos({
+          win: {
+            width: this.$refs.ReviewBody.clientWidth,
+            height: this.$refs.ReviewBody.clientHeight
+          },
+          img: {
+            width: naturalWidth,
+            height: naturalHeight
+          },
+          rotate: 0
+        });
+
+        this.styles = Object.assign(this.styles, {
+          width: imageSize.width + "px",
+          height: imageSize.height + "px",
+          top: imageSize.top + "px",
+          left: imageSize.left + "px",
+          transform: ""
+        });
+        this.transform = {
+          scale: 1,
+          rotate: 0
+        };
+        setTimeout(() => {
+          this.nosition = false;
+        }, 100);
+      };
+    },
+    getImageSizePos({ win, img, rotate }) {
+      const imageSize = {
+        width: 0,
+        height: 0,
+        top: 0,
+        left: 0
+      };
+      const isHorizontal = !!(rotate % 180);
+
+      const rateWin = isHorizontal
+        ? win.height / win.width
+        : win.width / win.height;
+      const hwin = isHorizontal
+        ? {
+            width: win.height,
+            height: win.width
+          }
+        : win;
+
+      const rateImg = img.width / img.height;
+
+      if (rateImg <= rateWin) {
+        imageSize.height = Math.min(hwin.height, img.height);
+        imageSize.width = Math.floor(
+          (imageSize.height * img.width) / img.height
+        );
+      } else {
+        imageSize.width = Math.min(hwin.width, img.width);
+        imageSize.height = Math.floor(
+          (imageSize.width * img.height) / img.width
+        );
+      }
+      imageSize.left = (win.width - imageSize.width) / 2;
+      imageSize.top = (win.height - imageSize.height) / 2;
+      return imageSize;
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    showPrev() {
+      if (this.isFirst) {
+        this.curIndex = this.lastIndex;
+      } else {
+        this.curIndex -= 1;
+      }
+    },
+    showNext() {
+      if (this.isLast) {
+        this.curIndex = 1;
+      } else {
+        this.curIndex += 1;
+      }
+    },
+    // dome-move
+    mouseMove({ left, top }) {
+      this.styles.left = left + "px";
+      this.styles.top = top + "px";
+    },
+    setStyleTransform() {
+      const { scale, rotate } = this.transform;
+      this.styles.transform = `scale(${scale}, ${scale}) rotate(${rotate}deg)`;
+    },
+    toOrigin() {
+      this.transform.scale = 1;
+      this.setStyleTransform();
+    },
+    toMagnify() {
+      const scale = (this.transform.scale * 1.2).toFixed(2);
+      this.transform.scale = scale >= this.maxScale ? this.maxScale : scale;
+      this.setStyleTransform();
+    },
+    toShrink() {
+      const scale = (this.transform.scale * 0.75).toFixed(2);
+      this.transform.scale = scale <= this.minScale ? this.minScale : scale;
+      this.setStyleTransform();
+    },
+    toRotate() {
+      this.transform.rotate = this.transform.rotate + 90;
+      this.setStyleTransform();
+      // 调整图片尺寸
+      const { naturalWidth, naturalHeight } = this.$refs.PreviewImgDetail;
+      const imageSize = this.getImageSizePos({
+        win: {
+          width: this.$refs.ReviewBody.clientWidth,
+          height: this.$refs.ReviewBody.clientHeight
+        },
+        img: {
+          width: naturalWidth,
+          height: naturalHeight
+        },
+        rotate: this.transform.rotate
+      });
+
+      this.styles = Object.assign(this.styles, {
+        width: imageSize.width + "px",
+        height: imageSize.height + "px",
+        top: imageSize.top + "px",
+        left: imageSize.left + "px"
+      });
+      // 360度无缝切换到0度
+      setTimeout(() => {
+        if (this.transform.rotate >= 360) {
+          this.nosition = true;
+          this.transform.rotate = 0;
+          this.setStyleTransform();
+          setTimeout(() => {
+            this.nosition = false;
+          }, 100);
+        }
+      }, 200);
+    }
+  }
+};
+</script>

+ 2 - 0
src/components/common/ImagePreview/index.js

@@ -0,0 +1,2 @@
+import ImagePreview from "./ImagePreview.vue";
+export default ImagePreview;

+ 51 - 0
src/components/common/ImagePreview/move-ele.js

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

+ 190 - 0
src/components/common/ImportFile/ImportFile.vue

@@ -0,0 +1,190 @@
+<template>
+  <div :class="prefixCls">
+    <el-dialog
+      class="modify-data"
+      :visible.sync="modalIsShow"
+      :title="title"
+      top="10vh"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+      @open="visibleChange"
+    >
+      <div style="footer"></div>
+      <div :class="[`${prefixCls}-body`]">
+        <el-upload
+          drag
+          :action="uploadUrl"
+          :headers="headers"
+          :max-size="maxSize"
+          :format="format"
+          :accept="accept"
+          :data="uploadData"
+          :before-upload="handleBeforeUpload"
+          :on-error="handleError"
+          :on-success="handleSuccess"
+          :on-remove="handleRemove"
+          ref="UploadComp"
+        >
+          <i class="el-icon-upload"></i><br />
+          <em class="el-upload__text">将文件拖到此处,或<em>点击上传</em></em>
+          <p
+            slot="tip"
+            :class="[
+              `${prefixCls}-tips`,
+              {
+                'cc-tips-success': res.success,
+                'cc-tips-error': !res.success
+              }
+            ]"
+            v-if="res.msg"
+          >
+            {{ res.msg }}
+          </p>
+        </el-upload>
+      </div>
+      <div :class="[`${prefixCls}-footer`]" v-if="downloadUrl">
+        文件下载:
+        <a :href="downloadUrl" :download="dfilename">{{ dfilename }}</a>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+const prefixCls = "cc-import-file";
+
+export default {
+  name: "import-file",
+  props: {
+    title: {
+      type: String,
+      default: "文件上传"
+    },
+    downloadUrl: {
+      type: String,
+      default: ""
+    },
+    downloadFilename: String,
+    format: {
+      type: Array,
+      default() {
+        return ["jpg", "jpeg", "png"];
+      }
+    },
+    uploadUrl: {
+      type: String,
+      required: true
+    },
+    uploadData: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    headers: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024
+    },
+    addFilenameParam: {
+      type: String,
+      default: "fileName"
+    }
+  },
+  data() {
+    return {
+      prefixCls,
+      modalIsShow: false,
+      res: {
+        success: true,
+        msg: ""
+      }
+    };
+  },
+  computed: {
+    dfilename() {
+      return this.downloadFilename || this.downloadUrl.split("/").pop();
+    },
+    accept() {
+      return this.format.map(el => `.${el}`).join();
+    }
+  },
+  methods: {
+    visibleChange() {
+      this.res = {
+        success: true,
+        msg: ""
+      };
+    },
+    checkFileFormat(fileType) {
+      const _file_format = fileType
+        .split("/")
+        .pop()
+        .toLocaleLowerCase();
+      return this.format.some(
+        item => item.toLocaleLowerCase() === _file_format
+      );
+    },
+    handleBeforeUpload(file) {
+      if (this.addFilenameParam)
+        this.uploadData[this.addFilenameParam] = file.name;
+
+      if (file.size > this.maxSize) {
+        this.handleExceededSize();
+        return Promise.reject();
+      }
+
+      if (!this.checkFileFormat(file.type)) {
+        this.handleFormatError();
+        return Promise.reject();
+      }
+
+      return true;
+    },
+    handleError(error) {
+      this.res = {
+        success: false,
+        msg: error.message
+      };
+      this.$emit("upload-error", error);
+    },
+    handleSuccess(response) {
+      this.res = {
+        success: true,
+        msg: "导入成功!"
+      };
+      this.$emit("upload-success", response);
+    },
+    handleFormatError() {
+      this.res = {
+        success: false,
+        msg: "只支持文件格式为" + this.format.join("/")
+      };
+      this.$refs.UploadComp.clearFiles();
+    },
+    handleExceededSize() {
+      this.res = {
+        success: false,
+        msg: "文件大小不能超过" + Math.floor(this.maxSize / 1024) + "M"
+      };
+      this.$refs.UploadComp.clearFiles();
+    },
+    handleRemove() {
+      this.$refs.UploadComp.abort();
+      this.$refs.UploadComp.clearFiles();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    }
+  }
+};
+</script>

+ 2 - 0
src/components/common/ImportFile/index.js

@@ -0,0 +1,2 @@
+import ImportFile from "./ImportFile.vue";
+export default ImportFile;

+ 74 - 0
src/components/common/ImportFile/intro.md

@@ -0,0 +1,74 @@
+# ImportFile api
+
+## 实例
+
+```html
+<template>
+  <div class="demo">
+    <button type="primary" @click="open">to upload</button>
+
+    <import-file
+      :title="title"
+      :upload-url="uploadUrl"
+      :download-url="downloadUrl"
+      :download-filename="downloadFilename"
+      :headers="headers"
+      :format="['xls', 'xlsx']"
+      @confirm="fileUploaded"
+      ref="ImportFile"
+    >
+    </import-file>
+  </div>
+</template>
+
+<script>
+  import ImportFile from "@/components/common/ImportFile";
+
+  export default {
+    name: "demo",
+    components: {
+      ImportFile
+    },
+    data() {
+      return {
+        title: "导入文件",
+        uploadUrl: this.GLOBAL.domain + "yourUploadPath",
+        uploadData: {},
+        downloadFilename: "要下载的文件.xls",
+        downloadUrl: "/path/要下载的文件.xls",
+        headers: {
+          Authorization: "token"
+        }
+      };
+    },
+    methods: {
+      open() {
+        this.$refs.ImportFile.modalIsShow = true;
+      },
+      fileUploaded(file) {
+        // file:上传文件的url地址
+        console.log(file);
+      }
+    }
+  };
+</script>
+```
+
+## ImportFile props
+
+| 属性             | 说明                                               | 类型   | 默认值         |
+| ---------------- | -------------------------------------------------- | ------ | -------------- |
+| title            | 标题文字                                           | String | 文件上传       |
+| downloadUrl      | 要下载的文件地址                                   | String | -              |
+| downloadFilename | 要下载的文件名称,默认是 downloadUrl 所指向的文件名 | String | -              |
+| format           | 支持上传的文件格式                                 | Array  | ["jpg", "png"] |
+| uploadUrl        | 上传地址,必须                                     | String | -              |
+| uploadData       | 上传时额外的数据                                   | Object | {}             |
+| headers          | 上传时添加的 headers 信息                          | Object | {}             |
+| maxSize          | 限制的文件大小,单位 kb                            | Number | 10 \* 1024     |
+
+## ImportFile events
+
+| 事件名  | 说明                   | 返回值                       |
+| ------- | ---------------------- | ---------------------------- |
+| confirm | 确认文件上传时触发事件 | fileUrl:上传文件的 url 地址 |

+ 72 - 0
src/components/common/LabelFilter/LabelFilter.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="label-filter">
+    <label-select
+      v-model="filter.color"
+      label="颜色"
+      :options="colorList"
+      :label-width="60"
+      @on-change="colorChange"
+    ></label-select>
+    <label-select
+      v-model="filter.size"
+      label="尺码"
+      :options="sizeList"
+      :label-width="60"
+      @on-change="colorChange"
+      multiple
+    ></label-select>
+
+    <div class="label-list">
+      {{ strFilter }}
+    </div>
+  </div>
+</template>
+
+<script>
+import LabelSelect from "./LabelSelect";
+
+const colorList = "#"
+  .repeat(6)
+  .split("")
+  .map((item, index) => {
+    return {
+      id: index + 1,
+      name: "红色" + (index + 1)
+    };
+  });
+
+const sizeList = "#"
+  .repeat(6)
+  .split("")
+  .map((item, index) => {
+    return {
+      id: index + 1,
+      name: "大号" + (index + 1)
+    };
+  });
+
+export default {
+  name: "label-filter",
+  components: { LabelSelect },
+  data() {
+    return {
+      filter: {
+        color: "",
+        size: ""
+      },
+      colorList: colorList,
+      sizeList: sizeList
+    };
+  },
+  computed: {
+    strFilter() {
+      return JSON.stringify(this.filter);
+    }
+  },
+  methods: {
+    colorChange(val) {
+      console.log(val);
+    }
+  }
+};
+</script>

+ 205 - 0
src/components/common/LabelFilter/LabelSelect.vue

@@ -0,0 +1,205 @@
+<template>
+  <div :class="['label-select', prefixCls]">
+    <span
+      :class="[`${prefixCls}-label`]"
+      v-if="labelIsShow"
+      :style="labelStyles"
+      >{{ label }}</span
+    >
+    <ul :class="[`${prefixCls}-options`]" :style="optionsStyles">
+      <li
+        :class="{
+          [`${prefixCls}-selected`]: !selectedIds.length
+        }"
+        @click="selectAll"
+        v-if="showAllOptionTag"
+      >
+        全部
+      </li>
+      <li
+        v-for="(item, index) in optionList"
+        :key="index"
+        :class="{
+          [`${prefixCls}-selected`]: item.selected,
+          [`${prefixCls}-disabled`]: item.disabled
+        }"
+        @click="select(item)"
+      >
+        {{ item.name }}
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+const prefixCls = "cc-labels";
+
+export default {
+  name: "label-select",
+  props: {
+    value: {
+      type: [String, Number, Array]
+    },
+    label: {
+      type: String
+    },
+    labelWidth: {
+      type: Number
+    },
+    labelIsShow: {
+      type: Boolean,
+      default: true
+    },
+    showAllOptionTag: {
+      type: Boolean,
+      default: true
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    optionKey: {
+      type: String,
+      default: "id"
+    },
+    options: {
+      type: Array,
+      default() {
+        return [];
+      }
+    },
+    costom: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      prefixCls,
+      selectedIds: [],
+      optionList: []
+    };
+  },
+  computed: {
+    labelStyles() {
+      const width =
+        this.labelWidth || this.labelWidth === 0 ? this.labelWidth : 60;
+
+      return {
+        width: `${width}px`
+      };
+    },
+    optionsStyles() {
+      const width =
+        this.labelWidth || this.labelWidth === 0 ? this.labelWidth : 60;
+
+      return {
+        marginLeft: `${width}px`
+      };
+    }
+  },
+  watch: {
+    options: {
+      immediate: true,
+      handler(val) {
+        this.optionList = val.map(item => {
+          return {
+            ...item,
+            selected: this.selectedIds.indexOf(item[this.optionKey]) !== -1
+          };
+        });
+      }
+    },
+    value: {
+      immediate: true,
+      handler(val) {
+        if (this.multiple) {
+          this.selectedIds = val ? [...val] : [];
+        } else {
+          this.selectedIds = val ? [val] : [];
+        }
+        this.actOptions();
+      }
+    }
+  },
+  methods: {
+    select(option) {
+      const optkey = option[this.optionKey];
+      if (this.multiple) {
+        option.selected = !option.selected;
+        const optPoz = this.selectedIds.indexOf(optkey);
+        if (optPoz === -1) {
+          this.selectedIds.push(optkey);
+        } else {
+          this.selectedIds.splice(optPoz, 1);
+        }
+      } else {
+        this.optionList.map(item => {
+          item.selected = optkey === item[this.optionKey];
+        });
+        this.selectedIds = [optkey];
+      }
+      this.emitSelect();
+    },
+    actOptions() {
+      this.optionList.map(item => {
+        item.selected = this.selectedIds.indexOf(item[this.optionKey]) !== -1;
+      });
+    },
+    selectAll() {
+      this.optionList.map(item => (item.selected = false));
+      this.selectedIds = [];
+      this.emitSelect();
+    },
+    emitSelect() {
+      const selectOptions = this.optionList.filter(
+        item => this.selectedIds.indexOf(item[this.optionKey]) !== -1
+      );
+      this.$emit(
+        "input",
+        this.multiple ? this.selectedIds : this.selectedIds[0]
+      );
+      this.$emit("on-change", selectOptions);
+    }
+  }
+};
+</script>
+
+<style lang="css">
+.label-select {
+  margin-bottom: 8px;
+}
+.label-label {
+  display: block;
+  float: left;
+  height: 24px;
+  line-height: 24px;
+  font-size: 14px;
+  padding-right: 20px;
+  font-weight: 600;
+}
+.label-options {
+  margin-left: 60px;
+}
+.label-options li {
+  display: inline-block;
+  vertical-align: top;
+  margin: 0 5px 5px 0;
+  height: 24px;
+  line-height: 24px;
+  padding: 0 10px;
+  border-radius: 3px;
+  cursor: pointer;
+}
+.label-options li:hover {
+  color: #409eff;
+}
+.label-options li.label-selected {
+  background: #409eff;
+  color: #fff;
+}
+.label-options li.label-disabled {
+  color: #909399;
+  cursor: not-allowed;
+}
+</style>

+ 283 - 0
src/components/common/RichTextEditor/RichTextEditor.vue

@@ -0,0 +1,283 @@
+<template>
+  <div class="rich-text-editor cc-editor">
+    <quill-editor
+      :id="editorId"
+      :content="content"
+      ref="QuillEditor"
+      @change="onEditorChange"
+      :options="options"
+    ></quill-editor>
+    <input
+      style="display: none"
+      :id="uniqueInputId"
+      type="file"
+      name="fileinput"
+      @change="uploadImg"
+    />
+    <el-progress
+      :percentage="percentage"
+      :stroke-width="2"
+      :show-text="false"
+    ></el-progress>
+  </div>
+</template>
+
+<script>
+// 文档:https://github.com/surmon-china/vue-quill-editor
+import ajax from "../utils/ajax";
+import Quill from "quill";
+import Delta from "quill-delta";
+import { quillEditor } from "vue-quill-editor";
+import "quill/dist/quill.snow.css";
+import ImageResize from "quill-image-resize-module";
+Quill.register("modules/imageResize", ImageResize);
+
+export default {
+  name: "rich-text-editor",
+  props: {
+    value: String,
+    // upload
+    action: {
+      type: String,
+      required: true
+    },
+    headers: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    withCredentials: {
+      type: Boolean,
+      default: false
+    },
+    data: {
+      type: Object
+    },
+    name: {
+      type: String,
+      default: "file"
+    },
+    maxSize: {
+      type: Number,
+      default: 2 * 1024
+    },
+    rendFileUrl: {
+      type: Function,
+      default(fileInfo) {
+        return fileInfo;
+      }
+    },
+    onUploadSuccess: {
+      type: Function,
+      default() {
+        return {};
+      }
+    },
+    onUploadError: {
+      type: Function,
+      default() {
+        return {};
+      }
+    }
+  },
+  components: {
+    quillEditor
+  },
+  data() {
+    return {
+      content: "",
+      iframeSrc: "",
+      options: {
+        placeholder: "请输入内容",
+        modules: {
+          toolbar: [
+            ["bold", "italic", "underline", "strike"],
+            ["blockquote", "code-block"],
+            [{ list: "ordered" }, { list: "bullet" }],
+            [{ script: "sub" }, { script: "super" }],
+            [{ indent: "-1" }, { indent: "+1" }, { direction: "rtl" }],
+            [{ size: ["small", false, "large", "huge"] }],
+            [{ header: [1, 2, 3, 4, 5, 6, false] }],
+            [{ color: [] }, { background: [] }, { align: [] }],
+            ["clean", "link", "image"]
+          ],
+          imageResize: true,
+          history: {
+            delay: 1000,
+            maxStack: 50,
+            userOnly: false
+          }
+        }
+      },
+      percentage: 0,
+      uniqueInputId: "",
+      uploadImages: [],
+      editorId: "",
+      // math-modal
+      src: "",
+      mathModalIsShow: false
+    };
+  },
+  computed: {
+    editor() {
+      return this.$refs.QuillEditor.quill;
+    }
+  },
+  watch: {
+    value(val) {
+      this.content = val;
+    }
+  },
+  mounted() {
+    const serno = Math.random()
+      .toString(16)
+      .slice(-8);
+    this.editorId = "rich-text-editor-" + serno;
+    this.uniqueInputId = "file-input-" + serno;
+
+    this.registImageHandle();
+    this.registPasteEvent();
+    this.content = this.value;
+  },
+  methods: {
+    registPasteEvent() {
+      const inputBox = this.$el.querySelector(".ql-editor");
+      inputBox.addEventListener("paste", e => {
+        e.preventDefault();
+        let items = e.clipboardData ? e.clipboardData.items : [];
+        if (!items || !items.length) return;
+
+        for (let i = 0; i < items.length; i++) {
+          let item = items[i];
+          if (
+            item &&
+            item.kind === "string" &&
+            item.type.match(/^text\/plain/i)
+          ) {
+            this.textPasteHandle(item);
+            break;
+          } else if (
+            item &&
+            item.kind === "file" &&
+            item.type.match(/^image\//i)
+          ) {
+            this.imgPasteHandle(item);
+            break;
+          }
+        }
+      });
+    },
+    textPasteHandle(item) {
+      item.getAsString(content => {
+        this.insertText(content);
+      });
+    },
+    imgPasteHandle(item) {
+      let file = item.getAsFile();
+
+      // 展示文件
+      // const reader = new FileReader();
+      // reader.onload = e => {
+      //   this.insertImg(e.target.result);
+      // };
+      // reader.readAsDataURL(file);
+
+      this.upload(file)
+        .then(rep => {
+          const fileUrl = this.rendFileUrl(rep);
+          this.insertImg(fileUrl);
+          this.onUploadSuccess(fileUrl);
+        })
+        .catch(error => {
+          this.onUploadError(error);
+        });
+    },
+    registImageHandle() {
+      let that = this;
+      let imgHandler = async state => {
+        if (state) {
+          let fileInput = document.getElementById(that.uniqueInputId);
+          fileInput.click();
+        }
+      };
+      that.editor.getModule("toolbar").addHandler("image", imgHandler);
+    },
+    onEditorChange() {
+      this.$emit("input", this.content);
+    },
+    uploadImg() {
+      let file = document.getElementById(this.uniqueInputId).files[0];
+      this.upload(file)
+        .then(rep => {
+          const fileUrl = this.rendFileUrl(rep);
+          this.insertImg(fileUrl);
+          this.onUploadSuccess(fileUrl);
+        })
+        .catch(error => {
+          this.onUploadError(error);
+        });
+    },
+    getSelectionIndex() {
+      this.editor.focus();
+      const selectionRange = this.editor.getSelection();
+      if (selectionRange && selectionRange.length)
+        this.editor.deleteText(selectionRange.index, selectionRange.length);
+
+      return selectionRange ? selectionRange.index : 0;
+    },
+    insertText(content) {
+      const index = this.getSelectionIndex();
+      this.editor.insertEmbed(index, "text", content, "user");
+    },
+    insertImg(url, alt) {
+      const index = this.getSelectionIndex();
+      let delta = this.editor.insertEmbed(index, "image", url, "user");
+      this.editor.setSelection(index + 1);
+      if (alt) {
+        let retain = delta.ops[0].retain;
+        this.editor.updateContents(
+          new Delta().retain(retain).retain(1, { alt })
+        );
+      }
+    },
+    base64ToBlob(base64Str) {
+      var bytes = atob(base64Str.split(",")[1]);
+      let arr = new Uint8Array(bytes.length);
+      for (let i = 0; i < bytes.length; i++) {
+        arr[i] = bytes.charCodeAt(i);
+      }
+      return new Blob([arr], { type: "image/png" });
+    },
+    upload(file) {
+      if (file.size > this.maxSize * 1024) {
+        return Promise.reject({ msg: "图片过大,请重新编辑!" });
+      }
+      return new Promise((resolve, reject) => {
+        ajax({
+          headers: this.headers,
+          withCredentials: this.withCredentials,
+          file: file,
+          data: this.data,
+          filename: this.name,
+          action: this.action,
+          onProgress: e => {
+            this.percentage = e.percent || 0;
+            if (this.percentage === 100) {
+              setTimeout(() => {
+                this.percentage = 0;
+              }, 300);
+            }
+          },
+          onSuccess: res => {
+            resolve(res);
+          },
+          onError: (err, response) => {
+            reject({ err, response });
+          }
+        });
+      });
+    }
+  }
+};
+</script>

+ 2 - 0
src/components/common/RichTextEditor/index.js

@@ -0,0 +1,2 @@
+import RichTextEditor from "./RichTextEditor.vue";
+export default RichTextEditor;

+ 104 - 0
src/components/common/RichTextEditor/intro.md

@@ -0,0 +1,104 @@
+# RickEdit api
+
+- [官方git地址](https://github.com/surmon-china/vue-quill-editor)
+
+## 实例
+
+```html
+<template>
+  <div class="demo">
+    <rich-editor v-model="content" :headers="headers" :action="action" :rend-file-url="rendFileUrl" ref="RichEditor"></rich-editor>
+  </div>
+</template>
+
+<script>
+import RichEditor from "@/components/common/RichEditor";
+
+export default {
+  name: "demo",
+  components: {
+    RichEditor
+  },
+  computed: {
+    editor() {
+      // editor实例,实质时quill对象
+      return this.$refs.RichEditor.editor;
+    }
+  },
+  data() {
+    return {
+      content: "<p>这是一段默认内容</p>",
+      headers: {},
+      action: ""
+    };
+  },
+  mounted() {
+    this.headers = {
+      Authorization: "token"
+    };
+    this.action = "file upload url";
+  },
+  methods: {
+    rendFileUrl(rep) {
+      return 'file-url'
+    }
+  }
+};
+</script>
+```
+
+## RickEdit props
+
+| 属性              | 说明                                                      | 类型     | 默认值   |
+| ----------------- | --------------------------------------------------------- | -------- | -------- |
+| value             | 配合v-model实现文本数据的双向绑定                         | String   | -        |
+| action            | 上传文件的api地址                                         | String   | -        |
+| headers           | 上传文件的header信息                                      | Object   | {}       |
+| max-size          | 上传文件大小限制,单位kb                                   | Number   | 2 * 1024 |
+| rend-file-url     | 上传文件成功后,解析文件路径的方法,参数rep为response对象 | Function | {}       |
+| name              | 上传文件的字段名                                          | String   | file     |
+| on-upload-success | 上传文件成功后的回调方法                                  | Function | {}       |
+| on-upload-error   | 上传文件失败后的回调方法,参数error信息                   | Function | {}       |
+
+
+## 插件说明:
+
+### 1. 图片缩放插件
+
+- 官方推荐插件:[quill-image-resize-module](https://github.com/kensnyder/quill-image-resize-module)
+- **Tips:** quill-image-resize-module若按官方提供的方式引入会有问题。调整后的方式如下:
+
+**载入图片缩放插件**
+
+- `vue.config.js`文件中加入如下代码
+ 
+```bash
+configureWebpack: {
+    plugins: [
+      new webpack.ProvidePlugin({
+        'window.Quill': 'quill/dist/quill.js',
+        Quill: 'quill/dist/quill.js'
+      })
+    ]
+  }
+```
+
+- 引入插件
+
+```bash
+import Quill from "quill";
+import ImageResize from "quill-image-resize-module";
+Quill.register("modules/imageResize", ImageResize);
+```
+
+- 配置插件
+
+```bash
+options: {
+ modules: {
+   imageResize: true
+ }
+}
+```
+
+- **Tips:** 如果不需要图片缩放插件,则可以删除相应的文件及代码。

+ 28 - 0
src/components/common/StepsProgress/StepFour.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="step-four">
+    step-four
+  </div>
+</template>
+
+<script>
+export default {
+  name: "step-four",
+  data() {
+    return {};
+  },
+  mounted() {
+    setTimeout(() => {
+      // 提交步骤准备就绪事件
+      this.$emit("on-ready");
+    }, 500);
+  },
+  methods: {
+    checkValid() {
+      // some code
+
+      // 提交可执行下一步操作事件
+      this.$emit("on-next");
+    }
+  }
+};
+</script>

+ 28 - 0
src/components/common/StepsProgress/StepOne.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="step-one">
+    step-one
+  </div>
+</template>
+
+<script>
+export default {
+  name: "step-one",
+  data() {
+    return {};
+  },
+  mounted() {
+    setTimeout(() => {
+      // 提交步骤准备就绪事件
+      this.$emit("on-ready");
+    }, 500);
+  },
+  methods: {
+    checkValid() {
+      // some code
+
+      // 提交可执行下一步操作事件
+      this.$emit("on-next");
+    }
+  }
+};
+</script>

+ 28 - 0
src/components/common/StepsProgress/StepThree.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="step-three">
+    step-three
+  </div>
+</template>
+
+<script>
+export default {
+  name: "step-three",
+  data() {
+    return {};
+  },
+  mounted() {
+    setTimeout(() => {
+      // 提交步骤准备就绪事件
+      this.$emit("on-ready");
+    }, 500);
+  },
+  methods: {
+    checkValid() {
+      // some code
+
+      // 提交可执行下一步操作事件
+      this.$emit("on-next");
+    }
+  }
+};
+</script>

+ 28 - 0
src/components/common/StepsProgress/StepTwo.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="step-two">
+    step-two
+  </div>
+</template>
+
+<script>
+export default {
+  name: "step-two",
+  data() {
+    return {};
+  },
+  mounted() {
+    setTimeout(() => {
+      // 提交步骤准备就绪事件
+      this.$emit("on-ready");
+    }, 500);
+  },
+  methods: {
+    checkValid() {
+      // some code
+
+      // 提交可执行下一步操作事件
+      this.$emit("on-next");
+    }
+  }
+};
+</script>

+ 126 - 0
src/components/common/StepsProgress/StepsProgress.vue

@@ -0,0 +1,126 @@
+<template>
+  <div class="step-progress">
+    <div class="step-menu">
+      <el-steps :active="current" align-center process-status="finish">
+        <el-step
+          v-for="(step, index) in STEPS_LIST"
+          :key="index"
+          :title="step.title"
+        ></el-step>
+      </el-steps>
+    </div>
+    <div class="step-body">
+      <component
+        :is="currentComponent"
+        :ref="currentComponent"
+        @on-next="toNext"
+        @on-ready="stepReady"
+      ></component>
+    </div>
+
+    <div class="step-ctrl">
+      <el-button type="primary" @click="prevStep" :disabled="isFirstStep"
+        >上一步</el-button
+      >
+      <el-button
+        type="primary"
+        @click="nextStep"
+        :disabled="nextHolder || isLastStep"
+        >下一步</el-button
+      >
+    </div>
+  </div>
+</template>
+
+<script>
+import StepOne from "./StepOne";
+import StepTwo from "./StepTwo";
+import StepThree from "./StepThree";
+import StepFour from "./StepFour";
+
+const STEPS_LIST = [
+  {
+    name: "step-one",
+    title: "步骤1"
+  },
+  {
+    name: "step-two",
+    title: "步骤2"
+  },
+  {
+    name: "step-three",
+    title: "步骤3"
+  },
+  {
+    name: "step-four",
+    title: "步骤4"
+  }
+];
+
+export default {
+  name: "step-progress",
+  components: {
+    StepOne,
+    StepTwo,
+    StepThree,
+    StepFour
+  },
+  data() {
+    return {
+      STEPS_LIST,
+      current: 0,
+      nextHolder: true,
+      dataReady: true
+    };
+  },
+  computed: {
+    currentComponent() {
+      return this.STEPS_LIST[this.current].name;
+    },
+    isFirstStep() {
+      return this.current === 0;
+    },
+    isLastStep() {
+      return this.current === this.lastStep;
+    },
+    lastStep() {
+      return this.STEPS_LIST.length - 1;
+    }
+  },
+  watch: {
+    current() {
+      // 滚动条置顶
+      // document.getElementById("home-body").scrollTop = 0;
+    }
+  },
+  methods: {
+    prevStep() {
+      if (this.isFirstStep) return;
+      this.current -= 1;
+    },
+    nextStep() {
+      if (this.isLastStep || this.nextHolder) return;
+      this.$refs[this.currentComponent].checkValid();
+    },
+    toNext() {
+      if (this.isLastStep) return;
+      this.current += 1;
+      this.nextHolder = true;
+    },
+    stepReady() {
+      this.nextHolder = false;
+    }
+  }
+};
+</script>
+
+<style lang="css">
+.step-body {
+  min-height: 600px;
+  padding: 20px;
+}
+.step-ctrl {
+  text-align: center;
+  margin-top: 50px;
+}
+</style>

+ 82 - 0
src/components/common/utils/ajax.js

@@ -0,0 +1,82 @@
+// https://github.com/ElemeFE/element/blob/dev/packages/upload/src/ajax.js
+
+function getError(action, option, xhr) {
+  const msg = `fail to post ${action} ${xhr.status}'`;
+  const err = new Error(msg);
+  err.status = xhr.status;
+  err.method = "post";
+  err.url = action;
+  return err;
+}
+
+function getBody(xhr) {
+  const text = xhr.responseText || xhr.response;
+  if (!text) {
+    return text;
+  }
+
+  try {
+    return JSON.parse(text);
+  } catch (e) {
+    return text;
+  }
+}
+
+export default function upload(option) {
+  if (typeof XMLHttpRequest === "undefined") {
+    return;
+  }
+
+  const xhr = new XMLHttpRequest();
+  const action = option.action;
+
+  if (xhr.upload) {
+    xhr.upload.onprogress = function progress(e) {
+      if (e.total > 0) {
+        e.percent = (e.loaded / e.total) * 100;
+      }
+      option.onProgress(e);
+    };
+  }
+
+  const formData = new FormData();
+
+  if (option.data) {
+    Object.keys(option.data).map(key => {
+      formData.append(key, option.data[key]);
+    });
+  }
+
+  formData.append(option.filename, option.file);
+
+  xhr.onerror = function error(e) {
+    option.onError(e);
+  };
+
+  xhr.onload = function onload() {
+    if (xhr.status < 200 || xhr.status >= 300) {
+      return option.onError(getError(action, option, xhr), getBody(xhr));
+    }
+
+    option.onSuccess(getBody(xhr));
+  };
+
+  xhr.open("post", action, true);
+
+  if (option.withCredentials && "withCredentials" in xhr) {
+    xhr.withCredentials = true;
+  }
+
+  const headers = option.headers || {};
+
+  // if (headers['X-Requested-With'] !== null) {
+  //   xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+  // }
+
+  for (let item in headers) {
+    if (headers.hasOwnProperty(item) && headers[item] !== null) {
+      xhr.setRequestHeader(item, headers[item]);
+    }
+  }
+  xhr.send(formData);
+}

+ 6 - 0
src/config.js

@@ -0,0 +1,6 @@
+export default {
+  domain: process.env.VUE_APP_DOMAIN || window.location.origin,
+  timeout: process.env.VUE_APP_TIMEOUT * 1,
+  pageSize: process.env.VUE_APP_PAGE_SIZE * 1,
+  authTimeout: process.env.VUE_APP_AUTH_TIMEOUT * 1
+};

Некоторые файлы не были показаны из-за большого количества измененных файлов