zhengqiang před 5 roky
rodič
revize
0bd7ecf705
56 změnil soubory, kde provedl 5687 přidání a 49 odebrání
  1. 1 0
      .env.development
  2. 2 0
      .env.production
  3. 1 1
      README.md
  4. 15 3
      package.json
  5. 1 1
      public/index.html
  6. 19 17
      src/App.vue
  7. 10 0
      src/api/sys/backend.js
  8. 50 0
      src/api/sys/dataDictionary-test.js
  9. 50 0
      src/api/sys/dataDictionary.js
  10. 54 0
      src/api/sys/menu.js
  11. 58 0
      src/api/sys/permission.js
  12. 77 0
      src/api/sys/role.js
  13. 60 0
      src/api/sys/user.js
  14. binární
      src/assets/diagram.png
  15. binární
      src/assets/login_bg_element.png
  16. binární
      src/assets/logo.png
  17. 253 0
      src/components/MceEditor.vue
  18. 38 0
      src/components/MenuTreeItem/index.vue
  19. 111 0
      src/components/Tinymce/components/EditorImage.vue
  20. 59 0
      src/components/Tinymce/dynamicLoadScript.js
  21. 204 0
      src/components/Tinymce/index.vue
  22. 7 0
      src/components/Tinymce/plugins.js
  23. 6 0
      src/components/Tinymce/toolbar.js
  24. 7 0
      src/constant.js
  25. 8 1
      src/main.js
  26. 9 0
      src/plugins/AxiosPlugin.js
  27. 5 0
      src/plugins/element.js
  28. 79 0
      src/routers/index.js
  29. 70 0
      src/routers/modules/sys.js
  30. 23 0
      src/store/index.js
  31. 55 0
      src/store/modules/user.js
  32. 20 0
      src/styles/mixin.scss
  33. 15 0
      src/utils/auth.js
  34. 51 0
      src/utils/request.js
  35. 5 0
      src/views/About.vue
  36. 47 0
      src/views/Home.vue
  37. 240 0
      src/views/Login.vue
  38. 46 0
      src/views/Tree.vue
  39. 146 0
      src/views/layout/index.vue
  40. 184 0
      src/views/sys/dataDictionary-detail.vue
  41. 287 0
      src/views/sys/dataDictionary-list.vue
  42. 184 0
      src/views/sys/dataDictionary-test-detail.vue
  43. 287 0
      src/views/sys/dataDictionary-test-list.vue
  44. 208 0
      src/views/sys/menu-detail.vue
  45. 344 0
      src/views/sys/menu-list.vue
  46. 128 0
      src/views/sys/permission-detail.vue
  47. 137 0
      src/views/sys/permission-import.vue
  48. 320 0
      src/views/sys/permission-list.vue
  49. 121 0
      src/views/sys/role-detail.vue
  50. 348 0
      src/views/sys/role-list.vue
  51. 143 0
      src/views/sys/role-menu.vue
  52. 135 0
      src/views/sys/role-permission.vue
  53. 158 0
      src/views/sys/user-detail.vue
  54. 304 0
      src/views/sys/user-list.vue
  55. 21 0
      vue.config.js
  56. 476 26
      yarn.lock

+ 1 - 0
.env.development

@@ -0,0 +1 @@
+VUE_APP_BACKEND_URL=http://localhost:8080/epay-server

+ 2 - 0
.env.production

@@ -0,0 +1,2 @@
+VUE_APP_BACKEND_URL=/jzcqb
+VUE_APP_IMAGE_URL=http://39.104.144.104/jzcqb-upload

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-# epay-server-manager
+# epay-server-portal
 
 ## Project setup
 ```

+ 15 - 3
package.json

@@ -8,8 +8,14 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "axios": "^0.19.0",
     "core-js": "^3.3.2",
-    "vue": "^2.6.10"
+    "element-ui": "^2.4.5",
+    "js-cookie": "^2.2.1",
+    "nprogress": "0.2.0",
+    "vue": "^2.6.10",
+    "vue-router": "^3.0.3",
+    "vuex": "^3.1.2"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^4.0.0",
@@ -18,7 +24,9 @@
     "babel-eslint": "^10.0.3",
     "eslint": "^5.16.0",
     "eslint-plugin-vue": "^5.0.0",
-    "vue-template-compiler": "^2.6.10"
+    "vue-template-compiler": "^2.6.10",
+    "node-sass": "^4.12.0",
+    "sass-loader": "^7.1.0"
   },
   "eslintConfig": {
     "root": true,
@@ -29,7 +37,11 @@
       "plugin:vue/essential",
       "eslint:recommended"
     ],
-    "rules": {},
+    "rules": {
+      "indent": 0,
+      "no-unused-vars": "off",
+      "no-console": "off"
+    },
     "parserOptions": {
       "parser": "babel-eslint"
     }

+ 1 - 1
public/index.html

@@ -5,7 +5,7 @@
     <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>epay-server-manager</title>
+    <title>电费充值管理系统</title>
   </head>
   <body>
     <noscript>

+ 19 - 17
src/App.vue

@@ -1,28 +1,30 @@
 <template>
   <div id="app">
-    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <router-view/>
   </div>
 </template>
-
 <script>
-import HelloWorld from './components/HelloWorld.vue'
-
 export default {
-  name: 'app',
-  components: {
-    HelloWorld
+  name: "app",
+  data() {
+    return {};
+  },
+  methods: {
+    
   }
-}
+};
 </script>
-
-<style>
+<style rel="stylesheet/scss" lang="scss">
 #app {
-  font-family: 'Avenir', Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
+  position: absolute;
+  right: 0;
+  left: 0;
+  bottom: 0;
+  top: 0;
+  background: #efefef;
+  overflow: auto;
+  z-index: 900;
+  display: flex;
+  flex-direction: row;
 }
 </style>

+ 10 - 0
src/api/sys/backend.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function selectAll() {
+    return request.get(constant.serverUrl + "/sys/api/selectAll");
+}
+
+export default {
+    selectAll
+}

+ 50 - 0
src/api/sys/dataDictionary-test.js

@@ -0,0 +1,50 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function pageList(formData) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/pageList", formData);
+}
+
+function create() {
+  return request.get(constant.serverUrl + "/sys/dataDictionary/create");
+}
+
+function edit(id) {
+  return request.get(constant.serverUrl + "/sys/dataDictionary/edit/" + id);
+}
+
+function add(formModel) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/add", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function update(formModel) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/update", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function remove(id) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/delete/" + id);
+}
+
+function batchRemove(idList) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/batchDelete", idList, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function query(formData) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/query", formData);
+}
+
+export default {
+  pageList, create, edit, add, update, remove, batchRemove,query
+}

+ 50 - 0
src/api/sys/dataDictionary.js

@@ -0,0 +1,50 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function pageList(formData) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/pageList", formData);
+}
+
+function create() {
+  return request.get(constant.serverUrl + "/sys/dataDictionary/create");
+}
+
+function edit(id) {
+  return request.get(constant.serverUrl + "/sys/dataDictionary/edit/" + id);
+}
+
+function add(formModel) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/add", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function update(formModel) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/update", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function remove(id) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/delete/" + id);
+}
+
+function batchRemove(idList) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/batchDelete", idList, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function query(formData) {
+  return request.post(constant.serverUrl + "/sys/dataDictionary/query", formData);
+}
+
+export default {
+  pageList, create, edit, add, update, remove, batchRemove,query
+}

+ 54 - 0
src/api/sys/menu.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function pageList(formData) {
+  return request.post(constant.serverUrl + "/sys/menu/pageList", formData);
+}
+
+function create() {
+  return request.get(constant.serverUrl + "/sys/menu/create");
+}
+
+function edit(id) {
+  return request.get(constant.serverUrl + "/sys/menu/edit/" + id);
+}
+
+function add(formModel) {
+  return request.post(constant.serverUrl + "/sys/menu/add", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function update(formModel) {
+  return request.post(constant.serverUrl + "/sys/menu/update", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function remove(id) {
+  return request.post(constant.serverUrl + "/sys/menu/delete/" + id);
+}
+
+function batchRemove(idList) {
+  return request.post(constant.serverUrl + "/sys/menu/batchDelete", idList, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function query(formData) {
+  return request.post(constant.serverUrl + "/sys/menu/query", formData);
+}
+
+function getMenuTree() {
+  return request.get(constant.serverUrl + "/sys/menu/tree");
+}
+
+export default {
+  pageList, create, edit, add, update, remove, batchRemove, query, getMenuTree
+}

+ 58 - 0
src/api/sys/permission.js

@@ -0,0 +1,58 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function pageList(formData){
+  return request.post(constant.serverUrl + "/sys/permission/pageList", formData);
+}
+
+function create(){
+  return request.get(constant.serverUrl + "/sys/permission/create");
+}
+
+function edit(id){
+  return request.get(constant.serverUrl + "/sys/permission/edit/" + id);
+}
+
+function add(formModel){
+  return request.post(constant.serverUrl + "/sys/permission/add", formModel,{
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function selectAll(){
+  return request.get(constant.serverUrl + "/sys/permission/selectAll");
+}
+
+function batchImport(dataList){
+  return request.post(constant.serverUrl + "/sys/permission/batchImport", dataList,{
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function update(formModel){  
+  return request.post(constant.serverUrl + "/sys/permission/update", formModel,{
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function remove(id){
+  return request.post(constant.serverUrl + "/sys/permission/delete/" + id);
+}
+
+function batchRemove(idList){
+  return request.post(constant.serverUrl + "/sys/permission/batchDelete",idList,{
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+export default {
+  pageList,create,edit,add,update,remove,batchRemove,selectAll,batchImport
+}

+ 77 - 0
src/api/sys/role.js

@@ -0,0 +1,77 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function pageList(formData) {
+  return request.post(constant.serverUrl + "/sys/role/pageList", formData);
+}
+
+function create() {
+  return request.get(constant.serverUrl + "/sys/role/create");
+}
+
+function edit(id) {
+  return request.get(constant.serverUrl + "/sys/role/edit/" + id);
+}
+
+function add(formModel) {
+  return request.post(constant.serverUrl + "/sys/role/add", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function update(formModel) {
+  return request.post(constant.serverUrl + "/sys/role/update", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function remove(id) {
+  return request.post(constant.serverUrl + "/sys/role/delete/" + id);
+}
+
+function batchRemove(idList) {
+  return request.post(constant.serverUrl + "/sys/role/batchDelete", idList, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function selectAll() {
+  return request.get(constant.serverUrl + "/sys/role/selectAll");
+}
+
+function queryRelatedPerms(roleId) {
+  return request.get(constant.serverUrl + "/sys/role/queryRelatedPerms?roleId=" + roleId);
+}
+
+function saveRelatedPermission(formModel) {
+  return request.post(constant.serverUrl + "/sys/role/saveRelatedPermission", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function queryRelatedMenuList(roleId) {
+  return request.get(constant.serverUrl + "/sys/role/queryRelatedMenuList?roleId=" + roleId);
+}
+
+function saveRelatedMenu(formModel) {
+  return request.post(constant.serverUrl + "/sys/role/saveRelatedMenu", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+export default {
+  create, edit, add, update, remove, batchRemove,
+  pageList, selectAll,
+  queryRelatedPerms, queryRelatedMenuList,
+  saveRelatedPermission, saveRelatedMenu
+}

+ 60 - 0
src/api/sys/user.js

@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+import constant from '@/constant'
+
+function login(data) {
+  var formData = new FormData();
+
+  for (var key in data) {
+    formData.append(key, data[key]);
+  }
+
+  return request.post(constant.serverUrl + '/login', formData)
+}
+
+function pageList(formData) {
+  return request.post(constant.serverUrl + "/sys/user/pageList", formData);
+}
+
+function create() {
+  return request.get(constant.serverUrl + "/sys/user/create");
+}
+
+function edit(id) {
+  return request.get(constant.serverUrl + "/sys/user/edit/" + id);
+}
+
+function add(formModel) {
+  return request.post(constant.serverUrl + "/sys/user/add", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function update(formModel) {
+  return request.post(constant.serverUrl + "/sys/user/update", formModel, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function remove(id) {
+  return request.post(constant.serverUrl + "/sys/user/delete/" + id);
+}
+
+function batchRemove(idList) {
+  return request.post(constant.serverUrl + "/sys/user/batchDelete", idList, {
+    headers: {
+      "Content-Type": "application/json"
+    }
+  });
+}
+
+function userInfo() {
+  return request.get(constant.serverUrl + "/userInfo");
+}
+
+export default {
+  login, pageList, create, edit, add, update, remove, batchRemove, userInfo
+}

binární
src/assets/diagram.png


binární
src/assets/login_bg_element.png


binární
src/assets/logo.png


+ 253 - 0
src/components/MceEditor.vue

@@ -0,0 +1,253 @@
+<template>
+  <div>
+    <textarea :id="Id"></textarea>
+  </div>
+</template>
+<script>
+export default {
+  data() {
+    const Id = Date.now();
+    return {
+      hasChange: false,
+      hasInit: false,
+      Id: Id,
+      Editor: null,
+      DefaultConfig: {
+        language: 'zh_CN',
+        // GLOBAL
+        height: 500,
+        theme: "modern",
+        menubar: false,
+        toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image  media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code  link | undo redo | fullscreen `,
+        plugins: `
+            paste
+            importcss
+            image
+            code
+            table
+            advlist
+            fullscreen
+            link
+            media
+            lists
+            textcolor
+            colorpicker
+            hr
+            preview
+        `,
+        // CONFIG
+        forced_root_block: "p",
+        force_p_newlines: true,
+        importcss_append: true,
+        // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
+        content_style: `
+            *                         { padding:0; margin:0; }
+            html, body                { height:100%; }
+            img                       { max-width:100%; display:block;height:auto; }
+            a                         { text-decoration: none; }
+            iframe                    { width: 100%; }
+            p                         { line-height:1.6; margin: 0px; }
+            table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
+            .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
+            ul,ol                     { list-style-position:inside; }
+        `,
+        insert_button_items: "image link | inserttable",
+        // CONFIG: Paste
+        paste_retain_style_properties: "all",
+        paste_word_valid_elements: "*[*]", // word需要它
+        paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
+        paste_convert_word_fake_lists: false, // 插入word文档需要该属性
+        paste_webkit_styles: "all",
+        paste_merge_formats: true,
+        nonbreaking_force_tab: false,
+        paste_auto_cleanup_on_paste: false,
+        // CONFIG: Font
+        fontsize_formats: "10px 11px 12px 14px 16px 18px 20px 24px",
+        // CONFIG: StyleSelect
+        style_formats: [
+          {
+            title: "首行缩进",
+            block: "p",
+            styles: { "text-indent": "2em" }
+          },
+          {
+            title: "行高",
+            items: [
+              { title: "1", styles: { "line-height": "1" }, inline: "span" },
+              {
+                title: "1.5",
+                styles: { "line-height": "1.5" },
+                inline: "span"
+              },
+              { title: "2", styles: { "line-height": "2" }, inline: "span" },
+              {
+                title: "2.5",
+                styles: { "line-height": "2.5" },
+                inline: "span"
+              },
+              { title: "3", styles: { "line-height": "3" }, inline: "span" }
+            ]
+          }
+        ],
+        // FontSelect
+        font_formats: `
+        微软雅黑=微软雅黑;
+        宋体=宋体;
+        黑体=黑体;
+        仿宋=仿宋;
+        楷体=楷体;
+        隶书=隶书;
+        幼圆=幼圆;
+        Andale Mono=andale mono,times;
+        Arial=arial, helvetica,
+        sans-serif;
+        Arial Black=arial black, avant garde;
+        Book Antiqua=book antiqua,palatino;
+        Comic Sans MS=comic sans ms,sans-serif;
+        Courier New=courier new,courier;
+        Georgia=georgia,palatino;
+        Helvetica=helvetica;
+        Impact=impact,chicago;
+        Symbol=symbol;
+        Tahoma=tahoma,arial,helvetica,sans-serif;
+        Terminal=terminal,monaco;
+        Times New Roman=times new roman,times;
+        Trebuchet MS=trebuchet ms,geneva;
+        Verdana=verdana,geneva;
+        Webdings=webdings;
+        Wingdings=wingdings,zapf dingbats`,
+        // Tab
+        tabfocus_elements: ":prev,:next",
+        object_resizing: true,
+        // Image
+        imagetools_toolbar:
+          "rotateleft rotateright | flipv fliph | editimage imageoptions"
+      }
+    };
+  },
+  props: {
+    value: {
+      default: "",
+      type: String
+    },
+    config: {
+      type: Object,
+      default: () => {
+        return {
+          theme: "modern",
+          height: 300
+        };
+      }
+    },
+    url: {
+      default: "",
+      type: String
+    },
+    accept: {
+      default: "image/jpeg,image/png",
+      type: String
+    },
+    maxSize: {
+      default: 1024*1024*1000,
+      type: Number
+    },
+    withCredentials: {
+      default: false,
+      type: Boolean
+    }
+  },
+  watch: {
+    value(val) {
+      if (!this.hasChange && this.hasInit) {
+        this.$nextTick(() =>
+          window.tinymce.get(this.tinymceId).setContent(val || ''))
+      }
+    }
+  },
+  mounted() {
+    this.init();
+  },
+  beforeDestroy() {
+    // 销毁tinymce
+    this.$emit("on-destroy");
+    window.tinymce.remove(`#${this.Id}`);
+  },
+  methods: {
+    init() {
+      const self = this;
+      this.Editor = window.tinymce.init({
+        // 默认配置
+        ...this.DefaultConfig,
+
+        // 图片上传
+        images_upload_handler: function(blobInfo, success, failure) {
+          if (blobInfo.blob().size > self.maxSize) {
+            failure("文件体积过大");
+          }
+
+          if (self.accept.indexOf(blobInfo.blob().type) > 0) {
+            uploadPic();
+          } else {
+            failure("图片格式错误");
+          }
+          function uploadPic() {
+            const xhr = new XMLHttpRequest();
+            const formData = new FormData();
+            xhr.withCredentials = self.withCredentials;
+            xhr.open("POST", self.url);
+            xhr.onload = function() {
+              if (xhr.status !== 200) {
+                // 抛出 'on-upload-fail' 钩子
+                self.$emit("on-upload-fail");
+                failure("上传失败: " + xhr.status);
+                return;
+              }
+
+              const json = JSON.parse(xhr.responseText);
+
+              /* eslint-disable-next-line */
+              console.log(json);
+              success(json.location);
+
+              // 抛出 'on-upload-complete' 钩子
+              self.$emit("on-upload-complete", [json, success, failure]);
+            };
+            formData.append("file", blobInfo.blob());
+            xhr.send(formData);
+          }
+        },
+
+        // prop内传入的的config
+        ...this.config,
+
+        // 挂载的DOM对象
+        selector: `#${this.Id}`,
+
+        init_instance_callback: (editor) => {
+          if (self.value) {
+            editor.setContent(self.value)
+          }
+          self.hasInit = true;
+
+          editor.on('NodeChange Change KeyUp SetContent', () => {
+            self.hasChange = true
+            this.$emit('input', editor.getContent())
+          })
+        },
+        setup: editor => {
+          // 抛出 'on-ready' 事件钩子
+          editor.on("init", () => {
+            self.loading = false;
+            self.$emit("on-ready");
+            editor.setContent(self.value);
+          });
+          // 抛出 'input' 事件钩子,同步value数据
+          editor.on("input change undo redo", () => {
+            self.$emit("input", editor.getContent());
+          });
+        }
+      });
+    }
+  }
+};
+</script>

