Browse Source

Default Changelist

邝淑晶 4 years ago
commit
e5774b7125

+ 9 - 0
.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 4 - 0
.env.development

@@ -0,0 +1,4 @@
+NODE_ENV=development
+
+VUE_APP_HOME_API_URL=http://127.0.0.1:8080/base-in-back-end
+VUE_APP_QUESTION_BANK_API_URL=http://112.74.105.17:20003/question-bank

+ 4 - 0
.env.production

@@ -0,0 +1,4 @@
+NODE_ENV=production
+
+VUE_APP_HOME_API_URL=http://127.0.0.1:8080/base-in-back-end
+VUE_QUESTION_BANK_API_URL=http://112.74.105.17:20003/question-bank

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+/build/
+/config/
+/dist/
+/*.js

+ 30 - 0
.eslintrc.js

@@ -0,0 +1,30 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+    root: true,
+    parserOptions: {
+        parser: 'babel-eslint'
+    },
+    env: {
+        browser: true,
+    },
+    extends: [
+        // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
+        // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
+        'plugin:vue/essential',
+        // https://github.com/standard/standard/blob/master/docs/RULES-en.md
+        'standard'
+    ],
+    // required to lint *.vue files
+    plugins: [
+        'vue'
+    ],
+    // add your custom rules here
+    rules: {
+        'indent': ['off', 2],
+        // allow async-await
+        'generator-star-spacing': 'off',
+        // allow debugger during development
+        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
+    }
+}

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Example user template template
+### Example user template
+
+.DS_Store
+
+# IntelliJ project files
+.idea
+*.iml
+out
+gen
+
+node_modules
+dist
+/package-lock.json

+ 45 - 0
README.md

@@ -0,0 +1,45 @@
+# base-in-front-end
+管理后台基础框架
+
+# 目录结构
+```
+_screenshot             截图指南
+public                  静态文件(不会编译打包)
+src
+    -|  api             接口类(根据服务端接口文档/Swagger-ui)
+    -|  assets          静态文件(编译打包)
+    -|  components      业务封装的组件
+    -|  constants       静态常量
+    -|  filters         过滤器(Vue Filter)
+    -|  plugins         插件(第三方的依赖、给main.js文件解耦)
+    -|  router          路由配置(Vue-Router)
+    -|  utils           工具类(JS常用方法封装)
+    -|  views           视图页面
+    -|  App.vue
+    -|  main.js
+```
+
+# 知识准备
+## Vue(Vue、Vue-Router、Vuex、axios)
+## Element-ui
+
+# vue
+A Vue.js project
+- VueCli + Vue2.* + Axios + Element-UI
+- 效果图
+![](/_screenshot/WX20200225-144536.png)
+
+# 命令
+```
+# Project setup
+npm install
+
+# Compiles and hot-reloads for development
+npm run serve
+
+# Compiles and minifies for production
+npm run build
+
+# Lints and fixes files
+npm run lint
+```

BIN
_screenshot/WX20200225-144536.png


+ 5 - 0
babel.config.js

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

+ 33 - 0
package.json

@@ -0,0 +1,33 @@
+{
+  "name": "base-in-front-end",
+  "version": "0.0.1",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  },
+  "dependencies": {
+    "axios": "^0.19.0",
+    "cli-engine": "^4.7.6",
+    "core-js": "^3.4.3",
+    "element-ui": "^2.13.0",
+    "js-cookie": "^2.2.1",
+    "lodash": "^4.17.15",
+    "nprogress": "^0.2.0",
+    "quill": "^1.3.7",
+    "vue": "^2.6.10",
+    "vue-quill-editor": "^3.0.6",
+    "vue-router": "^3.1.3"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "^4.1.0",
+    "@vue/cli-plugin-router": "^4.1.1",
+    "@vue/cli-service": "^4.1.0",
+    "prettier": "^1.19.1",
+    "vue-template-compiler": "^2.6.10"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ]
+}

BIN
public/favicon.ico


+ 112 - 0
public/index.html

@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>基础前端</title>
+
+    <style>
+      html, body, h1, h2, h3, h4, h5, h6, div, dl, dt, dd, ul, ol, li, p, blockquote, pre, hr, figure, table, caption, th, td, form, fieldset, legend, input, button, textarea, menu {
+        margin: 0;
+        padding: 0;
+      }
+
+      header, footer, section, article, aside, nav, hgroup, address, figure, figcaption, menu, details {
+        display: block;
+      }
+
+      table {
+        border-collapse: collapse;
+        border-spacing: 0;
+      }
+
+      caption, th {
+        text-align: left;
+        font-weight: normal;
+      }
+
+      html, body, fieldset, img, iframe, abbr {
+        border: 0;
+      }
+
+      i, cite, em, var, address, dfn {
+        font-style: normal;
+      }
+
+      [hidefocus], summary {
+        outline: 0;
+      }
+
+      li {
+        list-style: none;
+      }
+
+      h1, h2, h3, h4, h5, h6, small {
+        font-size: 100%;
+      }
+
+      sup, sub {
+        font-size: 83%;
+      }
+
+      pre, code, kbd, samp {
+        font-family: inherit;
+      }
+
+      q:before, q:after {
+        content: none;
+      }
+
+      textarea {
+        overflow: auto;
+        resize: none;
+      }
+
+      label, summary {
+        cursor: default;
+      }
+
+      a, button {
+        cursor: pointer;
+      }
+
+      h1, h2, h3, h4, h5, h6, em, strong, b {
+        font-weight: normal;
+      }
+
+      del, ins, u, s, a, a:hover {
+        text-decoration: none;
+      }
+
+      body, textarea, input, button, select, keygen, legend {
+        font: 14px/1em 'microsoft yahei', arial;
+        color: #333;
+        outline: 0;
+      }
+
+      body {
+        background: #fff;
+      }
+
+      a {
+        line-height: 24px;
+      }
+
+      a {
+        color: #333;
+      }
+
+      a:hover {
+      }
+    </style>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but hello 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>

+ 21 - 0
src/App.vue

@@ -0,0 +1,21 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<style>
+  #app {
+    font-family: 'Avenir', Helvetica, Arial, sans-serif;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    text-align: center;
+    color: #2c3e50;
+  }
+
+  .search-label {
+    color: #606266;
+    line-height: 28px;
+    padding-right: 10px;
+  }
+</style>

+ 53 - 0
src/api/dataDictionaryApi.js

@@ -0,0 +1,53 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询数据字典
+   * @param form
+   */
+  page (form) {
+    return HttpKit.post(`/common/data-dict/page`, {
+      data: form
+    }).then(
+      res => res.data
+    )
+  },
+  /**
+   * 功能描述:根据[字典标识]查询所以下级
+   * @param dataDictId
+   */
+  findDataDictById (dataDictId) {
+    return HttpKit.get(`/common/data-dict/${dataDictId}/children`).then(
+      res => res.data
+    )
+  },
+  /**
+   * 功能描述:根据[字典唯一标识]查询所以下级
+   * @param dictKey
+   */
+  findDataDictByDictKey (dictKey) {
+    return HttpKit.get(`/common/data-dict/dict-key/${dictKey}`).then(
+      res => res.data
+    )
+  },
+  /**
+   * 功能描述:新增/更新词典
+   * @param form
+   */
+  saveOrUpdate (form) {
+    return HttpKit.post(`/common/data-dict`, {
+      data: form
+    }).then(
+      res => res.data
+    )
+  },
+  /**
+   * 功能描述:根据Id删除词典
+   * @param dataDictId
+   */
+  deleteDataDictPoint (dataDictId) {
+    return HttpKit.delete(`/common/data-dict/${dataDictId}`).then(
+      res => res.body
+    )
+  }
+}

+ 21 - 0
src/api/guanLianApi.js

@@ -0,0 +1,21 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询关卡
+   * @param form
+   */
+  page (form) {
+    return HttpKit.post(`/tiku/wx/stem/{stemId}`,form).then(
+      res => res.data
+    )
+
+  },
+    link (id) {
+      return HttpKit.post(`/tiku/admin/levelTimu/{id}/link`).then(
+        res => res.data
+      )
+
+    }
+
+}

+ 14 - 0
src/api/levelApi.js

@@ -0,0 +1,14 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询关卡
+   * @param form
+   */
+  page (form) {
+    return HttpKit.post(`/admin/level/page`,form).then(
+      res => res.data
+    )
+
+  }
+}

+ 13 - 0
src/api/paperApi.js

@@ -0,0 +1,13 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询试卷
+   * @param form
+   */
+  page (form) {
+    return HttpKit.post(`/admin/paper/page`,form).then(
+      res => res.data
+    )
+  }
+}

+ 14 - 0
src/api/searchApi.js

@@ -0,0 +1,14 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询关卡
+   * @param form
+   */
+  page (form) {
+    return HttpKit.post(`/tiku/wx/stem/search`,form).then(
+      res => res.data
+    )
+
+  }
+}

BIN
src/assets/bg.jpg


BIN
src/assets/logo.png


BIN
src/assets/moon.png


+ 167 - 0
src/components/layout/aside-menu.vue