+ 38 - 0
src/components/MenuTreeItem/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <!--这里由于包裹了一层div所以原生collpase样式无法起作用-->
+  <div class='menu-wrapper'>
+    <template v-for="item in routes">
+      <template v-if="item.children==null || item.children.length==0">     
+        <el-menu-item :index="item.menuUrl" :key="item.id">
+            <i :class="item.icon"></i>
+          <span>{{item.menuName}}</span>
+        </el-menu-item>
+      </template>
+      <template v-else>
+        <!--子菜单index不能重复,否则打开菜单时相同index菜单会同时开启-->
+        <el-submenu :index="item.id" :key="item.id">
+          <template slot="title">
+            <i :class="item.icon"></i>
+            <span>{{item.menuName}}</span>
+          </template>
+          <menu-tree-item class='nest-menu' :routes='item.children' :key="item.id"></menu-tree-item>
+        </el-submenu>
+      </template>
+    </template>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MenuTreeItem',
+  props: {
+    routes: {
+      type: Array
+    }
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 111 - 0
src/components/Tinymce/components/EditorImage.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="upload-container">
+    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
+      upload
+    </el-button>
+    <el-dialog :visible.sync="dialogVisible">
+      <el-upload
+        :multiple="true"
+        :file-list="fileList"
+        :show-file-list="true"
+        :on-remove="handleRemove"
+        :on-success="handleSuccess"
+        :before-upload="beforeUpload"
+        class="editor-slide-upload"
+        action="https://httpbin.org/post"
+        list-type="picture-card"
+      >
+        <el-button size="small" type="primary">
+          Click upload
+        </el-button>
+      </el-upload>
+      <el-button @click="dialogVisible = false">
+        Cancel
+      </el-button>
+      <el-button type="primary" @click="handleSubmit">
+        Confirm
+      </el-button>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+// import { getToken } from 'api/qiniu'
+
+export default {
+  name: 'EditorSlideUpload',
+  props: {
+    color: {
+      type: String,
+      default: '#1890ff'
+    }
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      listObj: {},
+      fileList: []
+    }
+  },
+  methods: {
+    checkAllSuccess() {
+      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
+    },
+    handleSubmit() {
+      const arr = Object.keys(this.listObj).map(v => this.listObj[v])
+      if (!this.checkAllSuccess()) {
+        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
+        return
+      }
+      this.$emit('successCBK', arr)
+      this.listObj = {}
+      this.fileList = []
+      this.dialogVisible = false
+    },
+    handleSuccess(response, file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          this.listObj[objKeyArr[i]].url = response.files.file
+          this.listObj[objKeyArr[i]].hasSuccess = true
+          return
+        }
+      }
+    },
+    handleRemove(file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          delete this.listObj[objKeyArr[i]]
+          return
+        }
+      }
+    },
+    beforeUpload(file) {
+      const _self = this
+      const _URL = window.URL || window.webkitURL
+      const fileName = file.uid
+      this.listObj[fileName] = {}
+      return new Promise((resolve, reject) => {
+        const img = new Image()
+        img.src = _URL.createObjectURL(file)
+        img.onload = function() {
+          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
+        }
+        resolve(true)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.editor-slide-upload {
+  margin-bottom: 20px;
+  /deep/ .el-upload--picture-card {
+    width: 100%;
+  }
+}
+</style>

+ 59 - 0
src/components/Tinymce/dynamicLoadScript.js

@@ -0,0 +1,59 @@
+let callbacks = []
+
+function loadedTinymce() {
+  // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
+  // check is successfully downloaded script
+  return window.tinymce
+}
+
+const dynamicLoadScript = (src, callback) => {
+  const existingScript = document.getElementById(src)
+  const cb = callback || function() {}
+
+  if (!existingScript) {
+    const script = document.createElement('script')
+    script.src = src // src url for the third-party library being loaded.
+    script.id = src
+    document.body.appendChild(script)
+    callbacks.push(cb)
+    const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
+    onEnd(script)
+  }
+
+  if (existingScript && cb) {
+    if (loadedTinymce()) {
+      cb(null, existingScript)
+    } else {
+      callbacks.push(cb)
+    }
+  }
+
+  function stdOnEnd(script) {
+    script.onload = function() {
+      // this.onload = null here is necessary
+      // because even IE9 works not like others
+      this.onerror = this.onload = null
+      for (const cb of callbacks) {
+        cb(null, script)
+      }
+      callbacks = null
+    }
+    script.onerror = function() {
+      this.onerror = this.onload = null
+      cb(new Error('Failed to load ' + src), script)
+    }
+  }
+
+  function ieOnEnd(script) {
+    script.onreadystatechange = function() {
+      if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
+      this.onreadystatechange = null
+      for (const cb of callbacks) {
+        cb(null, script) // there is no way to catch loading errors in IE8
+      }
+      callbacks = null
+    }
+  }
+}
+
+export default dynamicLoadScript

+ 204 - 0
src/components/Tinymce/index.vue

@@ -0,0 +1,204 @@
+<template>
+  <div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
+    <textarea :id="tinymceId" class="tinymce-textarea" />
+  </div>
+</template>
+<script>
+/**
+ * docs:
+ * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
+ */
+import plugins from './plugins'
+import toolbar from './toolbar'
+import load from './dynamicLoadScript'
+// why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
+const tinymceCDN =  'https://cdn.bootcss.com/tinymce/5.0.6/jquery.tinymce.min.js'
+
+export default {
+  name: 'Tinymce',
+  props: {
+    id: {
+      type: String,
+      default: function() {
+        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      }
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    toolbar: {
+      type: Array,
+      required: false,
+      default() {
+        return []
+      }
+    },
+    menubar: {
+      type: String,
+      default: 'file edit insert view format table'
+    },
+    height: {
+      type: [Number, String],
+      required: false,
+      default: 360
+    },
+    width: {
+      type: [Number, String],
+      required: false,
+      default: 'auto'
+    }
+  },
+  data() {
+    return {
+      hasChange: false,
+      hasInit: false,
+      tinymceId: this.id,
+      fullscreen: false,
+      languageTypeList: {
+        'en': 'en',
+        'zh': 'zh_CN',
+        'es': 'es_MX',
+        'ja': 'ja'
+      }
+    }
+  },
+  computed: {
+    containerWidth() {
+      const width = this.width
+      if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
+        return `${width}px`
+      }
+      return width
+    }
+  },
+  watch: {
+    value(val) {
+      if (!this.hasChange && this.hasInit) {
+        this.$nextTick(() =>
+          window.tinymce.get(this.tinymceId).setContent(val || ''))
+      }
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  activated() {
+    if (window.tinymce) {
+      this.initTinymce()
+    }
+  },
+  deactivated() {
+    this.destroyTinymce()
+  },
+  destroyed() {
+    this.destroyTinymce()
+  },
+  methods: {
+    init() {
+      // 在public/static/tinymce已放置一份
+      // dynamic load tinymce from cdn
+      // load(tinymceCDN, (err) => {
+      //   if (err) {
+      //     this.$message.error(err.message)
+      //     return
+      //   }
+        this.initTinymce()
+      // })
+    },
+    initTinymce() {
+      const _this = this
+      window.tinymce.init({
+        selector: `#${this.tinymceId}`,
+        language: this.languageTypeList['zh'],
+        height: this.height,
+        body_class: 'panel-body ',
+        object_resizing: false,
+        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
+        menubar: false,
+        plugins: plugins,
+        end_container_on_empty_block: true,
+        powerpaste_word_import: 'clean', // prompt
+        powerpaste_html_import: 'clean',
+        powerpaste_allow_local_images: true,
+        powerpaste_data_images: true,
+        code_dialog_height: 450,
+        code_dialog_width: 1000,
+        advlist_bullet_styles: 'square',
+        advlist_number_styles: 'default',
+        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
+        default_link_target: '_blank',
+        link_title: false,
+        nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
+        init_instance_callback: editor => {
+          if (_this.value) {
+            editor.setContent(_this.value)
+          }
+          _this.hasInit = true
+          editor.on('NodeChange Change KeyUp SetContent', () => {
+            this.hasChange = true
+            this.$emit('input', editor.getContent())
+          })
+        },
+        setup(editor) {
+          editor.on('FullscreenStateChanged', (e) => {
+            _this.fullscreen = e.state
+          })
+        },
+        images_upload_handler : (blobInfo, success, failure)=>{
+          this.$emit("imagesUploadHanlder",{blobInfo,success,failure});
+        }
+      })
+    },
+    destroyTinymce() {
+      const tinymce = window.tinymce.get(this.tinymceId)
+      if (this.fullscreen) {
+        tinymce.execCommand('mceFullScreen')
+      }
+
+      if (tinymce) {
+        tinymce.destroy()
+      }
+    },
+    setContent(value) {
+      window.tinymce.get(this.tinymceId).setContent(value)
+    },
+    getContent() {
+      window.tinymce.get(this.tinymceId).getContent()
+    },
+    imageSuccessCBK(arr) {
+      const _this = this
+      arr.forEach(v => {
+        window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.tinymce-container {
+  position: relative;
+  line-height: normal;
+}
+.tinymce-container>>>.mce-fullscreen {
+  z-index: 10000;
+}
+.tinymce-textarea {
+  visibility: hidden;
+  z-index: -1;
+}
+.editor-custom-btn-container {
+  position: absolute;
+  right: 4px;
+  top: 4px;
+  /*z-index: 2005;*/
+}
+.fullscreen .editor-custom-btn-container {
+  z-index: 10000;
+  position: fixed;
+}
+.editor-upload-btn {
+  display: inline-block;
+}
+</style>

+ 7 - 0
src/components/Tinymce/plugins.js

@@ -0,0 +1,7 @@
+// Any plugins you want to use has to be imported
+// Detail plugins list see https://www.tinymce.com/docs/plugins/
+// Custom builds see https://www.tinymce.com/download/custom-builds/
+
+const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak powerpaste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
+
+export default plugins

+ 6 - 0
src/components/Tinymce/toolbar.js

@@ -0,0 +1,6 @@
+// Here is a list of the toolbar
+// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
+
+const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
+
+export default toolbar

+ 7 - 0
src/constant.js

@@ -0,0 +1,7 @@
+var Constant = {
+	clientUrl:"/",
+	//根据当前环境修改
+	serverUrl:process.env.VUE_APP_BACKEND_URL
+};
+
+export default Constant;

+ 8 - 1
src/main.js

@@ -1,8 +1,15 @@
 import Vue from 'vue'
 import App from './App.vue'
+import router from './routers'
+import store from './store'
+import './plugins/element.js'
+import AxiosPlugin from './plugins/AxiosPlugin'
 
 Vue.config.productionTip = false
+Vue.use(AxiosPlugin)
 
 new Vue({
-  render: h => h(App),
+  router,
+  store,
+  render: h => h(App)
 }).$mount('#app')

+ 9 - 0
src/plugins/AxiosPlugin.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 安装axios作为vue插件
+export default ({
+  install: function (Vue, options) {
+    Vue.prototype.$http = request
+    Vue.http = request
+  }
+})

+ 5 - 0
src/plugins/element.js

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

+ 79 - 0
src/routers/index.js

@@ -0,0 +1,79 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import Layout from '@/views/layout'
+import Home from '@/views/Home.vue'
+import sysRouters from './modules/sys'
+import { Message } from 'element-ui'
+import NProgress from 'nprogress' // progress bar
+import 'nprogress/nprogress.css' // progress bar style
+import { getToken } from '@/utils/auth' // get token from cookie
+
+Vue.use(Router)
+
+NProgress.configure({ showSpinner: true }) // NProgress Configuration
+
+var routes = [
+  {
+    path: '/layout',
+    component:Layout,
+    children: [
+      {
+        path: '/home',
+        name: 'home',
+        component: Home
+      },
+      ...sysRouters
+    ]
+  },
+  {
+    path:'/',
+    redirect: '/home'
+  },
+  {
+    path: '/login',
+    component: () => import('@/views/Login')
+  }
+];
+
+// Array.prototype.push.apply(routes, caseRouters);
+// Array.prototype.push.apply(routes, sysRouters);
+
+var router = new Router({
+  routes
+})
+
+router.beforeEach((to, from, next) => {
+  NProgress.start();
+  console.log(`${to.path}`);
+
+  const hasToken = getToken()
+
+  if (hasToken != null) {
+    if (to.path === '/login') {
+      // if is logged in, redirect to the home page
+      next({ path: '/home' })
+    }
+    else {
+      next();
+    }
+
+    NProgress.done();
+  }
+  else {
+    if (to.path === '/login') {
+      next();
+    }
+    else{
+      next(`/login?redirect=${to.path}`);
+    }
+
+    NProgress.done();
+  }
+});
+
+router.afterEach(() => {
+  NProgress.done();
+});
+
+
+export default router;

+ 70 - 0
src/routers/modules/sys.js

@@ -0,0 +1,70 @@
+var routers = [
+        {
+                path: '/sys/user/list',
+                name: 'sys-user-list',
+                // route level code-splitting
+                // this generates a separate chunk (about.[hash].js) for this route
+                // which is lazy-loaded when the route is visited.
+                component: () => import('@/views/sys/user-list.vue'),
+                meta: {
+                        roles: ["admin"]
+                }
+        },
+        {
+                path: '/sys/role/list',
+                name: 'sys-role-list',
+                // route level code-splitting
+                // this generates a separate chunk (about.[hash].js) for this route
+                // which is lazy-loaded when the route is visited.
+                component: () => import('@/views/sys/role-list.vue'),
+                meta: {
+                        roles: ["admin"]
+                }
+        },
+        {
+                path: '/sys/menu/list',
+                name: 'sys-menu-list',
+                // route level code-splitting
+                // this generates a separate chunk (about.[hash].js) for this route
+                // which is lazy-loaded when the route is visited.
+                component: () => import('@/views/sys/menu-list.vue'),
+                meta: {
+                        roles: ["admin"]
+                }
+        },
+        {
+                path: '/sys/permission/list',
+                name: 'sys-permission-list',
+                // route level code-splitting
+                // this generates a separate chunk (about.[hash].js) for this route
+                // which is lazy-loaded when the route is visited.
+                component: () => import('@/views/sys/permission-list.vue'),
+                meta: {
+                        roles: ["admin"]
+                }
+        },
+        {
+                path: '/sys/dataDictionary/list',
+                name: 'sys-dataDictionary-list',
+                // route level code-splitting
+                // this generates a separate chunk (about.[hash].js) for this route
+                // which is lazy-loaded when the route is visited.
+                component: () => import('@/views/sys/dataDictionary-list.vue'),
+                meta: {
+                        roles: ["admin"]
+                }
+        },
+        {
+                path: '/sys/dataDictionary/test/list',
+                name: 'sys-dataDictionary-test-list',
+                // route level code-splitting
+                // this generates a separate chunk (about.[hash].js) for this route
+                // which is lazy-loaded when the route is visited.
+                component: () => import('@/views/sys/dataDictionary-test-list.vue'),
+                meta: {
+                        roles: ["admin"]
+                }
+        }
+]
+
+export default routers;

+ 23 - 0
src/store/index.js

@@ -0,0 +1,23 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import user from './modules/user'
+
+Vue.use(Vuex)
+
+const debug = process.env.NODE_ENV !== 'production'
+
+export default new Vuex.Store({
+  modules: {
+    user
+  },
+  strict: debug,
+  state:{
+    
+  },
+  mutations:{
+
+  },
+  actions:{
+
+  }
+})

+ 55 - 0
src/store/modules/user.js

@@ -0,0 +1,55 @@
+import userApi from '@/api/sys/user'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import { Message } from 'element-ui'
+
+const state = {
+    token: getToken(),
+}
+
+const mutations = {
+    SET_TOKEN: (state, token) => {
+        state.token = token
+    }
+}
+
+const actions = {
+    login({ commit }, userInfo) {
+        const { userName, password } = userInfo
+
+        return new Promise((resolve, reject) => {
+            userApi.login({ userName: userName.trim(), password: password }).then(response => {
+                var jsonData = response.data;
+                
+                console.log("result=" + jsonData.result);
+
+                if(jsonData.result){
+                    commit('SET_TOKEN', jsonData.data)
+                    setToken(jsonData.data)
+                }
+                else{
+                    Message.error(jsonData.message || 'Has Error')
+                }
+
+                resolve()
+            }).catch(error => {
+                reject(error)
+            })
+        })
+    },
+    logout({ commit }) {
+        return new Promise((resolve, reject) => {
+            removeToken();
+            commit('SET_TOKEN', null);
+
+            resolve();
+        });
+    }
+
+}
+
+export default {
+  namespaced: true, //必填,否则声明都为root,state,mutations,action 一定要对外声明后才能在外部调用
+  state,
+  mutations,
+  actions
+}

+ 20 - 0
src/styles/mixin.scss

@@ -0,0 +1,20 @@
+@mixin prefixer ($property, $value){
+    -webkit-#{$property}: $value;
+    -moz-#{$property}: $value;
+    -o-#{$property}: $value;
+    #{$property}: $value;
+}
+
+@mixin clearfix {
+    &:after {
+      content: "";
+      display: table;
+      clear: both;
+    }
+  }
+
+  @mixin relative {
+    position: relative;
+    width: 100%;
+    height: 100%;
+  }

+ 15 - 0
src/utils/auth.js

@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'Admin-Token'
+
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey)
+}

+ 51 - 0
src/utils/request.js

@@ -0,0 +1,51 @@
+import axios from 'axios'
+import { MessageBox, Message } from 'element-ui'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+
+// post 跨域
+axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded'
+
+axios.interceptors.request.use(function (config) {
+  config.headers['Authorization'] = getToken();
+  
+  return config;
+},function (error) {
+  return Promise.reject(error);
+});
+
+axios.interceptors.response.use(
+  response=>{
+    console.log(response.data.code);
+
+    var code = response.data.code;
+
+    if(code==415){
+      //Message.error(response.data.message);
+      removeToken();
+      window.location.href = "#/login";
+    }
+    else if(code==401){
+      Message({
+        message: response.data.message + "",
+        type: 'error',
+        duration: 3000
+      })
+    }
+
+    return response;
+  },
+  error => {
+    console.log(error);
+
+    Message({
+      message: error.message,
+      type: 'error',
+      duration: 2000
+    })
+
+    return error;
+  }
+)
+
+
+export default axios;

+ 5 - 0
src/views/About.vue

@@ -0,0 +1,5 @@
+<template>
+  <div class="about">
+    <h1>This is an about page</h1>
+  </div>
+</template>

+ 47 - 0
src/views/Home.vue

@@ -0,0 +1,47 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item><a href="/">控制台</a></el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <el-card class="diagram">
+      <div slot="header" class="header">
+        <span>系统使用情况</span>
+      </div>
+      
+    </el-card>
+  </div>
+</template>
+<script>
+// @ is an alias to /src
+export default {
+  name: 'home',
+  data() {
+    return {
+      
+    };
+  }
+}
+</script>
+<style lang="scss" scoped>
+.el-breadcrumb{
+  margin: 10px;
+  line-height: 20px;
+}
+
+.el-divider{
+  margin:5px 0;
+}
+
+.header{
+  text-align: left;
+}
+
+.diagram{
+  margin: 20px 20px;
+  width: 600px;
+  height:400px;
+}
+</style>
+

+ 240 - 0
src/views/Login.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="login-container">
+    <div class="login-form">
+      <h3>
+        <img src="../assets/logo.png" width="269" height="65"/>
+      </h3>
+      <el-card class="box-card">
+        <h3 class="title">电费充值管理系统</h3>
+        <el-form
+          class="card-box"
+          autocomplete="on"
+          :model="loginForm"
+          :rules="loginRules"
+          ref="loginForm"
+          label-position="left"
+        >
+          <el-form-item prop="userName">
+            <i class="el-icon-user-solid"></i>
+            <el-input
+              name="userName"
+              type="text"
+              v-model="loginForm.userName"
+              autocomplete="on"
+              placeholder="请输入用户名"
+            />
+          </el-form-item>
+          <el-form-item prop="password">
+            <i class="el-icon-lock"></i>
+            <el-input
+              name="password"
+              :type="pwdType"
+              @keyup.enter.native="handleLogin"
+              v-model="loginForm.password"
+              autocomplete="on"
+              placeholder="请输入密码"
+            />
+            <i class="el-icon-view" @click="showPwd"></i>
+          </el-form-item>
+          <el-button
+            type="primary"
+            style="width:100%;margin-bottom:30px;"
+            :loading="loading"
+            @click.native.prevent="handleLogin"
+          >登录</el-button>
+        </el-form>
+      </el-card>
+    </div>
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import { Message } from "element-ui";
+
+export default {
+  name: "login",
+  data() {
+    return {
+      loginForm: {
+        userName: "",
+        password: ""
+      },
+      loginRules: {
+        userName: [
+          { required: true, message: "请填写用户名", trigger: "blur" }
+        ],
+        password: [
+          { required: true, message: "请填写密码", trigger: "blur" },
+          {
+            type: "string",
+            min: 2,
+            message: "密码长度不能小于2位",
+            trigger: "blur"
+          }
+        ]
+      },
+      pwdType: "password",
+      loading: false,
+      redirect: undefined
+    };
+  },
+  watch: {
+    $route: {
+      handler: function(route) {
+        const query = route.query;
+        if (query) {
+          this.redirect = query.redirect;
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    showPwd() {
+      if (this.pwdType === "password") {
+        this.pwdType = "";
+      } else {
+        this.pwdType = "password";
+      }
+    },
+    handleLogin() {
+      var self = this;
+
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+
+          self.$store
+            .dispatch("user/login", self.loginForm)
+            .then(() => {
+              self.$router.push({ path: this.redirect || "/home" });
+              self.loading = false;
+            })
+            .catch(error => {
+              self.loading = false;
+              // self.$message.error(error || 'Has Error')
+
+              self.$message.error(error);
+
+              // self.$notify({
+              //   title: "系统提示",
+              //   message: error || "Has Error",
+              //   type: "warning"
+              // });
+            });
+        }
+      });
+    },
+    afterQRScan() {}
+  },
+  created() {
+    // window.addEventListener('hashchange', this.afterQRScan)
+  },
+  destroyed() {
+    // window.removeEventListener('hashchange', this.afterQRScan)
+  },
+  mounted() {}
+};
+</script>
+
+<style rel="stylesheet/scss" lang="scss">
+@import "src/styles/mixin.scss";
+$bg:rgba(242, 247, 253, 1);
+$icon_color: rgba(207,134,146,1);
+$text_color: black;
+
+.login-container {
+  @include relative;
+  height: 100vh;
+  background-color: $bg;
+  background-image:url('../assets/login_bg_element.png');
+  background-size:615px 258px;
+  background-position: bottom right;
+  background-repeat: no-repeat;
+
+  h3{
+    text-align:center;
+  }
+
+  input:-webkit-autofill {
+    -webkit-box-shadow: 0 0 0px 1000px #293444 inset !important;
+    -webkit-text-fill-color: #fff !important;
+  }
+
+  input {
+    background: transparent;
+    border: 0px;
+    -webkit-appearance: none;
+    border-radius: 0px;
+    padding: 12px 5px 12px 15px;
+    color: $text_color;
+    height: 47px;
+  }
+  .el-input {
+    display: inline-block;
+    height: 47px;
+    width: 85%;
+  }
+  .tips {
+    font-size: 14px;
+    color: #fff;
+    margin-bottom: 10px;
+  }
+
+  .title {
+    font-size: 18px;
+    font-weight:normal;
+    color: $text_color;
+    margin: 0px auto 20px auto;
+    text-align: center;
+  }
+
+  .login-form {
+    position: absolute;
+    left: 0;
+    right: 0;
+    width: 400px;
+    padding: 35px 35px 15px 35px;
+    margin: 60px auto;
+  }
+
+  .box-card{
+
+  }
+
+  .login-form i {
+    color: $icon_color;
+    font-size: 14px;
+  }
+
+  @media only screen and (max-width: 768px) {
+    .login-form {
+      position: absolute;
+      left: 0;
+      right: 0;
+      width: 320px;
+      padding: 15px 15px 15px 15px;
+      margin: 120px auto;
+    }
+  }
+
+  .el-form-item {
+    border: 1px solid rgba(228, 228, 228, 1);
+    background: rgba(242, 242, 242, 1);
+    border-radius: 5px;
+    padding-left:5px;
+  }
+  .show-pwd {
+    position: absolute;
+    right: 10px;
+    top: 7px;
+    font-size: 16px;
+    cursor: pointer;
+  }
+  .thirdparty-button {
+    position: absolute;
+    right: 35px;
+    bottom: 28px;
+  }
+}
+</style>

+ 46 - 0
src/views/Tree.vue

@@ -0,0 +1,46 @@
+<template>
+  <div>
+    <el-tree :data="menuData" :props="defaultProps">
+      <span class="custom-tree-node" slot-scope="{ node,data }">
+          <span>
+              <i :class="data.iconClass"></i>{{ data.label }}
+          </span>              
+      </span>         
+    </el-tree>
+  </div>
+</template>
+<script>
+// @ is an alias to /src
+
+export default {
+  name: 'tree',
+  data() {
+    return {
+      menuData: [{
+            label: '案件管理',
+            iconClass: "el-icon-menu",
+            children: [
+              {
+                label: '新增案件',
+                iconClass: "el-icon-menu"
+              },
+              {
+                label: '案件列表',
+                iconClass: "el-icon-menu"
+              }
+            ]
+          }, {
+            label: '服务督办',
+            iconClass: "el-icon-menu"
+          }, {
+            label: '考核评价',
+            iconClass: "el-icon-menu"
+      }],
+      defaultProps: {
+        children: 'children',
+        label: 'label'
+      }
+    };
+  }
+}
+</script>

+ 146 - 0
src/views/layout/index.vue

@@ -0,0 +1,146 @@
+<template>
+  <el-container class="outter-container">
+    <el-header>
+      <h3><img src="../../assets/logo.png" height="50"/></h3>
+      <div class="user-info">
+        <i class="el-icon-s-custom"></i>
+        <span v-html="user.realName" style="margin-right:10px;"></span>
+        <a href="#" @click="logout()">退出</a>
+      </div>
+    </el-header>
+    <el-container>
+      <el-aside width="200px" v-loading="loading">
+        <el-menu
+          class="el-menu-vertical-demo"
+          @open="handleOpen"
+          @close="handleClose"
+          @select="handleSelect"
+        >
+          <menu-tree-item :routes="menuList"></menu-tree-item>
+        </el-menu>
+      </el-aside>
+      <el-main>
+        <router-view/>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+<script>
+import MenuTreeItem from "@/components/MenuTreeItem"
+import menuApi from "@/api/sys/menu"
+import userApi from "@/api/sys/user"
+
+export default {
+  data() {
+    return {
+      menuList: [],
+      loading: false,
+      user: {}
+    };
+  },
+  methods: {
+    handleOpen(key, keyPath) {
+      console.log(key, keyPath);
+    },
+    handleClose(key, keyPath) {
+      console.log(key, keyPath);
+    },
+    handleSelect(key, keyPath) {
+      //console.log(key, keyPath);
+      this.$router.push({ path: key });
+    },
+    logout() {
+      this.$store.dispatch("user/logout").then(() => {
+        this.$router.push({ path: "/login" });
+      });
+    }
+  },
+  components: {
+    "menu-tree-item" : MenuTreeItem
+  },
+  mounted() {
+    this.loading = true;
+
+    userApi.userInfo().then(resp=>{
+      if(resp.data.result){
+        this.user = resp.data.data;
+      }
+    });
+
+    menuApi.getMenuTree().then(response=>{
+      console.log(response);
+
+      var jsonData = response.data;
+
+      this.menuList = jsonData.data;
+    
+      this.loading = false;
+    }).catch(exception=>{
+      this.$message.error(exception + "");
+      this.loading = false;
+    });
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.outter-container {
+  flex: 1;
+}
+
+.el-header {
+  position: relative;
+  background-color: #fff;
+  color: #333;
+  text-align: left;
+  height: 70px !important;
+  line-height:70px;
+  border-bottom: 2px solid rgb(193, 32, 26);
+}
+
+.el-header h3 {
+  margin: 5px;
+  padding: 0px;
+}
+
+.el-aside {
+  color: #333;
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+}
+
+.el-aside .el-menu {
+  flex: 1;
+  background-color:rgb(238, 238, 238);
+}
+
+.el-main {
+  background-color: #fff;
+  color: #333;
+  text-align: center;
+  padding: 0px;
+}
+
+.el-menu-vertical-demo {
+  text-align: left;
+}
+
+.user-info{
+  position: absolute;
+  right:20px;
+  bottom:10px;
+  font-size:14px;
+  line-height: 30px;
+
+  a{
+    color:blue;
+    cursor: pointer;
+    text-decoration: none;
+  }
+
+  a:hover{
+    text-decoration: underline;
+  }
+}
+</style>

+ 184 - 0
src/views/sys/dataDictionary-detail.vue

@@ -0,0 +1,184 @@
+<style scoped>
+.user-panel {
+  margin: 10px auto;
+}
+</style>
+<template>
+  <el-dialog
+    :visible.sync="showDialog"
+    :title="modalTitle"
+    :modal-append-to-body="false"
+    style="text-align:left;"
+    @close="closeDialog"
+  >
+    <div class="user-panel" v-loading="loading">
+      <el-form ref="form" :model="formModel" :rules="ruleValidate" :label-width="'100px'">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="formModel.name" placeholder="请输入名称" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="数值" prop="value">
+          <el-input v-model="formModel.value" placeholder="请输入数值" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="显示序号" prop="sortNo">
+          <el-input v-model="formModel.sortNo" placeholder="请输入显示序号" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="所属目录" prop="parentId">
+          <el-select
+            v-model="formModel.parentId"
+            filterable
+            remote
+            placeholder="请输入关键词"
+            :remote-method="queryMenu"
+            style="width:300px"
+          >
+            <template v-if="formModel.parentId!=null">
+              <el-option
+                :key="formModel.parentId"
+                :label="formModel.parentName"
+                :value="formModel.parentId"
+              ></el-option>
+            </template>
+            <el-option
+              v-for="dataDictionary in dataDictionaryListFilter"
+              :key="dataDictionary.id"
+              :label="dataDictionary.name"
+              :value="dataDictionary.id"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="字典类型" prop="dataType">
+          <el-select v-model="formModel.dataType">
+            <el-option label="字典目录" value="1"></el-option>
+            <el-option label="数据" value="2"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="closeDialog">取 消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+import Constant from "@/constant";
+import dataDictionaryApi from "@/api/sys/dataDictionary";
+
+export default {
+  props: ["dictId", "modalTitle"],
+  data() {
+    return {
+      formModel: {},
+      ruleValidate: {
+        name: [{ required: true, message: "名称不能为空", trigger: "blur" }],
+        value: [{ required: true, message: "数值不能为空", trigger: "blur" }],
+        sortNo: [
+          { required: true, message: "显示序号不能为空", trigger: "blur" }
+        ],
+        parentId: [
+          { required: true, message: "所属目录不能为空", trigger: "blur" }
+        ],
+        dataType: [
+          {
+            required: true,
+            message: "1-字典目录,2-数据不能为空",
+            trigger: "blur"
+          }
+        ]
+      },
+      dataDictionaryList: [],
+      showDialog: true,
+      loading: false,
+      submitting: false
+    };
+  },
+  computed: {
+    dataDictionaryListFilter() {
+      var self = this;
+      return self.dataDictionaryList.filter(dataDictionary => {
+        return self.formModel.parentId != dataDictionary.id;
+      });
+    }
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close", false);
+    },
+    handleSubmit() {
+      var self = this;
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          (function() {
+            var id = self.formModel.id;
+            alert(self.formModel.parentId);
+            if (id == null || id.length == 0) {
+              return dataDictionaryApi.add(self.formModel);
+            } else {
+              return dataDictionaryApi.update(self.formModel);
+            }
+          })().then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              self.$message({
+                message: "保存成功!",
+                type: "success"
+              });
+
+              self.$emit("close", true);
+            } else {
+              self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+
+              self.$emit("close", false);
+            }
+          });
+        }
+      });
+    },
+    queryMenu(keywords) {
+      var formData = new FormData();
+      formData.append("keywords", keywords);
+      formData.append("dataType", "1");
+      formData.append("excludeId", this.formModel.id);
+      formData.append("limit", 10);
+
+      return dataDictionaryApi.query(formData).then(response => {
+        var jsonData = response.data;
+
+        if (jsonData.result) {
+          this.dataDictionaryList = jsonData.data;
+        } else {
+          this.$message.error(jsonData.message + "");
+        }
+      });
+    }
+  },
+  mounted: function() {
+    var self = this;
+    self.loading = true;
+    (function() {
+      if (self.dictId.length == 0) {
+        return dataDictionaryApi.create();
+      } else {
+        return dataDictionaryApi.edit(self.dictId);
+      }
+    })()
+      .then(response => {
+        var jsonData = response.data;
+        self.loading = false;
+
+        if (jsonData.result) {
+          self.formModel = jsonData.data;
+        } else {
+          self.$message.error(jsonData.message + "");
+        }
+      })
+      .catch(error => {
+        self.$message.error(error + "");
+      });
+  }
+};
+</script>

+ 287 - 0
src/views/sys/dataDictionary-list.vue

@@ -0,0 +1,287 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="#">系统管理</a>
+      </el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="/sys/dataDictionary">字典管理</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <!--
+      要resetFields起作用,必须配置:model和prop
+    -->
+    <el-form ref="queryForm" :model="queryModel" inline class="demo-form-inline">
+      <el-form-item label="名称" prop="name">
+        <el-input type="text" size="mini" v-model="queryModel.name"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          size="mini"
+          icon="ios-search"
+          @click="changePage(1)"
+          :loading="loading"
+        >查询</el-button>&nbsp;
+        <el-button
+          type="info"
+          size="mini"
+          style="margin-left: 8px"
+          @click="handleReset('queryForm')"
+        >重置</el-button>&nbsp;
+      </el-form-item>
+    </el-form>
+    <el-divider></el-divider>
+    <el-row class="button-group">
+      <el-button type="primary" size="small" plain icon="el-icon-circle-plus" @click="handleAdd">新增</el-button>
+      <el-button
+        type="primary"
+        size="small"
+        plain
+        icon="el-icon-circle-plus"
+        :disabled="multipleSelection.length==0"
+        @click="handleBatchDelete"
+      >删除选中项</el-button>
+    </el-row>
+    <el-table
+      :data="tableData"
+      style="min-height:400px;"
+      v-loading="loading"
+      stripe
+      @sort-change="sortChange"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55"></el-table-column>
+      <el-table-column prop="name" sort-by="name_" label="名称" sortable="custom" width="180"></el-table-column>
+      <el-table-column prop="value" sort-by="value_" label="数值" sortable="custom" width="180"></el-table-column>
+      <el-table-column prop="sortNo" sort-by="sort_no" label="排序号" sortable="custom" width="180"></el-table-column>
+      <el-table-column
+        prop="parentName"
+        sort-by="parent_name"
+        label="所属目录"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="dataType"
+        sort-by="data_type"
+        label="1-字典目录,2-数据"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column label="操作">
+        <template slot-scope="{row}">
+          <el-button size="mini" type="warning" @click="handleEdit(row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      :current-page.sync="pageIndex"
+      :total="totalElements"
+      :page-sizes="pageSizeList"
+      @current-change="changePage"
+      @size-change="pageSizeChange"
+      layout="total, sizes, prev, pager, next, jumper"
+    ></el-pagination>
+    <dataDictionary-detail
+      v-if="showModal"
+      :dictId="dictId"
+      :modalTitle="modalTitle"
+      @close="onDetailModalClose"
+    ></dataDictionary-detail>
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import DataDictionaryDetail from "./dataDictionary-detail";
+import dataDictionaryApi from "@/api/sys/dataDictionary";
+import NProgress from "nprogress"; // progress bar
+import "nprogress/nprogress.css"; // progress bar style
+
+export default {
+  data() {
+    var self = this;
+
+    return {
+      queryModel: {
+        id: "",
+        name: "",
+        value: "",
+        sortNo: "",
+        parentId: "",
+        parentName: "",
+        dataType: "",
+        createBy: "",
+        createDate: "",
+        updateBy: "",
+        updateDate: "",
+        activated: ""
+      },
+      loading: false,
+      tableData: [],
+      pageIndex: 1,
+      pageSize: 10,
+      totalPages: 0,
+      totalElements: 0,
+      field: "",
+      direction: "",
+      pageSizeList: [10, 20, 30],
+      multipleSelection: [],
+      showModal: false,
+      modalTitle: "",
+      businessKey: ""
+    };
+  },
+  methods: {
+    changePage(pageIndex) {
+      var self = this;
+
+      self.loading = true;
+
+      self.pageIndex = pageIndex;
+      var formData = new FormData();
+
+      formData.append("pageIndex", self.pageIndex);
+      formData.append("pageSize", self.pageSize);
+      formData.append("name", self.queryModel.name);
+
+      if (this.field != null) {
+        formData.append("field", this.field);
+      }
+
+      if (this.direction != null) {
+        formData.append("direction", this.direction);
+      }
+
+      dataDictionaryApi
+        .pageList(formData)
+        .then(function(response) {
+          self.loading = false;
+
+          var jsonData = response.data.data;
+
+          self.tableData = jsonData.data;
+          self.totalPages = jsonData.totalPages;
+          self.totalElements = jsonData.recordsTotal;
+        })
+        .catch(error => {
+          self.loading = false;
+          // self.$message.error(error + "");
+        });
+    },
+    pageSizeChange(pageSize) {
+      this.pageSize = pageSize;
+    },
+    sortChange(data) {
+      this.field = data.column.field;
+      this.direction = data.order;
+
+      this.changePage(this.pageIndex);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val;
+    },
+    handleReset(name) {
+      this.$refs[name].resetFields();
+    },
+    handleAdd() {
+      this.modalTitle = "新增";
+      this.dictId = "";
+      this.showModal = true;
+    },
+    handleEdit(record) {
+      this.modalTitle = "编辑";
+      this.dictId = record.id;
+      this.showModal = true;
+    },
+    handleDelete(record) {
+      var self = this;
+
+      self
+        .$confirm("是否确认删除?", "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        })
+        .then(() => {
+          dataDictionaryApi.remove(record.id).then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              // var index = self.tableData.indexOf(record);
+              // self.tableData.splice(index, 1);
+              self.changePage(self.pageIndex);
+
+              self.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+            }
+          });
+        });
+    },
+    handleBatchDelete() {
+      var self = this;
+
+      var idList = this.multipleSelection.map(record => {
+        return record.id;
+      });
+
+      this.$confirm("是否确认删除选中项?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        dataDictionaryApi.batchRemove(idList).then(function(response) {
+          var jsonData = response.data;
+
+          if (jsonData.result) {
+            self.changePage(self.pageIndex);
+
+            self.$message({
+              type: "success",
+              message: "删除成功!"
+            });
+          }
+        });
+      });
+    },
+    onDetailModalClose(refreshed) {
+      //保存成功后回调
+      this.showModal = false;
+      if (refreshed) {
+        this.changePage(this.pageIndex);
+      }
+    }
+  },
+  mounted: function() {
+    this.changePage(1);
+  },
+  components: {
+    "dataDictionary-detail": DataDictionaryDetail
+  }
+};
+</script>
+<style lang="scss" scoped>
+.el-breadcrumb {
+  margin: 10px;
+  line-height: 20px;
+}
+
+.el-divider {
+  margin: 5px 0;
+}
+
+.demo-form-inline {
+  margin-left: 10px;
+  text-align: left;
+}
+
+.button-group {
+  margin-left: 10px;
+  text-align: left;
+}
+</style>