@@ -0,0 +1,167 @@
+<template>
+  <el-menu
+    class="el-menu-vertical"
+    :default-active="menuList[0].menuName"
+    :style="{ height: screenHeight - 60 + 'px' }"
+    :collapse="collapse"
+  >
+    <el-menu-item
+      v-for="menu in menuList"
+      :key="menu.menuName"
+      v-if="!menu.groupList"
+      :index="menu.menuName"
+    >
+      <i v-if="menu.icon" :class="menu.icon" />
+      <i v-if="menu.iconBase64">
+        <img :src="menu.iconBase64" />&nbsp;&nbsp;
+      </i>
+      <span slot="title" v-if="menu.routerName">
+        <router-link :to="{ name: menu.routerName }">
+          {{
+          menu.menuName
+          }}
+        </router-link>
+      </span>
+      <span v-if="menu.url" slot="title">{{ menu.menuName }}</span>
+    </el-menu-item>
+
+    <el-submenu v-else :index="menu.menuName">
+      <template slot="title">
+        <i v-if="menu.icon" :class="menu.icon" />
+        <i v-if="menu.iconBase64">
+          <img :src="menu.iconBase64" />&nbsp;&nbsp;
+        </i>
+
+        <span slot="title">{{ menu.menuName }}</span>
+      </template>
+      <el-menu-item-group>
+        <el-menu-item v-for="group in menu.groupList" :key="group.menuName" :index="group.menuName">
+          <i :class="group.icon" />
+          <i v-if="group.iconBase64">
+            <img :src="group.iconBase64" />&nbsp;&nbsp;
+          </i>
+
+          <span slot="title" v-if="group.routerName">
+            <router-link :to="{ name: group.routerName }">
+              {{
+              group.menuName
+              }}
+            </router-link>
+          </span>
+          <span v-if="group.url" slot="title">{{ group.menuName }}</span>
+        </el-menu-item>
+      </el-menu-item-group>
+    </el-submenu>
+  </el-menu>
+</template>
+
+<script>
+export default {
+  name: "LayoutAsideMenu",
+  components: {},
+  props: {
+    collapse: {
+      type: Boolean,
+      required: true,
+      default: false
+    }
+  },
+  data() {
+    return {
+      screenHeight: document.documentElement.clientHeight, // 获取可视屏幕高度
+
+      /**
+       * 左侧菜单对象
+       * routerName 与 url 不能并存
+       * routerName:内部链接,通过vue-router跳转
+       * url:外部链接,打开浏览器新的tab页
+       *
+       * icon 与 iconBase64 不能并存
+       * icon:element-ui自带的icon
+       * iconBase64:外部图片base64的编码(统一在[iconfont]查找icon, 颜色[#909399], 大小[16], 转换为base64编码)
+       *
+       * https://www.iconfont.cn
+       * http://imgbase64.duoshitong.com
+       * todo 后面增加属性支持页面嵌套打开
+       */
+      menuList: [
+        {
+          icon: "el-icon-user-solid",
+          menuName: "账号管理",
+          routerName: "AccountIndex"
+        },
+        {
+          icon: "el-icon-s-cooperation",
+          menuName: "系统管理",
+          groupList: [
+             {
+              icon: "el-icon-collection",
+              menuName: "关卡管理",
+              routerName: "level"
+             }
+          ]
+        }
+      ]
+    };
+  },
+  watch: {
+    screenHeight(val) {
+      // 为了避免频繁触发resize函数导致页面卡顿,使用定时器
+      if (!this.timer) {
+        // 一旦监听到的screenWidth值改变,就将其重新赋给data里的screenWidth
+        this.screenHeight = val;
+        this.timer = true;
+        const that = this;
+        setTimeout(function() {
+          that.timer = false;
+        }, 400);
+      }
+    }
+  },
+  mounted() {
+    const that = this;
+    window.onresize = () => {
+      return (() => {
+        window.screenHeight = document.documentElement.clientHeight;
+        that.screenHeight = window.screenHeight;
+      })();
+    };
+
+    // todo 在非[数据大盘]页面路由刷新后,默认选中的菜单没有调整
+  },
+  methods: {}
+};
+</script>
+
+<style>
+/* 左侧菜单[选中]高亮的颜色 */
+.el-menu-item.is-active {
+  color: #909399 !important;
+  background-color: #E6E6FA;
+
+}
+
+/* 左侧菜单[选中]高亮的颜色 */
+.el-menu-item.is-active span a {
+  color: #303133 !important;
+
+}
+
+.el-submenu__title {
+     font-size: 14px;
+     color: #303133;
+     padding: 0 20px;
+     cursor: pointer;
+     -webkit-transition: border-color .3s,background-color .3s,color .3s;
+     transition: border-color .3s,background-color .3s,color .3s;
+     -webkit-box-sizing: border-box;
+     box-sizing: border-box;
+     background-color: #E6E6FA;
+ }
+
+
+/* 左侧菜单[菜单组]背景色 */
+.el-menu-item-group {
+  background-color: #E6E6FA;
+}
+</style>

+ 10 - 0
src/components/layout/index.js

@@ -0,0 +1,10 @@
+import WhiteBoard from '@/components/layout/white-board'
+import SearchWhiteBoard from '@/components/layout/search-white-board'
+
+/* 全局安装通用组件 */
+export default {
+  install (Vue) {
+    Vue.component('white-board', WhiteBoard)
+    Vue.component('search-white-board', SearchWhiteBoard)
+  }
+}

+ 57 - 0
src/components/layout/search-white-board.vue

@@ -0,0 +1,57 @@
+<template>
+  <el-row style="background-color: #fff; padding: 10px 10px; margin: 0px 0px 10px 0px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);">
+    <el-col :span="20">
+      <el-row>
+        <slot name="base" />
+      </el-row>
+
+      <el-row>
+        <el-collapse-transition>
+          <div v-show="isCollapse">
+            <slot name="more" />
+          </div>
+        </el-collapse-transition>
+      </el-row>
+    </el-col>
+
+    <el-col v-if="showMore && isCollapse" :span="2" style="line-height: 40px; text-align: right;">
+      <el-button icon="el-icon-arrow-up" @click="changeCollapse">收起</el-button>
+    </el-col>
+    <el-col v-if="showMore && !isCollapse" :span="2" style="line-height: 40px; text-align: right;">
+      <el-button icon="el-icon-arrow-down" @click="changeCollapse">更多
+      </el-button>
+    </el-col>
+    <el-col v-if="!showMore" :span="2">&nbsp;</el-col>
+    <el-col :span="2" style="line-height: 40px; text-align: right;">
+      <el-button icon="el-icon-search" type="primary" plain @click="search">搜索</el-button>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+export default {
+  name: 'LayoutSearchWhiteBoard',
+  props: {
+    showMore: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    return {
+      isCollapse: false // 是否展开更多
+    }
+  },
+  methods: {
+    changeCollapse () {
+      this.isCollapse = !this.isCollapse
+    },
+    search () {
+      this.$emit('search')
+    }
+  }
+}
+</script>
+
+<style scoped>
+</style>

+ 16 - 0
src/components/layout/white-board.vue

@@ -0,0 +1,16 @@
+<template>
+  <el-row style="background-color: #fff; padding: 10px 10px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);">
+    <slot />
+  </el-row>
+</template>
+
+<script>
+export default {
+  name: 'LayoutWhiteBoard'
+}
+</script>
+
+<style scoped>
+
+</style>
+

+ 2 - 0
src/constants/constants.js

@@ -0,0 +1,2 @@
+export default {
+}

+ 27 - 0
src/filters/date-format.js

@@ -0,0 +1,27 @@
+import DateKit from '@/utils/date-kit'
+
+const dateFilter = {
+  /**
+   * 功能描述:格式化时间字段
+   * 使用方式:
+   * {{item.date | fmtDate}}
+   * {{row.createAt | fmtDate('yyyy-MM-dd')}}
+   * @param value
+   * @param format
+   * @returns {*}
+   */
+  formatDate (value, format = 'yyyy-MM-dd HH:mm:ss') {
+    if (!value) {
+      return ''
+    }
+
+    return DateKit.format(new Date(Date.parse(value.replace(/-/g, '/'))), format)
+  },
+
+  // 全局安装器
+  install (Vue) {
+    Vue.filter('fmtDate', this.formatDate)
+  }
+}
+
+export default dateFilter

+ 9 - 0
src/filters/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import DateFormat from './date-format'
+import MoneyFormat from './money-format'
+import StrFormat from './str-format'
+
+// 全局注册过滤器
+Vue.use(DateFormat)
+Vue.use(MoneyFormat)
+Vue.use(StrFormat)

+ 32 - 0
src/filters/money-format.js

@@ -0,0 +1,32 @@
+const MoneyFormat = {
+  /**
+   * 功能描述:格式化金额字段
+   * 使用方式
+   * {{item.bidAmount | moneyFormat}}
+   *
+   * @param number 金额
+   * @param places 小数点位数
+   * @param symbol 货币符号
+   * @param thousand 千分位符号
+   * @param decimal
+   * @returns {string}
+   */
+  moneyFormat (number, places, symbol, thousand, decimal) {
+    number = number || 0
+    places = !isNaN(places = Math.abs(places)) ? places : 2
+    symbol = symbol !== undefined ? symbol : '¥' // 货币单位
+    thousand = thousand || ',' // 千分位分割符
+    decimal = decimal || '.' // 小数位分割符
+    const negative = number < 0 ? '-' : ''
+    const i = parseInt(number = Math.abs(+number || 0).toFixed(places), 10) + ''
+    const j = i.length > 3 ? i.length % 3 : 0
+    return symbol + negative + (j ? i.substr(0, j) + thousand : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousand) + (places ? decimal + Math.abs(number - i).toFixed(places).slice(2) : '')
+  },
+
+  // 全局安装器
+  install (Vue) {
+    Vue.filter('moneyFormat', this.moneyFormat)
+  }
+}
+
+export default MoneyFormat

+ 71 - 0
src/filters/str-format.js

@@ -0,0 +1,71 @@
+import StrKit from '@/utils/str-kit'
+
+const strFilter = {
+  /**
+   * 功能描述:剪切字符串过滤器
+   * 使用方式:
+   * {{item.deviceName | cutStr(14)}}
+   * @param value
+   * @param len
+   * @param placeholder
+   * @returns {*}
+   */
+  cutStr (value, len, placeholder) {
+    if (!value) {
+      return ''
+    }
+    len = len || 18
+    placeholder = placeholder || '...'
+
+    let count = 0
+    const titleLenOld = StrKit.getByteLen(value)
+    let titleLenNew = 0 // 初始化重新构造的title的长度
+
+    for (var i = 0; i < value.length; i++) {
+      if (titleLenNew < len) {
+        ++count
+        if (value[i].match(/[^x00-xff]/ig) !== null) { // 全角
+          titleLenNew += 2
+        } else {
+          titleLenNew += 1
+        }
+      } else {
+        break
+      }
+    }
+    value = value.substring(0, count) + (titleLenOld <= len ? '' : placeholder)
+    return value
+  },
+
+  /**
+   * 功能描述:格式化手机号码
+   * 使用方式:
+   * {{item.deviceName | fmtPhoneNumber}}
+   * @param val
+   * @returns {string|string | *}
+   */
+  fmtPhoneNumber (val) {
+    if (!val) {
+      return ''
+    }
+
+    val = val.replace(/[^\d]/g, '').substr(0, 11)
+    if (val.length <= 3) {
+      return val
+    } else if (val.length <= 7) {
+      val = val.replace(/(\d{3})(\d{0,4})/, '$1-$2')
+    } else {
+      val = val.replace(/(\d{3})(\d{0,4})(\d{0,4})/, '$1-$2-$3')
+    }
+
+    return val
+  },
+
+  // 全局安装器
+  install (Vue) {
+    Vue.filter('cutStr', this.cutStr)
+    Vue.filter('fmtPhoneNumber', this.fmtPhoneNumber)
+  }
+}
+
+export default strFilter

+ 46 - 0
src/main.js

@@ -0,0 +1,46 @@
+import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+
+import jsCookie from 'js-cookie'
+
+/* 全局安装[通用组件] */
+import Layout from '@/components/layout'
+
+import Constants from '@/constants/constants'
+
+/* 全局安装[过滤器] */
+import './filters'
+
+import './plugins'
+
+
+import VueQuillEditor from 'vue-quill-editor'
+import 'quill/dist/quill.core.css'
+import 'quill/dist/quill.snow.css'
+import 'quill/dist/quill.bubble.css'
+
+
+/* 进度条(网络请求、页面路由切换) */
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+NProgress.configure({
+  easing: 'ease', // 动画方式
+  speed: 500, // 递增进度条的速度
+  showSpinner: false, // 是否显示加载ico
+  trickleSpeed: 200, // 自动递增间隔
+  minimum: 0.3 // 初始化时的最小百分比
+})
+
+Vue.config.productionTip = false
+Vue.prototype.constants = Constants
+Vue.prototype.$cookie = jsCookie
+
+Vue.use(VueQuillEditor)
+
+Vue.use(Layout)
+
+new Vue({
+  router,
+  render: h => h(App)
+}).$mount('#app')

+ 5 - 0
src/plugins/element-ui.js

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

+ 1 - 0
src/plugins/index.js

@@ -0,0 +1 @@
+import './element-ui'

+ 88 - 0
src/router/index.js

@@ -0,0 +1,88 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+import NProgress from 'nprogress'
+import Cookies from 'js-cookie'
+
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  routes: [{
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/login'),
+    meta: {title: '登录'}
+  }, {
+    path: '/logout',
+    name: 'Logout',
+    component: () => import('@/views/logout'),
+    meta: {title: '退出登录'}
+  }, {
+    path: '/',
+    redirect: '/data-market/main'
+  }, {
+    path: '/data-market/main',
+    component: () => import('@/views/main'),
+    children: [{
+      path: '/data-market/main',
+      name: 'DataMarketIndex',
+      meta: {title: '数据大盘'}
+    }]
+  }, {
+    path: '/system',
+    component: () => import('@/views/main'),
+    children: [{
+      path: '/system/data-dict',
+      name: 'dataDict',
+      component: () => import('@/views/system/dataDict/list'),
+      meta: {title: '系统管理 - 数据字典'}
+    },
+    {
+            path: '/system/paper',
+            name: 'paper',
+            component: () => import('@/views/system/paper/list'),
+            meta: {title: '系统管理 - 试卷管理'}
+    },
+    {
+             path: '/system/level',
+             name: 'level',
+             component: () => import('@/views/system/level/list'),
+             meta: {title: '系统管理 - 关卡管理'}
+     },
+      {
+                  path: '/system/level/detail',
+                  name: 'levelDetail',
+                  component: () => import('@/views/system/level/detail'),
+                  meta: {title: '我就瞎打'}
+          }]
+  }]
+})
+
+let URL_WHITE_TOKEN_LIST = [
+  '/login'
+]
+
+router.beforeEach((to, from, next) => {
+  NProgress.start() // 每次切换页面时,调用进度条
+
+  next()
+  // let token = Cookies.get('token')
+  // if (token) {
+  //   if (to.matched.length === 0) { // 匹配前往的路由不存在
+  //     from.name ? next({name: from.name}) : next('/error') // 判断此跳转路由的来源路由是否存在,存在的情况跳转到来源路由,否则跳转到404页面
+  //   } else {
+  //     next()
+  //   }
+  // } else {
+  //   if (URL_WHITE_TOKEN_LIST.indexOf(to.path) !== -1) {
+  //     next()
+  //   } else {
+  //     next(`/login?redirect=${to.path}`)
+  //   }
+  // }
+})
+
+router.afterEach(() => {
+  NProgress.done() // 在即将进入新的页面组件前,关闭掉进度条
+})
+
+export default router

+ 319 - 0
src/utils/date-kit.js