+ 184 - 0
src/views/sys/dataDictionary-test-detail.vue

@@ -0,0 +1,184 @@
+<style scoped>
+.user-panel {
+  margin: 10px auto;
+}
+</style>
+<template>
+  <el-dialog
+    :visible.sync="showDialog"
+    :title="modalTitle"
+    :modal-append-to-body="false"
+    style="text-align:left;"
+    @close="closeDialog"
+  >
+    <div class="user-panel" v-loading="loading">
+      <el-form ref="form" :model="formModel" :rules="ruleValidate" :label-width="'100px'">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="formModel.name" placeholder="请输入名称" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="数值" prop="value">
+          <el-input v-model="formModel.value" placeholder="请输入数值" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="显示序号" prop="sortNo">
+          <el-input v-model="formModel.sortNo" placeholder="请输入显示序号" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="所属目录" prop="parentId">
+          <el-select
+            v-model="formModel.parentId"
+            filterable
+            remote
+            placeholder="请输入关键词"
+            :remote-method="queryMenu"
+            style="width:300px"
+          >
+            <template v-if="formModel.parentId!=null">
+              <el-option
+                :key="formModel.parentId"
+                :label="formModel.parentName"
+                :value="formModel.parentId"
+              ></el-option>
+            </template>
+            <el-option
+              v-for="dataDictionary in dataDictionaryListFilter"
+              :key="dataDictionary.id"
+              :label="dataDictionary.name"
+              :value="dataDictionary.id"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="字典类型" prop="dataType">
+          <el-select v-model="formModel.dataType">
+            <el-option label="字典目录" value="1"></el-option>
+            <el-option label="数据" value="2"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="closeDialog">取 消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+import Constant from "@/constant";
+import dataDictionaryApi from "@/api/sys/dataDictionary";
+
+export default {
+  props: ["dictId", "modalTitle"],
+  data() {
+    return {
+      formModel: {},
+      ruleValidate: {
+        name: [{ required: true, message: "名称不能为空", trigger: "blur" }],
+        value: [{ required: true, message: "数值不能为空", trigger: "blur" }],
+        sortNo: [
+          { required: true, message: "显示序号不能为空", trigger: "blur" }
+        ],
+        parentId: [
+          { required: true, message: "所属目录不能为空", trigger: "blur" }
+        ],
+        dataType: [
+          {
+            required: true,
+            message: "1-字典目录,2-数据不能为空",
+            trigger: "blur"
+          }
+        ]
+      },
+      dataDictionaryList: [],
+      showDialog: true,
+      loading: false,
+      submitting: false
+    };
+  },
+  computed: {
+    dataDictionaryListFilter() {
+      var self = this;
+      return self.dataDictionaryList.filter(dataDictionary => {
+        return self.formModel.parentId != dataDictionary.id;
+      });
+    }
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close", false);
+    },
+    handleSubmit() {
+      var self = this;
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          (function() {
+            var id = self.formModel.id;
+            alert(self.formModel.parentId);
+            if (id == null || id.length == 0) {
+              return dataDictionaryApi.add(self.formModel);
+            } else {
+              return dataDictionaryApi.update(self.formModel);
+            }
+          })().then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              self.$message({
+                message: "保存成功!",
+                type: "success"
+              });
+
+              self.$emit("close", true);
+            } else {
+              self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+
+              self.$emit("close", false);
+            }
+          });
+        }
+      });
+    },
+    queryMenu(keywords) {
+      var formData = new FormData();
+      formData.append("keywords", keywords);
+      formData.append("dataType", "1");
+      formData.append("excludeId", this.formModel.id);
+      formData.append("limit", 10);
+
+      return dataDictionaryApi.query(formData).then(response => {
+        var jsonData = response.data;
+
+        if (jsonData.result) {
+          this.dataDictionaryList = jsonData.data;
+        } else {
+          this.$message.error(jsonData.message + "");
+        }
+      });
+    }
+  },
+  mounted: function() {
+    var self = this;
+    self.loading = true;
+    (function() {
+      if (self.dictId.length == 0) {
+        return dataDictionaryApi.create();
+      } else {
+        return dataDictionaryApi.edit(self.dictId);
+      }
+    })()
+      .then(response => {
+        var jsonData = response.data;
+        self.loading = false;
+
+        if (jsonData.result) {
+          self.formModel = jsonData.data;
+        } else {
+          self.$message.error(jsonData.message + "");
+        }
+      })
+      .catch(error => {
+        self.$message.error(error + "");
+      });
+  }
+};
+</script>