@@ -0,0 +1,319 @@
+/*eslint-disable*/
+// 把 YYYY-MM-DD 改成了 yyyy-MM-dd
+(function (main) {
+    'use strict';
+
+    /**
+     * Parse or format dates
+     * @class fecha
+     */
+    var fecha = {};
+    var token = /d{1,4}|M{1,4}|yy(?:yy)?|S{1,3}|Do|ZZ|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g;
+    var twoDigits = /\d\d?/;
+    var threeDigits = /\d{3}/;
+    var fourDigits = /\d{4}/;
+    var word = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
+    var noop = function () {
+    };
+
+    function shorten(arr, sLen) {
+        var newArr = [];
+        for (var i = 0, len = arr.length; i < len; i++) {
+            newArr.push(arr[i].substr(0, sLen));
+        }
+        return newArr;
+    }
+
+    function monthUpdate(arrName) {
+        return function (d, v, i18n) {
+            var index = i18n[arrName].indexOf(v.charAt(0).toUpperCase() + v.substr(1).toLowerCase());
+            if (~index) {
+                d.month = index;
+            }
+        };
+    }
+
+    function pad(val, len) {
+        val = String(val);
+        len = len || 2;
+        while (val.length < len) {
+            val = '0' + val;
+        }
+        return val;
+    }
+
+    var dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+    var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+    var monthNamesShort = shorten(monthNames, 3);
+    var dayNamesShort = shorten(dayNames, 3);
+    fecha.i18n = {
+        dayNamesShort: dayNamesShort,
+        dayNames: dayNames,
+        monthNamesShort: monthNamesShort,
+        monthNames: monthNames,
+        amPm: ['am', 'pm'],
+        DoFn: function DoFn(D) {
+            return D + ['th', 'st', 'nd', 'rd'][D % 10 > 3 ? 0 : (D - D % 10 !== 10) * D % 10];
+        }
+    };
+
+    var formatFlags = {
+        D: function (dateObj) {
+            return dateObj.getDay();
+        },
+        DD: function (dateObj) {
+            return pad(dateObj.getDay());
+        },
+        Do: function (dateObj, i18n) {
+            return i18n.DoFn(dateObj.getDate());
+        },
+        d: function (dateObj) {
+            return dateObj.getDate();
+        },
+        dd: function (dateObj) {
+            return pad(dateObj.getDate());
+        },
+        ddd: function (dateObj, i18n) {
+            return i18n.dayNamesShort[dateObj.getDay()];
+        },
+        dddd: function (dateObj, i18n) {
+            return i18n.dayNames[dateObj.getDay()];
+        },
+        M: function (dateObj) {
+            return dateObj.getMonth() + 1;
+        },
+        MM: function (dateObj) {
+            return pad(dateObj.getMonth() + 1);
+        },
+        MMM: function (dateObj, i18n) {
+            return i18n.monthNamesShort[dateObj.getMonth()];
+        },
+        MMMM: function (dateObj, i18n) {
+            return i18n.monthNames[dateObj.getMonth()];
+        },
+        yy: function (dateObj) {
+            return String(dateObj.getFullYear()).substr(2);
+        },
+        yyyy: function (dateObj) {
+            return dateObj.getFullYear();
+        },
+        h: function (dateObj) {
+            return dateObj.getHours() % 12 || 12;
+        },
+        hh: function (dateObj) {
+            return pad(dateObj.getHours() % 12 || 12);
+        },
+        H: function (dateObj) {
+            return dateObj.getHours();
+        },
+        HH: function (dateObj) {
+            return pad(dateObj.getHours());
+        },
+        m: function (dateObj) {
+            return dateObj.getMinutes();
+        },
+        mm: function (dateObj) {
+            return pad(dateObj.getMinutes());
+        },
+        s: function (dateObj) {
+            return dateObj.getSeconds();
+        },
+        ss: function (dateObj) {
+            return pad(dateObj.getSeconds());
+        },
+        S: function (dateObj) {
+            return Math.round(dateObj.getMilliseconds() / 100);
+        },
+        SS: function (dateObj) {
+            return pad(Math.round(dateObj.getMilliseconds() / 10), 2);
+        },
+        SSS: function (dateObj) {
+            return pad(dateObj.getMilliseconds(), 3);
+        },
+        a: function (dateObj, i18n) {
+            return dateObj.getHours() < 12 ? i18n.amPm[0] : i18n.amPm[1];
+        },
+        A: function (dateObj, i18n) {
+            return dateObj.getHours() < 12 ? i18n.amPm[0].toUpperCase() : i18n.amPm[1].toUpperCase();
+        },
+        ZZ: function (dateObj) {
+            var o = dateObj.getTimezoneOffset();
+            return (o > 0 ? '-' : '+') + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4);
+        }
+    };
+
+    var parseFlags = {
+        d: [twoDigits, function (d, v) {
+            d.day = v;
+        }],
+        M: [twoDigits, function (d, v) {
+            d.month = v - 1;
+        }],
+        yy: [twoDigits, function (d, v) {
+            var da = new Date(), cent = +('' + da.getFullYear()).substr(0, 2);
+            d.year = '' + (v > 68 ? cent - 1 : cent) + v;
+        }],
+        h: [twoDigits, function (d, v) {
+            d.hour = v;
+        }],
+        m: [twoDigits, function (d, v) {
+            d.minute = v;
+        }],
+        s: [twoDigits, function (d, v) {
+            d.second = v;
+        }],
+        yyyy: [fourDigits, function (d, v) {
+            d.year = v;
+        }],
+        S: [/\d/, function (d, v) {
+            d.millisecond = v * 100;
+        }],
+        SS: [/\d{2}/, function (d, v) {
+            d.millisecond = v * 10;
+        }],
+        SSS: [threeDigits, function (d, v) {
+            d.millisecond = v;
+        }],
+        D: [twoDigits, noop],
+        ddd: [word, noop],
+        MMM: [word, monthUpdate('monthNamesShort')],
+        MMMM: [word, monthUpdate('monthNames')],
+        a: [word, function (d, v, i18n) {
+            var val = v.toLowerCase();
+            if (val === i18n.amPm[0]) {
+                d.isPm = false;
+            } else if (val === i18n.amPm[1]) {
+                d.isPm = true;
+            }
+        }],
+        ZZ: [/[\+\-]\d\d:?\d\d/, function (d, v) {
+            var parts = (v + '').match(/([\+\-]|\d\d)/gi), minutes;
+
+            if (parts) {
+                minutes = +(parts[1] * 60) + parseInt(parts[2], 10);
+                d.timezoneOffset = parts[0] === '+' ? minutes : -minutes;
+            }
+        }]
+    };
+    parseFlags.DD = parseFlags.DD;
+    parseFlags.dddd = parseFlags.ddd;
+    parseFlags.Do = parseFlags.dd = parseFlags.d;
+    parseFlags.mm = parseFlags.m;
+    parseFlags.hh = parseFlags.H = parseFlags.HH = parseFlags.h;
+    parseFlags.MM = parseFlags.M;
+    parseFlags.ss = parseFlags.s;
+    parseFlags.A = parseFlags.a;
+
+
+    // Some common format strings
+    fecha.masks = {
+        'default': 'ddd MMM dd yyyy HH:mm:ss',
+        shortDate: 'M/D/yy',
+        mediumDate: 'MMM d, yyyy',
+        longDate: 'MMMM d, yyyy',
+        fullDate: 'dddd, MMMM d, yyyy',
+        shortTime: 'HH:mm',
+        mediumTime: 'HH:mm:ss',
+        longTime: 'HH:mm:ss.SSS'
+    };
+
+    /***
+     * Format a date
+     * @method format
+     * @param {Date|number} dateObj
+     * @param {string} mask Format of the date, i.e. 'mm-dd-yy' or 'shortDate'
+     */
+    fecha.format = function (dateObj, mask, i18nSettings) {
+        var i18n = i18nSettings || fecha.i18n;
+
+        if (typeof dateObj === 'number') {
+            dateObj = new Date(dateObj);
+        }
+
+        if (Object.prototype.toString.call(dateObj) !== '[object Date]' || isNaN(dateObj.getTime())) {
+            throw new Error('Invalid Date in fecha.format');
+        }
+
+        mask = fecha.masks[mask] || mask || fecha.masks['default'];
+
+        return mask.replace(token, function ($0) {
+            return $0 in formatFlags ? formatFlags[$0](dateObj, i18n) : $0.slice(1, $0.length - 1);
+        });
+    };
+
+    /**
+     * Parse a date string into an object, changes - into /
+     * @method parse
+     * @param {string} dateStr Date string
+     * @param {string} format Date parse format
+     * @returns {Date|boolean}
+     */
+    fecha.parse = function (dateStr, format, i18nSettings) {
+        var i18n = i18nSettings || fecha.i18n;
+
+        if (typeof format !== 'string') {
+            throw new Error('Invalid format in fecha.parse');
+        }
+
+        format = fecha.masks[format] || format;
+
+        // Avoid regular expression denial of service, fail early for really long strings
+        // https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS
+        if (dateStr.length > 1000) {
+            return false;
+        }
+
+        var isValid = true;
+        var dateInfo = {};
+        format.replace(token, function ($0) {
+            if (parseFlags[$0]) {
+                var info = parseFlags[$0];
+                var index = dateStr.search(info[0]);
+                if (!~index) {
+                    isValid = false;
+                } else {
+                    dateStr.replace(info[0], function (result) {
+                        info[1](dateInfo, result, i18n);
+                        dateStr = dateStr.substr(index + result.length);
+                        return result;
+                    });
+                }
+            }
+
+            return parseFlags[$0] ? '' : $0.slice(1, $0.length - 1);
+        });
+
+        if (!isValid) {
+            return false;
+        }
+
+        var today = new Date();
+        if (dateInfo.isPm === true && dateInfo.hour != null && +dateInfo.hour !== 12) {
+            dateInfo.hour = +dateInfo.hour + 12;
+        } else if (dateInfo.isPm === false && +dateInfo.hour === 12) {
+            dateInfo.hour = 0;
+        }
+
+        var date;
+        if (dateInfo.timezoneOffset != null) {
+            dateInfo.minute = +(dateInfo.minute || 0) - +dateInfo.timezoneOffset;
+            date = new Date(Date.UTC(dateInfo.year || today.getFullYear(), dateInfo.month || 0, dateInfo.day || 1,
+                dateInfo.hour || 0, dateInfo.minute || 0, dateInfo.second || 0, dateInfo.millisecond || 0));
+        } else {
+            date = new Date(dateInfo.year || today.getFullYear(), dateInfo.month || 0, dateInfo.day || 1,
+                dateInfo.hour || 0, dateInfo.minute || 0, dateInfo.second || 0, dateInfo.millisecond || 0);
+        }
+        return date;
+    };
+
+    /* istanbul ignore next */
+    if (typeof module !== 'undefined' && module.exports) {
+        module.exports = fecha;
+    } else if (typeof define === 'function' && define.amd) {
+        define(function () {
+            return fecha;
+        });
+    } else {
+        main.fecha = fecha;
+    }
+})(this);

+ 136 - 0
src/utils/http-kit.js