+ 287 - 0
src/views/sys/dataDictionary-test-list.vue

@@ -0,0 +1,287 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="#">系统管理</a>
+      </el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="/sys/dataDictionary">字典管理test</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <!--
+      要resetFields起作用,必须配置:model和prop
+    -->
+    <el-form ref="queryForm" :model="queryModel" inline class="demo-form-inline">
+      <el-form-item label="test名称" prop="name">
+        <el-input type="text" size="mini" v-model="queryModel.name"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          size="mini"
+          icon="ios-search"
+          @click="changePage(1)"
+          :loading="loading"
+        >查询</el-button>&nbsp;
+        <el-button
+          type="info"
+          size="mini"
+          style="margin-left: 8px"
+          @click="handleReset('queryForm')"
+        >重置</el-button>&nbsp;
+      </el-form-item>
+    </el-form>
+    <el-divider></el-divider>
+    <el-row class="button-group">
+      <el-button type="primary" size="small" plain icon="el-icon-circle-plus" @click="handleAdd">新增</el-button>
+      <el-button
+        type="primary"
+        size="small"
+        plain
+        icon="el-icon-circle-plus"
+        :disabled="multipleSelection.length==0"
+        @click="handleBatchDelete"
+      >删除选中项</el-button>
+    </el-row>
+    <el-table
+      :data="tableData"
+      style="min-height:400px;"
+      v-loading="loading"
+      stripe
+      @sort-change="sortChange"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55"></el-table-column>
+      <el-table-column prop="name" sort-by="name_" label="名称" sortable="custom" width="180"></el-table-column>
+      <el-table-column prop="value" sort-by="value_" label="数值" sortable="custom" width="180"></el-table-column>
+      <el-table-column prop="sortNo" sort-by="sort_no" label="排序号" sortable="custom" width="180"></el-table-column>
+      <el-table-column
+        prop="parentName"
+        sort-by="parent_name"
+        label="所属目录"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="dataType"
+        sort-by="data_type"
+        label="1-字典目录,2-数据"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column label="操作">
+        <template slot-scope="{row}">
+          <el-button size="mini" type="warning" @click="handleEdit(row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      :current-page.sync="pageIndex"
+      :total="totalElements"
+      :page-sizes="pageSizeList"
+      @current-change="changePage"
+      @size-change="pageSizeChange"
+      layout="total, sizes, prev, pager, next, jumper"
+    ></el-pagination>
+    <dataDictionary-detail
+      v-if="showModal"
+      :dictId="dictId"
+      :modalTitle="modalTitle"
+      @close="onDetailModalClose"
+    ></dataDictionary-detail>
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import DataDictionaryDetail from "./dataDictionary-detail";
+import dataDictionaryApi from "@/api/sys/dataDictionary";
+import NProgress from "nprogress"; // progress bar
+import "nprogress/nprogress.css"; // progress bar style
+
+export default {
+  data() {
+    var self = this;
+
+    return {
+      queryModel: {
+        id: "",
+        name: "",
+        value: "",
+        sortNo: "",
+        parentId: "",
+        parentName: "",
+        dataType: "",
+        createBy: "",
+        createDate: "",
+        updateBy: "",
+        updateDate: "",
+        activated: ""
+      },
+      loading: false,
+      tableData: [],
+      pageIndex: 1,
+      pageSize: 10,
+      totalPages: 0,
+      totalElements: 0,
+      field: "",
+      direction: "",
+      pageSizeList: [10, 20, 30],
+      multipleSelection: [],
+      showModal: false,
+      modalTitle: "",
+      businessKey: ""
+    };
+  },
+  methods: {
+    changePage(pageIndex) {
+      var self = this;
+
+      self.loading = true;
+
+      self.pageIndex = pageIndex;
+      var formData = new FormData();
+
+      formData.append("pageIndex", self.pageIndex);
+      formData.append("pageSize", self.pageSize);
+      formData.append("name", self.queryModel.name);
+
+      if (this.field != null) {
+        formData.append("field", this.field);
+      }
+
+      if (this.direction != null) {
+        formData.append("direction", this.direction);
+      }
+
+      dataDictionaryApi
+        .pageList(formData)
+        .then(function(response) {
+          self.loading = false;
+
+          var jsonData = response.data.data;
+
+          self.tableData = jsonData.data;
+          self.totalPages = jsonData.totalPages;
+          self.totalElements = jsonData.recordsTotal;
+        })
+        .catch(error => {
+          self.loading = false;
+          // self.$message.error(error + "");
+        });
+    },
+    pageSizeChange(pageSize) {
+      this.pageSize = pageSize;
+    },
+    sortChange(data) {
+      this.field = data.column.field;
+      this.direction = data.order;
+
+      this.changePage(this.pageIndex);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val;
+    },
+    handleReset(name) {
+      this.$refs[name].resetFields();
+    },
+    handleAdd() {
+      this.modalTitle = "新增";
+      this.dictId = "";
+      this.showModal = true;
+    },
+    handleEdit(record) {
+      this.modalTitle = "编辑";
+      this.dictId = record.id;
+      this.showModal = true;
+    },
+    handleDelete(record) {
+      var self = this;
+
+      self
+        .$confirm("是否确认删除?", "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        })
+        .then(() => {
+          dataDictionaryApi.remove(record.id).then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              // var index = self.tableData.indexOf(record);
+              // self.tableData.splice(index, 1);
+              self.changePage(self.pageIndex);
+
+              self.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+            }
+          });
+        });
+    },
+    handleBatchDelete() {
+      var self = this;
+
+      var idList = this.multipleSelection.map(record => {
+        return record.id;
+      });
+
+      this.$confirm("是否确认删除选中项?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        dataDictionaryApi.batchRemove(idList).then(function(response) {
+          var jsonData = response.data;
+
+          if (jsonData.result) {
+            self.changePage(self.pageIndex);
+
+            self.$message({
+              type: "success",
+              message: "删除成功!"
+            });
+          }
+        });
+      });
+    },
+    onDetailModalClose(refreshed) {
+      //保存成功后回调
+      this.showModal = false;
+      if (refreshed) {
+        this.changePage(this.pageIndex);
+      }
+    }
+  },
+  mounted: function() {
+    this.changePage(1);
+  },
+  components: {
+    "dataDictionary-detail": DataDictionaryDetail
+  }
+};
+</script>
+<style lang="scss" scoped>
+.el-breadcrumb {
+  margin: 10px;
+  line-height: 20px;
+}
+
+.el-divider {
+  margin: 5px 0;
+}
+
+.demo-form-inline {
+  margin-left: 10px;
+  text-align: left;
+}
+
+.button-group {
+  margin-left: 10px;
+  text-align: left;
+}
+</style>