@@ -0,0 +1,136 @@
+import {Message} from 'element-ui'
+import NProgress from 'nprogress'
+import axios from 'axios'
+// import Cookies from 'js-cookie'
+
+axios.defaults.baseURL = process.env.VUE_APP_HOME_API_URL
+axios.defaults.timeout = 30 * 1000 // 设置接口响应时间
+
+// let token = Cookies.get('token')
+let token = 'swagger-ui'
+
+/* URL地址token白名单 */
+const URL_WHITE_TOKEN_LIST = [
+  '/admin/account/login'
+]
+
+/**
+ * 功能描述:Http Request 拦截器
+ */
+axios.interceptors.request.use((request) => {
+  NProgress.start() // 展示进度条
+  /* 当前请求地址不是白名单,则增加请求头token */
+  if(request.url.startsWith('/tiku')){
+    request.url = request.url.replace('/tiku','')
+    request.baseURL = process.env.VUE_APP_QUESTION_BANK_API_URL
+  }
+  if (URL_WHITE_TOKEN_LIST.indexOf(request.url) === -1) {
+    request.headers.common['Authorization'] = `Bearer ${token}`
+  }
+  request.headers.common['Content-Type'] = 'application/json;charset=UTF-8'
+
+  return request
+}, (error) => { // 请求错误时做些事(接口错误、超时等)
+  NProgress.done() // 隐藏进度条
+  Message.error(`请求参数错误`, 10)
+
+  /* 1.判断请求超时 */
+  if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
+    // return service.request(originalRequest);//例如再重复请求一次
+  }
+
+  /* 2.需要重定向到错误页面 */
+  if (error.response) {
+    // error =errorInfo.data//页面那边catch的时候就能拿到详细的错误信息,看最下边的Promise.reject
+    // router.push({
+    //   path: `/error/${errorInfo.status}`// 404 403 500 ... 等
+    // })
+  }
+
+  return Promise.reject(error) // 在调用的那边可以拿到(catch)你想返回的错误信息
+})
+
+/**
+ * 功能描述:Http Response 拦截器
+ */
+axios.interceptors.response.use((res) => {
+  NProgress.done() // 隐藏进度条
+
+  /* 判断当前请求响应是Excel导出的,直接返回 */
+  if (res.config.url.indexOf('/export-excel') >= 0) {
+    return Promise.resolve(res)
+  }
+
+  let isTips = res.config.showMessage === undefined || res.config.showMessage === true
+  let data = res.data
+
+  /* 根据返回的code值来做不同的处理(和后端约定) */
+  switch (data.code) {
+    case 200:
+      return Promise.resolve(data)
+    default:
+      isTips && Message.error(`服务器返回异常:${data.message}`, 10)
+      return Promise.reject(res)
+  }
+}, (error) => {
+  Message.error(`服务器返回异常:${error}`, 10)
+
+  if (error && error.response) {
+    switch (error.response.status) {
+      case 400:
+        break
+      default:
+        break
+    }
+  }
+
+  return Promise.reject(error)
+})
+
+export default {
+  get (url, data = {}, config = {}) {
+    return new Promise((resolve, reject) => {
+      axios.get(url, data, config).then(response => {
+        resolve(response)
+      }).catch(err => {
+        reject(err)
+      })
+    })
+  },
+  delete (url, data = {}, config = {}) {
+    return new Promise((resolve, reject) => {
+      axios.delete(url, data, config).then(response => {
+        resolve(response)
+      }).catch(err => {
+        reject(err)
+      })
+    })
+  },
+  post (url, data = {}, config = {}) {
+    return new Promise((resolve, reject) => {
+      axios.post(url, data, config).then(response => {
+        resolve(response)
+      }, err => {
+        reject(err)
+      })
+    })
+  },
+  put (url, data = {}, config = {}) {
+    return new Promise((resolve, reject) => {
+      axios.put(url, data, config).then(response => {
+        resolve(response)
+      }, err => {
+        reject(err)
+      })
+    })
+  },
+  patch (url, data = {}, config = {}) {
+    return new Promise((resolve, reject) => {
+      axios.patch(url, data, config).then(response => {
+        resolve(response)
+      }, err => {
+        reject(err)
+      })
+    })
+  }
+}

+ 23 - 0
src/utils/str-kit.js

@@ -0,0 +1,23 @@
+export default {
+  /**
+   * 功能描述:计算文本的长度
+   * @param strKit
+   * @returns {number}
+   */
+  getByteLen (strKit) {
+    let len = 0
+    if (typeof strKit === 'string' && strKit.constructor === String) {
+      for (let i = 0; i < strKit.length; i++) {
+        if (strKit[i].match(/[^x00-xff]/ig)) { // 全角
+          len += 2
+        } else {
+          len += 1
+        }
+      }
+    }
+    return len
+  },
+  cutoutChar (str, length = 2) {
+    return str.match(/[\u4e00-\u9fa5]/g).splice(0, length).join('')
+  }
+}