+ 208 - 0
src/views/sys/menu-detail.vue

@@ -0,0 +1,208 @@
+<style scoped>
+.user-panel {
+  margin: 10px auto;
+}
+</style>
+<template>
+  <el-dialog
+    :visible.sync="showDialog"
+    :title="modalTitle"
+    :modal-append-to-body="false"
+    style="text-align:left;"
+    @close="closeDialog"
+  >
+    <div class="user-panel" v-loading="loading">
+      <el-form ref="form" :model="formModel" :rules="ruleValidate" :label-width="'100px'">
+        <el-form-item label="菜单名称" prop="menuName">
+          <el-input v-model="formModel.menuName" placeholder="请输入菜单名称" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="上级菜单" prop="parentId">
+          <el-select
+            v-model="formModel.parentId"
+            filterable
+            remote
+            placeholder="请输入关键词"
+            :remote-method="queryMenu"
+            style="width:300px"
+          >
+            <!-- <template v-if="formModel.parentId!=null">
+              <el-option
+                :key="formModel.parentId"
+                :label="formModel.parentName"
+                :value="formModel.parentId"
+              ></el-option>
+            </template> -->
+            <el-option
+              v-for="menu in menuListFilter"
+              :key="menu.id"
+              :label="menu.menuName"
+              :value="menu.id"
+            ></el-option>
+          </el-select>
+          <el-button icon="el-icon-search" circle size="small" @click="innerVisible=true;"></el-button>
+        </el-form-item>
+        <el-form-item label="排序号" prop="sortNo">
+          <el-input v-model="formModel.sortNo" placeholder="请输入排序号" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="菜单地址" prop="menuUrl">
+          <el-input v-model="formModel.menuUrl" placeholder="请输入菜单地址" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="图标样式" prop="icon">
+          <el-input v-model="formModel.icon" placeholder="请输入图标样式" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="菜单类型" prop="menuType">
+          <el-select v-model="formModel.menuType">
+            <el-option label="后端接口" value="1"></el-option>
+            <el-option label="前端页面" value="2"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="closeDialog">取 消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+    </span>    
+    <el-dialog
+        width="30%"
+        title="内层 Dialog"
+        :visible.sync="innerVisible"
+        append-to-body>
+      </el-dialog>
+  </el-dialog>
+</template>
+<script>
+import Constant from "@/constant";
+import menuApi from "@/api/sys/menu";
+
+export default {
+  props: ["menuId","modalTitle"],
+  data() {
+    return {
+      formModel : {},
+      innerVisible: false,
+      ruleValidate: {
+        menuName: [
+          { required: true, message: "菜单名称不能为空", trigger: "blur" }
+        ],
+        sortNo: [
+          { required: true, message: "排序号不能为空", trigger: "blur" }
+        ],
+        menuUrl: [
+          { required: true, message: "菜单地址不能为空", trigger: "blur" }
+        ],
+        menuType: [
+          {
+            required: true,
+            message: "菜单类型不能为空",
+            trigger: "blur"
+          }
+        ]
+      },
+      menuList:[],
+      loading: false,
+      submitting: false,
+      showDialog: true
+    };
+  },
+  computed: {
+    menuListFilter () {
+      var self = this;
+
+      return self.menuList.filter((menu)=>{
+        return self.formModel.parentId != menu.id;
+      });
+    }
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close",false);
+    },
+    handleSubmit() {
+      var self = this;
+
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          (function() {
+            var id = self.formModel.id;
+
+            if (id == null || id.length == 0) {
+              return menuApi.add(self.formModel);
+            } else {
+              return menuApi.update(self.formModel);
+            }
+          })().then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              self.$message({
+                message: "保存成功!",
+                type: "success"
+              });
+
+              self.$emit("close",true);
+            } else {
+              self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+
+              self.$emit("close",false);
+            }
+          });
+        } else {
+          this.$emit("error");
+        }
+      });
+    },
+    queryMenu(keywords) {
+      var formData = new FormData();
+      formData.append("keywords",keywords);
+      formData.append("excludeId",this.formModel.id);
+      formData.append("limit",10);
+
+      return menuApi.query(formData).then(response=>{
+        var jsonData = response.data;
+
+        if(jsonData.result){
+          this.menuList = jsonData.data;
+        }
+        else{
+          this.$message.error(jsonData.message + "");
+        }
+      });
+    }
+  },
+  async mounted() {
+    var self = this;
+    self.loading = true;
+
+    await this.queryMenu('');
+
+    (function(){
+      if(self.menuId.length==0){
+        return menuApi.create()
+      }
+      else{
+        return menuApi.edit(self.menuId)
+      }
+    })().then(response => {
+      var jsonData = response.data;
+      self.loading = false;
+
+      if (jsonData.result) {
+        self.formModel = jsonData.data;
+
+        // 增加初始值
+        self.menuList.push({
+          id: self.formModel.parentId,
+          name: self.formModel.parentName
+        });
+      } else {
+        self.$message.error(jsonData.message + "");
+      }
+    }).catch(error => {
+      self.$message.error(error + "");
+    });
+  }
+};
+</script>

+ 344 - 0
src/views/sys/menu-list.vue

@@ -0,0 +1,344 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="#">系统管理</a>
+      </el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="/sys/menu">菜单管理</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <!--
+      要resetFields起作用,必须配置:model和prop
+    -->
+    <el-form ref="queryForm" :model="queryModel" inline class="demo-form-inline">
+      <el-form-item label="菜单名称" prop="menuName">
+        <el-input type="text" size="mini" v-model="queryModel.menuName"></el-input>
+      </el-form-item>
+      <el-form-item label="菜单类型" prop="menuType">
+        <el-select v-model="queryModel.menuType">
+          <el-option label="后端接口" value="1"></el-option>
+          <el-option label="前端页面" value="2"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="上级菜单" prop="parentId">
+        <el-select
+            v-model="queryModel.parentId"
+            filterable
+            remote
+            placeholder="请输入关键词"
+            :remote-method="queryMenu"
+          >
+            <el-option
+              v-for="menu in queryMenuResult"
+              :key="menu.id"
+              :label="menu.menuName"
+              :value="menu.id"
+            ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          size="mini"
+          icon="ios-search"
+          @click="changePage(1)"
+          :loading="loading"
+        >查询</el-button>&nbsp;
+        <el-button
+          type="info"
+          size="mini"
+          style="margin-left: 8px"
+          @click="handleReset('queryForm')"
+        >重置</el-button>&nbsp;
+      </el-form-item>
+    </el-form>
+    <el-divider></el-divider>
+    <el-row class="button-group">
+      <el-button type="primary" size="small" plain icon="el-icon-circle-plus" @click="handleAdd">新增</el-button>
+      <el-button
+        type="primary"
+        size="small"
+        plain
+        icon="el-icon-circle-plus"
+        :disabled="multipleSelection.length==0"
+        @click="handleBatchDelete"
+      >删除选中项</el-button>
+    </el-row>
+    <div style="min-height:400px;">
+    <el-table
+      :data="tableData"
+      v-loading="loading"
+      stripe
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55"></el-table-column>
+      <el-table-column
+        prop="menuName"
+        sort-by="menu_name"
+        label="菜单名称"
+        width="180"
+      >
+       <template slot-scope="{row}">
+         <i :class="row.icon"></i><span v-html="row.menuName"></span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="parentName"
+        sort-by="parent_id"
+        label="上级菜单"
+        width="180"
+      ></el-table-column>
+      <el-table-column prop="sortNo" label="排序号" :sortable="true" width="180"></el-table-column>
+      <el-table-column prop="menuType" label="菜单类型" :sortable="true" width="180">
+        <template slot-scope="{row}">{{row.menuType == 1 ? "后端接口" : "前端页面"}}</template>
+      </el-table-column>
+      <el-table-column prop="menuUrl" label="菜单地址" :sortable="true" width="180"></el-table-column>
+      <el-table-column prop="createTime" label="创建时间" :sortable="true" width="180"></el-table-column>
+      <el-table-column prop="updateTime" label="更新时间" :sortable="true" width="180"></el-table-column>
+      <el-table-column label="操作" width="200" fixed="right">
+        <template slot-scope="{row}">
+          <el-button size="mini" type="warning" @click="handleEdit(row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    </div>
+    <el-pagination
+      :current-page.sync="pageIndex"
+      :total="totalElements"
+      :page-sizes="pageSizeList"
+      @current-change="changePage"
+      @size-change="pageSizeChange"
+      layout="total, sizes, prev, pager, next, jumper"
+    ></el-pagination>
+
+      <menu-detail
+        v-if="showDetailModal"
+        @close="onDetailModalClose"
+        :menuId="menuId"
+        :modalTitle="modalTitle"
+      ></menu-detail>
+
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import MenuDetail from "./menu-detail";
+import menuApi from "@/api/sys/menu";
+import NProgress from "nprogress"; // progress bar
+import "nprogress/nprogress.css"; // progress bar style
+
+export default {
+  data() {
+    var self = this;
+
+    return {
+      queryModel: {
+        menuName: "",
+        menuType: "",
+        parentId: ""
+      },
+      mLoading: false,
+      modalTitle: "",
+      loading: false,
+      tableData: [],
+      pageIndex: 1,
+      pageSize: 10,
+      totalPages: 0,
+      totalElements: 0,
+      field: "",
+      direction: "",
+      pageSizeList: [10, 20, 30],
+      multipleSelection: [],
+      showDetailModal: false,
+      menuId: "",
+      queryMenuResult: []
+    };
+  },
+  methods: {
+    changePage(pageIndex) {
+      var self = this;
+
+      self.loading = true;
+
+      self.pageIndex = pageIndex;
+      var formData = new FormData();
+
+      formData.append("pageIndex", self.pageIndex);
+      formData.append("pageSize", self.pageSize);
+
+      formData.append("parentId", self.queryModel.parentId);
+      formData.append("menuName", self.queryModel.menuName);
+      formData.append("menuType", self.queryModel.menuType);
+
+      if (this.field != null) {
+        formData.append("field", this.field);
+      }
+
+      if (this.direction != null) {
+        formData.append("direction", this.direction);
+      }
+
+      menuApi
+        .pageList(formData)
+        .then(function(response) {
+          self.loading = false;
+
+          var jsonData = response.data;
+
+          if(jsonData.result){
+            var pageInfo = jsonData.data;
+
+            self.tableData = pageInfo.data;
+            self.totalPages = pageInfo.totalPages;
+            self.totalElements = pageInfo.recordsTotal;
+          }
+          else {
+            self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+          }
+        })
+        .catch(error => {
+          self.loading = false;
+          // self.$message.error(error + "");
+        });
+    },
+    pageSizeChange(pageSize) {
+      this.pageSize = pageSize;
+    },
+    sortChange(data) {
+      this.field = data.column.field;
+      this.direction = data.order;
+
+      this.changePage(this.pageIndex);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val;
+    },
+    handleReset(name) {
+      this.$refs[name].resetFields();
+    },
+    handleAdd() {
+      var self = this;
+
+      self.modalTitle = "新增";
+      self.menuId = "";
+      self.showDetailModal = true;
+
+    },
+    handleEdit(record) {
+      var self = this;
+
+      self.modalTitle = "编辑";
+      self.menuId = record.id;
+      self.showDetailModal = true;
+    },
+    handleDelete(record) {
+      var self = this;
+
+      self
+        .$confirm("是否确认删除?", "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        })
+        .then(() => {
+          menuApi.remove(record.id).then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              // var index = self.tableData.indexOf(record);
+              // self.tableData.splice(index, 1);
+              self.changePage(self.pageIndex);
+
+              self.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+            }
+          });
+        });
+    },
+    handleBatchDelete() {
+      var self = this;
+
+      var idList = this.multipleSelection.map(record => {
+        return record.id;
+      });
+
+      this.$confirm("是否确认删除选中项?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        menuApi.batchRemove(idList).then(function(response) {
+          var jsonData = response.data;
+
+          if (jsonData.result) {
+            self.changePage(self.pageIndex);
+
+            self.$message({
+              type: "success",
+              message: "删除成功!"
+            });
+          }
+        });
+      });
+    },   
+    onDetailModalClose(refreshed){
+      this.showDetailModal = false;
+
+      if(refreshed){
+        this.changePage(this.pageIndex);
+      }
+    },
+    queryMenu(keywords) {
+      var formData = new FormData();
+      formData.append("keywords",keywords);
+      formData.append("limit",10);
+
+      return menuApi.query(formData).then(response=>{
+        var jsonData = response.data;
+
+        if(jsonData.result){
+          this.queryMenuResult = jsonData.data;
+        }
+        else{
+          this.$message.error(jsonData.message + "");
+        }
+      });
+    }
+  },
+  mounted: function() {
+    this.changePage(1);
+  },
+  components: {
+    "menu-detail": MenuDetail
+  }
+};
+</script>
+<style lang="scss" scoped>
+.el-breadcrumb {
+  margin: 10px;
+  line-height: 20px;
+}
+
+.el-divider {
+  margin: 5px 0;
+}
+
+.demo-form-inline {
+  margin-left: 10px;
+  text-align: left;
+}
+
+.button-group {
+  margin-left: 10px;
+  text-align: left;
+}
+</style>

+ 128 - 0
src/views/sys/permission-detail.vue

@@ -0,0 +1,128 @@
+<style scoped>
+.user-panel {
+  margin: 10px auto;
+}
+</style>
+<template>
+    <el-dialog
+      :visible.sync="showDialog"
+      :title="modalTitle"
+      :modal-append-to-body="false"
+      style="text-align:left;"
+      @close="closeDialog"
+    >
+  <div class="user-panel" v-loading="loading">
+    <el-form ref="form" :model="formModel" :rules="ruleValidate" :label-width="'100px'">
+      <el-form-item label="访问路径" prop="path">
+        <el-input v-model="formModel.path" placeholder="请输入访问路径" style="width:300px"></el-input>
+      </el-form-item>
+      <el-form-item label="访问方式" prop="method">
+        <el-select v-model="formModel.method" placeholder="请选择访问方式" style="width:300px">
+          <el-option label="get" value="get"></el-option>
+          <el-option label="post" value="post"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="简介" prop="summary">
+        <el-input v-model="formModel.summary" placeholder="请输入简介" style="width:300px"></el-input>
+      </el-form-item>
+    </el-form>
+  </div>
+  <span slot="footer" class="dialog-footer">
+        <el-button @click="closeDialog">取 消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+      </span>
+    </el-dialog>
+</template>
+<script>
+import Constant from "@/constant";
+import permissionApi from "@/api/sys/permission";
+
+export default {
+  props: ["permId","modalTitle"],
+  data() {
+    return {
+      loading: false,
+      submitting: false,
+      showDialog: true,
+      formModel:{},
+      ruleValidate: {
+        path: [
+          { required: true, message: "访问路径不能为空", trigger: "blur" }
+        ],
+        method: [
+          { required: true, message: "访问方式不能为空", trigger: "blur" }
+        ],
+        summary: [{ required: true, message: "简介不能为空", trigger: "blur" }]
+      }
+    };
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close",false);
+    },
+    handleSubmit() {
+      var self = this;
+
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          (function() {
+            var id = self.formModel.id;
+
+            if (id == null || id.length == 0) {
+              return permissionApi.add(self.formModel);
+            } else {
+              return permissionApi.update(self.formModel);
+            }
+          })().then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              self.$message({
+                message: "保存成功!",
+                type: "success"
+              });
+
+              self.$emit("close",true);
+            } else {
+              self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+
+              self.$emit("close",false);
+            }
+          });
+        } else {
+          this.$emit("error");
+        }
+      });
+    }
+  },
+  mounted: function() {
+    var self = this;
+    self.loading = true;
+    
+    (function(){
+      if(self.permId==null || self.permId.length==0){
+        return permissionApi.create();
+      }
+      else{
+        return permissionApi.edit(self.permId);
+      }
+    })().then(response => {
+      var jsonData = response.data;
+      self.loading = false;
+
+      if (jsonData.result) {
+        self.formModel = jsonData.data;
+      } else {
+        self.$message.error(jsonData.message + "");
+      }
+    })
+    .catch(error => {
+      self.$message.error(error + "");
+      self.loading = false;
+    });
+  }
+};
+</script>

+ 137 - 0
src/views/sys/permission-import.vue

@@ -0,0 +1,137 @@
+<template>
+    <el-dialog
+      :visible.sync="showDialog"
+      title="接口导入"
+      :modal-append-to-body="false"
+      style="text-align:left;"
+      width="800px"
+      @close="closeDialog"
+    >
+  <div class="permission-import">
+    <el-transfer
+      v-model="relatedPerms"
+      :data="apiList"
+      v-loading="loading"
+      filterable
+      :filter-method="filterMethod"
+      filter-placeholder="请输入关键字"
+      :props="{key: 'id',label: 'description'}"
+      :titles="['未导入接口','已导入接口']"
+    ></el-transfer>
+  </div>
+        <span slot="footer" class="dialog-footer">
+        <el-button @click="closeDialog">取 消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+      </span>
+    </el-dialog>
+</template>
+<script>
+import backendApi from "@/api/sys/backend";
+import permissionApi from "@/api/sys/permission";
+
+export default {
+  props: ["permId"],
+  data() {
+    return {
+      showDialog: true,
+      apiList: [],
+      relatedPerms: [],
+      loading: false,
+      submitting: false
+    };
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close", false);
+    },
+    handleSubmit() {
+      var self = this;
+      self.submitting = true;
+
+      var dataList = this.apiList.filter(item => {
+          return self.relatedPerms.indexOf(item.id)>=0;
+      }).map(item=>{
+        return {
+          method: item.method,
+          path: item.path,
+          summary: item.summary
+        }
+      });
+
+      permissionApi.batchImport(dataList).then(function(response) {
+        var jsonData = response.data;
+        self.submitting = false;
+
+        if (jsonData.result) {
+          self.$message({
+            message: "保存成功!",
+            type: "success"
+          });
+
+          self.$emit("close", true);
+        } else {
+          self.$message({
+            message: jsonData.message + "",
+            type: "warning"
+          });
+
+          self.$emit("close", false);
+        }
+      });
+    },
+    filterMethod(query, item) {
+      return item.description.toLowerCase().indexOf(query) > -1;
+    }
+  },
+  mounted() {
+    var self = this;
+    self.loading = true;
+
+    backendApi.selectAll().then(response => {
+      var jsonData = eval(response.data);
+
+      for (var path in jsonData.paths) {
+        for (var method in jsonData.paths[path]) {
+          var summary = jsonData.paths[path][method].summary;
+
+          self.apiList.push({
+            id: method + " " + path,
+            description: method + " " + path + " " + summary,
+            method,
+            path,
+            summary
+          });
+        }
+      }
+
+      return permissionApi.selectAll();
+    }).then(response=>{
+      var jsonData = response.data;
+
+      self.relatedPerms = jsonData.data.map(item => {
+        return item.method + " " + item.path;
+      });
+
+      self.loading = false;
+    });
+  }
+};
+</script>
+<style lang="scss">
+.permission-import {
+
+  .el-transfer-panel {
+    border: 1px solid #ebeef5;
+    border-radius: 4px;
+    overflow: hidden;
+    background: #fff;
+    display: inline-block;
+    vertical-align: middle;
+    width: 320px;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    position: relative;
+  }
+
+}
+</style>

+ 320 - 0
src/views/sys/permission-list.vue

@@ -0,0 +1,320 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="#">系统管理</a>
+      </el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="/sys/permission">接口权限管理</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <!--
+      要resetFields起作用,必须配置:model和prop
+    -->
+    <el-form ref="queryForm" :model="queryModel" inline class="demo-form-inline">
+      <el-form-item label="访问路径" prop="path">
+        <el-input type="text" size="mini" v-model="queryModel.path"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          size="mini"
+          icon="ios-search"
+          @click="changePage(1)"
+          :loading="loading"
+        >查询</el-button>&nbsp;
+        <el-button
+          type="info"
+          size="mini"
+          style="margin-left: 8px"
+          @click="handleReset('queryForm')"
+        >重置</el-button>&nbsp;
+      </el-form-item>
+    </el-form>
+    <el-divider></el-divider>
+    <el-row class="button-group">
+      <el-button
+        type="primary"
+        size="small"
+        plain
+        icon="el-icon-document-add"
+        @click="handleImport"
+      >导入</el-button>
+      <el-button type="primary" size="small" plain icon="el-icon-circle-plus" @click="handleAdd">新增</el-button>
+      <el-button
+        type="primary"
+        size="small"
+        plain
+        icon="el-icon-circle-plus"
+        :disabled="multipleSelection.length==0"
+        @click="handleBatchDelete"
+      >删除选中项</el-button>
+    </el-row>
+    <el-table
+      :data="tableData"
+      style="min-height:400px;"
+      v-loading="loading"
+      stripe
+      @sort-change="sortChange"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55"></el-table-column>
+      <el-table-column prop="path" sort-by="path_" label="访问路径" sortable="custom" width="210"></el-table-column>
+      <el-table-column prop="method" sort-by="method_" label="访问方式" sortable="custom" width="150"></el-table-column>
+      <el-table-column prop="summary" sort-by="summary_" label="简介" sortable="custom" width="180"></el-table-column>
+      <el-table-column
+        prop="createTime"
+        sort-by="create_time"
+        label="创建时间"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="updateTime"
+        sort-by="update_time"
+        label="更新时间"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column label="操作">
+        <template slot-scope="{row}">
+          <el-button size="mini" type="warning" @click="handleEdit(row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      :current-page.sync="pageIndex"
+      :total="totalElements"
+      :page-sizes="pageSizeList"
+      @current-change="changePage"
+      @size-change="pageSizeChange"
+      layout="total, sizes, prev, pager, next, jumper"
+    ></el-pagination>
+      <permission-detail
+        v-if="showDetailModal"
+        @close="onDetailModalClose"
+        :modalTitle="modalTitle"
+        :permId="permId"
+      ></permission-detail>
+
+      <permission-import
+        v-if="showImportModal"
+        @close="onImportModalClose"
+        :permId="permId"
+      ></permission-import>
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import PermissionImport from "./permission-import";
+import PermissionDetail from "./permission-detail";
+import permissionApi from "@/api/sys/permission";
+import NProgress from "nprogress"; // progress bar
+import "nprogress/nprogress.css"; // progress bar style
+
+export default {
+  data() {
+    var self = this;
+
+    return {
+      queryModel: {
+        path: ""
+      },
+      loading: false,
+      tableData: [],
+      pageIndex: 1,
+      pageSize: 10,
+      totalPages: 0,
+      totalElements: 0,
+      field: "",
+      direction: "",
+      pageSizeList: [10, 20, 30],
+      multipleSelection: [],
+      permId:"",
+      modalTitle: "",
+      showDetailModal: false,
+      showImportModal: false
+    };
+  },
+  methods: {
+    changePage(pageIndex) {
+      var self = this;
+
+      self.loading = true;
+
+      self.pageIndex = pageIndex;
+      var formData = new FormData();
+
+      formData.append("pageIndex", self.pageIndex);
+      formData.append("pageSize", self.pageSize);
+
+      formData.append("path", self.queryModel.path);
+
+      if (this.field != null) {
+        formData.append("field", this.field);
+      }
+
+      if (this.direction != null) {
+        formData.append("direction", this.direction);
+      }
+
+      permissionApi
+        .pageList(formData)
+        .then(function(response) {
+          self.loading = false;
+
+          var jsonData = response.data;
+
+          if(jsonData.result){
+            var pageInfo = jsonData.data;
+
+            self.tableData = pageInfo.data;
+            self.totalPages = pageInfo.totalPages;
+            self.totalElements = pageInfo.recordsTotal;
+          }
+          else {
+            self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+          }
+        })
+        .catch(error => {
+          self.loading = false;
+          // self.$message.error(error + "");
+        });
+    },
+    pageSizeChange(pageSize) {
+      this.pageSize = pageSize;
+    },
+    sortChange(data) {
+      this.field = data.column.field;
+      this.direction = data.order;
+
+      this.changePage(this.pageIndex);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val;
+    },
+    handleReset(name) {
+      this.$refs[name].resetFields();
+    },
+    handleAdd() {
+      var self = this;
+      
+      self.modalTitle = "新增";
+      self.permId = "";
+      self.showDetailModal = true;
+    },
+    handleEdit(record) {
+      var self = this;
+
+      self.modalTitle = "编辑";
+      self.permId = record.id;
+      self.showDetailModal = true;
+    },
+    handleDelete(record) {
+      var self = this;
+
+      self
+        .$confirm("是否确认删除?", "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        })
+        .then(() => {
+          permissionApi.remove(record.id).then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              // var index = self.tableData.indexOf(record);
+              // self.tableData.splice(index, 1);
+              self.changePage(self.pageIndex);
+
+              self.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+            }
+          });
+        });
+    },
+    handleBatchDelete() {
+      var self = this;
+
+      var idList = this.multipleSelection.map(record => {
+        return record.id;
+      });
+
+      this.$confirm("是否确认删除选中项?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        permissionApi.batchRemove(idList).then(function(response) {
+          var jsonData = response.data;
+
+          if (jsonData.result) {
+            self.changePage(self.pageIndex);
+
+            self.$message({
+              type: "success",
+              message: "删除成功!"
+            });
+          }
+        });
+      });
+    },
+    handleImport() {
+      // 读取所有菜单
+      var self = this;
+
+      self.showImportModal = true;
+    },
+    onDetailModalClose(refreshed){
+      this.showDetailModal = false;
+
+      if(refreshed){
+        this.changePage(this.pageIndex);
+      }
+    },
+    onImportModalClose(refreshed){
+      this.showImportModal = false;
+
+      if(refreshed){
+        this.changePage(this.pageIndex);
+      }
+    }
+  },
+  mounted: function() {
+    this.changePage(1);
+  },
+  components: {
+    "permission-detail": PermissionDetail,
+    "permission-import": PermissionImport
+  }
+};
+</script>
+<style lang="scss" scoped>
+.el-breadcrumb {
+  margin: 10px;
+  line-height: 20px;
+}
+
+.el-divider {
+  margin: 5px 0;
+}
+
+.demo-form-inline {
+  margin-left: 10px;
+  text-align: left;
+}
+
+.button-group {
+  margin-left: 10px;
+  text-align: left;
+}
+</style>