+ 94 - 0
src/views/login.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="login-container">
+    <el-form ref="form" :model="form" :rules="ruleForm" class="login-page">
+      <h3 style="line-height: 75px;font-size: 26px;font-weight: 800;">系统登录</h3>
+      <el-form-item prop="username">
+        <el-input v-model="form.username" type="text" placeholder="用户名">
+          <template slot="prepend">
+            <i class="el-icon-user-solid" />
+          </template>
+        </el-input>
+      </el-form-item>
+      <el-form-item prop="password">
+        <el-input v-model="form.password" type="password" placeholder="密码" @keyup.enter.native="handleSubmit">
+          <template slot="prepend">
+            <i class="el-icon-s-goods" />
+          </template>
+        </el-input>
+      </el-form-item>
+      <el-form-item style="width:100%;">
+        <el-button type="primary" style="width:100%;" :loading="logining" @click="handleSubmit">登录</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+// import accountApi from '@/api/accountApi'
+
+export default {
+  name: 'Login',
+  data () {
+    return {
+      logining: false,
+      form: {
+        username: '',
+        password: ''
+      },
+      ruleForm: {
+        username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
+        password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
+      }
+    }
+  },
+  mounted () {
+    /* 访问该页面重置token值 */
+    this.$cookie.remove('token')
+  },
+  methods: {
+    handleSubmit () {
+      this.$refs.form.validate((valid) => {
+        if (!valid) {
+          return false
+        }
+
+        this.logining = true
+        // accountApi.login(this.form).then(data => {
+        //   this.logining = false
+        //
+        //   if (data) {
+        //     this.$cookie.set('token', data, {expires: 7, path: '/'})
+        //
+        //     if (this.$route.query.redirect) {
+        //       this.$router.push({path: `${this.$route.query.redirect}`})
+        //       return
+        //     }
+        //
+        //     this.$router.push({path: '/'})
+        //   }
+        // }).catch(() => {
+        //   this.logining = false
+        // })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .login-container {
+    width: 100%;
+    height: 100%;
+  }
+
+  .login-page {
+    -webkit-border-radius: 5px;
+    border-radius: 5px;
+    margin: 125px auto;
+    width: 350px;
+    padding: 15px 25px;
+    background: #fff;
+    border: 1px solid #eaeaea;
+    box-shadow: 0 0 25px #cac6c6;
+  }
+</style>

+ 21 - 0
src/views/logout.vue

@@ -0,0 +1,21 @@
+<template>
+  <div />
+</template>
+
+<script>
+export default {
+  name: 'Logout',
+  components: {},
+  data () {
+    return {}
+  },
+  mounted () {
+    this.$router.push({ path: '/login' })
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 120 - 0
src/views/main.vue

@@ -0,0 +1,120 @@
+<template>
+  <el-container>
+    <el-aside width="200px">
+      <AsideMenu :collapse="isCollapseMenu" />
+    </el-aside>
+
+    <el-container>
+      <el-header>
+        <el-row>
+          <el-col :span="4" style="text-align: left;padding-left: 10px;">
+             <i class="el-icon-star-on"></i>
+            &nbsp;
+            <label style="font-size: 16px;">真不错</label>
+          </el-col>
+          <el-col :span="2" :offset="18">
+            <el-dropdown style="cursor: pointer;">
+              <el-avatar icon="el-icon-user-solid" style="box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);"/>
+              <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item>
+                  <router-link :to="{name: 'Logout'}">退出登录</router-link>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </el-col>
+        </el-row>
+      </el-header>
+      <el-main :style="{height: (screenHeight - 60) + 'px', padding: 0 + 'px ' + 10 + 'px', paddingTop: 10 + 'px'}">
+        <transition name="el-fade-in-linear">
+          <router-view />
+        </transition>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script>
+import AsideMenu from '@/components/layout/aside-menu'
+
+export default {
+  name: 'Main',
+  components: {
+    AsideMenu
+  },
+  data () {
+    return {
+      screenHeight: document.documentElement.clientHeight, // 获取可视屏幕高度
+      isCollapseMenu: false
+    }
+  },
+  watch: {
+    screenHeight (val) {
+      // 为了避免频繁触发resize函数导致页面卡顿,使用定时器
+      if (!this.timer) {
+        // 一旦监听到的screenWidth值改变,就将其重新赋给data里的screenWidth
+        this.screenHeight = val
+        this.timer = true
+        const that = this
+        setTimeout(function () {
+          that.timer = false
+        }, 400)
+      }
+    }
+  },
+  mounted () {
+    const that = this
+    window.onresize = () => {
+      return (() => {
+        window.screenHeight = document.documentElement.clientHeight
+        that.screenHeight = window.screenHeight
+      })()
+    }
+  },
+  methods: {
+    collapseMenu () {
+      /* el-aside有一个默认值为300,暂时没有很好的方式来处理伸缩菜单栏来处理样式问题... */
+      this.isCollapseMenu = !!this.isCollapseMenu
+    }
+  }
+}
+</script>
+
+<style>
+  .el-header, .el-footer {
+    background-color: #fff;
+    color: #333;
+    text-align: center;
+    line-height: 60px;
+    padding-left: 0px;
+    padding-right: 0px;
+  }
+
+  .el-footer {
+    height: 40px !important;
+    line-height: 40px !important;
+  }
+
+  .el-main {
+    background-color: #eee;
+    color: #333;
+    text-align: center;
+    padding: 0px;
+  }
+
+  body > .el-container {
+    margin-bottom: 40px;
+  }
+
+  .el-container:nth-child(5) .el-aside,
+  .el-container:nth-child(6) .el-aside {
+    line-height: 260px;
+  }
+
+  .el-container:nth-child(7) .el-aside {
+    line-height: 320px;
+  }
+  .el-icon-star-on{
+  color:yellow;
+  font-size:50px;
+  }
+</style>

+ 266 - 0
src/views/system/dataDict/list.vue

@@ -0,0 +1,266 @@
+<template>
+  <el-row>
+    <el-row>
+      <white-board>
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+          <el-breadcrumb-item>系统管理</el-breadcrumb-item>
+          <el-breadcrumb-item>数据字典</el-breadcrumb-item>
+        </el-breadcrumb>
+      </white-board>
+    </el-row>
+
+    <el-row>
+      <el-form ref="searchForm" :model="searchForm">
+        <search-white-board :show-more="true" @search="list">
+          <div slot="base">
+            <el-row style="line-height: 40px; text-align: right;">
+              <el-col :span="3"><label class="search-label">字典唯一标识</label></el-col>
+              <el-col :span="5">
+                <el-input v-model.trim="searchForm.dictKey" placeholder="请输入字典唯一标识" clearable></el-input>
+              </el-col>
+              <el-col :span="2" :offset="1"><label class="search-label">字典编码</label></el-col>
+              <el-col :span="5">
+                <el-input v-model.trim="searchForm.code" placeholder="请输入字典编码" clearable></el-input>
+              </el-col>
+            </el-row>
+          </div>
+        </search-white-board>
+      </el-form>
+    </el-row>
+
+    <el-row>
+      <white-board>
+        <el-row style="line-height: 40px; text-align: right;">
+          <el-col :span="6" :offset="18" style="text-align: right;">
+            <el-button icon="el-icon-plus" type="success" plain @click="handleAdd">新增</el-button>
+          </el-col>
+        </el-row>
+
+        <el-row style="padding: 10px 0px;">
+          <el-table :data="tableData" row-key="id" lazy :load="load" ref="ttable" @row-click="rowClick" style="width: 100%;" highlight-current-row stripe>
+            <el-table-column prop="id" label="标识" width="180"></el-table-column>
+            <el-table-column prop="parentId" label="父级标识" width="150"></el-table-column>
+            <el-table-column prop="dictKey" label="字典唯一标识" width="200"></el-table-column>
+            <el-table-column prop="code" label="字典编码"  width="100"></el-table-column>
+            <el-table-column prop="value" label="字典值"></el-table-column>
+            <el-table-column prop="attr" label="拓展字段"></el-table-column>
+            <el-table-column prop="remarks" label="备注"></el-table-column>
+            <el-table-column label="创建" width="135">
+              <template slot-scope="scope">
+                <div>{{scope.row.createdBy}}</div>
+                <div>{{scope.row.createDate}}</div>
+              </template>
+            </el-table-column>
+            <el-table-column label="更新" width="135">
+              <template slot-scope="scope">
+                <div>{{scope.row.lastModifiedBy}}</div>
+                <div>{{scope.row.lastModifiedDate}}</div>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="150">
+              <template slot-scope="scope">
+                <el-button type="primary" plain @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
+                <el-button type="danger" plain @click="handleDel(scope.$index, scope.row)">删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-row>
+      </white-board>
+    </el-row>
+
+    <el-dialog :visible.sync="dialogVisible" ref="form">
+      <el-divider></el-divider>
+      <label slot="title" style="float: left;">{{dialogTitle}}</label>
+
+      <el-row>
+        <el-col :span="18" :offset="3">
+          <el-form ref="form" :model="form" :rules="formRules" label-width="80px">
+            <el-form-item label="标识" prop="id" hidden="hidden">
+              <el-input v-model.trim="form.id" v-bind:disabled="form.id !== ''" hidden="hidden" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="父级标识" prop="parentId" hidden="hidden">
+              <el-input v-model.trim="form.parentId" placeholder="请输入父级标识" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="唯一标识" prop="dictKey">
+              <el-input v-model.trim="form.dictKey" placeholder="字典唯一标识" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="字典编码" prop="code">
+              <el-input v-model.trim="form.code" placeholder="字典编码" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="字典值" prop="value">
+              <el-input v-model.trim="form.value" placeholder="字典值" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="拓展字段">
+              <el-input v-model.trim="form.attr" placeholder="拓展字段" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="备注" prop="remarks">
+              <el-input v-model.trim="form.remarks" placeholder="备注" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="排序" prop="sort" >
+              <el-input v-model.trim="form.sort" placeholder="排序" type="number" clearable></el-input>
+            </el-form-item>
+          </el-form>
+
+        </el-col>
+      </el-row>
+
+      <div slot="footer" class="dialog-footer">
+        <el-divider></el-divider>
+        <el-button @click="dialogVisible = false" size="mini">取 消</el-button>
+        <el-button type="primary" @click="handleSave" size="mini">确 定</el-button>
+      </div>
+    </el-dialog>
+  </el-row>
+</template>
+
+<script>
+  import dataDictionaryApi from "@/api/dataDictionaryApi"
+
+  export default {
+    name: 'dataDict',
+    components: {
+    },
+    data () {
+      return {
+        defaultProps: {
+          label: 'name',
+          children: 'child',
+        },
+        searchForm: {
+          dictKey: '', // 字典唯一标识
+          code: '', // 字典编码
+          start: 1,
+          limit: 10
+        },
+        tableData: [],
+        uploadData: {
+          tree: null,
+          treeNode: null,
+          resolve: null
+        },
+
+        dialogTitle: '',
+        dialogVisible: false,
+        dialogdelVisible: false,
+        currentSelectRow: {}, // 当前选中行
+        form: {
+          id: '',
+          parentId: '',
+          dictKey: '',
+          code: '',
+          value: '',
+          attr: '',
+          remarks: '',
+          sort: ''
+        },
+        formRules: {
+          dictKey: [{required: true, message: '请输入字典唯一标识', trigger: 'blur'}],
+          code: [{required: true, message: '请输入字典编码', trigger: 'blur'}],
+          value: [{required: true, message: '请输入字典值', trigger: 'blur'}],
+        }
+      }
+    },
+    mounted () {
+      this.list()
+    },
+    methods: {
+      filterNode (value, data) {
+        if (!value) {
+          return true
+        }
+
+        return data.name.indexOf(value) !== -1
+      },
+      list () {
+        this.currentSelectRow = {}
+        this.tableData = []
+        dataDictionaryApi.page(this.searchForm).then(data => {
+          this.total = data.total
+          data.data.forEach(item => {
+            item.hasChildren = []
+          })
+          this.tableData = data.data
+        })
+      },
+      rowClick (row, event, column) {
+        this.currentSelectRow = row
+      },
+      handleAdd () {
+        /* 重置表单 */
+        this.form['id'] = ''
+        this.form['parentId'] = this.currentSelectRow['id']
+        this.form['dictKey'] = ''
+        this.form['code'] = ''
+        this.form['value'] = ''
+        this.form['attr'] = ''
+        this.form['remarks'] = ''
+        this.form['sort'] = ''
+        this.dialogTitle = '新增'
+        this.dialogVisible = true
+      },
+      handleEdit (index,row) {
+        this.form['id'] = row.id
+        this.form['parentId'] = row.parentId
+        this.form['dictKey'] = row.dictKey
+        this.form['code'] = row.code
+        this.form['value'] = row.value
+        this.form['attr'] = row.attr
+        this.form['remarks'] =row.remark
+        this.form['sort'] = row.sort
+        this.dialogTitle = '编辑'
+        this.dialogVisible = true
+      },
+      handleDel (index,row) {
+        this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          dataDictionaryApi.deleteDataDictPoint(row.id).then(data => {
+            this.$message({
+              type: 'success',
+              message: '删除成功'
+            })
+
+            this.list()
+            this.load(this.uploadData.tree, this.uploadData.treeNode, this.uploadData.resolve)
+          })
+        })
+      },
+      load(tree, treeNode, resolve) {
+        this.uploadData.tree = tree
+        this.uploadData.treeNode = treeNode
+        this.uploadData.resolve = resolve
+        let value = tree.id
+
+        dataDictionaryApi.findDataDictById(value).then(data => {
+          data.forEach(item => {
+            //判断是否有子节点
+            if (item.size > 0) {
+              item.hasChildren = []
+            }
+          })
+
+          resolve(data)
+        })
+      },
+      handleSave () {
+        this.$refs['form'].validate((valid) => {
+          if (!valid) {
+            return
+          }
+
+          dataDictionaryApi.saveOrUpdate(this.form).then(data => {
+            this.dialogVisible = false
+            this.$refs['form'].resetFields()
+            this.list()
+          })
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+</style>

+ 120 - 0
src/views/system/level/detail.vue

@@ -0,0 +1,120 @@
+<template>
+  <el-row>
+    <el-row>
+      <white-board>
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+          <el-breadcrumb-item>系统管理</el-breadcrumb-item>
+          <el-breadcrumb-item>关卡管理</el-breadcrumb-item>
+          <el-breadcrumb-item>关卡详情管理</el-breadcrumb-item>
+        </el-breadcrumb>
+      </white-board>
+    </el-row>
+-{{levelId}}-
+    <el-row>
+      <el-form ref="searchForm" :model="searchForm">
+        <search-white-board :show-more="true" @search="list">
+          <div slot="base">
+            <el-row style="line-height: 40px; text-align: right;">
+              <el-col :span="3"><label class="search-label">题干内容</label></el-col>
+              <el-col :span="5">
+                <el-input v-model.trim="searchForm.name" placeholder="输入题干内容" clearable></el-input>
+              </el-col>
+            </el-row>
+          </div>
+        </search-white-board>
+      </el-form>
+    </el-row>
+
+    <el-row>
+      <white-board>
+        <el-row style="padding: 10px 0px;">
+          <el-table :data="tableData" row-key="id" lazy ref="ttable" style="width: 100%;" highlight-current-row stripe>
+            <el-table-column prop="id" label="标识" width="180"></el-table-column>
+            <el-table-column prop="name" label="题干内容" width="150"></el-table-column>
+             <el-table-column prop="optionList" label="答案" width="150">
+             <template slot-scope="scope">
+             <div v-for="item of scope.row.optionList">
+                 <span v-if="item.isRight === true">{{item.content}}</span>
+             </div>
+             </template>
+             </el-table-column>
+             <el-table-column prop="analysis" label="答案解析" width="150"></el-table-column>
+            <el-table-column label="操作" width="250">
+              <template slot-scope="scope">
+                <el-button v-if="isLink(scope.row.id)" type="danger" plain>取消关联</el-button>
+                <el-button v-if="!isLink(scope.row.id)" @click="guanqia(scope.row.id)" type="primary" plain>关联</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-row>
+      </white-board>
+    </el-row>
+  </el-row>
+</template>
+
+<script>
+  import levelApi from "@/api/levelApi"
+  import searchApi from "@/api/searchApi"
+
+  export default {
+    name: 'level',
+    components: {
+    },
+    data () {
+      return {
+        levelId: '',
+        searchForm: {
+         name: '',
+         page: 0,
+         size: 30,
+        },
+        total:0,
+        tableData: [],
+        linkIdList: [753214996644626432,753214991095562240,753214994362925056] // 已关联的id集合
+      }
+    },
+    mounted () {
+      this.levelId = this.$route.params.id
+      this.getStemIdList()
+      this.list()
+    },
+    methods: {
+    getStemIdList() {
+
+    },
+      guanqia(id) {
+      console.log(id)
+      },
+      filterNode (value, data) {
+        if (!value) {
+          return true
+        }
+
+        return data.name.indexOf(value) !== -1
+      },
+      list () {
+        this.tableData = []
+        let _data = {
+          tenantId: '752950178499002368',
+          page: this.searchForm.page,
+          size: this.searchForm.size,
+        }
+        searchApi.page(_data).then(data => {
+          this.total = data.total
+          this.tableData = data.data
+        })
+        // 查询当前关卡所关联的题目id
+
+
+      },
+      isLink (stemId) {
+        // 判断stemId是否在已关联的集合中
+        return this.linkIdList.indexOf(stemId) >= 0
+      }
+    }
+  }
+</script>
+
+<style scoped>
+</style>

+ 231 - 0
src/views/system/level/list.vue

@@ -0,0 +1,231 @@
+<template>
+  <el-row>
+    <el-row>
+      <white-board>
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+          <el-breadcrumb-item>系统管理</el-breadcrumb-item>
+          <el-breadcrumb-item>关卡管理</el-breadcrumb-item>
+        </el-breadcrumb>
+      </white-board>
+    </el-row>
+
+    <el-row>
+      <el-form ref="searchForm" :model="searchForm">
+        <search-white-board :show-more="true" @search="list">
+          <div slot="base">
+            <el-row style="line-height: 40px; text-align: right;">
+              <el-col :span="3"><label class="search-label">关卡名称</label></el-col>
+              <el-col :span="5">
+                <el-input v-model.trim="searchForm.name" placeholder="输入关卡名称" clearable></el-input>
+              </el-col>
+            </el-row>
+          </div>
+        </search-white-board>
+      </el-form>
+    </el-row>
+
+    <el-row>
+      <white-board>
+          <el-row style="line-height: 40px; text-align: right;">
+          <el-col :span="6" :offset="18" style="text-align: right;">
+          <el-button icon="el-icon-plus" type="success" plain @click="handleAdd">新增</el-button>
+          </el-col>
+        </el-row>
+
+        <el-row style="padding: 10px 0px;">
+          <el-table :data="tableData" row-key="id" lazy :load="load" ref="ttable" @row-click="rowClick" style="width: 100%;" highlight-current-row stripe>
+            <el-table-column prop="id" label="标识" width="180"></el-table-column>
+            <el-table-column prop="name" label="关卡名称" width="150"></el-table-column>
+            <el-table-column label="创建" width="135">
+              <template slot-scope="scope">
+                <div>{{scope.row.createdBy}}</div>
+                <div>{{scope.row.createDate}}</div>
+              </template>
+            </el-table-column>
+            <el-table-column label="更新" width="135">
+              <template slot-scope="scope">
+                <div>{{scope.row.lastModifiedBy}}</div>
+                <div>{{scope.row.lastModifiedDate}}</div>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="150">
+              <template slot-scope="scope">
+                <el-button type="primary" plain @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
+                <el-button type="danger" plain @click="handleDel(scope.$index, scope.row)">删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-row>
+      </white-board>
+    </el-row>
+
+    <el-dialog :visible.sync="dialogVisible" ref="form">
+      <el-divider></el-divider>
+      <label slot="title" style="float: left;">{{dialogTitle}}</label>
+
+
+
+      <div slot="footer" class="dialog-footer">
+        <el-divider></el-divider>
+        <el-button @click="dialogVisible = false" size="mini">取 消</el-button>
+        <el-button type="primary" @click="handleSave" size="mini">确 定</el-button>
+      </div>
+    </el-dialog>
+  </el-row>
+</template>
+
+<script>
+  import levelApi from "@/api/levelApi"
+
+
+  export default {
+    name: 'level',
+    components: {
+    },
+    data () {
+      return {
+        defaultProps: {
+          label: 'name',
+          children: 'child',
+        },
+        searchForm: {
+         name: '',
+         start: 1,
+         limit: 10
+        },
+        total:0,
+        tableData: [],
+        uploadData: {
+          tree: null,
+          treeNode: null,
+          resolve: null
+        },
+
+        dialogTitle: '',
+        dialogVisible: false,
+        dialogdelVisible: false,
+        currentSelectRow: {}, // 当前选中行
+        form: {
+          id: '',
+          parentId: '',
+          dictKey: '',
+          code: '',
+          value: '',
+          attr: '',
+          remarks: '',
+          sort: ''
+        },
+      }
+    },
+    mounted () {
+      this.list()
+    },
+    methods: {
+      filterNode (value, data) {
+        if (!value) {
+          return true
+        }
+
+        return data.name.indexOf(value) !== -1
+      },
+      list () {
+        this.currentSelectRow = {}
+        this.tableData = []
+
+        let _data = {
+          data: {
+            name: this.searchForm.name,
+          },
+          limit: this.searchForm.limit,
+          start: this.searchForm.start
+        }
+        levelApi.page(_data).then(data => {
+          this.total = data.total
+          this.tableData = data.data
+        })
+
+
+      },
+      rowClick (row, event, column) {
+        this.$router.push({name: 'levelDetail', params:{id: row.id}})  /* 关卡跳转,接参数 */
+        this.currentSelectRow = row
+      },
+      handleAdd () {
+        /* 重置表单 */
+        this.form['id'] = ''
+        this.form['parentId'] = this.currentSelectRow['id']
+        this.form['dictKey'] = ''
+        this.form['code'] = ''
+        this.form['value'] = ''
+        this.form['attr'] = ''
+        this.form['remarks'] = ''
+        this.form['sort'] = ''
+        this.dialogTitle = '新增'
+        this.dialogVisible = true
+      },
+      handleEdit (index,row) {
+        this.form['id'] = row.id
+        this.form['parentId'] = row.parentId
+        this.form['dictKey'] = row.dictKey
+        this.form['code'] = row.code
+        this.form['value'] = row.value
+        this.form['attr'] = row.attr
+        this.form['remarks'] =row.remark
+        this.form['sort'] = row.sort
+        this.dialogTitle = '编辑'
+        this.dialogVisible = true
+      },
+      handleDel (index,row) {
+        this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          dataDictionaryApi.deleteDataDictPoint(row.id).then(data => {
+            this.$message({
+              type: 'success',
+              message: '删除成功'
+            })
+
+            this.list()
+            this.load(this.uploadData.tree, this.uploadData.treeNode, this.uploadData.resolve)
+          })
+        })
+      },
+      load(tree, treeNode, resolve) {
+        this.uploadData.tree = tree
+        this.uploadData.treeNode = treeNode
+        this.uploadData.resolve = resolve
+        let value = tree.id
+
+        dataDictionaryApi.findDataDictById(value).then(data => {
+          data.forEach(item => {
+            //判断是否有子节点
+            if (item.size > 0) {
+              item.hasChildren = []
+            }
+          })
+
+          resolve(data)
+        })
+      },
+      handleSave () {
+        this.$refs['form'].validate((valid) => {
+          if (!valid) {
+            return
+          }
+
+          dataDictionaryApi.saveOrUpdate(this.form).then(data => {
+            this.dialogVisible = false
+            this.$refs['form'].resetFields()
+            this.list()
+          })
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+</style>

+ 277 - 0
src/views/system/paper/list.vue

@@ -0,0 +1,277 @@
+<template>
+  <el-row>
+    <el-row>
+      <white-board>
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+          <el-breadcrumb-item>系统管理</el-breadcrumb-item>
+          <el-breadcrumb-item>试卷管理</el-breadcrumb-item>
+        </el-breadcrumb>
+      </white-board>
+    </el-row>
+
+    <el-row>
+      <el-form ref="searchForm" :model="searchForm">
+        <search-white-board :show-more="true" @search="list">
+          <div slot="base">
+            <el-row style="line-height: 40px; text-align: right;">
+              <el-col :span="3"><label class="search-label">试卷名称</label></el-col>
+              <el-col :span="5">
+                <el-input v-model.trim="searchForm.name" placeholder="输入试卷名称" clearable></el-input>
+              </el-col>
+              <el-col :span="2" :offset="1"><label class="search-label">年级</label></el-col>
+              <el-col :span="5">
+                <el-input v-model.trim="searchForm.gradeName" placeholder="请输入年级" clearable></el-input>
+              </el-col>
+              <el-col :span="2" :offset="1"><label class="search-label">学科</label></el-col>
+               <el-col :span="5">
+                <el-input v-model.trim="searchForm.subject" placeholder="请输入学科" clearable></el-input>
+               </el-col>
+            </el-row>
+          </div>
+        </search-white-board>
+      </el-form>
+    </el-row>
+
+    <el-row>
+      <white-board>
+        <el-row style="line-height: 40px; text-align: right;">
+          <el-col :span="6" :offset="18" style="text-align: right;">
+            <el-button icon="el-icon-plus" type="success" plain @click="handleAdd">新增</el-button>
+          </el-col>
+        </el-row>
+
+        <el-row style="padding: 10px 0px;">
+          <el-table :data="tableData" row-key="id" lazy :load="load" ref="ttable" @row-click="rowClick" style="width: 100%;" highlight-current-row stripe>
+            <el-table-column prop="id" label="标识" width="180"></el-table-column>
+            <el-table-column prop="name" label="试卷名称" width="150"></el-table-column>
+            <el-table-column prop="gradeName" label="年级" width="200"></el-table-column>
+            <el-table-column prop="subject" label="学科"  width="100"></el-table-column>
+            <el-table-column label="创建" width="135">
+              <template slot-scope="scope">
+                <div>{{scope.row.createdBy}}</div>
+                <div>{{scope.row.createDate}}</div>
+              </template>
+            </el-table-column>
+            <el-table-column label="更新" width="135">
+              <template slot-scope="scope">
+                <div>{{scope.row.lastModifiedBy}}</div>
+                <div>{{scope.row.lastModifiedDate}}</div>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="150">
+              <template slot-scope="scope">
+                <el-button type="primary" plain @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
+                <el-button type="danger" plain @click="handleDel(scope.$index, scope.row)">删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-row>
+      </white-board>
+    </el-row>
+
+    <el-dialog :visible.sync="dialogVisible" ref="form">
+      <el-divider></el-divider>
+      <label slot="title" style="float: left;">{{dialogTitle}}</label>
+
+      <el-row>
+        <el-col :span="18" :offset="3">
+          <el-form ref="form" :model="form" :rules="formRules" label-width="80px">
+            <el-form-item label="标识" prop="id" hidden="hidden">
+              <el-input v-model.trim="form.id" v-bind:disabled="form.id !== ''" hidden="hidden" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="父级标识" prop="parentId" hidden="hidden">
+              <el-input v-model.trim="form.parentId" placeholder="请输入父级标识" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="唯一标识" prop="dictKey">
+              <el-input v-model.trim="form.dictKey" placeholder="字典唯一标识" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="字典编码" prop="code">
+              <el-input v-model.trim="form.code" placeholder="字典编码" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="字典值" prop="value">
+              <el-input v-model.trim="form.value" placeholder="字典值" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="拓展字段">
+              <el-input v-model.trim="form.attr" placeholder="拓展字段" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="备注" prop="remarks">
+              <el-input v-model.trim="form.remarks" placeholder="备注" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="排序" prop="sort" >
+              <el-input v-model.trim="form.sort" placeholder="排序" type="number" clearable></el-input>
+            </el-form-item>
+          </el-form>
+
+        </el-col>
+      </el-row>
+
+      <div slot="footer" class="dialog-footer">
+        <el-divider></el-divider>
+        <el-button @click="dialogVisible = false" size="mini">取 消</el-button>
+        <el-button type="primary" @click="handleSave" size="mini">确 定</el-button>
+      </div>
+    </el-dialog>
+  </el-row>
+</template>
+
+<script>
+  import dataDictionaryApi from "@/api/dataDictionaryApi"
+  import paperApi from "@/api/paperApi"
+
+  export default {
+    name: 'dataDict',
+    components: {
+    },
+    data () {
+      return {
+        defaultProps: {
+          label: 'name',
+          children: 'child',
+        },
+        searchForm: {
+         gradeName: '',
+         name: '',
+         subject: '',
+         start: 1,
+         limit: 10
+        },
+        total:0,
+        tableData: [],
+        uploadData: {
+          tree: null,
+          treeNode: null,
+          resolve: null
+        },
+
+        dialogTitle: '',
+        dialogVisible: false,
+        dialogdelVisible: false,
+        currentSelectRow: {}, // 当前选中行
+        form: {
+          id: '',
+          parentId: '',
+          dictKey: '',
+          code: '',
+          value: '',
+          attr: '',
+          remarks: '',
+          sort: ''
+        },
+        formRules: {
+          dictKey: [{required: true, message: '请输入字典唯一标识', trigger: 'blur'}],
+          code: [{required: true, message: '请输入字典编码', trigger: 'blur'}],
+          value: [{required: true, message: '请输入字典值', trigger: 'blur'}],
+        }
+      }
+    },
+    mounted () {
+      this.list()
+    },
+    methods: {
+      filterNode (value, data) {
+        if (!value) {
+          return true
+        }
+
+        return data.name.indexOf(value) !== -1
+      },
+      list () {
+        this.currentSelectRow = {}
+        this.tableData = []
+
+        let _data = {
+          data: {
+            gradeName: this.searchForm.gradeName,
+            name: this.searchForm.name,
+            subject: this.searchForm.subject
+          },
+          limit: this.searchForm.limit,
+          start: this.searchForm.start
+        }
+        paperApi.page(_data).then(data => {
+          this.total = data.total
+          this.tableData = data.data
+        })
+      },
+      rowClick (row, event, column) {
+        this.currentSelectRow = row
+      },
+      handleAdd () {
+        /* 重置表单 */
+        this.form['id'] = ''
+        this.form['parentId'] = this.currentSelectRow['id']
+        this.form['dictKey'] = ''
+        this.form['code'] = ''
+        this.form['value'] = ''
+        this.form['attr'] = ''
+        this.form['remarks'] = ''
+        this.form['sort'] = ''
+        this.dialogTitle = '新增'
+        this.dialogVisible = true
+      },
+      handleEdit (index,row) {
+        this.form['id'] = row.id
+        this.form['parentId'] = row.parentId
+        this.form['dictKey'] = row.dictKey
+        this.form['code'] = row.code
+        this.form['value'] = row.value
+        this.form['attr'] = row.attr
+        this.form['remarks'] =row.remark
+        this.form['sort'] = row.sort
+        this.dialogTitle = '编辑'
+        this.dialogVisible = true
+      },
+      handleDel (index,row) {
+        this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          dataDictionaryApi.deleteDataDictPoint(row.id).then(data => {
+            this.$message({
+              type: 'success',
+              message: '删除成功'
+            })
+
+            this.list()
+            this.load(this.uploadData.tree, this.uploadData.treeNode, this.uploadData.resolve)
+          })
+        })
+      },
+      load(tree, treeNode, resolve) {
+        this.uploadData.tree = tree
+        this.uploadData.treeNode = treeNode
+        this.uploadData.resolve = resolve
+        let value = tree.id
+
+        dataDictionaryApi.findDataDictById(value).then(data => {
+          data.forEach(item => {
+            //判断是否有子节点
+            if (item.size > 0) {
+              item.hasChildren = []
+            }
+          })
+
+          resolve(data)
+        })
+      },
+      handleSave () {
+        this.$refs['form'].validate((valid) => {
+          if (!valid) {
+            return
+          }
+
+          dataDictionaryApi.saveOrUpdate(this.form).then(data => {
+            this.dialogVisible = false
+            this.$refs['form'].resetFields()
+            this.list()
+          })
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+</style>

+ 9 - 0
vue.config.js

@@ -0,0 +1,9 @@
+module.exports = {
+  publicPath: './',
+  productionSourceMap: false,
+  devServer: {
+    port: 8088,
+    disableHostCheck: true
+  }
+}
+