+ 121 - 0
src/views/sys/role-detail.vue

@@ -0,0 +1,121 @@
+<style scoped>
+.user-panel {
+  margin: 10px auto;
+}
+</style>
+<template>
+    <el-dialog
+      :visible.sync="showModal"
+      :title="modalTitle"
+      :modal-append-to-body="false"
+      style="text-align:left;"
+    >
+  <div class="user-panel" v-loading="loading">
+    <el-form ref="form" :model="formModel" :rules="ruleValidate" :label-width="'100px'">
+      <el-form-item label="角色名称" prop="name">
+        <el-input v-model="formModel.name" placeholder="请输入角色名称" style="width:300px"></el-input>
+      </el-form-item>
+      <el-form-item label="角色描述" prop="description">
+        <el-input v-model="formModel.description" placeholder="请输入角色描述" style="width:300px"></el-input>
+      </el-form-item>
+    </el-form>
+  </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="handleCancel">取 消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+      </span>
+    </el-dialog>
+</template>
+<script>
+import Constant from "@/constant";
+import roleApi from "@/api/sys/role";
+import NProgress from "nprogress"; // progress bar
+
+export default {
+  props: ["roleId","modalTitle"],
+  data() {
+    return {
+      showModal: true,
+      loading: false,
+      submitting: false,
+      formModel:{},
+      ruleValidate: {
+        name: [
+          { required: true, message: "角色名称不能为空", trigger: "blur" }
+        ],
+        description: [
+          { required: true, message: "角色描述不能为空", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  methods: {
+    handleCancel() {
+      this.$emit("close",false);
+    },
+    handleSubmit() {
+      var self = this;
+
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          (function() {
+            var id = self.formModel.id;
+
+            if (id == null || id.length == 0) {
+              return roleApi.add(self.formModel);
+            } else {
+              return roleApi.update(self.formModel);
+            }
+          })().then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              self.$message({
+                message: "保存成功!",
+                type: "success"
+              });
+
+              self.$emit("close",true);
+            } else {
+              self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+
+              self.$emit("close",false);
+            }
+          });
+        }
+      });
+    }
+  },
+  mounted: function() {
+      var self = this;
+
+      self.loading = true;
+
+      (function(){
+        if(self.roleId==null || self.roleId.length==0){
+          return roleApi.create();
+        }
+        else{
+          return roleApi.edit(self.roleId);
+        }
+      })().then(response => {
+        var jsonData = response.data;
+        self.loading = false;
+
+        if (jsonData.result) {
+          self.formModel = jsonData.data;
+        } else {
+          self.$message.error(jsonData.message + "");
+        }
+      })
+      .catch(error => {
+        self.loading = false;
+
+        self.$message.error(error + "");
+      });
+  }
+};
+</script>

+ 348 - 0
src/views/sys/role-list.vue

@@ -0,0 +1,348 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="#">系统管理</a>
+      </el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="/sys/role/list">角色管理</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <!--
+      要resetFields起作用,必须配置:model和prop
+    -->
+    <el-form ref="queryForm" :model="queryModel" inline class="demo-form-inline">
+      <el-form-item label="角色名称" prop="name">
+        <el-input type="text" size="mini" v-model="queryModel.name"></el-input>
+      </el-form-item>
+      <el-form-item label="角色描述" prop="description">
+        <el-input type="text" size="mini" v-model="queryModel.description"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          size="mini"
+          icon="ios-search"
+          @click="changePage(1)"
+          :loading="loading"
+        >查询</el-button>&nbsp;
+        <el-button
+          type="info"
+          size="mini"
+          style="margin-left: 8px"
+          @click="handleReset('queryForm')"
+        >重置</el-button>&nbsp;
+      </el-form-item>
+    </el-form>
+    <el-divider></el-divider>
+    <el-row class="button-group">
+      <el-button type="primary" size="small" plain icon="el-icon-circle-plus" @click="handleAdd">新增</el-button>
+      <el-button
+        type="primary"
+        size="small"
+        plain
+        icon="el-icon-circle-plus"
+        :disabled="multipleSelection.length==0"
+        @click="handleBatchDelete"
+      >删除选中项</el-button>
+    </el-row>
+    <el-table
+      :data="tableData"
+      style="min-height:400px;"
+      v-loading="loading"
+      stripe
+      @sort-change="sortChange"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55"></el-table-column>
+      <el-table-column prop="name" sort-by="name_" label="角色名称" sortable="custom" width="180"></el-table-column>
+      <el-table-column
+        prop="description"
+        sort-by="description_"
+        label="角色描述"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="createTime"
+        sort-by="create_time"
+        label="创建时间"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="updateTime"
+        sort-by="update_time"
+        label="更新时间"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column label="操作">
+        <template slot-scope="{row}">
+          <el-button size="mini" type="warn" @click="handleEdit(row)">编辑</el-button>
+          <el-button size="mini" type="primary" @click="allocApiPerm(row)">分配接口</el-button>
+          <el-button size="mini" type="primary" @click="allocMenu(row)">分配菜单</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      :current-page.sync="pageIndex"
+      :total="totalElements"
+      :page-sizes="pageSizeList"
+      @current-change="changePage"
+      @size-change="pageSizeChange"
+      layout="total, sizes, prev, pager, next, jumper"
+    ></el-pagination>
+    <!--角色信息-->
+    <role-detail
+      v-if="showDetailModal"
+      :title="modalTitle"
+      :roleId="roleId"
+      @close="onDetailModalClose"
+    ></role-detail>
+
+    <!--分配菜单-->
+    <role-menu
+      v-if="showMenuModal"
+      @close="onMenuModalClose"
+      :roleId="roleId"
+    ></role-menu>
+
+    <!--分配接口权限-->
+    <role-permission
+      v-if="showPermModal"
+      @close="onPermModalClose"
+      :roleId="roleId"
+    ></role-permission>
+
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import RoleDetail from "./role-detail";
+import RoleMenu from "./role-menu";
+import RolePermission from "./role-permission";
+import roleApi from "@/api/sys/role";
+import NProgress from "nprogress"; // progress bar
+import "nprogress/nprogress.css"; // progress bar style
+
+export default {
+  data() {
+    var self = this;
+
+    return {
+      queryModel: {
+        name: "",
+        description: ""
+      },
+      loading: false,
+      tableData: [],
+      pageIndex: 1,
+      pageSize: 10,
+      totalPages: 0,
+      totalElements: 0,
+      field: "",
+      direction: "",
+      pageSizeList: [10, 20, 30],
+      multipleSelection: [],
+      roleId: "",
+      modalTitle: "",
+      showDetailModal: false,
+      showMenuModal: false,
+      showPermModal: false
+    };
+  },
+  methods: {
+    changePage(pageIndex) {
+      var self = this;
+      self.loading = true;
+      
+      self.pageIndex = pageIndex;
+      var formData = new FormData();
+
+      formData.append("pageIndex", self.pageIndex);
+      formData.append("pageSize", self.pageSize);
+
+      formData.append("id", self.queryModel.id);
+
+      formData.append("roleName", self.queryModel.name);
+      formData.append("roleDesc", self.queryModel.description);
+
+      if (this.field != null) {
+        formData.append("field", this.field);
+      }
+
+      if (this.direction != null) {
+        formData.append("direction", this.direction);
+      }
+
+      roleApi
+        .pageList(formData)
+        .then(function(response) {
+          self.loading = false;
+
+          var jsonData = response.data;
+
+          if(jsonData.result){
+            var pageInfo = jsonData.data;
+
+            self.tableData = pageInfo.data;
+            self.totalPages = pageInfo.totalPages;
+            self.totalElements = pageInfo.recordsTotal;
+          }
+          else {
+            self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+          }
+        })
+        .catch(error => {
+          self.loading = false;
+        });
+    },
+    pageSizeChange(pageSize) {
+      this.pageSize = pageSize;
+    },
+    sortChange(data) {
+      this.field = data.column.field;
+      this.direction = data.order;
+
+      this.changePage(this.pageIndex);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val;
+    },
+    handleReset(name) {
+      this.$refs[name].resetFields();
+    },
+    handleAdd() {
+      var self = this;
+      self.modalTitle = "新增";
+      self.roleId = "";
+      self.showDetailModal = true;
+    },
+    handleEdit(record) {
+      var self = this;
+
+      self.modalTitle = "编辑";
+      self.roleId = record.id;
+      self.showDetailModal = true;
+    },
+    handleDelete(record) {
+      var self = this;
+
+      self
+        .$confirm("是否确认删除" + record.description + "?", "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        })
+        .then(() => {
+          roleApi.remove(record.id).then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              // var index = self.tableData.indexOf(record);
+              // self.tableData.splice(index, 1);
+              self.changePage(self.pageIndex);
+
+              self.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+            }
+          });
+        });
+    },
+    handleBatchDelete() {
+      var self = this;
+
+      var idList = this.multipleSelection.map(record => {
+        return record.id;
+      });
+
+      this.$confirm("是否确认删除选中项?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        roleApi.batchRemove(idList).then(function(response) {
+          var jsonData = response.data;
+
+          if (jsonData.result) {
+            self.changePage(self.pageIndex);
+
+            self.$message({
+              type: "success",
+              message: "删除成功!"
+            });
+          }
+        });
+      });
+    },
+    onDetailModalClose(refresh) {
+      console.log("onDetailModalClose");
+
+      this.showDetailModal = false;
+
+      if(refresh) {
+        this.changePage(this.pageIndex);
+      }
+    },
+    onPermModalClose(refresh) {
+      this.showPermModal = false;
+
+      if(refresh) {
+        this.changePage(this.pageIndex);
+      }
+    },
+    onMenuModalClose(refresh) {
+      this.showMenuModal = false;
+
+      if(refresh) {
+        this.changePage(this.pageIndex);
+      }
+    },
+    allocApiPerm(row) {
+      this.roleId = row.id;
+      this.showPermModal = true;
+    },
+    allocMenu(row) {
+      this.roleId = row.id;
+      this.showMenuModal = true;
+    }
+  },
+  mounted: function() {
+    this.changePage(1);
+  },
+  components: {
+    "role-detail": RoleDetail,
+    "role-menu": RoleMenu,
+    "role-permission": RolePermission
+  }
+};
+</script>
+<style lang="scss" scoped>
+.el-breadcrumb {
+  margin: 10px;
+  line-height: 20px;
+}
+
+.el-divider {
+  margin: 5px 0;
+}
+
+.demo-form-inline {
+  margin-left: 10px;
+  text-align: left;
+}
+
+.button-group {
+  margin-left: 10px;
+  text-align: left;
+}
+</style>

+ 143 - 0
src/views/sys/role-menu.vue

@@ -0,0 +1,143 @@
+<template>
+    <el-dialog
+      :visible.sync="showDialog"
+      title="分配菜单"
+      width="800px"
+      :modal-append-to-body="false"
+      style="text-align:left;"
+      @close="closeDialog"
+    >
+  <div class="role-permission">
+    <el-transfer
+      v-model="relatedMenus"
+      :data="menuList"
+      v-loading="loading"
+      filterable
+      :filter-method="filterMethod"
+      filter-placeholder="请输入关键字"
+      :props="{key: 'id',label: 'description'}"
+      :titles="['未分配菜单','已分配菜单']"
+    ></el-transfer>
+  </div>
+        <span slot="footer" class="dialog-footer">
+        <el-button @click="closeDialog">取 消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+      </span>
+    </el-dialog>
+</template>
+<script>
+import menuApi from "@/api/sys/menu";
+import permissionApi from "@/api/sys/permission";
+import roleApi from "@/api/sys/role";
+
+export default {
+  props: ["roleId"],
+  data() {
+    return {
+      menuList: [],
+      relatedMenus: [],
+      loading: false,
+      showDialog: true,
+      submitting: false
+    };
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close",false);
+    },
+    handleSubmit() {
+      var self = this;
+
+      roleApi
+        .saveRelatedMenu({
+          roleId: this.roleId,
+          relatedList: this.relatedMenus
+        })
+        .then(function(response) {
+          var jsonData = response.data;
+          self.submitting = false;
+
+          if (jsonData.result) {
+            self.$message({
+              message: "保存成功!",
+              type: "success"
+            });
+
+            self.$emit("close",true);
+          } else {
+            self.$message({
+              message: jsonData.message + "",
+              type: "warning"
+            });
+
+            self.$emit("close",false);
+          }
+        });
+    },
+    filterMethod(query, item) {
+      return item.description.indexOf(query) > -1;
+    },
+    queryRelatedMenuList() {
+      var self = this;
+
+      self.loading = true;
+
+      self.menuList = [];
+      self.relatedMenus= [];
+
+      var queryData = new FormData();
+      queryData.append("limit",1000);
+
+      menuApi.query(queryData)
+        .then(response => {
+          var jsonData = eval(response.data);
+
+          self.menuList = jsonData.data.map(item => {
+            var description = "";
+
+            if(item.parentName!=null){
+              description = item.parentName + ">";
+            }
+
+            description += item.menuName;
+
+            return {
+              id: item.id,
+              description: description
+            };
+          });
+
+          return roleApi.queryRelatedMenuList(self.roleId);
+        })
+        .then(response => {
+          var jsonData = response.data;
+
+            self.relatedMenus = jsonData.data.map(item => {
+            return item.menuId;
+          });
+
+          self.loading = false;
+        });
+    }
+  },
+  mounted() {
+    this.queryRelatedMenuList();
+  }
+};
+</script>
+<style lang="scss">
+.role-permission {
+  .el-transfer-panel {
+    border: 1px solid #ebeef5;
+    border-radius: 4px;
+    overflow: hidden;
+    background: #fff;
+    display: inline-block;
+    vertical-align: middle;
+    width: 320px;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    position: relative;
+  }
+}
+</style>

+ 135 - 0
src/views/sys/role-permission.vue

@@ -0,0 +1,135 @@
+<template>
+      <el-dialog
+        title="分配接口权限"
+        width="800px"
+        :visible="showDialog"
+        :modal-append-to-body="false"
+        style="text-align:left;"
+        @close="closeDialog"
+      >
+      <div class="role-permission">
+        <el-transfer
+          v-model="relatedPerms"
+          :data="permList"
+          v-loading="loading"
+          filterable
+          :filter-method="filterMethod"
+          filter-placeholder="请输入关键字"
+          :props="{key: 'id',label: 'description'}"
+          :titles="['未分配接口','已分配接口']"
+        ></el-transfer>
+      </div>
+        <span slot="footer" class="dialog-footer">
+        <el-button @click="handleCancel">取 消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+      </span>
+    </el-dialog>
+</template>
+<script>
+import roleApi from "@/api/sys/role";
+import permissionApi from "@/api/sys/permission";
+
+export default {
+  props: ["roleId"],
+  data() {
+    return {
+      permList: [],
+      relatedPerms: [],
+      showDialog: true,
+      loading: false,
+      submitting: false
+    };
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close",false);
+    },
+    handleCancel (){
+      this.$emit("close",false);
+    },
+    handleSubmit() {
+      var self = this;
+      
+      self.submitting = true;
+
+      roleApi
+        .saveRelatedPermission({
+          roleId: this.roleId,
+          relatedList: this.relatedPerms
+        })
+        .then(function(response) {
+          var jsonData = response.data;
+            self.submitting = false;
+
+          if (jsonData.result) {
+            self.$message({
+              message: "保存成功!",
+              type: "success"
+            });
+
+            self.$emit("close",true);
+          } else {
+            self.$message({
+              message: jsonData.message + "",
+              type: "warning"
+            });
+
+            self.$emit("close",false);
+          }
+        });
+    },
+    filterMethod(query, item) {
+      return item.description.indexOf(query) > -1;
+    },
+    queryRelatedPerms() {
+      var self = this;
+      self.loading = true;
+
+      this.permList = [];
+      this.relatedPerms= [];
+
+      permissionApi.selectAll().then(response => {
+          var jsonData = eval(response.data);
+
+          this.permList = jsonData.data.map(item => {
+            return {
+              id: item.id,
+              description: item.method + " " + item.path + " " + item.summary
+            };
+          });
+
+          return roleApi.queryRelatedPerms(this.roleId);
+        })
+        .then((response) => {
+          var jsonData = response.data;
+
+            this.relatedPerms = jsonData.data.map(item => {
+              return item.permId;
+            });
+
+          self.loading = false;
+        });
+    }
+  },
+  mounted() {
+    // 采用v-if控制组件是否显示,则会每次显示时执行mounted方法
+    this.queryRelatedPerms();
+  }
+};
+</script>
+<style lang="scss">
+.role-permission {
+  .el-transfer-panel {
+    border: 1px solid #ebeef5;
+    border-radius: 4px;
+    overflow: hidden;
+    background: #fff;
+    display: inline-block;
+    vertical-align: middle;
+    width: 320px;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    position: relative;
+  }
+}
+</style>

+ 158 - 0
src/views/sys/user-detail.vue

@@ -0,0 +1,158 @@
+<style scoped>
+.user-panel {
+  margin: 10px auto;
+}
+</style>
+<template>
+  <el-dialog
+    :visible.sync="showDialog"
+    :title="title"
+    :modal-append-to-body="false"
+    style="text-align:left;"
+    @close="closeDialog"
+  >
+    <div class="user-panel" v-loading="loading">
+      <el-form ref="form" :model="formModel" :rules="ruleValidate" :label-width="'100px'">
+        <el-form-item label="用户名" prop="userName">
+          <el-input v-model="formModel.userName" placeholder="请输入用户名" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item label="真实名称" prop="realName">
+          <el-input v-model="formModel.realName" placeholder="请输入真实名称" style="width:300px"></el-input>
+        </el-form-item>
+        <el-form-item v-show="ruleValidate.password[0].required" label="密码" prop="password">
+          <el-input
+            type="password"
+            v-model="formModel.password"
+            placeholder="请输入密码"
+            style="width:300px"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="分配角色" prop="roles">
+          <el-select v-model="formModel.roles" 
+              filterable multiple placeholder="请选择" style="width:300px">
+            <el-option
+              v-for="role in roleList"
+              :key="role.id"
+              :label="role.description"
+              :value="role.id"
+            ></el-option>
+          </el-select>
+          <!-- <el-transfer v-model="formModel.roles" :data="roleList" 
+          :props="{key: 'id',label: 'description'}" :titles="['所有角色','已分配角色']"></el-transfer>-->
+        </el-form-item>
+      </el-form>
+    </div>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="closeDialog">取 消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitting">确 定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+import Constant from "@/constant";
+import userApi from "@/api/sys/user";
+import roleApi from "@/api/sys/role";
+
+export default {
+  props: ["businessKey", "title"],
+  data() {
+    return {
+      showPwd: true,
+      ruleValidate: {
+        userName: [
+          { required: true, message: "用户名不能为空", trigger: "blur" }
+        ],
+        realName: [
+          { required: true, message: "真实名称不能为空", trigger: "blur" }
+        ],
+        password: [
+          { required: true, message: "密码不能为空", trigger: "blur" }
+        ],
+        roles: [
+          { required: true, message: "分配角色不能为空", trigger: "blur" }
+        ]
+      },
+      roleList: [],
+      formModel: {},
+      showDialog: true,
+      loading: false,
+      submitting: false
+    };
+  },
+  methods: {
+    closeDialog() {
+      this.$emit("close", false);
+    },
+    handleSubmit() {
+      var self = this;
+
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          (function() {
+            var id = self.formModel.id;
+
+            if (id == null || id.length == 0) {
+              return userApi.add(self.formModel);
+            } else {
+              return userApi.update(self.formModel);
+            }
+          })().then(function(response) {
+            var jsonData = response.data;
+
+            if (jsonData.result) {
+              self.$message({
+                message: "保存成功!",
+                type: "success"
+              });
+
+              this.$emit("close", true);
+            } else {
+              self.$message({
+                message: jsonData.message + "",
+                type: "warning"
+              });
+
+              this.$emit("close", false);
+            }
+          });
+        }
+      });
+    }
+  },
+  async mounted() {
+    var self = this;
+    self.loading = true;
+
+    // 不需要先加载列表值,再加载选中值了
+    await roleApi.selectAll().then(response => {
+      this.roleList = response.data.data;
+    });
+
+    (function() {
+      if (self.businessKey != null && self.businessKey.length > 0) {
+        self.ruleValidate.password[0].required = false;
+        return userApi.edit(self.businessKey);
+      } else {
+        self.ruleValidate.password[0].required = true;
+        return userApi.create();
+      }
+    })()
+      .then(response => {
+        var jsonData = response.data;
+
+        if (jsonData.result) {
+          self.formModel = jsonData.data;
+          self.showModal = true;
+        } else {
+          self.$message.error(jsonData.message + "");
+        }
+
+        self.loading = false;
+      })
+      .catch(error => {
+        self.$message.error(error + "");
+        self.loading = false;
+      });
+  }
+};
+</script>

+ 304 - 0
src/views/sys/user-list.vue

@@ -0,0 +1,304 @@
+<template>
+  <div>
+    <el-breadcrumb separator=">">
+      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="#">系统管理</a>
+      </el-breadcrumb-item>
+      <el-breadcrumb-item>
+        <a href="/sys/user">用户管理</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-divider></el-divider>
+    <!--
+      要resetFields起作用,必须配置:model和prop
+    -->
+    <el-form ref="queryForm" :model="queryModel" inline class="demo-form-inline">
+      <el-form-item label="用户名" prop="userName">
+        <el-input type="text" size="mini" v-model="queryModel.userName"></el-input>
+      </el-form-item>
+      <el-form-item label="真实名称" prop="realName">
+        <el-input type="text" size="mini" v-model="queryModel.realName"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          size="mini"
+          icon="ios-search"
+          @click="changePage(1)"
+          :loading="loading"
+        >查询</el-button>&nbsp;
+        <el-button
+          type="info"
+          size="mini"
+          style="margin-left: 8px"
+          @click="handleReset('queryForm')"
+        >重置</el-button>&nbsp;
+      </el-form-item>
+    </el-form>
+    <el-divider></el-divider>
+    <el-row class="button-group">
+      <el-button type="primary" size="small" plain icon="el-icon-circle-plus" @click="handleAdd">新增</el-button>
+      <el-button
+        type="primary"
+        size="small"
+        plain
+        icon="el-icon-circle-plus"
+        :disabled="multipleSelection.length==0"
+        @click="handleBatchDelete"
+      >删除选中项</el-button>
+    </el-row>
+    <el-table
+      :data="tableData"
+      style="min-height:400px;"
+      v-loading="loading"
+      stripe
+      @sort-change="sortChange"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55"></el-table-column>
+      <el-table-column
+        prop="userName"
+        sort-by="user_name"
+        label="用户名"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="realName"
+        sort-by="real_name"
+        label="真实名称"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="createTime"
+        sort-by="create_time"
+        label="创建时间"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column
+        prop="updateTime"
+        sort-by="update_time"
+        label="更新时间"
+        sortable="custom"
+        width="180"
+      ></el-table-column>
+      <el-table-column label="操作">
+        <template slot-scope="{row}">
+          <el-button size="mini" type="warning" @click="handleEdit(row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      :current-page.sync="pageIndex"
+      :total="totalElements"
+      :page-sizes="pageSizeList"
+      @current-change="changePage"
+      @size-change="pageSizeChange"
+      layout="total, sizes, prev, pager, next, jumper"
+    ></el-pagination>
+    <user-detail
+      v-if="showModal"
+      ref="userDetail"
+      :businessKey="businessKey"
+      :title="modalTitle"
+      @close="onDetailModalClose"
+    ></user-detail>
+  </div>
+</template>
+<script>
+import Constant from "@/constant";
+import UserDetail from "./user-detail";
+import userApi from "@/api/sys/user";
+import NProgress from "nprogress"; // progress bar
+import "nprogress/nprogress.css"; // progress bar style
+
+export default {
+  data() {
+    var self = this;
+
+    return {
+      queryModel: {
+        userName: "",
+        realName: ""
+      },
+      loading: false,
+      tableData: [],
+      pageIndex: 1,
+      pageSize: 10,
+      totalPages: 0,
+      totalElements: 0,
+      field: "",
+      direction: "",
+      pageSizeList: [10, 20, 30],
+      multipleSelection: [],
+      modalTitle: "",      
+      businessKey: "",
+      showModal: false
+    };
+  },
+  methods: {
+    changePage(pageIndex) {
+      var self = this;
+
+      console.log(pageIndex);
+
+      self.pageIndex = pageIndex;
+      var formData = new FormData();
+
+      formData.append("pageIndex", self.pageIndex);
+      formData.append("pageSize", self.pageSize);
+
+      formData.append("userName", self.queryModel.userName);
+      formData.append("realName", self.queryModel.realName);
+
+      if (this.field != null) {
+        formData.append("field", this.field);
+      }
+
+      if (this.direction != null) {
+        formData.append("direction", this.direction);
+      }
+
+      self.loading = true;
+
+      userApi.pageList(formData).then(function(response) {
+        self.loading = false;
+
+        var jsonData = response.data;
+
+        if(jsonData.result){
+          var pageInfo = jsonData.data;
+
+          self.tableData = pageInfo.data;
+          self.totalPages = pageInfo.totalPages;
+          self.totalElements = pageInfo.recordsTotal;
+        }
+        else {
+          self.$message({
+              message: jsonData.message + "",
+              type: "warning"
+            });
+        }
+      }).catch((error)=>{
+        self.loading = false;
+      });
+    },
+    pageSizeChange(pageSize) {
+      this.pageSize = pageSize;
+    },
+    sortChange(data) {
+      this.field = data.column.field;
+      this.direction = data.order;
+
+      this.changePage(this.pageIndex);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val;
+    },
+    handleReset(name) {
+      this.$refs[name].resetFields();
+    },
+    handleAdd() {
+      var self = this;
+      
+      self.modalTitle = "新增用户";
+      self.businessKey = "";
+      self.showModal = true;
+    },
+    handleEdit(record) {
+      var self = this;
+      
+      self.modalTitle = "编辑用户";
+      self.businessKey = record.id;
+      self.showModal = true;
+    },
+    handleDelete(record) {
+      var self = this;
+
+      this.$confirm("是否确认删除" + record.userName + "?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        userApi.remove(record.id).then(function(response) {
+          var jsonData = response.data;
+
+          if (jsonData.result) {
+            // var index = self.tableData.indexOf(record);
+            // self.tableData.splice(index, 1);
+            self.changePage(self.pageIndex);
+
+            self.$message({
+              type: "success",
+              message: "删除成功!"
+            });
+          }
+        });
+      });
+    },
+    handleBatchDelete() {
+      var self = this;
+
+      var idList = this.multipleSelection.map(record => {
+        return record.id;
+      });
+
+      this.$confirm("是否确认删除选中项?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        userApi.batchRemove(idList).then(function(response) {
+          var jsonData = response.data;
+
+          if (jsonData.result) {
+            self.changePage(self.pageIndex);
+
+            self.$message({
+              type: "success",
+              message: "删除成功!"
+            });
+          }
+        });
+      });
+    },
+    onDetailModalClose(refreshed) {
+      this.showModal = false;
+
+      if(refreshed) {
+        this.changePage(this.pageIndex);
+      }
+    }
+  },
+  mounted: function() {
+    this.changePage(1);
+  },
+  components: {
+    "user-detail": UserDetail
+  }
+};
+</script>
+<style lang="scss" scoped>
+.el-breadcrumb {
+  margin: 10px;
+  line-height: 20px;
+}
+
+.el-divider {
+  margin: 5px 0;
+}
+
+.demo-form-inline {
+  margin-left: 10px;
+  text-align: left;
+}
+
+.button-group {
+  margin-left: 10px;
+  text-align: left;
+}
+</style>

+ 21 - 0
vue.config.js

@@ -0,0 +1,21 @@
+const path = require('path');
+function resolve (dir) {
+    return path.join(__dirname, dir)
+}
+
+module.exports = {
+    // 选项...
+    publicPath:"./",
+    // 输出文件目录
+    outputDir: 'dist',
+    // eslint-loader 是否在保存的时候检查
+    lintOnSave: true,
+    devServer:{
+        host: '0.0.0.0',
+        port: 8083
+    },
+    chainWebpack: (config)=>{
+        config.resolve.alias
+            .set('@', resolve('src'))
+    }
+  }

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 476 - 26
yarn.lock


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů