MrShi
2023-12-04 a14f9c307be39f8e824003452ef1be5a61bcb637
Mr.Shi
已添加126个文件
已修改1个文件
22335 ■■■■■ 文件已修改
.gitignore 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/.env 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/.env.development 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/.env.production 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/.env.staging 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/.gitignore 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/README.md 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/babel.config.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/package-lock.json 14280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/package.json 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/public/avatar/man.png 补丁 | 查看 | 原始文档 | blame | 历史
admin/public/avatar/woman.png 补丁 | 查看 | 原始文档 | blame | 历史
admin/public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
admin/public/index.html 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/App.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/common.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/dataPermission.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/department.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/dict.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/dictData.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/loginLog.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/menu.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/monitor.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/permission.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/position.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/role.js 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/traceLog.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/system/user.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/ext/index.scss 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/department.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/dictionary.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/index.scss 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/listener.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/log-login.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/log-opera.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/log.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/permission.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/position.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/pwd.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/role.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/icons/system/shield.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/images/404-tip.png 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/images/404.png 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/images/alipay.jpeg 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/images/login.jpg 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/images/not-allow.png 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/images/qq.png 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/images/wxpay.jpeg 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/style/element-variables.scss 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/style/style.scss 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/assets/style/variables.scss 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/base/BaseOpera.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/base/BasePage.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/base/BaseTable.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/ColumnDetail.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/DepartmentSelect.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/GlobalWindow.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/Header.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/Light.vue 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/Menu.vue 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/MenuItems.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/MenuSelect.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/NotAllow.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/Pagination.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/PositionSelect.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/Profile.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/Scrollbar.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/SearchFormCollapse.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/TreeSelect.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/common/Value.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/datapermission/CustomSelect.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/datapermission/DataPermModuleSelect.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/datapermission/DataPermTypeSelect.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/datapermission/OperaDataPermissionWindow.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/department/DepartmentUserWindow.vue 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/department/OperaDepartmentWindow.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/dict/DictDataManagerWindow.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/dict/OperaDictDataWindow.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/dict/OperaDictWindow.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/menu/OperaMenuWindow.vue 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/permission/OperaPermissionWindow.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/position/OperaPositionWindow.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/position/PositionUserWindow.vue 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/role/MenuConfigWindow.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/role/OperaRoleWindow.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/role/PermissionConfigWindow.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/role/RoleSelect.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/user/OperaUserWindow.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/user/ResetPwdWindow.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/system/user/RoleConfigWindow.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/directives/index.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/directives/v-permissions.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/directives/v-roles.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/directives/v-trim.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/filters/index.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/layouts/AppLayout.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/layouts/TableLayout.vue 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/main.js 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/plugins/cache.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/plugins/consts.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/plugins/download.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/plugins/index.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/plugins/message.js 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/plugins/messagebox.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/router/index.js 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/store/index.js 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/utils/form.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/utils/icons.js 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/utils/request.js 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/utils/util.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/index.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/login.vue 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/no-permissions.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/not-found.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/data-permission.vue 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/department.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/dict.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/loginLog.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/menu.vue 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/monitor.vue 308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/permission.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/position.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/role.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/traceLog.vue 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/system/user.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/vue.config.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -1,3 +1,8 @@
.DS_Store
coderd.json
node_modules
/dist
*.class
# Mobile Tools for Java (J2ME)
admin/.env
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
# çŽ¯å¢ƒé€šç”¨é…ç½®
# è·¯ç”±æ–¹å¼
VUE_APP_ROUTER_MODE = 'hash'
# é¡¹ç›®ä¸Šä¸‹æ–‡è·¯å¾„
VUE_APP_CONTEXT_PATH = '/'
# æŽ¥å£å‰ç¼€
VUE_APP_API_PREFIX = '/api'
admin/.env.development
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
# å¼€å‘环境配置
NODE_ENV = 'development'
admin/.env.production
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
# ç”Ÿäº§çŽ¯å¢ƒé…ç½®
NODE_ENV = 'production'
admin/.env.staging
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
# æµ‹è¯•环境配置
NODE_ENV = 'production'
admin/.gitignore
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
.DS_Store
coderd.json
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
admin/README.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
框架:Eva v2.0
Eva官网:http://eva.adjust-rd.com
# é¡¹ç›®ç›®å½•说明
- src/assets:静态资源目录
- src/components:自定义组件目录
- src/plugins:插件目录
- src/router:路由配置目录
- src/store:vuex store目录
- src/utils:工具目录
- src/views:页面目录
# æŠ€æœ¯æ ˆ
- è·¯ç”±ï¼šVue-Router-3.2.0
- é¢„编译:SASS-4.12.0
- HTTP库:axios-0.21.1
- UI库:element-ui-2.3.6
- ä»£ç è§„范检查:eslint-6.7.2
# ä»£ç æ£€æŸ¥
代码检查规则配置在package.json的eslintConfig对象中
# è·¯ç”±
路由配置在src/router/index.js中
# æŽ¥å£ä»£ç†å’Œè°ƒç”¨
接口代理路径配置在vue.config.js文件中,接口调用工具封装在src/utils/request.js中,调用示例如下
```javascript
import request from '@/utils/request'
request.post('/api/myinterface', {
  param1: 'param value'
})
    .then(data => {
      console.log('请求成功,接口返回', data)
    })
    .catch(e => {
      console.log('调用失败', e)
    })
```
admin/babel.config.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}
admin/package-lock.json
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
admin/package.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
{
  "name": "dmtiaotiao",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "build:staging": "vue-cli-service build --mode staging",
    "lint": "vue-cli-service lint",
    "fix": "eslint --ext .js,.vue src --fix"
  },
  "dependencies": {
    "@riophae/vue-treeselect": "^0.4.0",
    "axios": "^0.21.1",
    "core-js": "^3.6.5",
    "element-ui": "^2.3.6",
    "js-cookie": "^2.2.1",
    "js-file-download": "^0.4.12",
    "vue": "^2.6.11",
    "vue-clipboard2": "^0.3.1",
    "vue-router": "^3.5.1",
    "vuescroll": "^4.17.3",
    "vuex": "^3.4.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/eslint-config-standard": "^5.1.2",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-import": "^2.20.2",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.0",
    "eslint-plugin-vue": "^6.2.2",
    "lint-staged": "^9.5.0",
    "node-sass": "^4.12.0",
    "sass-loader": "^8.0.2",
    "vue-cli-plugin-element-ui": "~1.1.4",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "@vue/standard"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {
      "indent": [
        "error",
        2
      ],
      "generator-star-spacing": "off",
      "no-debugger": "error"
    }
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ],
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,vue}": [
      "vue-cli-service lint",
      "git add"
    ]
  }
}
admin/public/avatar/man.png
admin/public/avatar/woman.png
admin/public/favicon.ico
admin/public/index.html
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>豆米跳跳</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but è±†ç±³è·³è·³ doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
admin/src/App.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<template>
  <router-view/>
</template>
<style lang="scss">
// å¼•入全局样式
@import "assets/style/style";
// å¼•入系统内置图标
@import "assets/icons/system/index";
// å¼•入自定义图标
@import "assets/icons/ext/index";
</style>
admin/src/api/system/common.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import request from '@/utils/request'
// èŽ·å–å›¾ç‰‡éªŒè¯ç 
export function getCaptcha () {
  return request.get('/common/captcha')
}
// æ ¹æ®å¯†ç ç™»å½•
export function loginByPassword (data) {
  return request.post('/system/login', data)
}
// ç™»å‡º
export function logout (data) {
  return request.post('/system/logout', data)
}
// ä¿®æ”¹å¯†ç 
export function updatePwd (data) {
  return request.post('/system/updatePwd', data)
}
// èŽ·å–å·²ç™»å½•çš„ç”¨æˆ·ä¿¡æ¯
export function getUserInfo () {
  return request.get('/system/getUserInfo', {
    autoLogin: false
  })
}
admin/src/api/system/dataPermission.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
import request from '../../utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/dataPermission/page', data, {
    trim: true
  })
}
// æŸ¥è¯¢æ•°æ®æƒé™æ¨¡å—
export function fetchModules () {
  return request.cache('DATA_PERMISSION_MODULES').get('/system/dataPermission/modules')
}
// æŸ¥è¯¢æ•°æ®æƒé™ç±»åž‹
export function fetchTypes () {
  return request.cache('DATA_PERMISSION_TYPES').get('/system/dataPermission/types')
}
// åˆ›å»º
export function create (data) {
  return request.post('/system/dataPermission/create', data)
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/dataPermission/updateById', data)
}
// ä¿®æ”¹çŠ¶æ€
export function updateStatus (data) {
  return request.post('/system/dataPermission/updateStatus', data)
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/dataPermission/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/dataPermission/delete/batch', {
    params: {
      ids
    }
  })
}
admin/src/api/system/department.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
import request from '@/utils/request'
// æŸ¥è¯¢
export function fetchTree () {
  return request.post('/system/department/tree')
}
// æŸ¥è¯¢éƒ¨é—¨ç”¨æˆ·
export function fetchUserList (data) {
  return request.post('/system/department/users', data)
}
// æ–°å»º
export function create (data) {
  return request.post('/system/department/create', data)
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/department/updateById', data)
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/department/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/department/delete/batch', {
    params: {
      ids
    }
  })
}
admin/src/api/system/dict.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
import request from '@/utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/dict/page', data)
}
// æ–°å»º
export function create (data) {
  return request.post('/system/dict/create', data)
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/dict/updateById', data)
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/dict/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/dict/delete/batch', {
    params: {
      ids
    }
  })
}
admin/src/api/system/dictData.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
import request from '@/utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/dictData/page', data)
}
// æ–°å»º
export function create (data) {
  return request.post('/system/dictData/create', data)
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/dictData/updateById', data)
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/dictData/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/dictData/delete/batch', {
    params: {
      ids
    }
  })
}
admin/src/api/system/loginLog.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
import request from '../../utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/loginLog/page', data, {
    trim: true
  })
}
// å¯¼å‡ºExcel
export function exportExcel (data) {
  return request.post('/system/loginLog/exportExcel', data, {
    download: true,
    trim: true
  })
}
admin/src/api/system/menu.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
import request from '@/utils/request'
// æŸ¥è¯¢
export function fetchTree (data) {
  return request.post('/system/menu/treeList', data)
}
// æ–°å»º
export function create (data) {
  return request.post('/system/menu/create', data)
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/menu/updateById', data)
}
// ä¿®æ”¹çŠ¶æ€
export function updateStatus (data) {
  return request.post('/system/menu/updateStatus', data)
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/menu/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/menu/delete/batch', {
    params: {
      ids
    }
  })
}
// æŸ¥è¯¢èœå•æ ‘
export function fetchMenuTree () {
  return request.get('/system/menu/treeNodes')
}
// æŽ’序
export function sort (data) {
  return request.post('/system/menu/updateSort', data)
}
admin/src/api/system/monitor.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
import request from '@/utils/request'
// èŽ·å–ç³»ç»Ÿä¿¡æ¯
export function getSystemInfo () {
  return request.get('/system/monitor/getSystemInfo')
}
admin/src/api/system/permission.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
import request from '@/utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/permission/page', data)
}
// æŸ¥è¯¢æ‰€æœ‰
export function fetchAll () {
  return request.get('/system/permission/all')
}
// æ–°å»º
export function create (data) {
  return request.post('/system/permission/create', data, {
    trim: true
  })
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/permission/updateById', data, {
    trim: true
  })
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/permission/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/permission/delete/batch', {
    params: {
      ids
    }
  })
}
admin/src/api/system/position.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
import request from '@/utils/request'
// æŸ¥è¯¢åˆ—表树
export function fetchTree (data) {
  return request.post('/system/position/tree', data)
}
// æŸ¥è¯¢åˆ—表
export function fetchList (data) {
  return request.post('/system/position/list', data)
}
// æ–°å»º
export function create (data) {
  return request.post('/system/position/create', data)
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/position/updateById', data)
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/position/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/position/delete/batch', {
    params: {
      ids
    }
  })
}
admin/src/api/system/role.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
import request from '@/utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/role/page', data)
}
// æŸ¥è¯¢æ‰€æœ‰
export function fetchAll () {
  return request.get('/system/role/all')
}
// æ–°å»º
export function create (data) {
  return request.post('/system/role/create', data, {
    trim: true
  })
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/role/updateById', data, {
    trim: true
  })
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/role/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/role/delete/batch', {
    params: {
      ids
    }
  })
}
// é…ç½®æƒé™
export function createRolePermission (data) {
  return request.post('/system/role/createRolePermission', data)
}
// é…ç½®èœå•
export function createRoleMenu (data) {
  return request.post('/system/role/createRoleMenu', data)
}
admin/src/api/system/traceLog.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
import request from '../../utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/traceLog/page', data, {
    trim: true
  })
}
// å¯¼å‡ºExcel
export function exportExcel (data) {
  return request.post('/system/traceLog/exportExcel', data, {
    download: true,
    trim: true
  })
}
admin/src/api/system/user.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
import request from '@/utils/request'
// æŸ¥è¯¢
export function fetchList (data) {
  return request.post('/system/user/page', data)
}
// æ–°å»º
export function create (data) {
  return request.post('/system/user/create', data, {
    trim: true
  })
}
// ä¿®æ”¹
export function updateById (data) {
  return request.post('/system/user/updateById', data, {
    trim: true
  })
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/system/user/delete/${id}`)
}
// æ‰¹é‡åˆ é™¤
export function deleteByIdInBatch (ids) {
  return request.get('/system/user/delete/batch', {
    params: {
      ids
    }
  })
}
// é…ç½®ç”¨æˆ·è§’色
export function createUserRole (data) {
  return request.post('/system/user/createUserRole', data)
}
// é‡ç½®å¯†ç 
export function resetPwd (data) {
  return request.post('/system/user/resetPwd', data)
}
admin/src/assets/icons/ext/index.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
/**
 * æ­¤æ–‡ä»¶ç”¨äºŽæ·»åŠ è‡ªå®šä¹‰çš„å›¾æ ‡
 */
admin/src/assets/icons/system/department.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621750709361" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7590" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M890.96986 479.671711 545.11094 479.671711 545.11094 271.38592 684.78875 271.38592 684.78875 64.500013 343.028339 64.500013 343.028339 271.38592 480.338121 271.38592 480.338121 479.671711 133.397522 479.671711 133.397522 532.849864 133.397522 548.629245 133.397522 755.515153 64.284683 755.515153 64.284683 959.499987 270.160836 959.499987 270.160836 755.515153 202.498081 755.515153 202.498081 548.629245 480.338121 548.629245 480.338121 755.515153 409.786454 755.515153 409.786454 959.499987 615.662607 959.499987 615.662607 755.515153 545.11094 755.515153 545.11094 548.629245 822.225426 548.629245 822.225426 755.515153 753.863725 755.515153 753.863725 959.499987 959.714294 959.499987 959.714294 755.515153 890.96986 755.515153Z" p-id="7591" fill="#f7f7f7"></path></svg>
admin/src/assets/icons/system/dictionary.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621751616853" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12750" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M225.5 596.7h63.8V652h-63.8v-55.3z m0-165.8h63.8v55.3h-63.8v-55.3z m0-165.7h63.8v55.3h-63.8v-55.3z m127.6 331.5h446.3V652H353.1v-55.3z m0-165.8h446.3v55.3H353.1v-55.3z m0-165.7h446.3v55.3H353.1v-55.3zM98 156.3v694.3c0 46.6 45 77.7 96.4 77.7h732.5v-46.6H194.4c-19.3 0-32.2-10.4-32.2-25.9 0-15.5 12.8-25.9 32.2-25.9h732.5V99.4H168.7c-32.1 5.1-64.3 31.1-70.7 56.9z" fill="#f7f7f7" p-id="12751"></path></svg>
admin/src/assets/icons/system/index.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
/**
 * æ­¤æ–‡ä»¶ä¸ºç³»ç»Ÿè‡ªå¸¦çš„图标定义,如您需要扩展图标请在ext.scss文件中处理。
 */
// å›¾æ ‡æ ·å¼
[class^="eva-icon-"], [class*=" eva-icon-"] {
  display: inline-block;
  width: 18px;
  height: 18px;
  background-size: 13px;
  background-repeat: no-repeat;
  background-position: center;
}
// å¯†ç 
.eva-icon-password {
  background-image: url("assets/icons/system/pwd.svg");
}
// ç›¾ç‰Œ
.eva-icon-shield {
  background-image: url("assets/icons/system/shield.svg");
}
// æƒé™
.eva-icon-permission {
  background-image: url("assets/icons/system/permission.svg");
}
// è§’色
.eva-icon-role {
  background-image: url("assets/icons/system/role.svg");
}
// éƒ¨é—¨
.eva-icon-department {
  background-image: url("assets/icons/system/department.svg");
}
// å²—位
.eva-icon-position {
  background-image: url("assets/icons/system/position.svg");
}
// å­—å…¸
.eva-icon-dictionary {
  background-image: url("assets/icons/system/dictionary.svg");
}
// ç›‘测
.eva-icon-listener {
  background-image: url("assets/icons/system/listener.svg");
}
// æ—¥å¿—
.eva-icon-log {
  background-image: url("assets/icons/system/log.svg");
}
// æ“ä½œæ—¥å¿—
.eva-icon-log-opera {
  background-image: url("assets/icons/system/log-opera.svg");
}
// ç™»å½•日志
.eva-icon-log-login {
  background-image: url("assets/icons/system/log-login.svg");
}
admin/src/assets/icons/system/listener.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621751666911" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13701" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M64.28 671v80.3c0 34.76 26.84 62.94 60 62.94h297.47v94.15H285.53c-24.17 0-48.38-0.49-72.54 0h-1.06c-12.54 0-24.55 11.58-24 25.18s10.53 25.17 24 25.17h525.88c24.16 0 48.37 0.49 72.53 0h1.06c12.54 0 24.56-11.58 24-25.17s-10.54-25.18-24-25.18H601.59v-94.11H899.1c33.11 0 59.95-28.18 59.95-62.94V671zM899.77 65H124.9A60 60 0 0 0 65 125v499.3h894.72V125a60 60 0 0 0-59.95-60zM757.46 296.75l-6.89 7.53-77 84.15-36.78 40.22c0.08 1.2 0.14 2.41 0.14 3.63a51.09 51.09 0 1 1-100.6-12.67l-55.74-65.31-38.46-45a51 51 0 0 1-21.7 0.18l-6 5.78-71.92 69.71-30.85 29.9a45.45 45.45 0 1 1-36.1-35.1l2.85-2.76 71.92-69.72 32.2-31.21A51.09 51.09 0 1 1 482 259.44a51.66 51.66 0 0 1-1.28 11.34l10.62 12.44L561.13 365l14.72 17.23a51.85 51.85 0 0 1 10-1 50.84 50.84 0 0 1 22.14 5l76.82-84q15.3-16.73 30.59-33.44a45.42 45.42 0 1 1 42 28z" p-id="13702" fill="#f7f7f7"></path></svg>
admin/src/assets/icons/system/log-login.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622347607410" class="icon" viewBox="0 0 1038 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3794" width="64.875" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M76.778433 76.778433h776.41112v328.249368h77.209773v-345.071609A59.956192 59.956192 0 0 0 872.599832 0H59.956192A59.956192 59.956192 0 0 0 0 59.956192v898.048863a59.956192 59.956192 0 0 0 59.956192 59.956192H474.473463v-75.915754H76.778433z" p-id="3795" fill="#f7f7f7"></path><path d="M209.631003 256.647009h513.294018v81.091828H209.631003zM209.631003 472.748104h389.068239v81.091829H209.631003zM209.631003 684.535805h278.213984v81.091828H209.631003zM914.871104 774.685762a163.046335 163.046335 0 1 0-227.747262 0 180.731255 180.731255 0 0 0-122.069082 180.731256c0 68.582982 102.227464 68.582982 243.706824 68.582982h86.267902c59.956192 0 101.364785-3.450716 125.088458-27.605729a57.799495 57.799495 0 0 0 17.68492-43.133951 172.535805 172.535805 0 0 0-122.93176-178.574558zM733.277169 590.503791a94.894693 94.894693 0 0 1 67.288964-28.037068 92.306655 92.306655 0 1 1-67.288964 26.74305z m122.500422 364.913227h-46.584668a862.679023 862.679023 0 0 1-172.535804-10.352149c5.607414-99.639427 92.737995-121.206403 166.065712-121.206402 144.498736 0 164.771693 70.73968 166.497051 125.951137a480.943555 480.943555 0 0 1-113.442291 5.607414z" p-id="3796" fill="#f7f7f7"></path></svg>
admin/src/assets/icons/system/log-opera.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622347629380" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4602" width="64.0625" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M548.075474 961.502824H37.281249a39.060735 39.060735 0 0 1-37.257932-40.262603V103.368526a39.060735 39.060735 0 0 1 37.257932-40.262604h717.515653a39.060735 39.060735 0 0 1 37.257931 40.262604V513.205775h-60.093438V128.006836H61.919559v773.40255H487.982036l60.093438 63.699045z" fill="#f7f7f7" p-id="4603"></path><path d="M733.764198 513.205775a223.54759 223.54759 0 1 1-225.350393 223.54759 227.153197 227.153197 0 0 1 225.350393-223.54759m0-63.699045A287.246635 287.246635 0 1 0 1022.813636 736.753365a287.246635 287.246635 0 0 0-289.049438-287.246635zM210.951285 0.007812h18.028032a22.835507 22.835507 0 0 1 22.835506 22.835507v145.42612a23.436441 23.436441 0 0 1-22.835506 23.436441h-18.028032a23.436441 23.436441 0 0 1-23.436441-23.436441V25.247056a22.835507 22.835507 0 0 1 22.835507-25.239244z m385.19894 0h18.028031a22.835507 22.835507 0 0 1 23.436441 22.835507v145.42612a23.436441 23.436441 0 0 1-23.436441 23.436441h-18.028031a23.436441 23.436441 0 0 1-22.835507-23.436441V25.247056a22.835507 22.835507 0 0 1 22.835507-25.239244zM157.468125 321.507707h514.399832a31.849522 31.849522 0 0 1 31.849522 31.849522 31.849522 31.849522 0 0 1-31.849522 31.849522H157.468125a31.849522 31.849522 0 0 1-31.849522-31.849522 31.849522 31.849522 0 0 1 31.849522-31.849522z m0 191.698068h257.199916a31.849522 31.849522 0 0 1 31.849522 31.849522 31.849522 31.849522 0 0 1-31.849522 31.849522H157.468125a31.849522 31.849522 0 0 1-31.849522-31.849522 31.849522 31.849522 0 0 1 31.849522-31.849522z m0 191.698068h192.899937a31.248588 31.248588 0 0 1 31.849522 31.849522 31.849522 31.849522 0 0 1-31.849522 31.849522H157.468125a31.849522 31.849522 0 0 1-31.849522-31.849522 31.849522 31.849522 0 0 1 31.849522-31.849522z" fill="#f7f7f7" p-id="4604"></path><path d="M870.777237 829.29726h-152.637333v-212.730771a30.647654 30.647654 0 0 1 30.647654-30.647654 30.647654 30.647654 0 0 1 30.647653 30.647654v152.036398h91.342026a30.046719 30.046719 0 0 1 30.04672 30.647654 30.046719 30.046719 0 0 1-30.04672 30.046719z" fill="#f7f7f7" p-id="4605"></path></svg>
admin/src/assets/icons/system/log.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1622347409604" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2382" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M299.1872 255.5904a42.496 42.496 0 0 0 42.5728-42.5472V42.752a42.5728 42.5728 0 0 0-85.1712 0v170.24c0 23.552 19.0464 42.5984 42.5984 42.5984zM512.1536 255.5904a42.496 42.496 0 0 0 42.5984-42.5472V42.752a42.5728 42.5728 0 1 0-85.1712 0v170.24c0 23.552 19.0976 42.5984 42.5728 42.5984zM384.4096 81.792h42.5984v85.0688h-42.5984zM597.4784 81.792h42.5728v85.0688H597.504z" fill="#f7f7f7" p-id="2383"></path><path d="M1023.3088 896.1024v-691.2c0.1536-70.6048-57.2416-127.6928-127.744-127.6928h-85.1712v85.0688h85.1712c23.4752 0 42.7264 19.072 42.7264 42.5216v691.3024c0 23.4496-19.2512 42.5984-42.752 42.5984H128.768a42.7264 42.7264 0 0 1-42.5728-42.5984H85.504l0.6656-691.2c0-23.4752 19.0976-42.5472 42.5728-42.5472h85.1712V77.2096H128.768A127.6928 127.6928 0 0 0 1.024 204.8768v691.2a127.616 127.616 0 0 0 127.744 127.6928h766.7968a127.3344 127.3344 0 0 0 127.232-121.344c0.0768-0.3584 0.512-0.7936 0.512-1.152v-3.7888c0-0.512 0.1536-0.9472 0.1536-1.4592h-0.1536v0.0768z" fill="#f7f7f7" p-id="2384"></path><path d="M725.248 255.5904c23.4496 0 42.5728-19.0208 42.5728-42.5472V42.752a42.5728 42.5728 0 0 0-85.1712 0v170.24c0 23.552 18.8672 42.5984 42.5728 42.5984zM255.872 426.4448h511.2064c23.3472 0 42.5728-19.072 42.5728-42.5216 0-23.552-19.2256-42.5472-42.5728-42.5472H255.8464a42.496 42.496 0 1 0 0 85.0688zM767.0784 551.3728H255.8464a42.5728 42.5728 0 1 0 0 85.0688h511.232c23.3216 0 42.5728-19.0976 42.5728-42.5472 0-23.4496-19.2512-42.5216-42.5728-42.5216zM767.0784 769.5872H255.8464a42.5216 42.5216 0 1 0 0 85.0688h511.232c23.3216 0 42.5728-19.1488 42.5728-42.6752 0-23.3984-19.2512-42.3936-42.5728-42.3936z" fill="#f7f7f7" p-id="2385"></path></svg>
admin/src/assets/icons/system/permission.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621746809427" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4923" width="64.0625" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M928.612599 95.57028a325.17778 325.17778 0 0 0-523.501496 370.143341l-383.852354 383.852354a72.018013 72.018013 0 0 0 0 102.177841L73.352997 1002.741343a72.018013 72.018013 0 0 0 102.177841 0L329.254566 847.920893l19.192618 19.192618a73.114734 73.114734 0 0 0 102.177841 0l51.180314-51.180314a73.114734 73.114734 0 0 0 0-102.177841L482.978295 694.928312l75.490963-75.490963A325.17778 325.17778 0 0 0 928.612599 95.57028zM777.447886 402.469376a109.672101 109.672101 0 1 1-2.741802-155.186023 109.672101 109.672101 0 0 1 2.741802 155.186023z" p-id="4924" fill="#f7f7f7"></path></svg>
admin/src/assets/icons/system/position.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621751435396" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11906" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M870 96H154c-49.5 0-90 40.6-90 90.2v651.6c0 49.6 40.5 90.2 90 90.2h716c49.5 0 90-40.6 90-90.2V186.2c0-49.6-40.5-90.2-90-90.2zM177.5 314.4h167.3v59.9H177.5v-59.9z m0 223.5h167.3v59.9H177.5v-59.9z m646.4 229.9v-0.2H362.8v0.2h-59.7c0-20.1 2.3-39.9 6.9-59.1v-1.2h0.3c3.8-15.7 9.1-31 16-46 14.8-32.3 35.9-61.3 62.7-86.1 36.8-34 81.6-57.7 130.5-69.7-39.8-24.8-66.3-69-66.3-119.4 0-77.6 62.7-140.6 140.2-140.6 77.5 0 140.2 63 140.2 140.6 0 50.4-26.5 94.6-66.3 119.4 48.9 11.9 93.7 35.6 130.5 69.7 26.8 24.8 47.9 53.8 62.7 86.1 15.4 33.7 23.2 69.4 23.2 106.3h-59.8z" fill="#f7f7f7" p-id="11907"></path></svg>
admin/src/assets/icons/system/pwd.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621744473001" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2398" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M822.857143 475.428571a36.571429 36.571429 0 0 1 36.571428 36.571429v91.428571a347.428571 347.428571 0 0 1-694.857142 0v-91.428571a36.571429 36.571429 0 0 1 36.571428-36.571429h621.714286m0-73.142857H201.142857a109.714286 109.714286 0 0 0-109.714286 109.714286v91.428571a420.571429 420.571429 0 0 0 420.571429 420.571429 420.571429 420.571429 0 0 0 420.571429-420.571429v-91.428571a109.714286 109.714286 0 0 0-109.714286-109.714286z" p-id="2399" fill="#c1c4cc"></path><path d="M731.428571 475.428571a36.571429 36.571429 0 0 1-36.571428-36.571428V256a182.857143 182.857143 0 0 0-365.714286 0v182.857143a36.571429 36.571429 0 0 1-73.142857 0V256a256 256 0 0 1 512 0v182.857143a36.571429 36.571429 0 0 1-36.571429 36.571428zM512 768a36.571429 36.571429 0 0 1-36.571429-36.571429v-146.285714a36.571429 36.571429 0 0 1 73.142858 0v146.285714a36.571429 36.571429 0 0 1-36.571429 36.571429z" p-id="2400" fill="#c1c4cc"></path></svg>
admin/src/assets/icons/system/role.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621750614796" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6622" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M782.215429 604.888761l-18.436207-15.623905-4.609052-3.749737c-3.827857-3.2029-7.811952-6.249562-11.48357-9.296223l-5.234008-3.905976-1.093673-0.859315-6.56204-4.765291c-3.359139-2.421705-6.796398-4.765291-10.233658-7.030757l-2.343585-1.56239a382.082586 382.082586 0 0 0-109.367332-49.293419 264.668943 264.668943 0 1 0-204.438791 0 385.129247 385.129247 0 0 0-108.0393 48.98094l-2.265466 1.562391c-3.515379 2.265466-6.952638 4.609052-10.233658 7.030757l-6.640159 4.84341-0.937434 0.624957-5.312128 4.062215c-3.905976 2.968542-7.811952 6.093323-11.952287 9.608701l-4.062215 3.437259-18.592446 15.623905 0.937434 0.546836A397.159653 397.159653 0 0 0 122.183582 889.321943V914.085832a109.757929 109.757929 0 0 0 109.757929 109.367331h559.257663a109.757929 109.757929 0 0 0 109.367332-109.367331v-24.998248a396.847175 396.847175 0 0 0-119.288511-283.730106z" fill="#f7f7f7" p-id="6623"></path><path d="M232.253989 576.297015c3.984096-3.359139 7.811952-6.327681 11.327331-9.139984l5.858964-4.452813 0.859315-0.624956 6.952637-5.077769 10.936734-7.811952 2.421705-1.562391a403.409215 403.409215 0 0 1 74.526024-38.512924 286.698648 286.698648 0 0 1-138.115316-244.748465c0-6.405801 0-12.733482 0.703076-18.983044a171.081755 171.081755 0 0 0 24.52953 330.914298zM225.926308 581.609143a252.951015 252.951015 0 0 0-102.805292 39.059761l-1.562391 1.015554c-2.265466 1.56239-4.530932 3.046661-6.718278 4.687171l-4.374694 3.202901c-1.406151 1.015554-2.812303 2.031108-4.140334 3.124781-2.577944 1.952988-5.155888 3.984096-7.811953 6.093322l-0.859314 0.781196-2.187347 1.718629A262.950313 262.950313 0 0 0 5.004298 839.872285v17.342534a63.979889 63.979889 0 0 0 63.979889 63.979889h12.186645v-21.482869a421.220466 421.220466 0 0 1 107.648703-281.230281zM678.863301 509.426704a406.221518 406.221518 0 0 1 74.526024 38.512925l2.343586 1.56239 11.014853 7.811952 6.874518 5.077769 0.937434 0.624956 5.858964 4.452813c3.593498 2.812303 7.343235 5.780845 11.327331 9.139984a171.081755 171.081755 0 0 0 24.52953-330.914298c0.390598 6.249562 0.703076 12.577243 0.703076 18.983044A286.698648 286.698648 0 0 1 678.863301 509.426704zM928.220817 641.136219l-2.109227-1.718629-0.937434-0.781196c-2.499825-2.109227-4.999649-4.140335-7.811953-6.093322l-4.140334-3.124781-4.374694-3.202901c-2.187347-1.64051-4.452813-3.124781-6.796398-4.687171l-1.484271-1.015554a253.341612 253.341612 0 0 0-102.805292-39.059761l37.106773 36.716175a421.142347 421.142347 0 0 1 107.570583 281.230282v21.482869h12.186646a64.058009 64.058009 0 0 0 64.058008-63.979889v-17.030056a262.715955 262.715955 0 0 0-90.462407-198.736066z" fill="#f7f7f7" p-id="6624"></path></svg>
admin/src/assets/icons/system/shield.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621745695957" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3315" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M517.094527 1024c-153.345274 0-438.129353-228.234826-438.129353-444.242786V167.60995c0-10.189055 8.151244-17.830846 18.849751-18.340298l24.963184-0.509453c1.018905 0 98.324378-2.037811 198.686568-41.775124C423.864677 66.228856 490.093532 19.359204 491.112438 18.849751l14.774129-10.189054c3.056716-2.037811 7.132338-3.566169 11.20796-3.56617 4.075622 0 8.151244 1.018905 11.207961 3.56617l14.774129 10.698507c0.509453 0.509453 67.247761 47.379104 170.157214 88.135323 100.362189 39.737313 197.667662 41.775124 198.686567 41.775125l24.453731 0.509452c10.189055 0 18.849751 8.151244 18.849752 18.340299v412.147264c0 215.498507-284.78408 443.733333-438.129354 443.733333z m370.881592-808.501493c-37.699502-3.056716-117.174129-12.736318-199.196019-45.341293-84.569154-33.114428-146.212935-70.304478-171.685573-87.116418-25.472637 16.302488-87.116418 54.00199-171.685572 87.116418-82.021891 32.604975-160.987065 42.284577-199.19602 45.341293v364.258707c0 169.138308 248.103483 376.485572 370.881592 376.485572 47.379104 0 140.099502-38.718408 230.78209-123.287562 87.625871-81.512438 140.099502-176.270647 140.099502-253.19801V215.498507z m-406.03383 433.544279c-6.113433 6.622886-14.774129 10.189055-23.944279 10.189055-9.170149 0-17.321393-3.566169-23.944279-10.189055l-101.381094-101.890547a33.827662 33.827662 0 0 1 0-47.888557c13.245771-13.245771 34.133333-13.245771 47.379104-0.509453l0.509453 0.509453 77.436816 77.946268 195.120398-196.648756c12.736318-13.245771 34.133333-13.245771 47.379104-0.509453l0.509453 0.509453c13.245771 13.245771 13.245771 34.642786 0 47.888557l-219.064676 220.593035z" p-id="3316" fill="#c1c4cc"></path></svg>
admin/src/assets/images/404-tip.png
admin/src/assets/images/404.png
admin/src/assets/images/alipay.jpeg
admin/src/assets/images/login.jpg
admin/src/assets/images/not-allow.png
admin/src/assets/images/qq.png
admin/src/assets/images/wxpay.jpeg
admin/src/assets/logo.png
admin/src/assets/style/element-variables.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
@import "variables";
/* theme color */
$--color-primary: $primary-color;
/* icon font path, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";
admin/src/assets/style/style.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
@import "variables.scss";
// æ ·å¼é‡ç½®
html {
  height: 100%;
  padding: 0;
  margin: 0;
  color: $font-color;
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  body {
    height: 100%;
    padding: 0;
    margin: 0;
  }
  h1,h2,h3,h4,h5,h6,ul {
    margin: 0;
    padding: 0;
  }
  ul {
    list-style: none;
  }
  #app {
    height: 100%;
    min-width: $page-min-width;
  }
}
// ç©¿æ¢­æ¡†çš„æŒ‰é’®
.el-transfer__buttons {
  padding: 0 16px !important;
}
admin/src/assets/style/variables.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
// ä¸»è‰²è°ƒ
$primary-color: #2E68EC;
// å¤´éƒ¨é«˜åº¦
$header-height: 60px;
// èœå•宽度
$menu-width: 208px;
// é¡µé¢æœ€å°å®½åº¦
$page-min-width: 1000px;
// å­—体
$font-color: #282828; // é¢œè‰²
$font-size: 12px; // å¤§å°
admin/src/components/base/BaseOpera.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
<script>
export default {
  name: 'BaseOpera',
  data () {
    return {
      title: '',
      visible: false,
      isWorking: false,
      // æŽ¥å£
      api: null,
      // é…ç½®æ•°æ®
      configData: {
        'field.id': 'id'
      }
    }
  },
  methods: {
    // é…ç½®
    config (extParams = {}) {
      if (extParams == null) {
        throw new Error('Parameter can not be null of method \'config\' .')
      }
      if (extParams.api == null) {
        throw new Error('Missing config option \'api\'.')
      }
      this.api = require('@/api' + extParams.api)
      extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
    },
    /**
     * æ‰“开窗口
     * @title çª—口标题
     * @target ç¼–辑的对象
     */
    open (title, target) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form[this.configData['field.id']] = null
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        for (const key in this.form) {
          this.form[key] = target[key]
        }
      })
    },
    // ç¡®è®¤æ–°å»º/修改
    confirm () {
      if (this.form.id == null || this.form.id === '') {
        this.__confirmCreate()
        return
      }
      this.__confirmEdit()
    },
    // ç¡®è®¤æ–°å»º
    __confirmCreate () {
      this.$refs.form.validate((valid) => {
        if (!valid) {
          return
        }
        // è°ƒç”¨æ–°å»ºæŽ¥å£
        this.isWorking = true
        this.api.create(this.form)
          .then(() => {
            this.visible = false
            this.$tip.apiSuccess('新建成功')
            this.$emit('success')
          })
          .catch(e => {
            this.$tip.apiFailed(e)
          })
          .finally(() => {
            this.isWorking = false
          })
      })
    },
    // ç¡®è®¤ä¿®æ”¹
    __confirmEdit () {
      this.$refs.form.validate((valid) => {
        if (!valid) {
          return
        }
        // è°ƒç”¨æ–°å»ºæŽ¥å£
        this.isWorking = true
        this.api.updateById(this.form)
          .then(() => {
            this.visible = false
            this.$tip.apiSuccess('修改成功')
            this.$emit('success')
          })
          .catch(e => {
            this.$tip.apiFailed(e)
          })
          .finally(() => {
            this.isWorking = false
          })
      })
    }
  }
}
</script>
admin/src/components/base/BasePage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
<script>
import { mapState } from 'vuex'
export default {
  name: 'BasePage',
  data () {
    return {
      // è¶…级管理员角色code
      adminCode: 'admin'
    }
  },
  computed: {
    ...mapState(['userInfo']),
    // æ˜¯å¦ä¸ºè¶…级管理员
    isAdmin () {
      return this.userInfo.roles.findIndex(code => code === this.adminCode) > -1
    }
  },
  methods: {
    // æ˜¯å¦åŒ…含指定角色
    containRoles (roles) {
      if (roles == null) {
        return true
      }
      if (this.userInfo == null) {
        return false
      }
      if (this.userInfo.roles == null || this.userInfo.roles.length === 0) {
        return false
      }
      for (const code of roles) {
        if (this.userInfo.roles.findIndex(r => r === code) > -1) {
          return true
        }
      }
      return false
    },
    // æ˜¯å¦åŒ…含指定权限
    containPermissions (permissions) {
      if (permissions == null) {
        return true
      }
      if (this.userInfo == null) {
        return false
      }
      if (this.userInfo.permissions == null || this.userInfo.permissions.length === 0) {
        return false
      }
      for (const code of permissions) {
        if (this.userInfo.permissions.findIndex(p => p === code) > -1) {
          return true
        }
      }
      return false
    }
  }
}
</script>
admin/src/components/base/BaseTable.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,215 @@
<script>
import BasePage from './BasePage'
export default {
  name: 'BaseTable',
  extends: BasePage,
  data () {
    return {
      // æŽ¥å£
      api: null,
      // æ¨¡å—名称
      module: '数据',
      // é…ç½®æ•°æ®
      configData: {
        // id字段
        'field.id': 'id',
        // ä¸»å­—段
        'field.main': 'name'
      },
      // æ˜¯å¦æ­£åœ¨æ‰§è¡Œ
      isWorking: {
        // æœç´¢ä¸­
        search: false,
        // åˆ é™¤ä¸­
        delete: false,
        // å¯¼å‡ºä¸­
        export: false
      },
      // è¡¨æ ¼æ•°æ®
      tableData: {
        // å·²é€‰ä¸­çš„æ•°æ®
        selectedRows: [],
        // æŽ’序的字段
        sorts: [],
        // å½“前页数据
        list: [],
        // åˆ†é¡µ
        pagination: {
          pageIndex: 1,
          pageSize: 10,
          total: 0
        }
      }
    }
  },
  methods: {
    // é…ç½®
    config (extParams) {
      if (extParams == null) {
        throw new Error('Parameter can not be null of method \'config\' .')
      }
      if (extParams.api == null) {
        throw new Error('Missing config option \'api\'.')
      }
      this.api = require('@/api' + extParams.api)
      extParams.module && (this.module = extParams.module)
      extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
      extParams['field.main'] && (this.configData['field.main'] = extParams['field.main'])
      this.tableData.sorts = extParams.sorts
    },
    // æœç´¢
    search () {
      this.handlePageChange(1)
    },
    // å¯¼å‡ºExcel
    exportExcel () {
      this.__checkApi()
      this.$dialog.exportConfirm('确认导出吗?')
        .then(() => {
          this.isWorking.export = true
          this.api.exportExcel({
            page: this.tableData.pagination.pageIndex,
            capacity: 1000000,
            model: this.searchForm,
            sorts: this.tableData.sorts
          })
            .then(response => {
              this.download(response)
            })
            .catch(e => {
              this.$tip.apiFailed(e)
            })
            .finally(() => {
              this.isWorking.export = false
            })
        })
        .catch(() => {})
    },
    // æœç´¢æ¡†é‡ç½®
    reset () {
      this.$refs.searchForm.resetFields()
      this.search()
    },
    // æ¯é¡µæ˜¾ç¤ºæ•°é‡å˜æ›´å¤„理
    handleSizeChange (pageSize) {
      this.tableData.pagination.pageSize = pageSize
      this.search()
    },
    // è¡Œé€‰ä¸­å¤„理
    handleSelectionChange (selectedRows) {
      this.tableData.selectedRows = selectedRows
    },
    // æŽ’序
    handleSortChange (sortData) {
      this.tableData.sorts = []
      if (sortData.order != null) {
        this.tableData.sorts.push({
          property: sortData.column.sortBy,
          direction: sortData.order === 'descending' ? 'DESC' : 'ASC'
        })
      }
      this.handlePageChange()
    },
    // é¡µç å˜æ›´å¤„理
    handlePageChange (pageIndex) {
      this.__checkApi()
      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
      this.isWorking.search = true
      this.api.fetchList({
        page: this.tableData.pagination.pageIndex,
        capacity: this.tableData.pagination.pageSize,
        model: this.searchForm,
        sorts: this.tableData.sorts
      })
        .then(data => {
          this.tableData.list = data.records
          this.tableData.pagination.total = data.total
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking.search = false
        })
    },
    // åˆ é™¤
    deleteById (row, childConfirm = true) {
      this.__checkApi()
      let message = `确认删除${this.module}【${row[this.configData['field.main']]}】吗?`
      if (childConfirm && row.children != null && row.children.length > 0) {
        message = `确认删除${this.module}【${row[this.configData['field.main']]}】及其子${this.module}吗?`
      }
      this.$dialog.deleteConfirm(message)
        .then(() => {
          this.isWorking.delete = true
          this.api.deleteById(row[this.configData['field.id']])
            .then(() => {
              this.$tip.apiSuccess('删除成功')
              this.__afterDelete()
            })
            .catch(e => {
              this.$tip.apiFailed(e)
            })
            .finally(() => {
              this.isWorking.delete = false
            })
        })
        .catch(() => {})
    },
    /**
     * æ‰¹é‡åˆ é™¤
     * @treeMode æ˜¯å¦æ·»åŠ å­èŠ‚ç‚¹åˆ é™¤ç¡®è®¤
     */
    deleteByIdInBatch (childConfirm = true) {
      this.__checkApi()
      if (this.tableData.selectedRows.length === 0) {
        this.$tip.warning('请至少选择一条数据')
        return
      }
      let message = `确认删除已选中的 ${this.tableData.selectedRows.length} æ¡${this.module}记录吗?`
      if (childConfirm) {
        const containChildrenRows = []
        for (const row of this.tableData.selectedRows) {
          if (row.children != null && row.children.length > 0) {
            containChildrenRows.push(row[this.configData['field.main']])
          }
        }
        if (containChildrenRows.length > 0) {
          message = `本次将删除${this.module}【${containChildrenRows.join('、')}】及其子${this.module}记录,确认删除吗?`
        }
      }
      this.$dialog.deleteConfirm(message)
        .then(() => {
          this.isWorking.delete = true
          this.api.deleteByIdInBatch(this.tableData.selectedRows.map(row => row.id).join(','))
            .then(() => {
              this.$tip.apiSuccess('删除成功')
              this.__afterDelete(this.tableData.selectedRows.length)
            })
            .catch(e => {
              this.$tip.apiFailed(e)
            })
            .finally(() => {
              this.isWorking.delete = false
            })
        })
        .catch(() => {})
    },
    // åˆ é™¤å¤„理
    __afterDelete (deleteCount = 1) {
      // åˆ é™¤å½“前页最后一条记录时查询上一页数据
      if (this.tableData.list.length - deleteCount === 0) {
        this.handlePageChange(this.tableData.pagination.pageIndex - 1 === 0 ? 1 : this.tableData.pagination.pageIndex - 1)
      } else {
        this.handlePageChange(this.tableData.pagination.pageIndex)
      }
    },
    // æ£€æŸ¥æŽ¥å£æ˜¯å¦é…ç½®
    __checkApi () {
      if (this.api == null) {
        throw new Error('The page is not initialized, you can use method \'this.config\' to initialize this page.')
      }
    }
  }
}
</script>
admin/src/components/common/ColumnDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
<template>
  <span v-if="content.length <= limit">{{content}}</span>
  <el-popover
    v-else
    v-model="visible"
    popper-class="eva-column-detail-popover"
    trigger="click"
  >
    <div class="eva-column-detail">
      <pre class="eva-column-detail__main">{{formattedContent}}</pre>
      <div class="eva-column-detail__action">
        <el-button size="mini" @click="cancel">关闭</el-button>
        <el-button
          size="mini"
          type="primary"
          v-clipboard:copy="formattedContent"
          v-clipboard:success="copySuccess"
          v-clipboard:error="copyFailed"
          @click="confirm"
        >{{ confirmButtonText }}</el-button>
      </div>
    </div>
    <el-button slot="reference" :type="buttonType">查看</el-button>
  </el-popover>
</template>
<script>
export default {
  name: 'ColumnDetail',
  props: {
    // æŒ‰é’®ç±»åž‹
    buttonType: {
      type: String
    },
    // å†…容
    content: {
      type: String,
      default: ''
    },
    // é™åˆ¶ï¼Œå¤§äºŽé™åˆ¶æ—¶å±•示查看按钮
    limit: {
      type: Number,
      default: 12
    },
    // è‡ªåŠ¨è¯†åˆ«æ•°æ®ç±»åž‹å¹¶æ ¼å¼åŒ–
    analyse: {
      type: Boolean,
      default: true
    },
    // æ˜¯å¦å…è®¸å¤åˆ¶
    allowCopy: {
      type: Boolean,
      default: true
    }
  },
  data () {
    return {
      visible: false
    }
  },
  computed: {
    // ç¡®è®¤æŒ‰é’®æ–‡æ¡ˆ
    confirmButtonText () {
      return this.allowCopy ? '复制' : '确定'
    },
    // æ ¼å¼åŒ–后的内容
    formattedContent () {
      let content = this.content
      if (this.analyse) {
        try {
          content = JSON.stringify(JSON.parse(this.content), null, 2)
        } catch (e) {
        }
      }
      return content
    }
  },
  methods: {
    // ç‚¹å‡»ç¡®è®¤
    confirm () {
      this.visible = false
      this.$emit('confirm')
    },
    // ç‚¹å‡»å–消
    cancel () {
      this.visible = false
      this.$emit('cancel')
    },
    // å¤åˆ¶æˆåŠŸ
    copySuccess () {
      this.$tip.success('复制成功')
    },
    // å¤åˆ¶å¤±è´¥
    copyFailed () {
      this.$tip.error('复制失败')
    }
  }
}
</script>
<style lang="scss">
.eva-column-detail-popover {
  max-width: 80%;
}
</style>
<style scoped lang="scss">
.eva-column-detail {
  .eva-column-detail__main {
    max-height: 500px;
    overflow: auto;
  }
  .eva-column-detail__action {
    text-align: right;
  }
}
</style>
admin/src/components/common/DepartmentSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
<template>
  <TreeSelect
    :placeholder="placeholder"
    :value="value"
    :data="data"
    :clearable="clearable"
    :append-to-body="appendToBody"
    :inline="inline"
    :multiple="multiple"
    :flat="multiple"
    @input="$emit('input', $event)"
  />
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/department'
export default {
  name: 'DepartmentSelect',
  components: { TreeSelect },
  props: {
    value: {},
    inline: {
      default: true
    },
    multiple: {
      default: false
    },
    placeholder: {
      default: '请选择部门'
    },
    // æ˜¯å¦å¯æ¸…空
    clearable: {
      default: false
    },
    appendToBody: {
      default: false
    },
    // éœ€è¢«æŽ’除的部门ID
    excludeId: {}
  },
  data () {
    return {
      data: []
    }
  },
  watch: {
    excludeId () {
      this.fetchData()
    }
  },
  methods: {
    // èŽ·å–æ‰€æœ‰éƒ¨é—¨
    fetchData () {
      fetchTree()
        .then(records => {
          this.data = []
          this.__fillData(this.data, records)
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    },
    // å¡«å……部门树
    __fillData (list, pool) {
      for (const dept of pool) {
        if (dept.id === this.excludeId) {
          continue
        }
        const deptNode = {
          id: dept.id,
          label: dept.name
        }
        list.push(deptNode)
        if (dept.children != null && dept.children.length > 0) {
          deptNode.children = []
          this.__fillData(deptNode.children, dept.children)
          if (deptNode.children.length === 0) {
            deptNode.children = undefined
          }
        }
      }
    }
  },
  created () {
    this.fetchData()
  }
}
</script>
admin/src/components/common/GlobalWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
<template>
  <el-drawer
    class="global-window"
    title="title"
    :visible="visible"
    :with-header="true"
    :size="width"
    :close-on-press-escape="false"
    :wrapper-closable="false"
    :append-to-body="true"
    @close="close"
  >
    <div slot="title" class="window__header">
      <span class="header__btn-back" @click="close"><i class="el-icon-arrow-left"></i></span>{{title}}
    </div>
    <div class="window__body">
      <slot></slot>
    </div>
    <div v-if="withFooter" class="window__footer">
      <slot name="footer">
        <el-button @click="confirm" :loading="confirmWorking" type="primary">确定</el-button>
        <el-button @click="close">取消</el-button>
      </slot>
    </div>
  </el-drawer>
</template>
<script>
export default {
  name: 'GlobalWindow',
  props: {
    width: {
      type: String,
      default: '36%'
    },
    // æ˜¯å¦åŒ…含底部操作
    withFooter: {
      type: Boolean,
      default: true
    },
    // ç¡®è®¤æŒ‰é’®loading状态
    confirmWorking: {
      type: Boolean,
      default: false
    },
    // æ ‡é¢˜
    title: {
      type: String,
      default: ''
    },
    // æ˜¯å¦å±•示
    visible: {
      type: Boolean,
      required: true
    }
  },
  methods: {
    confirm () {
      this.$emit('confirm')
    },
    close () {
      this.$emit('update:visible', false)
    }
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
// è¾“入框高度
$input-height: 32px;
.global-window {
  // å¤´éƒ¨æ ‡é¢˜
  /deep/ .el-drawer__header {
    padding: 0 10px 0 0;
    line-height: 40px;
    border-bottom: 1px solid #eee;
    // è¿”回按钮
    .header__btn-back {
      display: inline-block;
      width: 30px;
      background: $primary-color;
      color: #fff;
      text-align: center;
      margin-right: 12px;
      border-right: 1px solid #eee;
    }
    .el-drawer__close-btn:focus {
      outline: none;
    }
  }
  // ä¸»ä½“
  /deep/ .el-drawer__body {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 40px;
    bottom: 0;
    width: 100%;
    overflow: hidden;
    // å†…容
    .window__body {
      height: 100%;
      overflow-y: auto;
      padding: 12px 16px;
      // æ ‡ç­¾
      .el-form-item__label {
        float: none;
      }
      // å…ƒç´ å®½åº¦ä¸º100%
      .el-form-item__content > *{
        width: 100%;
      }
    }
    // å°¾éƒ¨
    .window__footer {
      user-select: none;
      border-top: 1px solid #eee;
      height: 60px;
      line-height: 60px;
      text-align: center;
    }
  }
}
</style>
admin/src/components/common/Header.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,200 @@
<template>
  <div class="main-header">
    <div class="header">
      <h2>
        <i class="el-icon-s-unfold" v-if="menuData.collapse" @click="switchCollapseMenu(null)"></i>
        <i class="el-icon-s-fold" v-else @click="switchCollapseMenu(null)"></i>
        {{title}}
      </h2>
      <div class="user">
        <el-dropdown trigger="click">
          <span class="el-dropdown-link">
            <img v-if="userInfo != null" :src="userInfo.avatar == null ? '@/assets/images/avatar/man.png' : userInfo.avatar" alt="">{{userInfo | displayName}}<i class="el-icon-arrow-down el-icon--right"></i>
          </span>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item @click.native="changePwd">修改密码</el-dropdown-item>
            <el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </div>
    </div>
    <!-- ä¿®æ”¹å¯†ç  -->
    <GlobalWindow
      title="修改密码"
      :visible.sync="visible.changePwd"
      @confirm="confirmChangePwd"
      @close="visible.changePwd = false"
    >
      <el-form :model="changePwdData.form" ref="changePwdDataForm" :rules="changePwdData.rules">
        <el-form-item label="原始密码" prop="oldPwd" required>
          <el-input v-model="changePwdData.form.oldPwd" type="password" placeholder="请输入原始密码" maxlength="30" show-password></el-input>
        </el-form-item>
        <el-form-item label="新密码" prop="newPwd" required>
          <el-input v-model="changePwdData.form.newPwd" type="password" placeholder="请输入新密码" maxlength="30" show-password></el-input>
        </el-form-item>
        <el-form-item label="确认新密码" prop="confirmPwd" required>
          <el-input v-model="changePwdData.form.confirmPwd" type="password" placeholder="请再次输入新密码" maxlength="30" show-password></el-input>
        </el-form-item>
      </el-form>
    </GlobalWindow>
  </div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import GlobalWindow from './GlobalWindow'
import { logout, updatePwd } from '@/api/system/common'
export default {
  name: 'Header',
  components: { GlobalWindow },
  data () {
    return {
      visible: {
        // ä¿®æ”¹å¯†ç 
        changePwd: false
      },
      isWorking: {
        // ä¿®æ”¹å¯†ç 
        changePwd: false
      },
      username: 'bob', // ç”¨æˆ·å
      // ä¿®æ”¹å¯†ç å¼¹æ¡†
      changePwdData: {
        form: {
          oldPwd: '',
          newPwd: '',
          confirmPwd: ''
        },
        rules: {
          oldPwd: [
            { required: true, message: '请输入原始密码' }
          ],
          newPwd: [
            { required: true, message: '请输入新密码' }
          ],
          confirmPwd: [
            { required: true, message: '请再次输入新密码' }
          ]
        }
      }
    }
  },
  computed: {
    ...mapState(['menuData', 'userInfo']),
    title () {
      return this.$route.meta.title
    }
  },
  filters: {
    // å±•示名称
    displayName (userInfo) {
      if (userInfo == null) {
        return ''
      }
      if (userInfo.realname != null && userInfo.realname.trim().length > 0) {
        return userInfo.realname
      }
      return userInfo.username
    }
  },
  methods: {
    ...mapMutations(['setUserInfo', 'switchCollapseMenu']),
    // ä¿®æ”¹å¯†ç 
    changePwd () {
      this.visible.changePwd = true
      this.$nextTick(() => {
        this.$refs.changePwdDataForm.resetFields()
      })
    },
    // ç¡®å®šä¿®æ”¹å¯†ç 
    confirmChangePwd () {
      if (this.isWorking.changePwd) {
        return
      }
      this.$refs.changePwdDataForm.validate((valid) => {
        if (!valid) {
          return
        }
        // éªŒè¯ä¸¤æ¬¡å¯†ç è¾“入是否一致
        if (this.changePwdData.form.newPwd !== this.changePwdData.form.confirmPwd) {
          this.$tip.warning('两次密码输入不一致')
          return
        }
        // æ‰§è¡Œä¿®æ”¹
        this.isWorking.changePwd = true
        updatePwd({
          oldPwd: this.changePwdData.form.oldPwd,
          newPwd: this.changePwdData.form.newPwd
        })
          .then(() => {
            this.$tip.apiSuccess('修改成功')
            this.visible.changePwd = false
          })
          .catch(e => {
            this.$tip.apiFailed(e)
          })
          .finally(() => {
            this.isWorking.changePwd = false
          })
      })
    },
    // é€€å‡ºç™»å½•
    logout () {
      logout()
        .then(() => {
          this.setUserInfo(null)
          this.$router.push({ name: 'login' })
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    }
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.header {
  overflow: hidden;
  padding: 0 25px;
  background: #fff;
  height: 100%;
  display: flex;
  h2 {
    width: 50%;
    flex-shrink: 0;
    line-height: $header-height;
    font-size: 19px;
    font-weight: 600;
    color: #606263;
    display: inline;
    & > i {
      font-size: 20px;
      margin-right: 12px;
    }
  }
  .user {
    width: 50%;
    flex-shrink: 0;
    text-align: right;
    .el-dropdown {
      top: 2px;
    }
    img {
      width: 32px;
      position: relative;
      top: 10px;
      margin-right: 10px;
    }
  }
}
// ä¸‹æ‹‰èœå•框
.el-dropdown-menu {
  width: 140px;
  .el-dropdown-menu__item:hover {
    background: #E3EDFB;
    color: $primary-color;
  }
}
</style>
admin/src/components/common/Light.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
<template>
  <div class="light" :class="{normal: !warn && !danger, warn: !danger && warn, danger, mini: mini}">
    <em><i></i></em>
  </div>
</template>
<script>
export default {
  name: 'Light',
  props: {
    warn: {
      type: Boolean,
      default: false
    },
    danger: {
      type: Boolean,
      default: false
    },
    mini: {
      type: Boolean,
      default: false
    }
  }
}
</script>
<style scoped lang="scss">
$cycle-size01: 16px;
$cycle-size02: 6px;
$normal-color: #00CC99;
$warn-color: #FFCC33;
$danger-color: #FF3300;
@mixin light-status ($cycle-bg) {
  em {
    background: $cycle-bg;
    i {
      background: $cycle-bg - 30;
    }
  }
}
.light {
  display: inline-block;
  border-radius: 50%;
  em {
    width: $cycle-size01;
    height: $cycle-size01;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    i {
      display: block;
      width: $cycle-size02;
      height: $cycle-size02;
      border-radius: 50%;
    }
  }
  &.mini {
    em {
      width: 12px;
      height: 12px;
    }
  }
  // æ­£å¸¸
  &.normal {
    @include light-status($normal-color);
    animation: shine-normal infinite 1s;
  }
  // è­¦å‘Š
  &.warn {
    @include light-status($warn-color);
    animation: shine-warn infinite .8s;
  }
  // å±é™©
  &.danger {
    @include light-status($danger-color);
    animation: shine-danger infinite .5s;
  }
}
@keyframes shine-normal {
  0% {
    box-shadow: 0 0 5px $normal-color + 10;
  }
  25% {
    box-shadow: 0 0 10px $normal-color + 10;
  }
  50% {
    box-shadow: 0 0 15px $normal-color + 10;
  }
  100% {
    box-shadow: 0 0 20px $normal-color + 10;
  }
}
@keyframes shine-warn {
  0% {
    box-shadow: 0 0 5px $warn-color - 50;
  }
  25% {
    box-shadow: 0 0 10px $warn-color - 50;
  }
  50% {
    box-shadow: 0 0 15px $warn-color - 50;
  }
  100% {
    box-shadow: 0 0 20px $warn-color - 50;
  }
}
@keyframes shine-danger {
  0% {
    box-shadow: 0 0 5px $danger-color + 10;
  }
  25% {
    box-shadow: 0 0 10px $danger-color + 10;
  }
  50% {
    box-shadow: 0 0 15px $danger-color + 10;
  }
  100% {
    box-shadow: 0 0 20px $danger-color + 10;
  }
}
</style>
admin/src/components/common/Menu.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,183 @@
<template>
  <div class="menu" :class="{collapse: menuData.collapse}">
    <div class="logo">
      <div><img src="@/assets/logo.png"></div>
      <h1 :class="{hidden: menuData.collapse}">豆米跳跳</h1>
    </div>
    <scrollbar>
      <el-menu
        ref="menu"
        :default-active="activeIndex"
        text-color="#fff"
        active-text-color="#fff"
        :collapse="menuData.collapse"
        :default-openeds="defaultOpeneds"
        :collapse-transition="false"
        @select="handleSelect"
      >
        <MenuItems v-for="menu in menuData.list" :key="menu.index" :menu="menu" :is-root-menu="true"/>
      </el-menu>
    </scrollbar>
  </div>
</template>
<script>
import { mapState } from 'vuex'
import MenuItems from './MenuItems'
import Scrollbar from './Scrollbar'
export default {
  name: 'Menu',
  components: { Scrollbar, MenuItems },
  computed: {
    ...mapState(['menuData']),
    // é€‰ä¸­çš„菜单index
    activeIndex () {
      let path = this.$route.path
      if (path.endsWith('/')) {
        path = path.substring(0, path.length - 1)
      }
      const menuConfig = this.__getMenuConfig(path, 'url', this.menuData.list)
      if (menuConfig == null) {
        return null
      }
      return menuConfig.index
    },
    // é»˜è®¤å±•开的菜单index
    defaultOpeneds () {
      return this.menuData.list.map(menu => menu.index)
    }
  },
  methods: {
    // å¤„理菜单选中
    handleSelect (menuIndex) {
      const menuConfig = this.__getMenuConfig(menuIndex, 'index', this.menuData.list)
      // æ‰¾ä¸åˆ°é¡µé¢
      try {
        require('@/views' + menuConfig.url)
      } catch (e) {
        this.$tip.error('未找到页面文件@/views' + menuConfig.url + '.vue,请检查菜单路径是否正确')
      }
      // ç‚¹å‡»å½“前菜单不做处理
      if (menuConfig.url === this.$route.path) {
        return
      }
      if (menuConfig.url == null || menuConfig.url.trim().length === 0) {
        return
      }
      this.$router.push(menuConfig.url)
    },
    // èŽ·å–èœå•é…ç½®
    __getMenuConfig (value, key, menus) {
      for (const menu of menus) {
        if (menu[key] === value) {
          return menu
        }
        if (menu.children != null && menu.children.length > 0) {
          const menuConfig = this.__getMenuConfig(value, key, menu.children)
          if (menuConfig != null) {
            return menuConfig
          }
        }
      }
      return null
    }
  }
}
</script>
<style lang="scss" scoped>
@import "@/assets/style/variables.scss";
.menu {
  height: 100%;
  display: flex;
  flex-direction: column;
  // LOGO
  .logo {
    height: 60px;
    flex-shrink: 0;
    line-height: 60px;
    overflow: hidden;
    display: flex;
    background: $primary-color - 20;
    padding: 0 16px;
    & > div {
      width: 32px;
      flex-shrink: 0;
      margin-right: 12px;
      img {
        width: 100%;
        flex-shrink: 0;
        vertical-align: middle;
        position: relative;
        top: -2px;
      }
    }
    h1 {
      font-size: 16px;
      font-weight: 500;
      transition: opacity ease .3s;
      overflow: hidden;
      &.hidden {
        opacity: 0;
      }
    }
  }
}
</style>
<style lang="scss">
@import "@/assets/style/variables.scss";
// èœå•样式
.el-menu {
  border-right: 0 !important;
  user-select: none;
  background: $primary-color !important;
  .el-menu-item {
    background: $primary-color;
    // é€‰ä¸­çŠ¶æ€
    &.is-active {
      background: $primary-color - 40 !important;
    }
    // æ‚¬æµ®
    &:hover {
      background-color: $primary-color - 12;
    }
    &:focus {
      background: $primary-color;
    }
  }
  // å­èœå•
  .el-submenu {
    .el-submenu__title{
      background-color: $primary-color;
    }
    &.is-active {
      .el-submenu__title{
        background-color: $primary-color - 20;
      }
      .el-menu .el-menu-item{
        background-color: $primary-color - 20;
        // æ‚¬æµ®
        &:hover {
          background-color: $primary-color - 30;
        }
      }
    }
    // èœå•上下箭头
    .el-submenu__title i {
      color: #f7f7f7;
    }
  }
  // èœå•图标
  i:not(.el-submenu__icon-arrow) {
    color: #f7f7f7 !important;
    position: relative;
    top: -1px;
    // è‡ªå®šä¹‰å›¾æ ‡
    &[class^="eva-icon-"] {
      width: 24px;
      margin-right: 5px;
      background-size: 15px;
    }
  }
}
</style>
admin/src/components/common/MenuItems.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
<template>
  <el-menu-item v-if="menu.children == null || menu.children.length == 0" :key="menu.index" :index="menu.index">
    <i :class="menu.icon"></i>
    <span slot="title">{{menu.label}}</span>
  </el-menu-item>
  <el-submenu v-else :index="menu.index">
    <template slot="title">
      <i :class="menu.icon"></i>
      <span slot="title">{{menu.label}}</span>
    </template>
    <MenuItems v-for="child in menu.children" :menu="child" :key="child.index"/>
  </el-submenu>
</template>
<script>
export default {
  name: 'MenuItems',
  props: {
    menu: {
      type: Object,
      required: true
    }
  }
}
</script>
admin/src/components/common/MenuSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
<template>
  <TreeSelect
    :placeholder="placeholder"
    :value="value"
    :data="data"
    :append-to-body="appendToBody"
    :clearable="clearable"
    :inline="inline"
    @input="$emit('input', $event)"
  />
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/menu'
export default {
  name: 'MenuSelect',
  components: { TreeSelect },
  props: {
    value: {},
    inline: {
      default: true
    },
    placeholder: {
      default: '请选择菜单'
    },
    // æ˜¯å¦å¯æ¸…空
    clearable: {
      default: false
    },
    appendToBody: {
      default: false
    },
    // éœ€è¢«æŽ’除的部门ID
    excludeId: {}
  },
  data () {
    return {
      data: []
    }
  },
  watch: {
    excludeId () {
      this.fetchData()
    }
  },
  methods: {
    // èŽ·å–æ‰€æœ‰èœå•
    fetchData () {
      fetchTree()
        .then(records => {
          this.data = []
          this.__fillData(this.data, records)
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    },
    // å¡«å……菜单树
    __fillData (list, pool) {
      for (const menu of pool) {
        if (menu.id === this.excludeId) {
          continue
        }
        const menuNode = {
          id: menu.id,
          label: menu.name
        }
        list.push(menuNode)
        if (menu.children != null && menu.children.length > 0) {
          menuNode.children = []
          this.__fillData(menuNode.children, menu.children)
          if (menuNode.children.length === 0) {
            menuNode.children = undefined
          }
        }
      }
    }
  },
  created () {
    this.fetchData()
  }
}
</script>
admin/src/components/common/NotAllow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
<template>
  <div class="not-allow">
    <slot>
      <div class="content">
        <img src="../../assets/images/not-allow.png">
        <h2>无权访问</h2>
        <p>如您需要访问该页面,请联系系统管理员</p>
      </div>
    </slot>
  </div>
</template>
<script>
export default {
  name: 'NotAllow'
}
</script>
<style scoped lang="scss">
.not-allow {
  height: 100%;
  background: #fff;
  box-sizing: border-box;
  padding-top: 160px;
  .content {
    height: 200px;
    text-align: center;
    h2 {
      font-size: 18px;
      font-weight: normal;
      margin-top: 8px;
    }
    p {
      font-size: 13px;
      color: #999;
      margin: 6px 0;
    }
  }
}
</style>
admin/src/components/common/Pagination.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
<template>
  <div class="table-pagination">
    <el-pagination
      :current-page="pagination.pageIndex"
      :page-sizes="[10, 20, 30, 40]"
      :page-size="pagination.pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="pagination.total"
      @size-change="sizeChange"
      @current-change="currentChange"
      background>
    </el-pagination>
  </div>
</template>
<script>
export default {
  name: 'Pagination',
  props: {
    pagination: {
      type: Object,
      default: function () {
        return {}
      }
    }
  },
  data () {
    return {
    }
  },
  methods: {
    sizeChange (value) {
      this.$emit('size-change', value)
    },
    currentChange (value) {
      this.$emit('current-change', value)
    }
  }
}
</script>
admin/src/components/common/PositionSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
<template>
  <TreeSelect
    :placeholder="placeholder"
    :value="value"
    :data="data"
    :clearable="clearable"
    :append-to-body="appendToBody"
    :inline="inline"
    :multiple="multiple"
    :flat="multiple"
    @input="$emit('input', $event)"
  />
</template>
<script>
import TreeSelect from './TreeSelect'
import { fetchTree } from '@/api/system/position'
export default {
  name: 'PositionSelect',
  components: { TreeSelect },
  props: {
    value: {},
    inline: {
      default: true
    },
    multiple: {
      default: false
    },
    placeholder: {
      default: '请选择岗位'
    },
    // æ˜¯å¦å¯æ¸…空
    clearable: {
      default: false
    },
    appendToBody: {
      default: false
    },
    // éœ€è¢«æŽ’除的部门ID
    excludeId: {}
  },
  data () {
    return {
      data: []
    }
  },
  watch: {
    excludeId () {
      this.fetchData()
    }
  },
  methods: {
    // èŽ·å–æ‰€æœ‰å²—ä½
    fetchData () {
      fetchTree()
        .then(records => {
          this.data = []
          this.__fillData(this.data, records)
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    },
    // å¡«å……岗位树
    __fillData (list, pool) {
      for (const dept of pool) {
        if (dept.id === this.excludeId) {
          continue
        }
        const deptNode = {
          id: dept.id,
          label: dept.name
        }
        list.push(deptNode)
        if (dept.children != null && dept.children.length > 0) {
          deptNode.children = []
          this.__fillData(deptNode.children, dept.children)
          if (deptNode.children.length === 0) {
            deptNode.children = undefined
          }
        }
      }
    }
  },
  created () {
    this.fetchData()
  }
}
</script>
<style scoped lang="scss">
.inline {
  width: 178px;
}
.vue-treeselect {
  line-height: 30px;
  /deep/ .vue-treeselect__control {
    height: 32px;
    .vue-treeselect__single-value {
      line-height: 30px;
    }
  }
}
</style>
admin/src/components/common/Profile.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
<template>
  <div v-if="containRoles(roles) && containPermissions(permissions)">
    <slot></slot>
  </div>
  <div v-else class="not-allow-wrap">
    <slot name="not-allow"><NotAllow/></slot>
  </div>
</template>
<script>
import BasePage from '@/components/base/BasePage'
import NotAllow from './NotAllow'
export default {
  name: 'Profile',
  components: { NotAllow },
  extends: BasePage,
  props: {
    permissions: {
      type: Array
    },
    roles: {
      type: Array
    }
  }
}
</script>
<style scoped lang="scss">
.not-allow-wrap {
  height: 100%;
  padding: 10px 16px;
  box-sizing: border-box;
}
</style>
admin/src/components/common/Scrollbar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <vue-scroll :ops="options">
    <slot></slot>
  </vue-scroll>
</template>
<script>
import VueScroll from 'vuescroll'
export default {
  name: 'Scrollbar',
  components: { VueScroll },
  data () {
    return {
      options: {
        bar: {
          background: 'rgba(20,20,20,.3)'
        }
      }
    }
  }
}
</script>
admin/src/components/common/SearchFormCollapse.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
<template>
  <div class="search-form-collapse" :class="{'collapse__hidden': !showMore}">
    <slot></slot>
    <el-button v-if="!showMore" class="collapse__switch" @click="showMore = true">更多查询...</el-button>
    <el-button v-else class="collapse__switch" @click="showMore = false">收起</el-button>
  </div>
</template>
<script>
export default {
  name: 'SearchFormCollapse',
  data () {
    return {
      showMore: false
    }
  }
}
</script>
<style scoped lang="scss">
.search-form-collapse {
  position: relative;
  padding-right: 75px;
  height: auto;
  .collapse__switch {
    position: absolute;
    top: 0;
    right: 0;
  }
  &.collapse__hidden {
    height: 50px;
    overflow: hidden;
    padding-right: 250px;
    /deep/ section {
      position: absolute;
      top: 0;
      right: 100px;
    }
  }
}
</style>
admin/src/components/common/TreeSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
<!-- ç»„件详情请参阅官方文档:https://www.vue-treeselect.cn/ -->
<template>
  <vue-tree-select
    :class="{inline}"
    :placeholder="placeholder"
    :value="value"
    :options="data"
    :clearable="clearable"
    :flat="flat"
    :append-to-body="appendToBody"
    :multiple="multiple"
    no-children-text="无记录"
    no-options-text="无记录"
    no-results-text="未匹配到数据"
    @input="$emit('input', $event)"
  />
</template>
<script>
import VueTreeSelect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
  name: 'TreeSelect',
  props: {
    inline: {
      default: false
    },
    multiple: {
      default: false
    },
    flat: {
      default: false
    },
    value: {},
    placeholder: {
      default: '请选择'
    },
    // æ˜¯å¦å¯æ¸…空
    clearable: {
      default: false
    },
    data: {
      type: Array,
      required: true
    },
    appendToBody: {
      default: false
    }
  },
  components: { VueTreeSelect }
}
</script>
<style scoped lang="scss">
  .inline {
    width: 178px;
  }
  .vue-treeselect {
    line-height: 30px;
    /deep/ .vue-treeselect__control {
      height: 32px;
      .vue-treeselect__single-value {
        line-height: 30px;
      }
    }
  }
</style>
admin/src/components/common/Value.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
<template>
  <div class="value">
    <i class="el-icon-loading" v-if="data == null"></i>
    <slot v-else>{{getValue()}}{{suffix}}</slot>
  </div>
</template>
<script>
export default {
  name: 'Value',
  props: {
    data: {
      type: Object
    },
    prop: {
      type: String
    },
    suffix: {
      type: String
    },
    handler: {
      type: Function
    }
  },
  methods: {
    getValue () {
      if (this.data == null) {
        return ''
      }
      if (this.prop == null) {
        return this.data
      }
      const props = this.prop.split('.')
      let i = 0
      let value = this.data
      while (i < props.length) {
        value = value[props[i]]
        i++
      }
      if (this.handler == null) {
        return value
      }
      return this.handler(value)
    }
  }
}
</script>
<style scoped lang="scss">
.value {
  word-break: break-all;
  .el-icon-loading {
    font-size: 16px;
    color: #999;
    position: relative;
    top: 1px;
  }
}
</style>
admin/src/components/system/datapermission/CustomSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
<template>
  <component :is="component" :value="values" :inline="false" @input="handleInput" multiple/>
</template>
<script>
export default {
  name: 'CustomSelect',
  props: {
    value: {},
    businessCode: {
      type: String,
      required: true
    }
  },
  computed: {
    // vuetreeselect值类型匹配(解决编辑时无法删除已有值的BUG)
    values () {
      if (this.businessCode === 'DEPARTMENT' || this.businessCode === 'POSITION') {
        const values = []
        for (const id of this.value) {
          values.push(parseInt(id))
        }
        return values
      }
      return this.value
    },
    component () {
      // éƒ¨é—¨é€‰æ‹©å™¨
      if (this.businessCode === 'DEPARTMENT') {
        return () => import('@/components/common/DepartmentSelect')
      }
      // å²—位选择器
      if (this.businessCode === 'POSITION') {
        return () => import('@/components/common/PositionSelect')
      }
      return null
    }
  },
  methods: {
    handleInput (value) {
      this.$emit('input', value)
      this.$emit('change', value)
    }
  }
}
</script>
<style scoped>
</style>
admin/src/components/system/datapermission/DataPermModuleSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
<template>
  <el-select
    class="data-perm-module-select"
    :class="{select__block: !inline}"
    :value="value"
    :placeholder="placeholder"
    :clearable="clearable"
    :disabled="disabled"
    @change="$emit('change', $event)"
    @input="$emit('input', $event)"
  >
    <el-option v-for="module in modules" :key="module.businessCode" :value="module.businessCode" :label="module.moduleName"/>
  </el-select>
</template>
<script>
import { fetchModules } from '@/api/system/dataPermission'
export default {
  name: 'DataPermModuleSelect',
  props: {
    value: {},
    placeholder: {
      default: '请选择权限模块'
    },
    inline: {
      default: true
    },
    disabled: {},
    clearable: {
      default: false
    }
  },
  data () {
    return {
      modules: []
    }
  },
  created () {
    fetchModules()
      .cache()
      .then(data => {
        this.modules = data
      })
  }
}
</script>
<style lang="scss" scoped>
  .select__block {
    display: block;
  }
</style>
admin/src/components/system/datapermission/DataPermTypeSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
<template>
  <el-select
    class="data-perm-type-select"
    :class="{select__block: !inline}"
    :value="value"
    :placeholder="placeholder"
    :clearable="clearable"
    :disabled="disabled"
    @change="$emit('change', $event)"
    @input="$emit('input', $event)"
  >
    <el-option v-for="type in filterTypes" :key="type.code" :value="type.code" :label="type.remark"/>
  </el-select>
</template>
<script>
import { fetchTypes } from '@/api/system/dataPermission'
export default {
  name: 'DataPermTypeSelect',
  props: {
    value: {},
    // æ¨¡å—名称
    module: {},
    placeholder: {
      default: '请选择权限类型'
    },
    inline: {
      default: true
    },
    disabled: {},
    clearable: {
      default: false
    }
  },
  data () {
    return {
      types: []
    }
  },
  computed: {
    filterTypes () {
      if (this.module == null || this.module === '') {
        return []
      }
      const types = []
      for (const type of this.types) {
        if (type.modules.length === 0 || type.modules.indexOf(this.module) !== -1) {
          types.push(type)
        }
      }
      return types
    }
  },
  created () {
    fetchTypes()
      .cache()
      .then(data => {
        this.types = data
      })
  }
}
</script>
<style lang="scss" scoped>
  .select__block {
    display: block;
  }
</style>
admin/src/components/system/datapermission/OperaDataPermissionWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
<template>
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="业务模块" prop="businessCode" required>
        <DataPermModuleSelect v-model="form.businessCode" :disabled="form.id != null" :inline="false" @change="handleBusinessChange"/>
      </el-form-item>
      <el-form-item label="角色" prop="roleId" required>
        <RoleSelect v-model="form.roleId" :disabled="form.id != null" :inline="false"/>
      </el-form-item>
      <el-form-item label="权限类型" prop="type" required>
        <DataPermTypeSelect v-model="form.type" :module="form.businessCode" :inline="false" @change="handleTypeChange"/>
      </el-form-item>
      <el-form-item v-show="showCustomData" label="自定义数据" prop="customData">
        <CustomSelect v-if="visible" v-model="customData" :business-code="form.businessCode" @change="handleCustomDataChange"/>
      </el-form-item>
      <el-form-item label="备注" prop="remark">
        <el-input type="textarea" v-model="form.remark" placeholder="请输入备注" v-trim :rows="3" maxlength="500"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import RoleSelect from '@/components/system/role/RoleSelect'
import DataPermModuleSelect from './DataPermModuleSelect'
import DataPermTypeSelect from './DataPermTypeSelect'
import CustomSelect from './CustomSelect'
export default {
  name: 'OperaDataPermissionWindow',
  extends: BaseOpera,
  components: { CustomSelect, RoleSelect, DataPermTypeSelect, DataPermModuleSelect, GlobalWindow },
  data () {
    return {
      // è‡ªå®šä¹‰æ•°æ®
      customData: [],
      // å±•示自定义数据标识
      showCustomData: false,
      // è¡¨å•数据
      form: {
        id: null,
        businessCode: '',
        roleId: '',
        type: '',
        remark: '',
        customData: ''
      },
      // éªŒè¯è§„则
      rules: {
        businessCode: [
          { required: true, message: '请选择业务模块' }
        ],
        roleId: [
          { required: true, message: '请选择角色' }
        ],
        type: [
          { required: true, message: '请选择权限类型' }
        ]
      }
    }
  },
  methods: {
    /**
     * @title çª—口标题
     * @target ç¼–辑的对象
     */
    open (title, target) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.$nextTick(() => {
          this.customData = []
          this.showCustomData = false
          this.$refs.form.resetFields()
          this.form[this.configData['field.id']] = null
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        for (const key in this.form) {
          this.form[key] = target[key]
        }
        this.customData = this.form.customData == null || this.form.customData === '' ? [] : this.form.customData.split(',')
        this.handleTypeChange()
      })
    },
    // ä¸šåŠ¡æ¨¡å—åˆ‡æ¢
    handleBusinessChange () {
      this.form.customData = ''
      this.customData = []
      this.handleTypeChange()
    },
    // æƒé™ç±»åž‹åˆ‡æ¢
    handleTypeChange () {
      if ((this.form.type === 11 || this.form.type === 21) && this.form.businessCode != null && this.form.businessCode !== '') {
        this.showCustomData = true
      } else {
        this.showCustomData = false
      }
    },
    // è‡ªå®šä¹‰æ•°æ®å˜åŒ–
    handleCustomDataChange (values) {
      this.form.customData = values.join(',')
    }
  },
  created () {
    this.config({
      api: '/system/dataPermission',
      'field.id': 'id'
    })
  }
}
</script>
admin/src/components/system/department/DepartmentUserWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,191 @@
<template>
  <GlobalWindow
    class="position-user-window"
    width="80%"
    :title="departmentName + '人员列表'"
    :visible.sync="visible"
    :with-footer="false"
  >
    <TableLayout :with-breadcrumb="false">
      <!-- æœç´¢è¡¨å• -->
      <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="80px" inline>
        <el-form-item label="用户名" prop="username">
          <el-input v-model="searchForm.username" placeholder="请输入用户名" v-trim @keypress.enter.native="search"/>
        </el-form-item>
        <el-form-item label="姓名" prop="realname">
          <el-input v-model="searchForm.realname" placeholder="请输入姓名" v-trim @keypress.enter.native="search"/>
        </el-form-item>
        <el-form-item label="手机号码" prop="mobile">
          <el-input v-model="searchForm.mobile" placeholder="请输入手机号码" v-trim @keypress.enter.native="search"/>
        </el-form-item>
        <section>
          <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
          <el-button @click="reset">重置</el-button>
        </section>
      </el-form>
      <div slot="space" class="toolbar">
        <el-switch v-model="onlyCurrentDept" @change="search" :disabled="isWorking.search"/>
        <label>仅查看当前部门人员</label>
      </div>
      <template v-slot:table-wrap>
        <el-table
          v-loading="isWorking.search"
          :data="tableData.list"
          stripe
          @selection-change="handleSelectionChange"
        >
          <el-table-column prop="avatar" label="头像" width="80px" class-name="table-column-avatar" fixed="left">
            <template slot-scope="{row}">
              <img :src="row.avatar == null ? '/static/avatar/man.png' : row.avatar">
            </template>
          </el-table-column>
          <el-table-column prop="realname" label="姓名" min-width="100px" fixed="left"></el-table-column>
          <el-table-column prop="username" label="用户名" min-width="100px"></el-table-column>
          <el-table-column prop="empNo" label="工号" min-width="80px"></el-table-column>
          <el-table-column prop="department" label="部门" min-width="120px">
            <template slot-scope="{row}">{{row.department == null ? '' : row.department.name}}</template>
          </el-table-column>
                    <el-table-column prop="position" label="岗位" min-width="120px" class-name="table-column-strings">
            <template slot-scope="{row}">
              <ul>
                <li v-for="position in row.positions" :key="position.id">{{position.name}}</li>
              </ul>
            </template>
          </el-table-column>
          <el-table-column prop="sex" label="性别" min-width="80px">
            <template slot-scope="{row}">
              {{row.sex | sex}}
            </template>
          </el-table-column>
          <el-table-column prop="mobile" label="手机号码" min-width="100px"></el-table-column>
          <el-table-column prop="email" label="邮箱" min-width="180px"></el-table-column>
          <el-table-column prop="birthday" label="生日" min-width="100px"></el-table-column>
          <el-table-column prop="birthday" label="角色" min-width="160px" class-name="table-column-role">
            <template slot-scope="{row}">
              <ul>
                <li v-for="role in row.roles" :key="role.id">{{role.name}}</li>
              </ul>
            </template>
          </el-table-column>
        </el-table>
        <pagination
          @size-change="handleSizeChange"
          @current-change="handlePageChange"
          :pagination="tableData.pagination"
        ></pagination>
      </template>
    </TableLayout>
  </GlobalWindow>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import GlobalWindow from '@/components/common/GlobalWindow'
import Pagination from '@/components/common/Pagination'
import { fetchUserList } from '@/api/system/department'
export default {
  name: 'DepartmentUserWindow',
  extends: BaseTable,
  components: { Pagination, GlobalWindow, TableLayout },
  data () {
    return {
      departmentId: null,
      departmentName: '',
      visible: false,
      // ä»…查看当前部门
      onlyCurrentDept: true,
      // æœç´¢è¡¨å•
      searchForm: {
        departmentId: null,
        username: '',
        realname: '',
        mobile: ''
      }
    }
  },
  methods: {
    // æ‰“开查看人员窗口
    open (departmentId, departmentName) {
      this.departmentId = departmentId
      this.departmentName = departmentName
      this.searchForm.departmentId = departmentId
      this.visible = true
      this.search()
    },
    // å¤„理分页
    handlePageChange (pageIndex) {
      // ä»…查看当前部门处理
      this.searchForm.strictDeptId = null
      this.searchForm.rootDeptId = this.searchForm.departmentId
      if (this.onlyCurrentDept) {
        this.searchForm.strictDeptId = this.searchForm.departmentId
        this.searchForm.rootDeptId = null
      }
      this.tableData.pagination.pageIndex = pageIndex
      this.isWorking.search = true
      fetchUserList({
        page: pageIndex,
        capacity: this.tableData.pagination.pageSize,
        model: this.searchForm
      })
        .then(data => {
          this.tableData.list = data.records
          this.tableData.pagination.total = data.total
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking.search = false
        })
    }
  }
}
</script>
<style scoped lang="scss">
.position-user-window {
  /deep/ .table-search-form {
    padding: 0;
  }
  /deep/ .window__body {
    background: #f7f7f7;
    .table-content {
      padding: 0;
      .table-wrap {
        padding: 0;
      }
    }
  }
  .toolbar {
    margin-top: 10px;
    padding: 6px 12px;
    background: #fff;
    font-size: 13px;
    label {
      margin-left: 8px;
      vertical-align: middle;
      color: #999;
    }
  }
  // åˆ—表头像处理
  .table-column-avatar {
    img {
      width: 48px;
    }
  }
  // åˆ—表角色处理
  .table-column-role {
    ul {
      li {
        display: inline-block;
        background: #eee;
        border-radius: 3px;
        padding: 0 3px;
        margin-right: 3px;
      }
    }
  }
}
</style>
admin/src/components/system/department/OperaDepartmentWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
<template>
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="上级部门" prop="parentId">
        <DepartmentSelect v-if="visible" ref="departmentSelect" placeholder="请选择上级部门" v-model="form.parentId" :exclude-id="excludeDeptId" :inline="false"/>
      </el-form-item>
      <el-form-item label="部门编码" prop="code" required>
        <el-input v-model="form.code" placeholder="请输入部门编码" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="部门名称" prop="name" required>
        <el-input v-model="form.name" placeholder="请输入部门名称" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="联系电话" prop="phone">
        <el-input v-model="form.phone" placeholder="请输入联系电话" v-trim maxlength="11"/>
      </el-form-item>
      <el-form-item label="部门邮箱" prop="email">
        <el-input v-model="form.email" placeholder="请输入部门邮箱" v-trim maxlength="200"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import DepartmentSelect from '@/components/common/DepartmentSelect'
import { checkMobile, checkEmail } from '@/utils/form'
export default {
  name: 'OperaDepartmentWindow',
  extends: BaseOpera,
  components: { DepartmentSelect, GlobalWindow },
  data () {
    return {
      // éœ€æŽ’除选择的部门ID
      excludeDeptId: null,
      // è¡¨å•数据
      form: {
        id: null,
        parentId: null,
        code: '',
        name: '',
        phone: '',
        email: ''
      },
      // éªŒè¯è§„则
      rules: {
        code: [
          { required: true, message: '请输入部门编码' }
        ],
        name: [
          { required: true, message: '请输入部门名称' }
        ],
        phone: [
          { validator: checkMobile }
        ],
        email: [
          { validator: checkEmail }
        ]
      }
    }
  },
  methods: {
    /**
     * @title çª—口标题
     * @target ç¼–辑的部门对象
     * @parent æ–°å»ºæ—¶çš„上级部门对象
     * @departmentList éƒ¨é—¨åˆ—表
     */
    open (title, target, parent) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.excludeDeptId = null
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form.id = null
          this.form.parentId = parent == null ? null : parent.id
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        this.excludeDeptId = target.id
        for (const key in this.form) {
          this.form[key] = target[key]
        }
      })
    }
  },
  created () {
    this.config({
      api: '/system/department'
    })
  }
}
</script>
admin/src/components/system/dict/DictDataManagerWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
<template>
  <GlobalWindow
    :title="dictName + '数据管理'"
    width="78%"
    :visible.sync="visible"
    :with-footer="false"
  >
    <TableLayout :with-breadcrumb="false">
      <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
      <template v-slot:table-wrap>
        <ul class="toolbar">
          <li><el-button type="primary" @click="$refs.operaDictDataWindow.open('新建字典数据', searchForm.dictId)" icon="el-icon-plus">新建</el-button></li>
          <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete">删除</el-button></li>
        </ul>
        <el-table
          v-loading="isWorking.search"
          :data="tableData.list"
          stripe
          @selection-change="handleSelectionChange"
        >
          <el-table-column type="selection" width="55"></el-table-column>
          <el-table-column prop="label" label="数据标签" min-width="100px"></el-table-column>
          <el-table-column prop="code" label="数据值" min-width="100px"></el-table-column>
          <el-table-column prop="disabled" label="状态" min-width="100px">
            <template slot-scope="{row}">{{row.disabled | disabledText}}</template>
          </el-table-column>
          <el-table-column prop="createUser" label="创建人" min-width="100px">
            <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
          </el-table-column>
          <el-table-column prop="updateUser" label="更新人" min-width="100px">
            <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
          </el-table-column>
          <el-table-column prop="createTime" label="创建时间" min-width="100px"></el-table-column>
          <el-table-column prop="updateTime" label="更新时间" min-width="100px"></el-table-column>
          <el-table-column
            label="操作"
            min-width="120"
            fixed="right"
          >
            <template slot-scope="{row}">
              <el-button type="text" @click="$refs.operaDictDataWindow.open('编辑字典数据', dictId, row)" icon="el-icon-edit">编辑</el-button>
              <el-button type="text" @click="deleteById(row)" icon="el-icon-delete">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <pagination
          @size-change="handleSizeChange"
          @current-change="handlePageChange"
          :pagination="tableData.pagination"
        ></pagination>
      </template>
      <!-- æ–°å»º/修改 -->
      <OperaDictDataWindow ref="operaDictDataWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
    </TableLayout>
  </GlobalWindow>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import Pagination from '@/components/common/Pagination'
import GlobalWindow from '@/components/common/GlobalWindow'
import TableLayout from '@/layouts/TableLayout'
import OperaDictDataWindow from './OperaDictDataWindow'
export default {
  name: 'DictDataManagerWindow',
  extends: BaseTable,
  components: { OperaDictDataWindow, TableLayout, GlobalWindow, Pagination },
  data () {
    return {
      visible: false,
      searchForm: {
        // å­—å…¸ID
        dictId: null
      },
      // å­—典名称
      dictName: ''
    }
  },
  methods: {
    // æ‰“开数据管理
    open (dictId, dictName) {
      this.searchForm.dictId = dictId
      this.dictName = dictName
      this.visible = true
      this.search()
    }
  },
  created () {
    this.config({
      api: '/system/dictData',
      'field.main': 'label'
    })
  }
}
</script>
<style scoped lang="scss">
/deep/ .window__body {
  .table-content {
    padding: 0;
    .table-wrap {
      padding-top: 0;
    }
  }
}
</style>
admin/src/components/system/dict/OperaDictDataWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
<template>
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking.create"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="数据标签" prop="label" required>
        <el-input v-model="form.label" placeholder="请输入数据标签" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="数据值" prop="code" required>
        <el-input v-model="form.code" placeholder="请输入数据值" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="状态" prop="disabled" required class="form-item-status">
        <el-switch v-model="form.disabled" :active-value="false" :inactive-value="true"/>
        <span class="status-text">{{form.disabled | disabledText}}</span>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
  name: 'OperaDictDataWindow',
  extends: BaseOpera,
  components: { GlobalWindow },
  data () {
    return {
      // è¡¨å•数据
      form: {
        id: null,
        dictId: null,
        code: '',
        label: '',
        disabled: false
      },
      // éªŒè¯è§„则
      rules: {
        label: [
          { required: true, message: '请输入数据标签' }
        ],
        code: [
          { required: true, message: '请输入数据值' }
        ]
      }
    }
  },
  methods: {
    /**
     * @title çª—口标题
     * @dict æ‰€å±žå­—å…¸ID
     * @target ç¼–辑的字典数据对象
     */
    open (title, dictId, target) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form.id = null
          this.form.dictId = dictId
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        for (const key in this.form) {
          this.form[key] = target[key]
        }
      })
    }
  },
  created () {
    this.config({
      api: '/system/dictData'
    })
  }
}
</script>
<style scoped lang="scss">
.form-item-status {
  .status-text {
    color: #999;
    margin-left: 6px;
    font-size: 13px;
    vertical-align: middle;
  }
}
</style>
admin/src/components/system/dict/OperaDictWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
<template>
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="字典编码" prop="code" required>
        <el-input v-model="form.code" placeholder="请输入字典编码" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="字典名称" prop="name" required>
        <el-input v-model="form.name" placeholder="请输入字典名称" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="备注" prop="remark">
        <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="3" v-trim maxlength="500"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
  name: 'OperaDictWindow',
  extends: BaseOpera,
  components: { GlobalWindow },
  data () {
    return {
      // è¡¨å•数据
      form: {
        id: null,
        code: '',
        name: '',
        remark: ''
      },
      // éªŒè¯è§„则
      rules: {
        code: [
          { required: true, message: '请输入字典编码' }
        ],
        name: [
          { required: true, message: '请输入字典名称' }
        ]
      }
    }
  },
  created () {
    this.config({
      api: '/system/dict'
    })
  }
}
</script>
admin/src/components/system/menu/OperaMenuWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,162 @@
<template>
  <GlobalWindow
    class="handle-table-dialog"
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <p class="tip" v-if="form.parent != null && form.id == null">为 <em>{{parentName}}</em> æ–°å»ºå­èœå•</p>
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="上级菜单" prop="parentId">
        <MenuSelect v-if="visible" v-model="form.parentId" placeholder="请选择上级菜单" :exclude-id="excludeMenuId" clearable :inline="false"/>
      </el-form-item>
      <el-form-item label="菜单名称" prop="name" required>
        <el-input v-model="form.name" placeholder="请输入菜单名称" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="访问路径" prop="path">
        <el-input v-model="form.path" placeholder="请输入访问路径" v-trim maxlength="200"/>
      </el-form-item>
      <el-form-item label="图标" prop="icon" class="form-item-icon">
        <el-radio-group v-model="form.icon">
          <el-radio :label="icon" v-for="icon in icons" :key="icon">
            <i :class="{[icon]: true}"></i>
          </el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="备注" prop="remark">
        <el-input type="textarea" v-model="form.remark" placeholder="请输入菜单备注" v-trim :rows="3" maxlength="500"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import MenuSelect from '@/components/common/MenuSelect'
import icons from '@/utils/icons'
export default {
  name: 'OperaMenuWindow',
  extends: BaseOpera,
  components: { MenuSelect, GlobalWindow },
  data () {
    return {
      icons,
      // ä¸Šçº§èœå•名称
      parentName: '',
      // éœ€æŽ’除选择的菜单ID
      excludeMenuId: null,
      // è¡¨å•数据
      form: {
        id: null,
        parentId: null,
        name: '',
        path: '',
        icon: '',
        remark: ''
      },
      // éªŒè¯è§„则
      rules: {
        name: [
          { required: true, message: '请输入菜单名称' }
        ]
      }
    }
  },
  methods: {
    /**
     * @title: çª—口标题
     * @target: ç¼–辑的菜单对象
     * @parent: æ–°å»ºæ—¶çš„上级菜单
     */
    open (title, target, parent) {
      this.title = title
      this.visible = true
      // æ–°å»ºï¼Œmenu为空时表示新建菜单
      if (target == null) {
        this.excludeMenuId = null
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form.id = null
          this.form.parentId = parent == null ? null : parent.id
          this.parentName = parent == null ? null : parent.name
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        this.excludeMenuId = target.id
        for (const key in this.form) {
          this.form[key] = target[key]
        }
      })
    }
  },
  created () {
    this.config({
      api: '/system/menu'
    })
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables";
$icon-background-color: $primary-color;
.global-window {
  .tip {
    margin-bottom: 12px;
    em {
      font-style: normal;
      color: $primary-color;
      font-weight: bold;
    }
  }
  // å›¾æ ‡
  /deep/ .form-item-icon {
    .el-form-item__content {
      height: 193px;
      overflow-y: auto;
    }
    .el-radio-group {
      background: $icon-background-color;
      padding: 4px;
      .el-radio {
        margin-right: 0;
        color: #fff;
        padding: 6px;
        &.is-checked {
          background: $icon-background-color - 30;
          border-radius: 10px;
        }
        .el-radio__input.is-checked + .el-radio__label {
          color: #fff;
        }
      }
      .el-radio__input {
        display: none;
      }
      .el-radio__label {
        padding-left: 0;
        // element-ui图标
        i {
          font-size: 30px;
        }
        // è‡ªå®šä¹‰å›¾æ ‡
        [class^="eva-icon-"], [class*=" eva-icon-"] {
          width: 30px;
          height: 30px;
          background-size: 25px;
          vertical-align: bottom;
        }
      }
      .el-radio--small {
        height: auto;
        padding: 8px;
        margin-left: 0;
      }
    }
  }
}
</style>
admin/src/components/system/permission/OperaPermissionWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<template>
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="权限编码" prop="code" required>
        <el-input v-model="form.code" placeholder="请输入权限编码" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="权限名称" prop="name" required>
        <el-input v-model="form.name" placeholder="请输入权限名称" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="权限备注" prop="remark">
        <el-input v-model="form.remark" placeholder="请输入权限备注" type="textarea" :rows="3" v-trim maxlength="500"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
  name: 'OperaPermissionWindow',
  extends: BaseOpera,
  components: { GlobalWindow },
  data () {
    return {
      // åŽŸæƒé™ç 
      originPermissionCode: '',
      // è¡¨å•数据
      form: {
        id: null,
        code: '',
        name: '',
        remark: ''
      },
      // éªŒè¯è§„则
      rules: {
        code: [
          { required: true, message: '请输入权限编码' }
        ],
        name: [
          { required: true, message: '请输入权限名称' }
        ]
      }
    }
  },
  methods: {
    open (title, target) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form[this.configData['field.id']] = null
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        this.originPermissionCode = target.code
        for (const key in this.form) {
          this.form[key] = target[key]
        }
      })
    },
    confirm () {
      if (this.form.id == null || this.form.id === '') {
        this.__confirmCreate()
        return
      }
      if (this.originPermissionCode === this.form.code) {
        this.__confirmEdit()
        return
      }
      // ä¿®æ”¹äº†æƒé™ç¼–码
      this.$dialog.confirm('检测到您修改了权限编码,权限编码修改后前后端均可能需要调整代码,确认修改吗?', '提示', {
        confirmButtonText: '确认修改',
        type: 'warning'
      })
        .then(() => {
          this.__confirmEdit()
        })
    }
  },
  created () {
    this.config({
      api: '/system/permission'
    })
  }
}
</script>
admin/src/components/system/position/OperaPositionWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
<template>
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="上级岗位" prop="parentId">
        <PositionSelect v-if="visible" v-model="form.parentId" placeholder="请选择上级岗位" :exclude-id="excludePositionId" clearable :inline="false"/>
      </el-form-item>
      <el-form-item label="岗位编码" prop="code" required>
        <el-input v-model="form.code" placeholder="请输入岗位编码" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="岗位名称" prop="name" required>
        <el-input v-model="form.name" placeholder="请输入岗位名称" v-trim maxlength="50"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import PositionSelect from '@/components/common/PositionSelect'
export default {
  name: 'OperaPositionWindow',
  extends: BaseOpera,
  components: { PositionSelect, GlobalWindow },
  data () {
    return {
      // éœ€æŽ’除选择的岗位ID
      excludePositionId: null,
      // è¡¨å•数据
      form: {
        id: null,
        parentId: null,
        code: '',
        name: ''
      },
      // éªŒè¯è§„则
      rules: {
        code: [
          { required: true, message: '请输入岗位编码' }
        ],
        name: [
          { required: true, message: '请输入岗位名称' }
        ]
      }
    }
  },
  methods: {
    /**
     * @title çª—口标题
     * @target ç¼–辑的岗位对象
     * @parent æ–°å»ºæ—¶çš„上级岗位对象
     * @positionList å²—位列表
     */
    open (title, target, parent) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.excludePositionId = null
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form.id = null
          this.form.parentId = parent == null ? null : parent.id
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        this.excludePositionId = target.id
        for (const key in this.form) {
          this.form[key] = target[key]
        }
      })
    }
  },
  created () {
    this.config({
      api: '/system/position'
    })
  }
}
</script>
admin/src/components/system/position/PositionUserWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,152 @@
<template>
  <GlobalWindow
    class="position-user-window"
    width="80%"
    :title="positionName + '人员列表'"
    :visible.sync="visible"
    :with-footer="false"
  >
    <TableLayout :with-breadcrumb="false">
      <!-- æœç´¢è¡¨å• -->
      <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="80px" inline>
        <el-form-item label="用户名" prop="username">
          <el-input v-model="searchForm.username" v-trim placeholder="请输入用户名" @keypress.enter.native="search"/>
        </el-form-item>
        <el-form-item label="姓名" prop="realname">
          <el-input v-model="searchForm.realname" v-trim placeholder="请输入姓名" @keypress.enter.native="search"/>
        </el-form-item>
        <el-form-item label="手机号码" prop="mobile">
          <el-input v-model="searchForm.mobile" v-trim placeholder="请输入手机号码" @keypress.enter.native="search"/>
        </el-form-item>
        <section>
          <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
          <el-button @click="reset">重置</el-button>
        </section>
      </el-form>
      <template v-slot:table-wrap>
        <el-table
          v-loading="isWorking.search"
          :data="tableData.list"
          stripe
          @selection-change="handleSelectionChange"
        >
          <el-table-column type="selection" width="55"></el-table-column>
          <el-table-column prop="avatar" label="头像" width="80px" class-name="table-column-avatar" fixed="left">
            <template slot-scope="{row}">
              <img :src="row.avatar == null ? '/static/avatar/man.png' : row.avatar">
            </template>
          </el-table-column>
          <el-table-column prop="realname" label="姓名" min-width="100px" fixed="left"></el-table-column>
          <el-table-column prop="username" label="用户名" min-width="100px"></el-table-column>
          <el-table-column prop="empNo" label="工号" min-width="80px"></el-table-column>
                    <el-table-column prop="department" label="部门" min-width="120px">
            <template slot-scope="{row}">{{row.department == null ? '' : row.department.name}}</template>
          </el-table-column>
          <el-table-column prop="position" label="岗位" min-width="120px" class-name="table-column-strings">
            <template slot-scope="{row}">
              <ul>
                <li v-for="position in row.positions" :key="position.id">{{position.name}}</li>
              </ul>
            </template>
          </el-table-column>
          <el-table-column prop="sex" label="性别" min-width="80px">
            <template slot-scope="{row}">
              {{row.sex | sex}}
            </template>
          </el-table-column>
          <el-table-column prop="mobile" label="手机号码" min-width="100px"></el-table-column>
          <el-table-column prop="email" label="邮箱" min-width="180px"></el-table-column>
          <el-table-column prop="birthday" label="生日" min-width="100px"></el-table-column>
          <el-table-column prop="birthday" label="角色" min-width="160px" class-name="table-column-role">
            <template slot-scope="{row}">
              <ul>
                <li v-for="role in row.roles" :key="role.id">{{role.name}}</li>
              </ul>
            </template>
          </el-table-column>
        </el-table>
        <pagination
          @size-change="handleSizeChange"
          @current-change="handlePageChange"
          :pagination="tableData.pagination"
        ></pagination>
      </template>
    </TableLayout>
  </GlobalWindow>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import GlobalWindow from '@/components/common/GlobalWindow'
import Pagination from '@/components/common/Pagination'
export default {
  name: 'PositionUserWindow',
  extends: BaseTable,
  components: { Pagination, GlobalWindow, TableLayout },
  data () {
    return {
      positionId: null,
      positionName: '',
      visible: false,
      // æœç´¢è¡¨å•
      searchForm: {
        positionId: null,
        username: '',
        realname: '',
        mobile: ''
      }
    }
  },
  methods: {
    // æ‰“开查看人员窗口
    open (positionId, positionName) {
      this.positionId = positionId
      this.positionName = positionName
      this.searchForm.positionId = positionId
      this.visible = true
      this.search()
    }
  },
  created () {
    this.config({
      api: '/system/user'
    })
  }
}
</script>
<style scoped lang="scss">
.position-user-window {
  /deep/ .table-search-form {
    padding: 0;
  }
  /deep/ .window__body {
    background: #f7f7f7;
    .table-content {
      padding: 0;
      .table-wrap {
        padding: 0;
      }
    }
  }
  // åˆ—表头像处理
  .table-column-avatar {
    img {
      width: 48px;
    }
  }
  // åˆ—表角色处理
  .table-column-role {
    ul {
      li {
        display: inline-block;
        background: #eee;
        border-radius: 3px;
        padding: 0 3px;
        margin-right: 3px;
      }
    }
  }
}
</style>
admin/src/components/system/role/MenuConfigWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
<template>
  <GlobalWindow
    class="menu-config-dialog"
    :visible.sync="visible"
    :confirm-working="isWorking"
    width="576px"
    title="授权菜单"
    @confirm="confirm"
  >
    <p class="tip" v-if="role != null">为角色 <em>{{role.name}}</em> é…ç½®å¯è®¿é—®çš„菜单</p>
    <el-tree
      ref="menuTree"
      :data="menus"
      show-checkbox
      node-key="id"
      default-expand-all
      :default-checked-keys="selectedIds"
      :expand-on-click-node="false"
      :check-on-click-node="true"
      :props="{children: 'children',label: 'name'}">
    </el-tree>
  </GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import { createRoleMenu } from '@/api/system/role'
import { fetchTree as fetchMenuList } from '@/api/system/menu'
export default {
  name: 'MenuConfigWindow',
  components: { GlobalWindow },
  data () {
    return {
      visible: false,
      isWorking: false,
      // è§’色
      role: null,
      // æ‰€æœ‰èœå•
      menus: [],
      // å·²é€‰ä¸­çš„菜单
      selectedIds: []
    }
  },
  methods: {
    /**
     * @role è§’色对象
     */
    open (role) {
      fetchMenuList({})
        .then(records => {
          this.role = role
          this.menus = records
          // å¦‚果为固定角色,则固定菜单不可更改
          this.__resetDisabled(this.menus, this.role)
          // æ‰¾å‡ºå¶èŠ‚ç‚¹
          role.menus = role.menus.filter(menu => role.menus.findIndex(m => m.parentId === menu.id) === -1)
          // åˆå§‹åŒ–选中
          this.selectedIds = role.menus.map(r => r.id)
          this.visible = true
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    },
    // ç¡®è®¤é€‰æ‹©èœå•
    confirm () {
      const selectedMenus = this.$refs.menuTree.getCheckedNodes(false, true)
      this.isWorking = true
      createRoleMenu({
        roleId: this.role.id,
        menuIds: selectedMenus.map(menu => menu.id)
      })
        .then(() => {
          this.$tip.apiSuccess('菜单授权成功')
          this.visible = false
          this.$emit('success')
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking = false
        })
    },
    // é‡ç½®disabled
    __resetDisabled (menus, role) {
      if (menus == null || menus.length === 0) {
        return
      }
      for (const menu of menus) {
        menu.disabled = false
        if (role.fixed && menu.fixed) {
          menu.disabled = true
        }
        this.__resetDisabled(menu.children, role)
      }
    }
  }
}
</script>
<style scoped lang="scss">
  @import "@/assets/style/variables.scss";
  .global-window {
    .tip {
      margin-bottom: 12px;
      em {
        font-style: normal;
        color: $primary-color;
        font-weight: bold;
      }
    }
  }
</style>
admin/src/components/system/role/OperaRoleWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<template>
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="角色编码" prop="code" required>
        <el-input v-model="form.code" placeholder="请输入角色编码" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="角色名称" prop="name" required>
        <el-input v-model="form.name" placeholder="请输入角色名称" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="角色备注" prop="remark">
        <el-input v-model="form.remark" type="textarea" placeholder="请输入角色备注" :rows="3" v-trim maxlength="500"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
  name: 'OperaRoleWindow',
  extends: BaseOpera,
  components: { GlobalWindow },
  data () {
    return {
      // åŽŸè§’è‰²ç 
      originRoleCode: '',
      // è¡¨å•数据
      form: {
        id: null,
        code: '',
        name: '',
        remark: ''
      },
      // éªŒè¯è§„则
      rules: {
        code: [
          { required: true, message: '请输入角色编码' }
        ],
        name: [
          { required: true, message: '请输入角色名称' }
        ]
      }
    }
  },
  methods: {
    open (title, target) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form[this.configData['field.id']] = null
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        this.originRoleCode = target.code
        for (const key in this.form) {
          this.form[key] = target[key]
        }
      })
    },
    confirm () {
      if (this.form.id == null || this.form.id === '') {
        this.__confirmCreate()
        return
      }
      if (this.originRoleCode === this.form.code) {
        this.__confirmEdit()
        return
      }
      // ä¿®æ”¹äº†è§’色编码
      this.$dialog.confirm('检测到您修改了角色编码,角色编码修改后前后端均可能需要调整代码,确认修改吗?', '提示', {
        confirmButtonText: '确认修改',
        type: 'warning'
      })
        .then(() => {
          this.__confirmEdit()
        })
    }
  },
  created () {
    this.config({
      api: '/system/role'
    })
  }
}
</script>
admin/src/components/system/role/PermissionConfigWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
<template>
  <GlobalWindow
    :visible.sync="visible"
    :confirm-working="isWorking"
    width="582px"
    title="配置角色权限"
    @confirm="confirm"
  >
    <p class="tip" v-if="role != null">为角色 <em>{{role.name}}</em> é…ç½®æƒé™</p>
    <p class="tip-warn"><i class="el-icon-warning"></i>提醒:权限配置后需重新登录后生效</p>
    <el-transfer
      ref="permissionTransfer"
      v-model="selectedIds"
      filterable
      :filter-method="filterPermissions"
      :titles="['未授权权限', '已授权权限']"
      :props="{
        key: 'id',
        label: 'name'
      }"
      :data="permissions">
    </el-transfer>
  </GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import { createRolePermission } from '@/api/system/role'
import { fetchAll } from '@/api/system/permission'
export default {
  name: 'PermissionConfigWindow',
  components: { GlobalWindow },
  data () {
    return {
      visible: false,
      isWorking: false,
      // è§’色对象
      role: null,
      // æƒé™åˆ—表
      permissions: [],
      // å·²é€‰ä¸­çš„æƒé™ID
      selectedIds: []
    }
  },
  methods: {
    /**
     * @role è§’色对象
     */
    open (role) {
      if (this.$refs.permissionTransfer) {
        this.$refs.permissionTransfer.clearQuery('left')
        this.$refs.permissionTransfer.clearQuery('right')
      }
      fetchAll()
        .then(records => {
          this.role = role
          this.permissions = records
          // å¦‚果为固定角色,则固定权限不可更改
          if (this.role.fixed) {
            for (const perm of this.permissions) {
              if (perm.fixed) {
                perm.disabled = true
              }
            }
          }
          this.selectedIds = role.permissions.map(r => r.id)
          this.visible = true
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    },
    // ç¡®è®¤é€‰æ‹©æƒé™
    confirm () {
      this.isWorking = true
      createRolePermission({
        roleId: this.role.id,
        permissionIds: this.selectedIds
      })
        .then(() => {
          this.$tip.apiSuccess('权限配置成功,用户重新登录后生效')
          this.visible = false
          this.$emit('success')
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking = false
        })
    },
    // æœç´¢æƒé™
    filterPermissions (query, item) {
      const lowerCaseQuery = query.toLowerCase()
      return item.code.toLowerCase().indexOf(lowerCaseQuery) > -1 || item.name.toLowerCase().indexOf(lowerCaseQuery) > -1
    }
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.global-window {
  .tip {
    em {
      font-style: normal;
      color: $primary-color;
      font-weight: bold;
    }
  }
  .tip-warn {
    margin: 4px 0 12px 0;
    font-size: 12px;
    color: #999;
    i {
      color: orange;
      margin-right: 4px;
      font-size: 14px;
      position: relative;
      top: 1px;
    }
  }
}
</style>
admin/src/components/system/role/RoleSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
<template>
  <el-select
    class="role-select"
    :class="{select__block: !inline}"
    :value="value"
    :placeholder="placeholder"
    :clearable="clearable"
    :disabled="disabled"
    @change="$emit('change', $event)"
    @input="$emit('input', $event)"
  >
    <el-option v-for="role in roles" :key="role.id" :value="role.id" :label="role.name"/>
  </el-select>
</template>
<script>
import { fetchAll } from '@/api/system/role'
export default {
  name: 'RoleSelect',
  props: {
    value: {},
    placeholder: {
      default: '请选择角色'
    },
    inline: {
      default: true
    },
    disabled: {},
    clearable: {
      default: false
    }
  },
  data () {
    return {
      roles: []
    }
  },
  created () {
    fetchAll()
      .then(data => {
        this.roles = data
      })
  }
}
</script>
<style lang="scss" scoped>
  .select__block {
    display: block;
  }
</style>
admin/src/components/system/user/OperaUserWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
<template>
  <!-- æ–°å»º/修改 -->
  <GlobalWindow
    :title="title"
    :visible.sync="visible"
    :confirm-working="isWorking"
    @confirm="confirm"
  >
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="用户名" prop="username" required>
        <el-input v-model="form.username" placeholder="请输入用户名" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="姓名" prop="realname" required>
        <el-input v-model="form.realname" placeholder="请输入姓名" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="性别" prop="sex" required>
        <el-radio-group v-model="form.sex">
          <el-radio label="1">男</el-radio>
          <el-radio label="0">女</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="头像" prop="avatar" required>
        <el-radio-group v-model="form.avatar" class="form-item-avatar">
          <el-radio label="/avatar/man.png" border><img src="/avatar/man.png" alt=""></el-radio>
          <el-radio label="/avatar/woman.png" border><img src="/avatar/woman.png" alt=""></el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item v-if="form.id == null" label="初始密码" prop="password" required>
        <el-input v-model="form.password" type="password" placeholder="请输入初始密码" maxlength="30" show-password/>
      </el-form-item>
            <el-form-item label="所属部门" prop="departmentId" required>
        <DepartmentSelect v-model="form.departmentId" placeholder="请选择用户所属部门" :inline="false" clearable/>
      </el-form-item>
      <el-form-item label="岗位" prop="positionId">
        <PositionSelect v-model="form.positionIds" placeholder="请选择用户所在岗位" :inline="false" :multiple="true" clearable/>
      </el-form-item>
      <el-form-item label="工号" prop="empNo">
        <el-input v-model="form.empNo" placeholder="请输入工号" v-trim maxlength="50"/>
      </el-form-item>
      <el-form-item label="手机号码" prop="mobile">
        <el-input v-model="form.mobile" placeholder="请输入手机号码" v-trim maxlength="11"/>
      </el-form-item>
      <el-form-item label="邮箱" prop="email">
        <el-input v-model="form.email" placeholder="请输入邮箱" v-trim maxlength="200"/>
      </el-form-item>
      <el-form-item label="生日" prop="birthday">
        <el-date-picker v-model="form.birthday" value-format="yyyy-MM-dd" placeholder="请选择用户生日"/>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import DepartmentSelect from '@/components/common/DepartmentSelect'
import PositionSelect from '@/components/common/PositionSelect'
import { checkMobile, checkEmail } from '@/utils/form'
export default {
  name: 'OperaUserWindow',
  extends: BaseOpera,
  components: { PositionSelect, DepartmentSelect, GlobalWindow },
  data () {
    return {
      // è¡¨å•数据
      form: {
        id: null,
        username: '', // ç”¨æˆ·å
        realname: '', // å§“名
        empNo: '', // å·¥å·
        departmentId: null, //  æ‰€å±žéƒ¨é—¨ID
        positionIds: [], //  æ‰€å±žå²—位ID集
        avatar: '/avatar/man.png', // å¤´åƒ
        password: '', // å¯†ç 
        mobile: '', // æ‰‹æœºå·ç 
        email: '', // é‚®ç®±
        sex: '1', // æ€§åˆ«
        birthday: '' // ç”Ÿæ—¥
      },
      // éªŒè¯è§„则
      rules: {
        username: [
          { required: true, message: '请输入用户名' }
        ],
        realname: [
          { required: true, message: '请输入姓名' }
        ],
        password: [
          { required: true, message: '请输入密码' }
        ],
        departmentId: [
          { required: true, message: '请选择部门' }
        ],
        avatar: [
          { required: true, message: '请选择用户头像' }
        ],
        sex: [
          { required: true, message: '请选择用户性别' }
        ],
        mobile: [
          { validator: checkMobile }
        ],
        email: [
          { validator: checkEmail }
        ]
      }
    }
  },
  methods: {
    /**
     * @title çª—口标题
     * @target ç¼–辑的用户对象
     */
    open (title, target) {
      this.title = title
      this.visible = true
      // æ–°å»º
      if (target == null) {
        this.$nextTick(() => {
          this.$refs.form.resetFields()
          this.form.id = null
          this.form.departmentId = null
          this.form.positionIds = []
        })
        return
      }
      // ç¼–辑
      this.$nextTick(() => {
        for (const key in this.form) {
          this.form[key] = target[key]
        }
        this.form.departmentId = target.department == null ? null : target.department.id
        this.form.positionIds = target.positions == null ? [] : target.positions.map(p => p.id)
      })
    }
  },
  created () {
    this.config({
      api: '/system/user'
    })
  }
}
</script>
<style lang="scss" scoped>
.global-window {
  /deep/ .el-date-editor {
    width: 100%;
  }
  // è¡¨å•头像处理
  /deep/ .form-item-avatar {
    .el-radio.is-bordered {
      height: auto;
      padding: 6px;
      margin: 0 10px 0 0;
      .el-radio__input {
        display: none;
      }
      .el-radio__label {
        padding: 0;
        width: 48px;
        display: block;
        img {
          width: 100%;
        }
      }
    }
  }
}
</style>
admin/src/components/system/user/ResetPwdWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
<template>
  <GlobalWindow
    :visible.sync="visible"
    :confirm-working="isWorking"
    width="576px"
    title="重置密码"
    @confirm="confirm"
  >
    <p class="tip" v-if="user != null">为用户 <em>{{user.realname}}</em> é‡ç½®å¯†ç </p>
    <el-form :model="form" ref="form" :rules="rules">
      <el-form-item label="新密码" prop="password" required>
        <el-input v-model="form.password" type="password" placeholder="请输入新密码" maxlength="30" show-password></el-input>
      </el-form-item>
    </el-form>
  </GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import { resetPwd } from '@/api/system/user'
export default {
  name: 'ResetPwdWindow',
  components: { GlobalWindow },
  data () {
    return {
      isWorking: false,
      visible: false,
      user: null,
      form: {
        password: ''
      },
      rules: {
        password: [
          { required: true, message: '请输入密码' }
        ]
      }
    }
  },
  methods: {
    open (user) {
      this.user = user
      this.visible = true
      this.$nextTick(() => {
        this.$refs.form.resetFields()
      })
    },
    // ç¡®è®¤é‡ç½®å¯†ç 
    confirm () {
      if (this.isWorking) {
        return
      }
      this.$refs.form.validate((valid) => {
        if (!valid) {
          return
        }
        this.isWorking = true
        resetPwd({
          id: this.user.id,
          password: this.form.password
        })
          .then(() => {
            this.$tip.apiSuccess('密码重置成功')
            this.visible = false
            this.$emit('success')
          })
          .catch(e => {
            this.$tip.apiFailed(e)
          })
          .finally(() => {
            this.isWorking = false
          })
      })
    }
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
// è§’色配置
.global-window {
  .tip {
    margin-bottom: 12px;
    em {
      font-style: normal;
      color: $primary-color;
      font-weight: bold;
    }
  }
}
</style>
admin/src/components/system/user/RoleConfigWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
<template>
  <GlobalWindow
    :visible.sync="visible"
    :confirm-working="isWorking"
    width="582px"
    title="配置用户角色"
    @confirm="confirm"
  >
    <p class="tip" v-if="user != null">为用户 <em>{{user.realname}}</em> é…ç½®è§’色</p>
    <p class="tip-warn"><i class="el-icon-warning"></i>提醒:角色配置后需重新登录后生效</p>
    <el-transfer
      v-model="selectedIds"
      :titles="['未授权角色', '已授权角色']"
      :props="{
        key: 'id',
        label: 'name'
      }"
      :data="roles">
    </el-transfer>
  </GlobalWindow>
</template>
<script>
import BasePage from '@/components/base/BasePage'
import GlobalWindow from '@/components/common/GlobalWindow'
import { createUserRole } from '@/api/system/user'
import { fetchAll as fetchAllRoles } from '@/api/system/role'
export default {
  name: 'RoleConfigWindow',
  extends: BasePage,
  components: { GlobalWindow },
  data () {
    return {
      visible: false,
      isWorking: false,
      // ç”¨æˆ·
      user: null,
      // è§’色列表
      roles: null,
      // å·²é€‰ä¸­çš„角色ID
      selectedIds: []
    }
  },
  methods: {
    open (user) {
      fetchAllRoles()
        .then(records => {
          this.roles = records
          this.user = user
          // å¦‚果为固定用户,则固定角色不可更改
          if (this.user.fixed) {
            for (const role of this.roles) {
              if (role.fixed) {
                role.disabled = true
              }
            }
          }
          // å¦‚果当前用户为非超级管理员用户,则不允许授权超级管理员角色
          if (!this.isAdmin) {
            for (const role of this.roles) {
              if (role.code === this.adminCode) {
                role.disabled = true
              }
            }
          }
          this.selectedIds = this.user.roles.map(r => r.id)
          this.visible = true
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    },
    // ç¡®è®¤é€‰æ‹©è§’色
    confirm () {
      if (this.isWorking) {
        return
      }
      this.isWorking = true
      createUserRole({
        userId: this.user.id,
        roleIds: this.selectedIds
      })
        .then(() => {
          this.$tip.apiSuccess('角色配置成功,用户重新登录后生效')
          this.visible = false
          this.$emit('success')
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking = false
        })
    },
    // å…³é—­
    close () {
      this.$emit('update:visible', false)
    }
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
// è§’色配置
.global-window {
  .tip {
    em {
      font-style: normal;
      color: $primary-color;
      font-weight: bold;
    }
  }
  .tip-warn {
    margin: 4px 0 12px 0;
    font-size: 12px;
    color: #999;
    i {
      color: orange;
      margin-right: 4px;
      font-size: 14px;
      position: relative;
      top: 1px;
    }
  }
}
</style>
admin/src/directives/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
import vPermissions from './v-permissions'
import vRoles from './v-roles'
import vTrim from './v-trim'
export default {
  install (Vue) {
    // è§’色控制指令
    Vue.directive('roles', vRoles)
    // æƒé™æŽ§åˆ¶æŒ‡ä»¤
    Vue.directive('permissions', vPermissions)
    // è‡ªåŠ¨åŽ»ç©ºæŒ‡ä»¤
    Vue.directive('trim', vTrim)
  }
}
admin/src/directives/v-permissions.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
export default {
  inserted: function (el, binding, vnode) {
    // èŽ·å–ç”¨æˆ·ä¿¡æ¯
    const userInfo = vnode.context.$store.state.userInfo
    if (userInfo == null) {
      el.parentNode && el.parentNode.removeChild(el)
      return
    }
    // èŽ·å–é…ç½®æƒé™
    const configPermissions = binding.value
    if (configPermissions == null) {
      return
    }
    if (!(configPermissions instanceof Array)) {
      throw new Error('v-permissions的值必须为一个数组')
    }
    // éªŒè¯æƒé™
    if (configPermissions.findIndex(code => userInfo.permissions.findIndex(p => p === code) > -1) === -1) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  }
}
admin/src/directives/v-roles.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
export default {
  inserted: function (el, binding, vnode) {
    // èŽ·å–ç”¨æˆ·ä¿¡æ¯
    const userInfo = vnode.context.$store.state.userInfo
    if (userInfo == null) {
      el.parentNode && el.parentNode.removeChild(el)
      return
    }
    // èŽ·å–é…ç½®è§’è‰²
    const configRoles = binding.value
    if (configRoles == null) {
      return
    }
    if (!(configRoles instanceof Array)) {
      throw new Error('v-roles的值必须为一个数组')
    }
    // éªŒè¯æƒé™
    if (configRoles.findIndex(code => userInfo.roles.findIndex(r => r === code) > -1) === -1) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  }
}
admin/src/directives/v-trim.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
export default {
  inserted: function (el) {
    let input = el
    let classes = input.getAttribute('class') || ''
    if (classes != null) {
      classes = classes.split(' ')
    }
    // è¾“入框:<el-input/>
    if (classes.indexOf('el-input') > -1) {
      input = input.querySelector('input')
    }
    // å¤šè¡Œè¾“入框:<el-input type="textarea"/>
    if (classes.indexOf('el-textarea') > -1) {
      input = input.querySelector('textarea')
    }
    // å¤±åŽ»ç„¦ç‚¹æ—¶åŽ»æŽ‰ä¸¤ä¾§ç©ºæ ¼
    input.addEventListener('blur', (e) => {
      e.target.value = e.target.value.trim()
      input.dispatchEvent(new Event('input'))
    })
    // å›žè½¦æ—¶åŽ»æŽ‰ä¸¤ä¾§ç©ºæ ¼ï¼ˆä»…è¾“å…¥æ¡†ï¼‰
    if (classes.indexOf('el-input') > -1) {
      input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
          e.target.value = e.target.value.trim()
          input.dispatchEvent(new Event('input'))
        }
      })
    }
  }
}
admin/src/filters/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
export default {
  install (Vue) {
    // æ€§åˆ«
    Vue.filter('sex', (value) => {
      if (value === '1') {
        return '男'
      }
      if (value === '0') {
        return '女'
      }
      return '未知'
    })
    // å¯ç”¨ç¦ç”¨
    Vue.filter('disabledText', (value) => {
      if (value) {
        return '停用'
      }
      return '启用'
    })
  }
}
admin/src/layouts/AppLayout.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
<template>
  <el-container class="app-layout">
    <el-aside :class="{collapse:menuData.collapse}">
      <Menu/>
    </el-aside>
    <el-main>
      <header>
        <AppHeader/>
      </header>
      <main>
        <transition name="fade">
          <router-view></router-view>
        </transition>
      </main>
    </el-main>
  </el-container>
</template>
<script>
import { mapState } from 'vuex'
import Header from '@/components/common/Header'
import Menu from '@/components/common/Menu'
export default {
  name: 'DefaultLayout',
  components: { AppHeader: Header, Menu },
  computed: {
    ...mapState(['menuData'])
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.el-container {
  background: #F7F8F9;
  height: 100%;
  display: flex;
  overflow: hidden;
  // å·¦è¾¹èœå•
  .el-aside {
    width: $menu-width !important;
    flex-shrink: 0;
    height: 100%;
    overflow-y: auto;
    background: $primary-color;
    color: #fff;
    transition: width ease .3s;
    &.collapse {
      width: 64px !important;
    }
  }
  // å³è¾¹å†…容
  .el-main {
    width: 100%;
    height: 100%;
    padding: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    & > header {
      height: $header-height;
      flex-shrink: 0;
    }
    & > main {
      height: 100%;
      overflow-y: auto;
    }
  }
}
// é¡µé¢è¿‡æ¸¡åŠ¨ç”»
.fade-enter-active, .fade-leave-active {
  transition: all .5s;
  opacity: 1;
  position: absolute;
  width: 100%;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  transform: translateY(200px);
  opacity: 0;
  transition: all .5s;
  position: absolute;
}
</style>
admin/src/layouts/TableLayout.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,160 @@
<template>
  <div class="table-layout">
    <!-- å¤´éƒ¨ -->
    <div v-if="withBreadcrumb" class="table-header">
      <el-breadcrumb separator="/">
        <el-breadcrumb-item v-for="path in paths" :key="path">{{path}}</el-breadcrumb-item>
      </el-breadcrumb>
    </div>
    <Profile :roles="roles" :permissions="permissions">
      <!-- æœç´¢è¡¨å•部分 -->
      <div class="table-search-form">
        <div class="form-wrap">
          <slot name="search-form"></slot>
        </div>
      </div>
      <slot name="space"></slot>
      <!-- åˆ—表和分页部分 -->
      <div class="table-content">
        <div class="table-wrap">
          <slot name="table-wrap"></slot>
        </div>
      </div>
      <slot></slot>
    </Profile>
  </div>
</template>
<script>
import Profile from '../components/common/Profile'
export default {
  name: 'TableLayout',
  components: { Profile },
  props: {
    // è§’色
    roles: {
      type: Array
    },
    // æƒé™
    permissions: {
      type: Array
    },
    // æ˜¯å¦å±•示头部面包屑
    withBreadcrumb: {
      type: Boolean,
      default: true
    }
  },
  computed: {
    paths () {
      return this.$route.meta.paths
    }
  }
}
</script>
<style lang="scss">
@import "@/assets/style/variables.scss";
.table-layout {
  height: 100%;
  display: flex;
  flex-direction: column;
  .not-allow-wrap {
    padding-top: 0;
  }
}
// å¤´éƒ¨
.table-header {
  overflow: hidden;
  padding: 12px 16px;
  flex-shrink: 0;
  // é¡µé¢è·¯å¾„
  .el-breadcrumb {
    .el-breadcrumb__item {
      .el-breadcrumb__inner {
        color: #ABB2BE;
        font-size: 12px;
      }
      &:last-of-type .el-breadcrumb__inner {
        color: #606263;
        font-size: 14px;
      }
    }
  }
}
// æœç´¢
.table-search-form {
  display: flex;
  flex-wrap: wrap;
  padding: 0 16px;
  .form-wrap {
    padding: 16px 16px 0 16px;
    width: 100%;
    background: #fff;
    &:empty {
      padding: 0;
    }
  }
  section {
    display: inline-block;
    margin-left: 16px;
    margin-bottom: 18px;
  }
}
// åˆ—表和分页
.table-content {
  margin-top: 10px;
  padding: 0 16px;
  .table-wrap {
    padding: 16px 16px 0 16px;
    background: #fff;
    // å·¥å…·æ 
    .toolbar {
      border-bottom: 1px solid #eee;
      padding-bottom: 10px;
      li {
        display: inline-block;
        margin-right: 6px;
      }
    }
    // è¡¨æ ¼
    .el-table {
      th {
        .cell {
          color: #666;
        }
      }
      // å¤é€‰æ¡†åˆ—
      .el-table-column--selection {
        .cell {
          text-align: center !important;
        }
      }
      // å¤šå€¼å­—段
      .table-column-strings {
        ul {
          li {
            display: inline-block;
            background: #eee;
            border-radius: 3px;
            padding: 0 3px;
            margin-right: 3px;
            margin-bottom: 3px;
          }
        }
      }
      // æ ‘视觉调整
      [class*=el-table__row--level] .el-table__expand-icon {
        position: relative;
        left: -6px;
        margin-right: 0;
      }
    }
    // åˆ†é¡µ
    .table-pagination {
      padding: 16px 0;
      text-align: left;
    }
  }
}
</style>
admin/src/main.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import './assets/style/element-variables.scss'
import VueClipboard from 'vue-clipboard2'
import directives from './directives'
import filters from './filters'
import plugins from './plugins'
import { mapState, mapMutations } from 'vuex'
import { fetchMenuTree } from './api/system/menu'
Vue.config.productionTip = false
Vue.use(ElementUI, {
  size: 'small'
})
Vue.use(VueClipboard)
Vue.use(directives)
Vue.use(filters)
Vue.use(plugins)
new Vue({
  data: {
    loading: false
  },
  router,
  store,
  computed: {
    ...mapState(['userInfo', 'homePage'])
  },
  watch: {
    async userInfo () {
      if (this.userInfo == null) {
        return
      }
      await this.initRoutes()
    }
  },
  methods: {
    ...mapMutations(['switchCollapseMenu', 'setHomePage']),
    // åˆå§‹åŒ–本地配置
    initLocalConfig () {
      // èœå•状态配置
      const menuStatus = window.localStorage.getItem('MENU_STATUS')
      if (menuStatus != null) {
        this.switchCollapseMenu(menuStatus === 'true')
      }
    },
    // åˆå§‹åŒ–路由
    async initRoutes () {
      if (this.loading || this.userInfo == null) {
        return
      }
      this.loading = true
      // é‡ç½®èœå•
      this.$store.commit('resetMenus')
      // èŽ·å–èœå•
      const storeMenus = this.$store.state.menuData.list
      if (storeMenus.length > 0 && this.homePage == null) {
        this.setHomePage(storeMenus[0])
      }
      await fetchMenuTree()
        .then(menus => {
          // æ·»åŠ èœå•
          storeMenus.push.apply(storeMenus, menus)
          // æ·»åŠ è·¯ç”±
          this.__addRouters(storeMenus)
          // 404
          router.addRoute({
            path: '*',
            redirect: '/not-found'
          })
          // é¦–页
          router.addRoute({
            name: 'index',
            path: '/',
            redirect: this.homePage.url
          })
          // è·¯ç”±åŠ è½½å®ŒæˆåŽï¼Œå¦‚æžœè®¿é—®çš„æ˜¯/,跳转至动态识别的首页
          if (this.$route.path === '/') {
            this.$router.push(this.homePage.url)
          }
        })
        .catch(e => {
          throw e
        })
        .finally(() => {
          this.loading = false
        })
    },
    // æ–°å»ºè·¯ç”±
    __addRouters (routes, parents = []) {
      if (routes == null || routes.length === 0) {
        return
      }
      const rs = router.getRoutes()
      for (const route of routes) {
        const parentsDump = JSON.parse(JSON.stringify(parents))
        parentsDump.push(route)
        if (route.url == null || route.url === '') {
          this.__addRouters(route.children, parentsDump)
          continue
        }
        if (rs.findIndex(r => r.path === route.url) > -1) {
          this.__addRouters(route.children, parentsDump)
          continue
        }
        if (this.homePage == null) {
          this.setHomePage(route)
        }
        router.addRoute('layout', {
          path: route.url,
          name: route.label,
          meta: {
            title: route.label,
            paths: [...parents.map(p => p.label), route.label]
          },
          component: () => import('@/views' + route.url)
        })
        this.__addRouters(route.children, parentsDump)
      }
    }
  },
  async created () {
    if (this.userInfo == null) {
      return
    }
    await this.initRoutes()
      .catch(() => {})
  },
  mounted () {
    this.initLocalConfig()
  },
  render: h => h(App)
}).$mount('#app')
admin/src/plugins/cache.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
const sessionCache = {
  set (key, value) {
    if (!sessionStorage) {
      return
    }
    if (key != null && value != null) {
      sessionStorage.setItem(key, value)
    }
  },
  get (key) {
    if (!sessionStorage) {
      return null
    }
    if (key == null) {
      return null
    }
    return sessionStorage.getItem(key)
  },
  setJSON (key, jsonValue) {
    if (jsonValue != null) {
      this.set(key, JSON.stringify(jsonValue))
    }
  },
  getJSON (key) {
    const value = this.get(key)
    if (value != null) {
      return JSON.parse(value)
    }
  }
}
const localCache = {
  set (key, value) {
    if (!localStorage) {
      return
    }
    if (key != null && value != null) {
      localStorage.setItem(key, value)
    }
  },
  get (key) {
    if (!localStorage) {
      return null
    }
    if (key == null) {
      return null
    }
    return localStorage.getItem(key)
  },
  setJSON (key, jsonValue) {
    if (jsonValue != null) {
      this.set(key, JSON.stringify(jsonValue))
    }
  },
  getJSON (key) {
    const value = this.get(key)
    if (value != null) {
      return JSON.parse(value)
    }
  }
}
export default {
  /**
   * ä¼šè¯çº§ç¼“å­˜
   */
  session: sessionCache,
  /**
   * æœ¬åœ°ç¼“å­˜
   */
  local: localCache
}
admin/src/plugins/consts.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
/**
 * å¸¸é‡å®šä¹‰
 */
export default {
}
admin/src/plugins/download.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
import fileDownload from 'js-file-download'
import message from './message'
export default function (response, decode = true, mime = 'application/octet-stream') {
  // å½“下载接口没有成功返回流并且接口返回的是JSON时需要对响应流进行解析并提示错误。(处理下载接口出现未知异常的情况)
  if (response.headers['content-type'] === 'application/json') {
    const blob = new Blob([response.data])
    const fileReader = new FileReader()
    fileReader.readAsText(blob, 'utf-8')
    fileReader.onload = function () {
      message.apiFailed(JSON.parse(fileReader.result))
    }
    return
  }
  if (response.headers['content-length'] === '0') {
    message.error('无法下载文件,可能因为数据处理错误导致文件大小为0B')
    return
  }
  // ä¸‹è½½æŽ¥å£åœ¨å“åº”头eva-download-filename中存放文件名称,接口的返回的文件名称需采用url encode的方式进行编码
  fileDownload(response.data, decode ? decodeURI(response.headers['eva-download-filename']) : response.headers['eva-download-filename'], mime)
}
admin/src/plugins/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import consts from './consts'
import message from './message'
import messagebox from './messagebox'
import cache from './cache'
import download from './download'
export default {
  install (Vue) {
    // å¸¸é‡
    Vue.prototype.$consts = consts
    // æé†’对象
    Vue.prototype.$tip = message
    // æç¤ºæ¡†å¯¹è±¡
    Vue.prototype.$dialog = messagebox
    // ç¼“存对象
    Vue.prototype.$cache = cache
    // ä¸‹è½½æ–‡ä»¶
    Vue.prototype.download = download
  }
}
admin/src/plugins/message.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
import { Message } from 'element-ui'
export default {
  ...Message,
  // æŽ¥å£è°ƒç”¨æˆåŠŸ
  apiSuccess (message) {
    Message.success(message)
  },
  // æŽ¥å£è°ƒç”¨å¤±è´¥
  apiFailed (err) {
    // ä¸‹è½½æŽ¥å£è¿”回的是ArrayBuffer,此时需要解析为JSON并提示错误消息。(下载接口出现业务失败的情况,例如无权限等)
    if (err instanceof ArrayBuffer) {
      const blob = new Blob([err])
      const fileReader = new FileReader()
      fileReader.readAsText(blob, 'utf-8')
      fileReader.onload = function () {
        Message.error(JSON.parse(fileReader.result).message)
      }
      return
    }
    Message.error(err.message)
  }
}
admin/src/plugins/messagebox.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
import { MessageBox } from 'element-ui'
export default {
  ...MessageBox,
  // åˆ é™¤äºŒæ¬¡ç¡®è®¤
  deleteConfirm (message) {
    return MessageBox.confirm(message, '删除提醒', {
      confirmButtonText: '确认删除',
      cancelButtonText: '取消',
      type: 'warning'
    })
  },
  // ç¦ç”¨äºŒæ¬¡ç¡®è®¤
  disableConfirm (message) {
    return MessageBox.confirm(message, '禁用提醒', {
      confirmButtonText: '确认禁用',
      cancelButtonText: '取消',
      type: 'warning'
    })
  },
  // å¯¼å‡ºäºŒæ¬¡ç¡®è®¤
  exportConfirm (message) {
    return MessageBox.confirm(message, '导出提醒', {
      confirmButtonText: '确认导出',
      cancelButtonText: '取消',
      type: 'warning'
    })
  }
}
admin/src/router/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import AppLayout from '@/layouts/AppLayout'
import { getUserInfo } from '@/api/system/common'
const Login = () => import('@/views/login')
const ErrorNoPermissions = () => import('@/views/no-permissions')
const Error404 = () => import('@/views/not-found')
Vue.use(VueRouter)
const router = new VueRouter({
  base: process.env.VUE_APP_CONTEXT_PATH + (process.env.VUE_APP_ROUTER_MODE === 'hash' ? '#' : ''),
  mode: process.env.VUE_APP_ROUTER_MODE,
  routes: [
    // ç™»å½•
    {
      name: 'login',
      path: '/login',
      component: Login
    },
    // æ— æƒé™
    {
      name: 'no-permissions',
      path: '/no-permissions',
      component: ErrorNoPermissions
    },
    // 404
    {
      name: 'not-found',
      path: '/not-found',
      component: Error404
    },
    // å†…容页(动态加载)
    {
      name: 'layout',
      path: '',
      component: AppLayout,
      children: []
    }
  ]
})
router.beforeEach((to, from, next) => {
  // æ— æƒè®¿é—®&404页面可直接访问
  if (to.name === 'no-permissions' || to.name === 'not-found') {
    next()
    return
  }
  // å¦‚果访问的是layout(回退时可能存在该情况),直奔index
  if (to.name === 'layout') {
    next({ name: 'index' })
    return
  }
  // éªŒè¯ç”¨æˆ·æ˜¯å¦ç™»å½•
  const userInfo = router.app.$options.store.state.userInfo
  if (userInfo != null) {
    // å¦‚果用户不存在权限
    if (userInfo.permissions.length === 0) {
      next({ name: 'no-permissions' })
      return
    }
    // å¦‚果访问的是登录页面,则直接跳转至首页
    if (to.name === 'login') {
      next({ name: 'index' })
      return
    }
    next()
    return
  }
  getUserInfo()
    .then(userInfo => {
      // å¦‚果用户不存在权限
      if (userInfo.permissions.length === 0) {
        next({ name: 'no-permissions' })
        return
      }
      // å·²ç™»å½•,存储userInfo
      router.app.$store.commit('setUserInfo', userInfo)
      // å¦‚果访问的是登录页面,则直接跳转至首页
      if (to.name === 'login') {
        next({ name: 'index' })
        return
      }
      next()
    })
    .catch(e => {
      // å¦‚果访问的是登录页面,则直接放行
      if (to.name === 'login') {
        next()
        return
      }
      // æœªç™»å½•,跳转至登录页
      next({ name: 'login' })
    })
})
export default router
admin/src/store/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
  // ç™»å½•用户信息
  userInfo: null,
  // é¦–页
  homePage: null,
  // èœå•
  menuData: {
    // èœå•列表
    list: [],
    // æ˜¯å¦æ”¶èµ·
    collapse: false
  }
}
const mutations = {
  // åˆ‡æ¢èœå•状态
  switchCollapseMenu (state, value) {
    if (value != null) {
      state.menuData.collapse = value
    } else {
      state.menuData.collapse = !state.menuData.collapse
    }
    window.localStorage.setItem('MENU_STATUS', state.menuData.collapse)
  },
  // è®¾ç½®å·²ç™»å½•的用户信息
  setUserInfo: (state, data) => {
    state.userInfo = data
  },
  // è®¾ç½®é¦–页路由信息
  setHomePage (state, homePage) {
    state.homePage = homePage
  },
  // é‡ç½®èœå•
  resetMenus: (state) => {
    state.menuData.list = []
  }
}
const actions = {}
const getters = {}
export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters
})
admin/src/utils/form.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
// éªŒè¯æ‰‹æœºå·ç 
export function checkMobile (rule, value, callback) {
  if (value == null || value.trim() === '') {
    callback()
    return
  }
  if (!/^1\d{10}$/.test(value)) {
    callback(new Error('手机号码格式不正确'))
    return
  }
  callback()
}
// éªŒè¯é‚®ç®±
export function checkEmail (rule, value, callback) {
  if (value == null || value.trim() === '') {
    callback()
    return
  }
  if (!/^\S+@\S+\.\S+$/.test(value)) {
    callback(new Error('邮箱格式不正确'))
    return
  }
  callback()
}
admin/src/utils/icons.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,293 @@
export default [
  // è‡ªå®šä¹‰å›¾æ ‡
  'eva-icon-role',
  'eva-icon-permission',
  'eva-icon-department',
  'eva-icon-position',
  'eva-icon-dictionary',
  'eva-icon-listener',
  'eva-icon-log',
  'eva-icon-log-opera',
  'eva-icon-log-login',
  // element-ui图标
  'el-icon-platform-eleme',
  'el-icon-eleme',
  'el-icon-delete-solid',
  'el-icon-delete',
  'el-icon-s-tools',
  'el-icon-setting',
  'el-icon-user-solid',
  'el-icon-user',
  'el-icon-phone',
  'el-icon-phone-outline',
  'el-icon-more',
  'el-icon-more-outline',
  'el-icon-star-on',
  'el-icon-star-off',
  'el-icon-s-goods',
  'el-icon-goods',
  'el-icon-warning',
  'el-icon-warning-outline',
  'el-icon-question',
  'el-icon-info',
  'el-icon-remove',
  'el-icon-circle-plus',
  'el-icon-success',
  'el-icon-error',
  'el-icon-zoom-in',
  'el-icon-zoom-out',
  'el-icon-remove-outline',
  'el-icon-circle-plus-outline',
  'el-icon-circle-check',
  'el-icon-circle-close',
  'el-icon-s-help',
  'el-icon-help',
  'el-icon-minus',
  'el-icon-plus',
  'el-icon-check',
  'el-icon-close',
  'el-icon-picture',
  'el-icon-picture-outline',
  'el-icon-picture-outline-round',
  'el-icon-upload',
  'el-icon-upload2',
  'el-icon-download',
  'el-icon-camera-solid',
  'el-icon-camera',
  'el-icon-video-camera-solid',
  'el-icon-video-camera',
  'el-icon-message-solid',
  'el-icon-bell',
  'el-icon-s-cooperation',
  'el-icon-s-order',
  'el-icon-s-platform',
  'el-icon-s-fold',
  'el-icon-s-unfold',
  'el-icon-s-operation',
  'el-icon-s-promotion',
  'el-icon-s-home',
  'el-icon-s-release',
  'el-icon-s-ticket',
  'el-icon-s-management',
  'el-icon-s-open',
  'el-icon-s-shop',
  'el-icon-s-marketing',
  'el-icon-s-flag',
  'el-icon-s-comment',
  'el-icon-s-finance',
  'el-icon-s-claim',
  'el-icon-s-custom',
  'el-icon-s-opportunity',
  'el-icon-s-data',
  'el-icon-s-check',
  'el-icon-s-grid',
  'el-icon-menu',
  'el-icon-share',
  'el-icon-d-caret',
  'el-icon-caret-left',
  'el-icon-caret-right',
  'el-icon-caret-bottom',
  'el-icon-caret-top',
  'el-icon-bottom-left',
  'el-icon-bottom-right',
  'el-icon-back',
  'el-icon-right',
  'el-icon-bottom',
  'el-icon-top',
  'el-icon-top-left',
  'el-icon-top-right',
  'el-icon-arrow-left',
  'el-icon-arrow-right',
  'el-icon-arrow-down',
  'el-icon-arrow-up',
  'el-icon-d-arrow-left',
  'el-icon-d-arrow-right',
  'el-icon-video-pause',
  'el-icon-video-play',
  'el-icon-refresh',
  'el-icon-refresh-right',
  'el-icon-refresh-left',
  'el-icon-finished',
  'el-icon-sort',
  'el-icon-sort-up',
  'el-icon-sort-down',
  'el-icon-rank',
  'el-icon-loading',
  'el-icon-view',
  'el-icon-c-scale-to-original',
  'el-icon-date',
  'el-icon-edit',
  'el-icon-edit-outline',
  'el-icon-folder',
  'el-icon-folder-opened',
  'el-icon-folder-add',
  'el-icon-folder-remove',
  'el-icon-folder-delete',
  'el-icon-folder-checked',
  'el-icon-tickets',
  'el-icon-document-remove',
  'el-icon-document-delete',
  'el-icon-document-copy',
  'el-icon-document-checked',
  'el-icon-document',
  'el-icon-document-add',
  'el-icon-printer',
  'el-icon-paperclip',
  'el-icon-takeaway-box',
  'el-icon-search',
  'el-icon-monitor',
  'el-icon-attract',
  'el-icon-mobile',
  'el-icon-scissors',
  'el-icon-umbrella',
  'el-icon-headset',
  'el-icon-brush',
  'el-icon-mouse',
  'el-icon-coordinate',
  'el-icon-magic-stick',
  'el-icon-reading',
  'el-icon-data-line',
  'el-icon-data-board',
  'el-icon-pie-chart',
  'el-icon-data-analysis',
  'el-icon-collection-tag',
  'el-icon-film',
  'el-icon-suitcase',
  'el-icon-suitcase-1',
  'el-icon-receiving',
  'el-icon-collection',
  'el-icon-files',
  'el-icon-notebook-1',
  'el-icon-notebook-2',
  'el-icon-toilet-paper',
  'el-icon-office-building',
  'el-icon-school',
  'el-icon-table-lamp',
  'el-icon-house',
  'el-icon-no-smoking',
  'el-icon-smoking',
  'el-icon-shopping-cart-full',
  'el-icon-shopping-cart-1',
  'el-icon-shopping-cart-2',
  'el-icon-shopping-bag-1',
  'el-icon-shopping-bag-2',
  'el-icon-sold-out',
  'el-icon-sell',
  'el-icon-present',
  'el-icon-box',
  'el-icon-bank-card',
  'el-icon-money',
  'el-icon-coin',
  'el-icon-wallet',
  'el-icon-discount',
  'el-icon-price-tag',
  'el-icon-news',
  'el-icon-guide',
  'el-icon-male',
  'el-icon-female',
  'el-icon-thumb',
  'el-icon-cpu',
  'el-icon-link',
  'el-icon-connection',
  'el-icon-open',
  'el-icon-turn-off',
  'el-icon-set-up',
  'el-icon-chat-round',
  'el-icon-chat-line-round',
  'el-icon-chat-square',
  'el-icon-chat-dot-round',
  'el-icon-chat-dot-square',
  'el-icon-chat-line-square',
  'el-icon-message',
  'el-icon-postcard',
  'el-icon-position',
  'el-icon-turn-off-microphone',
  'el-icon-microphone',
  'el-icon-close-notification',
  'el-icon-bangzhu',
  'el-icon-time',
  'el-icon-odometer',
  'el-icon-crop',
  'el-icon-aim',
  'el-icon-switch-button',
  'el-icon-full-screen',
  'el-icon-copy-document',
  'el-icon-mic',
  'el-icon-stopwatch',
  'el-icon-medal-1',
  'el-icon-medal',
  'el-icon-trophy',
  'el-icon-trophy-1',
  'el-icon-first-aid-kit',
  'el-icon-discover',
  'el-icon-place',
  'el-icon-location',
  'el-icon-location-outline',
  'el-icon-location-information',
  'el-icon-add-location',
  'el-icon-delete-location',
  'el-icon-map-location',
  'el-icon-alarm-clock',
  'el-icon-timer',
  'el-icon-watch-1',
  'el-icon-watch',
  'el-icon-lock',
  'el-icon-unlock',
  'el-icon-key',
  'el-icon-service',
  'el-icon-mobile-phone',
  'el-icon-bicycle',
  'el-icon-truck',
  'el-icon-ship',
  'el-icon-basketball',
  'el-icon-football',
  'el-icon-soccer',
  'el-icon-baseball',
  'el-icon-wind-power',
  'el-icon-light-rain',
  'el-icon-lightning',
  'el-icon-heavy-rain',
  'el-icon-sunrise',
  'el-icon-sunrise-1',
  'el-icon-sunset',
  'el-icon-sunny',
  'el-icon-cloudy',
  'el-icon-partly-cloudy',
  'el-icon-cloudy-and-sunny',
  'el-icon-moon',
  'el-icon-moon-night',
  'el-icon-dish',
  'el-icon-dish-1',
  'el-icon-food',
  'el-icon-chicken',
  'el-icon-fork-spoon',
  'el-icon-knife-fork',
  'el-icon-burger',
  'el-icon-tableware',
  'el-icon-sugar',
  'el-icon-dessert',
  'el-icon-ice-cream',
  'el-icon-hot-water',
  'el-icon-water-cup',
  'el-icon-coffee-cup',
  'el-icon-cold-drink',
  'el-icon-goblet',
  'el-icon-goblet-full',
  'el-icon-goblet-square',
  'el-icon-goblet-square-full',
  'el-icon-refrigerator',
  'el-icon-grape',
  'el-icon-watermelon',
  'el-icon-cherry',
  'el-icon-apple',
  'el-icon-pear',
  'el-icon-orange',
  'el-icon-coffee',
  'el-icon-ice-tea',
  'el-icon-ice-drink',
  'el-icon-milk-tea',
  'el-icon-potato-strips',
  'el-icon-lollipop',
  'el-icon-ice-cream-square',
  'el-icon-ice-cream-round'
]
admin/src/utils/request.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
import axios from 'axios'
import Cookies from 'js-cookie'
import pkg from '../../package'
import { trim } from './util'
import cache from '../plugins/cache'
axios.defaults.headers.common['Content-Type'] = 'application/json;charset=UTF-8'
const axiosInstance = axios.create({
  baseURL: process.env.VUE_APP_API_PREFIX,
  // è¯·æ±‚è¶…æ—¶æ—¶é—´
  timeout: 60000
})
// æ–°å»ºè¯·æ±‚拦截器
axiosInstance.interceptors.request.use(config => {
  // å‚数去空格
  if (config.trim === true) {
    if (config.data != null) {
      config.data = trim(config.data)
    }
    if (config.params != null) {
      config.params = trim(config.params)
    }
  }
  // å¯¼å‡ºå¤„理
  if (config.download === true) {
    config.responseType = 'blob'
  }
  // è®¾ç½®æ“ä½œå¹³å°
  config.headers['eva-platform'] = `pc-${pkg.version}`
  // è®¾ç½®è®¤è¯å¤´
  const authToken = Cookies.get('eva-auth-token')
  if (authToken != null) {
    config.headers['eva-auth-token'] = authToken
  }
  return config
}, function (error) {
  return Promise.reject(error)
})
// æ–°å»ºå“åº”拦截器
axiosInstance.interceptors.response.use((response) => {
  // è¯·æ±‚失败
  if (response.status !== 200) {
    return Promise.reject(new Error('服务器繁忙,请稍后再试'))
  }
  // ä¸‹è½½æŽ¥å£å¤„理
  if (response.headers['eva-opera-type'] === 'download') {
    return Promise.resolve(response)
  }
  // æœªç™»å½•
  if (response.data.code === 401) {
    if (response.config.autoLogin !== false) {
      window.location.href = process.env.VUE_APP_ROUTER_MODE === 'history' ? '/#/login' : '/login'
    }
    return Promise.reject(response.data)
  }
  // ä¸šåŠ¡å¤±è´¥
  if (!response.data.success) {
    return Promise.reject(response.data)
  }
  return response.data.data
}, function (error) {
  if (error.code == null) {
    return Promise.reject(new Error('服务器繁忙,请稍后再试'))
  }
  if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
    return Promise.reject(new Error('服务器响应超时,请稍后再试'))
  }
  return Promise.reject(error)
})
// ç¼“存请求结果
const buildCachePromise = (cacheKey, method, args, cacheImpl) => {
  return {
    __cacheImpl: cache[cacheImpl],
    __arguments: args,
    __result_promise: null,
    // å¼€å¯ç¼“å­˜
    cache () {
      const data = this.__cacheImpl.getJSON(cacheKey)
      if (data != null) {
        this.__result_promise = Promise.resolve(data)
      }
      if (this.__result_promise != null) {
        return this.__result_promise
      }
      return this
    },
    then () {
      return this.__access('then', arguments)
    },
    catch () {
      return this.__access('catch', arguments)
    },
    finally () {
      return this.__access('finally', arguments)
    },
    __access (methodName, args) {
      if (this.__result_promise != null) {
        return this.__result_promise
      }
      this.__result_promise = axiosInstance[method].apply(axiosInstance, this.__arguments)
      this.__result_promise.then(data => {
        this.__cacheImpl.setJSON(cacheKey, data)
        return data
      })
      return this.__result_promise[methodName].apply(this.__result_promise, args)
    }
  }
}
const methods = ['get', 'post', 'delete', 'put', 'head', 'options', 'patch', 'request']
axiosInstance.cache = function (cacheKey, isLocal = false) {
  if (cacheKey == null) {
    throw Error('Request cache key can not be null.')
  }
  const cacheAxiosInstance = {}
  for (const method of methods) {
    cacheAxiosInstance[method] = function () {
      return buildCachePromise(cacheKey, method, arguments, isLocal ? 'local' : 'session')
    }
  }
  return cacheAxiosInstance
}
export default axiosInstance
admin/src/utils/util.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
// åŽ»ç©º
export function trim (data) {
  if (data == null) {
    return null
  }
  if (typeof data === 'string') {
    return data.trim()
  }
  if (data instanceof Array) {
    for (const item of data) {
      trim(item)
    }
  }
  if (typeof data === 'object') {
    for (const key in data) {
      data[key] = trim(data[key])
    }
  }
  return data
}
admin/src/views/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
<template>
  <div class="home">
    <div class="wrap">
      <h2>欢迎使用伊娃极速开发框架</h2>
      <p>您使用的此套开源框架没有任何版权问题,可学习可商用,请放心使用!</p>
      <p>伊娃致力于打造简洁、合理、高效的开发体验,为此我们将不断升级,感谢您的支持!</p>
      <p style="margin-top: 12px;">
        <a href="https://gitee.com/coderd-repos/eva" target="_blank">GITEE</a>
        <a href="https://github.com/coderd-repos/eva" target="_blank">GITHUB</a>
      </p>
      <div class="guide">
        <a href="http://eva.adjustrd.com" target="_blank">前往官网</a>
        <a href="http://coderd.adjustrd.com/template/308/default" target="_blank">前往CodeRd</a>
      </div>
      <img src="@/assets/images/qq.png">
      <p>你可以扫码加入群聊以获得技术支持</p>
      <div class="award">
        <h4>激励作者做得更好</h4>
        <img src="@/assets/images/alipay.jpeg">
        <img src="@/assets/images/wxpay.jpeg">
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'Index',
  data () {
    return {}
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.home {
  text-align: center;
  color: #777;
  .wrap {
    margin-top: 80px;
    img {
      width: 240px;
    }
  }
  h2 {
    font-size: 32px;
    color: #555;
    margin-bottom: 20px;
  }
  p {
    line-height: 24px;
    margin: 0;
  }
  .start-up {
    margin-top: 8px;
  }
  .guide {
    margin: 30px 0 40px 0;
    display: flex;
    justify-content: center;
    a {
      margin-right: 12px;
      padding: 12px 40px;
      border-radius: 30px;
      background: $primary-color;
      color: #fff;
      text-decoration: none;
    }
  }
  em,a {
    font-style: normal;
    font-weight: bold;
    margin: 0 3px;
    color: $primary-color;
  }
  .award {
    position: absolute;
    right: 20px;
    bottom: 60px;
    display: flex;
    flex-direction: column;
    text-align: left;
    padding: 12px;
    border: 1px solid #eee;
    box-shadow: -1px 1px 10px #ccc;
    h4 {
      font-weight: bold;
      margin-bottom: 8px;
    }
    img {
      width: 160px;
      margin-bottom: 20px;
      transition: opacity ease .3s;
    }
  }
}
</style>
admin/src/views/login.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,211 @@
<template>
  <div class="wrap">
    <div class="introduce">
      <h2>豆米跳跳</h2>
      <h3></h3>
    </div>
    <div class="login">
      <h1>系统登录&nbsp;/&nbsp;LOGIN IN</h1>
      <div class="info-input">
        <el-input v-model="username" placeholder="请输入用户名" prefix-icon="el-icon-user-solid" maxlength="50" v-trim/>
        <el-input v-model="password" placeholder="请输入密码" type="password" prefix-icon="eva-icon-password" maxlength="30" show-password/>
        <div class="captcha-input">
          <el-input v-model="captcha.value" placeholder="图片验证码" prefix-icon="eva-icon-shield" maxlength="4" @keypress.enter.native="login"/>
          <img v-if="!captcha.loading" :src="captcha.uri" @click="refreshCaptcha">
          <span v-else><i class="el-icon-loading"></i></span>
        </div>
      </div>
      <el-button :loading="loading" @click="login">登&nbsp;&nbsp;录</el-button>
    </div>
  </div>
</template>
<script>
import { mapMutations } from 'vuex'
import { getCaptcha, loginByPassword } from '@/api/system/common'
export default {
  name: 'Login',
  data () {
    return {
      loading: false,
      username: '',
      password: '',
      // éªŒè¯ç 
      captcha: {
        loading: false,
        value: '',
        uuid: '',
        uri: ''
      }
    }
  },
  methods: {
    ...mapMutations(['setUserInfo']),
    // ç™»å½•
    login () {
      if (this.loading) {
        return
      }
      if (!this.__check()) {
        return
      }
      this.loading = true
      loginByPassword({
        username: this.username.trim(),
        password: this.password,
        code: this.captcha.value.trim(),
        uuid: this.captcha.uuid
      })
        .then(() => {
          window.location.href = process.env.VUE_APP_CONTEXT_PATH
        })
        .catch(e => {
          this.refreshCaptcha()
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.loading = false
        })
    },
    // åˆ·æ–°éªŒè¯ç 
    refreshCaptcha () {
      this.captcha.loading = true
      getCaptcha()
        .then(data => {
          this.captcha.uri = data.image
          this.captcha.uuid = data.uuid
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          setTimeout(() => {
            this.captcha.loading = false
          }, 150)
        })
    },
    // ç™»å½•前验证
    __check () {
      if (this.username.trim() === '') {
        this.$tip.error('请输入用户名')
        return false
      }
      if (this.password === '') {
        this.$tip.error('请输入密码')
        return false
      }
      if (this.captcha.value.trim() === '') {
        this.$tip.error('请输入图片验证码')
        return false
      }
      return true
    }
  },
  created () {
    this.refreshCaptcha()
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
$input-gap: 30px;
.wrap {
  display: flex;
  width: 100%;
  height: 100vh;
  background-image: url("../assets/images/login.jpg");
  background-repeat: no-repeat;
  background-size: auto 180%;
  background-clip: content-box;
  background-position: center;
  // å·¦è¾¹ä»‹ç»
  .introduce {
    padding-left: 10%;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    color: #fff;
    background: rgba(0, 0, 0, 0.4);
    display: flex;
    flex-direction: column;
    justify-content: center;
    h2 {
      font-size:34px;
      font-style: italic;
      font-weight: 900;
      margin-top: 50px;
    }
    h3 {
      font-size: 49px;
      font-weight: 300;
      margin: 25px 0;
    }
  }
  // å³è¾¹ç™»å½•
  .login {
    height: 100%;
    width: 38%;
    max-width: 560px;
    min-width: 460px;
    flex-shrink: 0;
    text-align: center;
    background: #fff;
    padding: 0 80px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    box-sizing: border-box;
    h1 {
      font-size: 28px;
      font-weight: 500;
    }
    .info-input {
      margin-top: $input-gap;
      margin-bottom: 60px;
      /deep/ .el-input {
        margin-top: 30px;
        .el-input__inner {
          height: 50px;
          background: #F9F9F9;
          border: 1px solid transparent;
          &:focus {
            border: 1px solid $primary-color;
          }
        }
      }
    }
    // éªŒè¯ç è¾“å…¥
    .captcha-input {
      display: flex;
      margin-top: $input-gap;
      height: 50px;
      .el-input {
        width: 100%;
        margin-top: 0;
      }
      span, img {
        width: 45%;
        height: 100%;
        flex-shrink: 0;
        margin-left: 16px;
      }
      span {
        display: flex;
        align-items: center;
        justify-content: center;
        background: #f7f7f7;
        border-radius: 8px;
      }
    }
    .el-button {
      height: 50px;
      width: 100%;
      color: #fff;
      font-size: 16px;
      background: linear-gradient(130deg, $primary-color + 20 0%, $primary-color - 20 100%);
    }
  }
}
</style>
admin/src/views/no-permissions.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
<template>
  <NotAllow>
    <div class="content">
      <img src="../assets/images/not-allow.png">
      <h2>无权访问</h2>
      <p>如您需要访问该系统,请联系系统管理员</p>
      <el-button @click="logout" type="primary">退出系统</el-button>
    </div>
  </NotAllow>
</template>
<script>
import NotAllow from '../components/common/NotAllow'
import { mapMutations } from 'vuex'
import { logout } from '@/api/system/common'
export default {
  name: 'NoPermissions',
  components: { NotAllow },
  methods: {
    ...mapMutations(['setUserInfo']),
    // é€€å‡ºç™»å½•
    logout () {
      logout()
        .then(() => {
          this.$router.push({ name: 'login' })
          this.setUserInfo(null)
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
    }
  }
}
</script>
<style scoped>
</style>
admin/src/views/not-found.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
<template>
  <div class="error-404">
    <div>
      <div class="left">
        <img src="../assets/images/404.png">
        <h1>找不到网页</h1>
        <p></p>
        <div class="opera">
          <el-button @click="$router.back()">返回</el-button>
          <el-button type="primary" @click="$router.push('/index')">前往首页</el-button>
        </div>
      </div>
      <div class="right">
        <h2>你可以尝试以下操作</h2>
        <p>1. å¦‚果是手动更改的网页地址,请检查网页地址是否正确</p>
        <p>2. ç‚¹å‡»ã€è¿”回】按钮刷新页面后重试</p>
        <p>3. è”系系统管理员</p>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'NotFound'
}
</script>
<style scoped lang="scss">
.error-404 {
  height: 100%;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  & > div {
    margin-top: -100px;
    display: flex;
  }
  .left {
    h1 {
      font-size: 26px;
    }
    .opera {
      margin-top: 30px;
    }
  }
  .right {
    text-align: left;
    margin-top: 16px;
    margin-left: 40px;
    padding: 30px 50px;
    border-radius: 16px;
    background: url("../assets/images/404-tip.png") no-repeat;
    background-size: 100% 100%;
    box-sizing: border-box;
    h2 {
      font-size: 16px;
      margin-bottom: 16px;
    }
    p {
      margin: 0;
      padding: 0;
      font-size: 14px;
      line-height: 26px;
    }
  }
}
</style>
admin/src/views/system/data-permission.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,197 @@
<template>
  <TableLayout :permissions="['system:datapermission:query']">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="业务模块" prop="businessCode">
        <DataPermModuleSelect v-model="searchForm.businessCode" clearable @change="search"/>
      </el-form-item>
      <el-form-item label="角色" prop="roleId">
        <RoleSelect v-model="searchForm.roleId" clearable @change="search"/>
      </el-form-item>
      <section>
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button @click="reset">重置</el-button>
      </section>
    </el-form>
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:datapermission:create', 'system:datapermission:delete']">
        <li><el-button type="primary" @click="$refs.operaDataPermissionWindow.open('新建数据权限')" icon="el-icon-plus" v-permissions="['system:datapermission:create']">新建</el-button></li>
        <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:datapermission:delete']">删除</el-button></li>
      </ul>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        stripe
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55"></el-table-column>
        <el-table-column prop="businessCode" label="业务模块" min-width="100px">
          <template slot-scope="{row}">{{row.businessCode | moduleText(modules)}}</template>
        </el-table-column>
        <el-table-column prop="roleId" label="角色" min-width="100px">
          <template slot-scope="{row}">{{row.role.name}}</template>
        </el-table-column>
        <el-table-column prop="type" label="权限类型" min-width="140px">
          <template slot-scope="{row}">{{row.type | typeText(types)}}</template>
        </el-table-column>
        <el-table-column prop="disabled" label="是否启用" min-width="100px">
          <template slot-scope="{row}">
            <el-switch v-model="row.disabled" :active-value="false" :inactive-value="true" @change="switchDisabled(row)"/>
          </template>
        </el-table-column>
        <el-table-column prop="remark" label="备注" min-width="100px"></el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" min-width="140px"></el-table-column>
        <el-table-column prop="updateUser" label="修改人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="修改时间" min-width="140px"></el-table-column>
        <el-table-column
          v-if="containPermissions(['system:datapermission:update', 'system:datapermission:delete'])"
          label="操作"
          min-width="120"
          fixed="right"
        >
          <template slot-scope="{row}">
            <el-button type="text" @click="$refs.operaDataPermissionWindow.open('编辑数据权限', row)" icon="el-icon-edit" v-permissions="['system:datapermission:update']">编辑</el-button>
            <el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:datapermission:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
        :pagination="tableData.pagination"
      >
      </pagination>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaDataPermissionWindow ref="operaDataPermissionWindow" @success="handlePageChange"/>
  </TableLayout>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import TableLayout from '@/layouts/TableLayout'
import Pagination from '@/components/common/Pagination'
import OperaDataPermissionWindow from '@/components/system/datapermission/OperaDataPermissionWindow'
import DataPermModuleSelect from '@/components/system/datapermission/DataPermModuleSelect'
import RoleSelect from '../../components/system/role/RoleSelect'
// èŽ·å–æ¨¡å—åç§°
const __getModuleName = function (businessCode, modules) {
  for (const module of modules) {
    if (module.businessCode === businessCode) {
      return module.moduleName
    }
  }
  return '未知'
}
export default {
  name: 'DataPermission',
  extends: BaseTable,
  components: { RoleSelect, DataPermModuleSelect, TableLayout, Pagination, OperaDataPermissionWindow },
  data () {
    return {
      // æ•°æ®æƒé™æ¨¡å—
      modules: [],
      // æ•°æ®æƒé™ç±»åž‹
      types: [],
      // æœç´¢
      searchForm: {
        businessCode: '',
        roleId: null,
        type: ''
      }
    }
  },
  filters: {
    // æ•°æ®æƒé™ç±»åž‹æ–‡æ¡ˆ
    typeText (value, types) {
      for (const type of types) {
        if (type.code === value) {
          return type.remark
        }
      }
      return '未知'
    },
    // æ•°æ®æƒé™æ¨¡å—文案
    moduleText (value, modules) {
      return __getModuleName(value, modules)
    }
  },
  methods: {
    // å¯ç”¨/禁用菜单
    switchDisabled (row) {
      if (!row.disabled) {
        this.__updateStatus(row)
        return
      }
      this.$dialog.disableConfirm(`确认禁用 ${__getModuleName(row.businessCode, this.modules)}/${row.role.name} æ•°æ®æƒé™å—?`)
        .then(() => {
          this.__updateStatus(row)
        }).catch(() => {
          row.disabled = !row.disabled
        })
    },
    // åˆ é™¤è¡Œ
    deleteById (row) {
      this.$dialog.deleteConfirm(`确认删除【${__getModuleName(row.businessCode, this.modules)}/${row.role.name}】数据权限吗?`)
        .then(() => {
          this.isWorking.delete = true
          this.api.deleteById(row.id)
            .then(() => {
              this.$tip.apiSuccess('删除成功')
              this.__afterDelete()
            })
            .catch(e => {
              this.$tip.apiFailed(e)
            })
            .finally(() => {
              this.isWorking.delete = false
            })
        })
        .catch(() => {})
    },
    // ä¿®æ”¹çŠ¶æ€
    __updateStatus (row) {
      this.api.updateStatus({
        id: row.id,
        disabled: row.disabled
      })
        .then(() => {
          this.$tip.apiSuccess('修改成功')
        })
        .catch(e => {
          row.disabled = !row.disabled
          this.$tip.apiFailed(e)
        })
    }
  },
  async created () {
    this.config({
      module: '数据权限',
      api: '/system/dataPermission'
    })
    // åˆå§‹åŒ–数据权限模块
    await this.api.fetchModules()
      .then(data => {
        this.modules = data
      })
    // åˆå§‹åŒ–数据权限模块
    await this.api.fetchTypes()
      .then(data => {
        this.types = data
      })
      .catch(e => {
        console.log(e)
      })
    // æ‰§è¡Œæœç´¢
    this.search()
  }
}
</script>
admin/src/views/system/department.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
<template>
  <TableLayout :permissions="['system:department:query']">
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:department:create', 'system:department:delete']">
        <li><el-button type="primary" @click="$refs.operaDepartmentWindow.open('新建部门')" icon="el-icon-plus" v-permissions="['system:department:create']">新建</el-button></li>
        <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:department:delete']">删除</el-button></li>
      </ul>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
        row-key="id"
        stripe
        default-expand-all
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" fixed="left" width="55"></el-table-column>
        <el-table-column prop="name" label="部门名称" fixed="left" min-width="200px"></el-table-column>
        <el-table-column prop="code" label="部门编码" fixed="left" min-width="100px"></el-table-column>
        <el-table-column prop="userCount" label="部门人数" min-width="100px"></el-table-column>
        <el-table-column prop="phone" label="联系电话" min-width="100px"></el-table-column>
        <el-table-column prop="email" label="部门邮箱" min-width="180px"></el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" min-width="140px"></el-table-column>
        <el-table-column prop="updateUser" label="更新人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="更新时间" min-width="140px"></el-table-column>
        <el-table-column
          v-if="containPermissions(['system:department:update', 'system:department:create', 'system:department:delete', 'system:department:queryUsers'])"
          label="操作"
          min-width="310"
          fixed="right"
        >
          <template slot-scope="{row}">
            <el-button type="text" @click="$refs.operaDepartmentWindow.open('编辑部门', row)" icon="el-icon-edit" v-permissions="['system:department:update']">编辑</el-button>
            <el-button type="text" @click="$refs.operaDepartmentWindow.open('新建下级部门', null, row)" icon="el-icon-edit" v-permissions="['system:department:create']">新建下级部门</el-button>
            <el-button type="text" @click="$refs.departmentUserWindow.open(row.id, row.name)" icon="el-icon-user-solid" v-permissions="['system:department:queryUsers']">查看人员</el-button>
            <el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:department:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaDepartmentWindow ref="operaDepartmentWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
    <!-- æŸ¥çœ‹äººå‘˜ -->
    <DepartmentUserWindow ref="departmentUserWindow"/>
  </TableLayout>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import { fetchTree } from '@/api/system/department'
import BaseTable from '@/components/base/BaseTable'
import OperaDepartmentWindow from '@/components/system/department/OperaDepartmentWindow'
import DepartmentUserWindow from '@/components/system/department/DepartmentUserWindow'
export default {
  name: 'SystemDepartment',
  extends: BaseTable,
  components: { DepartmentUserWindow, OperaDepartmentWindow, TableLayout },
  data () {
    return {
      // æœç´¢
      searchForm: {
        name: ''
      }
    }
  },
  methods: {
    // æŸ¥è¯¢æ•°æ®
    handlePageChange () {
      this.tableData.list.splice(0, this.tableData.list.length)
      this.isWorking.search = true
      fetchTree()
        .then(records => {
          this.tableData.list = records
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking.search = false
        })
    }
  },
  created () {
    this.config({
      module: '部门',
      api: '/system/department'
    })
    this.search()
  }
}
</script>
<style lang="scss" scoped>
.table-layout {
  /deep/ .table-content {
    margin-top: 0;
  }
}
</style>
admin/src/views/system/dict.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
<template>
  <TableLayout :permissions="['system:dict:query']">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="字典编码" prop="code">
        <el-input v-model="searchForm.code" v-trim placeholder="请输入字典编码" @keypress.enter.native="search"/>
      </el-form-item>
      <el-form-item label="字典名称" prop="name">
        <el-input v-model="searchForm.name" v-trim placeholder="请输入字典名称" @keypress.enter.native="search"/>
      </el-form-item>
      <section>
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button @click="reset">重置</el-button>
      </section>
    </el-form>
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:dict:create', 'system:dict:delete']">
        <li><el-button type="primary" @click="$refs.operaDictWindow.open('新建字典')" icon="el-icon-plus" v-permissions="['system:dict:create']">新建</el-button></li>
        <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:dict:delete']">删除</el-button></li>
      </ul>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        :default-sort = "{prop: 'createTime', order: 'descending'}"
        stripe
        @selection-change="handleSelectionChange"
        @sort-change="handleSortChange"
      >
        <el-table-column type="selection" fixed="left" width="55"></el-table-column>
        <el-table-column prop="code" label="字典编码" fixed="left" min-width="100px"></el-table-column>
        <el-table-column prop="name" label="字典名称" fixed="left" min-width="100px"></el-table-column>
        <el-table-column prop="remark" label="备注" min-width="100px"></el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" min-width="140px" sortable="custom" sort-by="dict.CREATE_TIME"></el-table-column>
        <el-table-column prop="updateUser" label="更新人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="更新时间" min-width="140px"></el-table-column>
        <el-table-column
          v-if="containPermissions(['system:dict:update', 'system:dict:delete'])"
          label="操作"
          min-width="210"
          fixed="right"
        >
          <template slot-scope="{row}">
            <el-button type="text" @click="$refs.operaDictWindow.open('编辑字典', row)" icon="el-icon-edit" v-permissions="['system:dict:update']">编辑</el-button>
            <el-button type="text" @click="$refs.dictDataManagerWindow.open(row.id, row.name)" icon="el-icon-edit" v-permissions="['system:dict:update']">数据管理</el-button>
            <el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:dict:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
        :pagination="tableData.pagination"
      ></pagination>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaDictWindow ref="operaDictWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
    <!-- æ•°æ®ç®¡ç† -->
    <DictDataManagerWindow ref="dictDataManagerWindow"/>
  </TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaDictWindow from '@/components/system/dict/OperaDictWindow'
import DictDataManagerWindow from '@/components/system/dict/DictDataManagerWindow'
export default {
  name: 'SystemDict',
  extends: BaseTable,
  components: { DictDataManagerWindow, OperaDictWindow, TableLayout, Pagination },
  data () {
    return {
      // æœç´¢
      searchForm: {
        code: '',
        name: ''
      }
    }
  },
  created () {
    this.config({
      module: '字典',
      api: '/system/dict',
      sorts: [{
        property: 'dict.CREATE_TIME',
        direction: 'DESC'
      }]
    })
    this.search()
  }
}
</script>
admin/src/views/system/loginLog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
<template>
  <TableLayout :permissions="['system:loginLog:query']">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="登录用户名" prop="loginUsername">
        <el-input v-model="searchForm.loginUsername" placeholder="请输入登录用户名" @keypress.enter.native="search"></el-input>
      </el-form-item>
      <el-form-item label="登录IP" prop="ip">
        <el-input v-model="searchForm.ip" placeholder="请输入登录IP" @keypress.enter.native="search"></el-input>
      </el-form-item>
      <el-form-item label="服务器IP" prop="serverIp">
        <el-input v-model="searchForm.serverIp" placeholder="请输入服务器IP" @keypress.enter.native="search"></el-input>
      </el-form-item>
      <el-form-item label="是否登录成功" prop="success">
        <el-select v-model="searchForm.success" placeholder="请选择是否登录状态" clearable @change="search">
          <el-option value="true" label="登录成功"/>
          <el-option value="false" label="登录失败"/>
        </el-select>
      </el-form-item>
      <el-form-item label="登录时间" prop="loginTime">
        <el-date-picker
          v-model="searchDateRange"
          type="datetimerange"
          range-separator="至"
          value-format="yyyy-MM-dd HH:mm:ss"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          @change="handleSearchTimeChange"
        ></el-date-picker>
      </el-form-item>
      <section>
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button type="primary" :loading="isWorking.export" @click="exportExcel">导出</el-button>
        <el-button @click="reset">重置</el-button>
      </section>
    </el-form>
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        stripe
        :default-sort="{prop: 'loginTime', order: 'descending'}"
        @sort-change="handleSortChange"
      >
        <el-table-column prop="loginUsername" label="登录用户名" min-width="100px"></el-table-column>
        <el-table-column prop="ip" label="登录IP" min-width="120px"></el-table-column>
        <el-table-column prop="location" label="登录地址" min-width="160px"></el-table-column>
        <el-table-column prop="clientInfo" label="客户端" min-width="160px"></el-table-column>
        <el-table-column prop="osInfo" label="操作系统" min-width="100px"></el-table-column>
        <el-table-column prop="platform" label="登录平台" min-width="100px"></el-table-column>
        <el-table-column prop="loginTime" label="登录时间" min-width="160px" sortable="custom" sort-by="LOGIN_TIME"></el-table-column>
        <el-table-column prop="systemVersion" label="系统版本" min-width="100px"></el-table-column>
        <el-table-column prop="serverIp" label="服务器IP" min-width="120px"></el-table-column>
        <el-table-column prop="success" label="状态" min-width="100px">
          <template slot-scope="{row}">
            {{row.success | statusText}}
          </template>
        </el-table-column>
        <el-table-column prop="reason" label="失败原因" min-width="160px"></el-table-column>
      </el-table>
      <pagination
          @size-change="handleSizeChange"
          @current-change="handlePageChange"
          :pagination="tableData.pagination"
      ></pagination>
    </template>
  </TableLayout>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import TableLayout from '@/layouts/TableLayout'
import Pagination from '@/components/common/Pagination'
export default {
  name: 'SystemLoginLog',
  extends: BaseTable,
  components: { TableLayout, Pagination },
  data () {
    return {
      // æœç´¢æ—¶é—´èŒƒå›´
      searchDateRange: [],
      // æœç´¢
      searchForm: {
        loginUsername: '',
        ip: '',
        serverIp: '',
        success: '',
        startTime: null,
        endTime: null
      }
    }
  },
  filters: {
    // ç™»å½•状态
    statusText (value) {
      if (value) {
        return '登录成功'
      }
      return '登录失败'
    }
  },
  methods: {
    // æ—¶é—´æœç´¢èŒƒå›´å˜åŒ–
    handleSearchTimeChange (value) {
      this.searchForm.startTime = null
      this.searchForm.endTime = null
      if (value != null) {
        this.searchForm.startTime = value[0]
        this.searchForm.endTime = value[1]
      }
      this.search()
    }
  },
  created () {
    this.config({
      module: '登录日志',
      api: '/system/loginLog',
      'field.id': 'id',
      'field.main': 'id',
      sorts: [{
        property: 'LOGIN_TIME',
        direction: 'DESC'
      }]
    })
    this.search()
  }
}
</script>
admin/src/views/system/menu.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,231 @@
<template>
  <TableLayout class="menu-layout" :permissions="['system:menu:query']">
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:menu:create', 'system:menu:delete', 'system:menu:sort']">
        <li><el-button type="primary" @click="$refs.operaMenuWindow.open('新建一级菜单')" icon="el-icon-plus" v-permissions="['system:menu:create']">新建</el-button></li>
        <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:menu:delete']">删除</el-button></li>
        <li><el-button @click="sort('top')" :loading="isWorking.sort" icon="el-icon-sort-up" v-permissions="['system:menu:sort']">上移</el-button></li>
        <li><el-button @click="sort('bottom')" :loading="isWorking.sort" icon="el-icon-sort-down" v-permissions="['system:menu:sort']">下移</el-button></li>
      </ul>
      <el-table
        ref="table"
        v-loading="isWorking.search"
        :data="tableData.list"
        :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
        row-key="id"
        stripe
        default-expand-all
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" fixed="left"></el-table-column>
        <el-table-column prop="name" label="菜单名称"  fixed="left" min-width="160px"></el-table-column>
        <el-table-column prop="icon" label="图标" min-width="80px" class-name="table-column-icon">
          <template slot-scope="{row}">
            <i v-if="row.icon != null && row.icon !== ''" :class="{[row.icon]: true}"></i>
            <template v-else>未设置</template>
          </template>
        </el-table-column>
        <el-table-column prop="path" label="访问路径" min-width="140px"></el-table-column>
        <el-table-column prop="remark" label="备注" min-width="120px"></el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" min-width="140px"></el-table-column>
        <el-table-column prop="updateUser" label="更新人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="更新时间" min-width="140px"></el-table-column>
        <el-table-column prop="disabled" label="是否启用" min-width="80px">
          <template slot-scope="{row}">
            <el-switch v-model="row.disabled" :active-value="false" :inactive-value="true" @change="switchDisabled(row)"/>
          </template>
        </el-table-column>
        <el-table-column
          v-if="containPermissions(['system:menu:update', 'system:menu:create', 'system:menu:delete'])"
          label="操作"
          min-width="220"
          fixed="right"
        >
          <template slot-scope="{row}">
            <el-button type="text" icon="el-icon-edit" @click="$refs.operaMenuWindow.open('编辑菜单', row)" v-permissions="['system:menu:update']">编辑</el-button>
            <el-button type="text" icon="el-icon-plus" @click="$refs.operaMenuWindow.open('新建子菜单', null, row)" v-permissions="['system:menu:create']">新建子菜单</el-button>
            <el-button v-if="!row.fixed" type="text" icon="el-icon-delete" @click="deleteById(row)" v-permissions="['system:menu:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaMenuWindow ref="operaMenuWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
  </TableLayout>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaMenuWindow from '@/components/system/menu/OperaMenuWindow'
import { fetchTree, updateStatus, sort } from '@/api/system/menu'
export default {
  name: 'SystemMenu',
  extends: BaseTable,
  components: { OperaMenuWindow, TableLayout },
  data () {
    return {
      // æ˜¯å¦æ­£åœ¨å¤„理中
      isWorking: {
        sort: false
      }
    }
  },
  methods: {
    // æŸ¥è¯¢æ•°æ®
    handlePageChange () {
      this.isWorking.search = true
      fetchTree()
        .then(records => {
          this.tableData.list = records
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking.search = false
        })
    },
    // æŽ’序
    sort (direction) {
      if (this.isWorking.sort) {
        return
      }
      if (this.tableData.selectedRows.length === 0) {
        this.$tip.warning('请选择一条数据')
        return
      }
      if (this.tableData.selectedRows.length > 1) {
        this.$tip.warning('排序时仅允许选择一条数据')
        return
      }
      const menuId = this.tableData.selectedRows[0].id
      // æ‰¾åˆ°èœå•范围
      let menuPool
      for (const rootMenu of this.tableData.list) {
        const parent = this.__findParent(menuId, rootMenu)
        if (parent != null) {
          menuPool = parent.children
        }
      }
      menuPool = menuPool || this.tableData.list
      const menuIndex = menuPool.findIndex(menu => menu.id === menuId)
      // ä¸Šç§»æ ¡éªŒ
      if (direction === 'top' && menuIndex === 0) {
        this.$tip.warning('菜单已到顶部')
        return
      }
      // ä¸‹ç§»æ ¡éªŒ
      if (direction === 'bottom' && menuIndex === menuPool.length - 1) {
        this.$tip.warning('菜单已到底部')
        return
      }
      this.isWorking.sort = true
      sort({
        id: this.tableData.selectedRows[0].id,
        direction
      })
        .then(() => {
          if (direction === 'top') {
            menuPool.splice(menuIndex, 0, menuPool.splice(menuIndex - 1, 1)[0])
          } else {
            menuPool.splice(menuIndex, 0, menuPool.splice(menuIndex + 1, 1)[0])
          }
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking.sort = false
        })
    },
    // å¯ç”¨/禁用菜单
    switchDisabled (row) {
      if (!row.disabled) {
        this.__updateMenuStatus(row)
        return
      }
      this.$dialog.disableConfirm(`确认禁用 ${row.name} èœå•吗?`)
        .then(() => {
          this.__updateMenuStatus(row)
        }).catch(() => {
          row.disabled = !row.disabled
        })
    },
    // æŸ¥è¯¢çˆ¶èŠ‚ç‚¹
    __findParent (id, parent) {
      if (parent.children === 0) {
        return
      }
      for (const menu of parent.children) {
        if (menu.id === id) {
          return parent
        }
        if (menu.children.length > 0) {
          const m = this.__findParent(id, menu)
          if (m != null) {
            return m
          }
        }
      }
      return null
    },
    // ä¿®æ”¹èœå•状态
    __updateMenuStatus (row) {
      updateStatus({
        id: row.id,
        parentId: row.parentId,
        disabled: row.disabled
      })
        .then(() => {
          this.$tip.apiSuccess('修改成功')
        })
        .catch(e => {
          row.disabled = !row.disabled
          this.$tip.apiFailed(e)
        })
    }
  },
  created () {
    this.config({
      module: '菜单',
      api: '/system/menu'
    })
    this.search()
  }
}
</script>
<style lang="scss" scoped>
@import "@/assets/style/variables.scss";
.menu-layout {
  /deep/ .table-content {
    margin-top: 0;
  }
}
// å›¾æ ‡åˆ—
.table-column-icon {
  // element-ui图标
  i {
    background-color: $primary-color;
    opacity: 0.72;
    font-size: 20px;
    color: #fff;
    padding: 4px;
    border-radius: 50%;
  }
  // è‡ªå®šä¹‰å›¾æ ‡
  [class^="eva-icon-"] {
    width: 20px;
    height: 20px;
    background-size: 16px;
    vertical-align: middle;
  }
}
</style>
admin/src/views/system/monitor.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,308 @@
<template>
  <Profile :permissions="['system:monitor:query']">
    <div class="monitor">
      <div class="toolbar">
        <el-switch v-model="autoRefresh" @change="changeAutoRefresh"/><label>{{autoRefresh | autoRefreshText}}</label>
      </div>
      <ul>
        <li class="wrap">
          <h2>CPU<Light v-if="data != null" :warn="data.cpu.useRatio > 60" :danger="data.cpu.useRatio > 80"/></h2>
          <div>
            <dl>
              <dt>物理核数</dt>
              <dd><Value :data="data" prop="cpu.physicalCount"/></dd>
            </dl>
            <dl>
              <dt>逻辑核数</dt>
              <dd><Value :data="data" prop="cpu.logicalCount"/></dd>
            </dl>
            <dl class="important">
              <dt>当前使用率</dt>
              <dd><Value :data="data" prop="cpu.useRatio" :handler="keep2decimals" suffix="%"/></dd>
            </dl>
            <dl>
              <dt>当前空闲率</dt>
              <dd><Value :data="data" prop="cpu.freeRatio" :handler="keep2decimals" suffix="%"/></dd>
            </dl>
          </div>
        </li>
        <li class="wrap">
          <h2>内存<Light v-if="data != null" :warn="data.memory.useRatio > 60" :danger="data.memory.useRatio > 80"/></h2>
          <div>
            <dl>
              <dt>总空间</dt>
              <dd><Value :data="data" prop="memory.size" suffix="G" :handler="toG"/></dd>
            </dl>
            <dl>
              <dt>空闲空间</dt>
              <dd><Value :data="data" prop="memory.freeSpace" suffix="G" :handler="toG"/></dd>
            </dl>
            <dl class="important">
              <dt>已用空间</dt>
              <dd><Value :data="data" prop="memory.usedSpace" suffix="G" :handler="toG"/></dd>
            </dl>
            <dl class="important">
              <dt>使用率</dt>
              <dd><Value :data="data" prop="memory.useRatio" suffix="%" :handler="keep2decimals"/></dd>
            </dl>
          </div>
        </li>
        <li class="wrap">
          <h2>JVM<Light v-if="data != null" :warn="data.jvm.memory.useRatio > 60" :danger="data.jvm.memory.useRatio > 80"/></h2>
          <div>
            <dl>
              <dt>安装路径</dt>
              <dd><Value :data="data" prop="jvm.home"/></dd>
            </dl>
            <dl>
              <dt>版本</dt>
              <dd><Value :data="data" prop="jvm.version"/></dd>
            </dl>
            <dl>
              <dt>启动时间</dt>
              <dd><Value :data="data" prop="jvm.bootTime"/></dd>
            </dl>
            <dl>
              <dt>运行时长</dt>
              <dd><Value :data="data" prop="jvm.runtime"/></dd>
            </dl>
            <dl>
              <dt>总空间</dt>
              <dd><Value :data="data" prop="jvm.memory.size" suffix="M" :handler="keep2decimals"/></dd>
            </dl>
            <dl>
              <dt>空闲空间</dt>
              <dd><Value :data="data" prop="jvm.memory.freeSpace" suffix="M" :handler="keep2decimals"/></dd>
            </dl>
            <dl class="important">
              <dt>已用空间</dt>
              <dd><Value :data="data" prop="jvm.memory.usedSpace" suffix="M" :handler="keep2decimals"/></dd>
            </dl>
            <dl class="important">
              <dt>使用率</dt>
              <dd><Value :data="data" prop="jvm.memory.useRatio" suffix="%" :handler="keep2decimals"/></dd>
            </dl>
          </div>
        </li>
        <li class="wrap">
          <h2>服务器</h2>
          <div>
            <dl>
              <dt>操作系统</dt>
              <dd><Value :data="data" prop="osName"/></dd>
            </dl>
            <dl>
              <dt>系统版本</dt>
              <dd><Value :data="data" prop="osVersion"/></dd>
            </dl>
            <dl>
              <dt>系统架构</dt>
              <dd><Value :data="data" prop="osArch"/></dd>
            </dl>
            <dl class="important">
              <dt>IP地址</dt>
              <dd><Value :data="data" prop="ip"/></dd>
            </dl>
            <dl>
              <dt>MAC地址</dt>
              <dd><Value :data="data" prop="mac"/></dd>
            </dl>
            <dl>
              <dt>服务器时间</dt>
              <dd><Value :data="data" prop="currentTime"/></dd>
            </dl>
          </div>
        </li>
      </ul>
      <div class="wrap">
        <h2>磁盘信息</h2>
        <el-table :data="data ? data.disks : []" v-loading="loading">
          <el-table-column prop="name" label="磁盘名称"/>
          <el-table-column prop="dir" label="磁盘路径"/>
          <el-table-column prop="fsType" label="文件系统"/>
          <el-table-column prop="size" label="总空间">
            <template slot-scope="{row}">
              {{toG(row.size)}}G
            </template>
          </el-table-column>
          <el-table-column prop="freeSpace" label="可用空间">
            <template slot-scope="{row}">
              {{toG(row.freeSpace)}}G
            </template>
          </el-table-column>
          <el-table-column prop="usedSpace" label="已用空间">
            <template slot-scope="{row}">
              <label class="important">{{toG(row.usedSpace)}}G</label>
            </template>
          </el-table-column>
          <el-table-column prop="useRatio" label="已用占比">
            <template slot-scope="{row}">
              <label class="important">{{keep2decimals(row.useRatio)}}%</label>
            </template>
          </el-table-column>
          <el-table-column>
            <template slot-scope="{row}">
              <Light :warn="row.useRatio > 60" :danger="row.useRatio > 80" :mini="true"/>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </div>
  </Profile>
</template>
<script>
import { getSystemInfo } from '@/api/system/monitor'
import Value from '@/components/common/Value'
import Light from '@/components/common/Light'
import Profile from '../../components/common/Profile'
export default {
  name: 'SystemMonitor',
  components: { Profile, Light, Value },
  data () {
    return {
      // åŠ è½½ä¸­
      loading: false,
      // è‡ªåŠ¨åˆ·æ–°æ ‡è¯†
      autoRefresh: false,
      // æ•°æ®
      data: null,
      // è‡ªåŠ¨åˆ·æ–°å®šæ—¶å™¨
      interval: null
    }
  },
  filters: {
    autoRefreshText (value) {
      if (value) {
        return '已开启自动刷新'
      }
      return '已关闭自动刷新'
    }
  },
  methods: {
    // åˆ‡æ¢è‡ªåŠ¨åˆ·æ–°
    changeAutoRefresh (value) {
      if (this.interval != null) {
        clearInterval(this.interval)
      }
      if (value) {
        this.getSystemInfo()
        this.interval = setInterval(() => {
          this.getSystemInfo()
        }, 3000)
      }
    },
    // èŽ·å–ç³»ç»Ÿä¿¡æ¯
    getSystemInfo () {
      if (this.loading) {
        return
      }
      this.loading = true
      getSystemInfo()
        .then(data => {
          this.data = data
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.loading = false
        })
    },
    // å•位转为G
    toG (value) {
      return Math.round(value / 1024 * 100) / 100
    },
    // è½¬ä¸ºæ¯”率
    keep2decimals (value) {
      return Math.round(value * 100) / 100
    }
  },
  beforeRouteLeave (from, to, next) {
    clearInterval(this.interval)
    next()
  },
  created () {
    this.getSystemInfo()
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
.monitor {
  padding: 20px 20px;
}
// å·¥å…·æ 
.toolbar {
  margin-bottom: 12px;
  background: #fff;
  padding: 8px 16px;
  label {
    font-size: 12px;
    margin-left: 8px;
    color: #999;
  }
}
ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  li {
    width: 49.5%;
    min-height: 200px;
    flex-shrink: 0;
  }
}
// ä¿¡æ¯æ¨¡å—
.wrap {
  background: #fff;
  box-shadow: 2px 2px 10px -5px #999;
  border-radius: 8px;
  margin-bottom: 16px;
  h2 {
    border-bottom: 1px solid #eee;
    font-size: 18px;
    font-weight: normal;
    line-height: 40px;
    padding: 0 16px;
    color: #555;
    position: relative;
    .light {
      position: absolute;
      top: 12px;
      right: 12px;
    }
  }
  & > div {
    padding: 0 16px;
    font-size: 14px;
    dl {
      display: flex;
      dt {
        width: 80px;
        text-align: right;
        flex-shrink: 0;
        color: #999;
      }
      dd {
        width: 100%;
        margin: 0;
        padding-left: 12px;
        color: #555;
        overflow: hidden;
      }
    }
  }
}
// é‡è¦ä¿¡æ¯
.important {
  color: $primary-color;
  font-weight: bold;
  & > dd > div {
    color: $primary-color;
    font-weight: bold;
  }
}
</style>
admin/src/views/system/permission.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<template>
  <TableLayout :permissions="['system:permission:query']">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="权限编码" prop="code">
        <el-input v-model="searchForm.code" v-trim placeholder="请输入权限编码" @keypress.enter.native="search"/>
      </el-form-item>
      <el-form-item label="权限名称" prop="name">
        <el-input v-model="searchForm.name" v-trim placeholder="请输入权限名称" @keypress.enter.native="search"/>
      </el-form-item>
      <section>
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button @click="reset">重置</el-button>
      </section>
    </el-form>
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:permission:create', 'system:permission:delete']">
        <li><el-button type="primary" @click="$refs.operaPermissionWindow.open('新建系统权限')" icon="el-icon-plus" v-permissions="['system:permission:create']">新建</el-button></li>
        <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:permission:delete']">删除</el-button></li>
      </ul>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        :default-sort = "{prop: 'createTime', order: 'descending'}"
        stripe
        @selection-change="handleSelectionChange"
        @sort-change="handleSortChange"
      >
        <el-table-column type="selection" fixed="left" width="55"></el-table-column>
        <el-table-column prop="code" label="权限编码" fixed="left" min-width="200px"></el-table-column>
        <el-table-column prop="name" label="权限名称" fixed="left" min-width="120px"></el-table-column>
        <el-table-column prop="remark" label="权限备注" min-width="120px"></el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" min-width="140px" sortable="custom" sort-by="perm.CREATE_TIME"></el-table-column>
        <el-table-column prop="updateUser" label="更新人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="更新时间" min-width="140px"></el-table-column>
        <el-table-column
          v-if="containPermissions(['system:permission:update', 'system:permission:delete'])"
          label="操作"
          min-width="130"
          fixed="right"
        >
          <template slot-scope="{row}">
            <el-button v-if="!row.fixed" type="text" @click="$refs.operaPermissionWindow.open('编辑系统权限', row)" icon="el-icon-edit" v-permissions="['system:permission:update']">编辑</el-button>
            <el-button v-if="!row.fixed" type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:permission:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
        :pagination="tableData.pagination"
      ></pagination>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaPermissionWindow ref="operaPermissionWindow" @success="handlePageChange"/>
  </TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaPermissionWindow from '@/components/system/permission/OperaPermissionWindow'
export default {
  name: 'SystemPermission',
  extends: BaseTable,
  components: { OperaPermissionWindow, TableLayout, Pagination },
  data () {
    return {
      // æœç´¢
      searchForm: {
        code: '',
        name: '',
        remark: ''
      }
    }
  },
  created () {
    this.config({
      module: '权限',
      api: '/system/permission',
      sorts: [{
        property: 'perm.CREATE_TIME',
        direction: 'DESC'
      }]
    })
    this.search()
  }
}
</script>
admin/src/views/system/position.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
<template>
  <TableLayout :permissions="['system:position:query']">
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:position:create', 'system:position:delete']">
        <li><el-button type="primary" @click="$refs.operaPositionWindow.open('新建岗位')" icon="el-icon-plus" v-permissions="['system:position:create']">新建</el-button></li>
        <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['system:position:delete']">删除</el-button></li>
      </ul>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
        row-key="id"
        stripe
        default-expand-all
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" fixed="left"></el-table-column>
        <el-table-column prop="name" label="岗位名称" fixed="left" min-width="200px"></el-table-column>
        <el-table-column prop="code" label="岗位编码" fixed="left" min-width="100px"></el-table-column>
        <el-table-column prop="userCount" label="岗位人数" min-width="100px"></el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" min-width="140px"></el-table-column>
        <el-table-column prop="updateUser" label="更新人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="更新时间" min-width="140px"></el-table-column>
        <el-table-column
          v-if="containPermissions(['system:position:update', 'system:position:query', 'system:position:delete'])"
          label="操作"
          min-width="310"
          fixed="right"
        >
          <template slot-scope="{row}">
            <el-button type="text" @click="$refs.operaPositionWindow.open('编辑岗位', row)" icon="el-icon-edit" v-permissions="['system:position:update']">编辑</el-button>
            <el-button type="text" @click="$refs.operaPositionWindow.open('新增下级岗位', null, row)" icon="el-icon-edit" v-permissions="['system:position:update']">新增下级岗位</el-button>
            <el-button type="text" @click="$refs.positionUserWindow.open(row.id, row.name)" icon="el-icon-user-solid" v-permissions="['system:position:queryUsers']">查看人员</el-button>
            <el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:position:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaPositionWindow ref="operaPositionWindow" @success="handlePageChange"/>
    <!-- äººå‘˜ç®¡ç† -->
    <PositionUserWindow ref="positionUserWindow"/>
  </TableLayout>
</template>
<script>
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaPositionWindow from '@/components/system/position/OperaPositionWindow'
import PositionUserWindow from '@/components/system/position/PositionUserWindow'
import { fetchTree } from '@/api/system/position'
export default {
  name: 'SystemPosition',
  extends: BaseTable,
  components: { PositionUserWindow, OperaPositionWindow, TableLayout },
  methods: {
    // æŸ¥è¯¢æ•°æ®
    handlePageChange () {
      this.isWorking.search = true
      fetchTree()
        .then(records => {
          this.tableData.list = records
        })
        .catch(e => {
          this.$tip.apiFailed(e)
        })
        .finally(() => {
          this.isWorking.search = false
        })
    }
  },
  created () {
    this.config({
      module: '岗位',
      api: '/system/position'
    })
    this.search()
  }
}
</script>
<style lang="scss" scoped>
.table-layout {
  /deep/ .table-content {
    margin-top: 0;
  }
}
</style>
admin/src/views/system/role.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
<template>
  <TableLayout :permissions="['system:role:query']">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="角色编码" prop="code">
        <el-input v-model="searchForm.code" v-trim placeholder="请输入角色编码" @keypress.enter.native="search"/>
      </el-form-item>
      <el-form-item label="角色名称" prop="name">
        <el-input v-model="searchForm.name" v-trim placeholder="请输入角色名称" @keypress.enter.native="search"/>
      </el-form-item>
      <section>
        <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
        <el-button @click="reset">重置</el-button>
      </section>
    </el-form>
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:role:create', 'system:role:delete']">
        <li v-permissions="['system:role:create']"><el-button type="primary" @click="$refs.operaRoleWindow.open('新建角色')" icon="el-icon-plus">新建</el-button></li>
        <li v-permissions="['system:role:delete']"><el-button @click="deleteByIdInBatch" icon="el-icon-delete">删除</el-button></li>
      </ul>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        :default-sort = "{prop: 'createTime', order: 'descending'}"
        stripe
        @selection-change="handleSelectionChange"
        @sort-change="handleSortChange"
      >
        <el-table-column type="selection" fixed="left" width="55"></el-table-column>
        <el-table-column prop="code" label="角色编码" fixed="left" min-width="100px"></el-table-column>
        <el-table-column prop="name" label="角色名称" fixed="left" min-width="100px"></el-table-column>
        <el-table-column prop="remark" label="角色备注" min-width="120px"></el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" min-width="140px" sortable="custom" sort-by="role.CREATE_TIME"></el-table-column>
        <el-table-column prop="updateUser" label="更新人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="更新时间" min-width="140px"></el-table-column>
        <el-table-column
          v-if="containPermissions(['system:role:update', 'system:role:createRolePermission', 'system:role:createRoleMenu', 'system:role:delete'])"
          label="操作"
          min-width="270"
          fixed="right"
        >
          <template v-if="isAdmin || (row.code !== adminCode && userInfo.roles.findIndex(code => code === row.code) === -1)" slot-scope="{row}">
            <el-button type="text" @click="$refs.operaRoleWindow.open('编辑角色', row)" icon="el-icon-edit" v-permissions="['system:role:update']">编辑</el-button>
            <el-button type="text" @click="$refs.permissionConfigWindow.open(row)" v-permissions="['system:role:createRolePermission']">配置权限</el-button>
            <el-button type="text" @click="$refs.menuConfigWindow.open(row)" icon="el-icon-menu" v-permissions="['system:role:createRoleMenu']">授权菜单</el-button>
            <el-button v-if="!row.fixed" type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['system:role:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
        :pagination="tableData.pagination"
      ></pagination>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaRoleWindow ref="operaRoleWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
    <!-- é…ç½®æƒé™ -->
    <PermissionConfigWindow ref="permissionConfigWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
    <!-- æŽˆæƒèœå• -->
    <MenuConfigWindow ref="menuConfigWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
  </TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaRoleWindow from '@/components/system/role/OperaRoleWindow'
import PermissionConfigWindow from '@/components/system/role/PermissionConfigWindow'
import MenuConfigWindow from '@/components/system/role/MenuConfigWindow'
export default {
  name: 'SystemRole',
  extends: BaseTable,
  components: { MenuConfigWindow, PermissionConfigWindow, OperaRoleWindow, TableLayout, Pagination },
  data () {
    return {
      // æœç´¢
      searchForm: {
        code: '',
        name: '',
        remark: ''
      }
    }
  },
  created () {
    this.config({
      module: '角色',
      api: '/system/role',
      sorts: [{
        property: 'role.CREATE_TIME',
        direction: 'DESC'
      }]
    })
    this.search()
  }
}
</script>
admin/src/views/system/traceLog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,254 @@
<template>
  <TableLayout :permissions="['system:traceLog:query']">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="用户姓名" prop="userRealname">
        <el-input v-model="searchForm.userRealname" placeholder="请输入固化用户姓名" @keypress.enter.native="search"></el-input>
      </el-form-item>
      <el-form-item label="业务模块" prop="operaModule">
        <el-input v-model="searchForm.operaModule" placeholder="请输入业务模块" @keypress.enter.native="search"></el-input>
      </el-form-item>
      <el-form-item label="请求地址" prop="requestUri">
        <el-input v-model="searchForm.requestUri" placeholder="请输入请求地址" @keypress.enter.native="search"></el-input>
      </el-form-item>
      <el-form-item label="状态" prop="status">
        <el-select v-model="searchForm.status" clearable @change="search">
          <el-option value="-1" label="未处理"/>
          <el-option value="0" label="失败"/>
          <el-option value="1" label="成功"/>
        </el-select>
      </el-form-item>
      <el-form-item label="异常等级" prop="exceptionLevel">
        <el-select v-model="searchForm.exceptionLevel" clearable @change="search">
          <el-option value="10" label="高"/>
          <el-option value="5" label="中"/>
          <el-option value="0" label="低"/>
        </el-select>
      </el-form-item>
      <el-form-item label="操作时间范围">
        <el-date-picker
          v-model="searchDateRange"
          type="datetimerange"
          range-separator="至"
          value-format="yyyy-MM-dd HH:mm:ss"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          @change="handleSearchTimeChange"
        ></el-date-picker>
      </el-form-item>
      <section>
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button type="primary" :loading="isWorking.export" @click="exportExcel">导出</el-button>
        <el-button @click="reset">重置</el-button>
      </section>
    </el-form>
    <div slot="space" class="status-bar">
      <label class="status-normal">正常</label>
      <label class="status-warn">警告异常(需排查)</label>
      <label class="status-danger">系统异常(需修复)</label>
    </div>
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        stripe
        :default-sort="{prop: 'operaTime', order: 'descending'}"
        :row-class-name="tableRowClassName"
        @sort-change="handleSortChange"
      >
        <el-table-column prop="operaModule" label="业务模块" min-width="100px"></el-table-column>
        <el-table-column prop="operaRemark" label="操作说明" min-width="100px"></el-table-column>
        <el-table-column prop="requestMethod" label="请求方式" min-width="80px"></el-table-column>
        <el-table-column prop="requestUri" label="请求地址" min-width="200px"></el-table-column>
        <el-table-column prop="status" label="状态" min-width="80px">
          <template slot-scope="{row}">
            {{row.status | statusText}}
          </template>
        </el-table-column>
        <el-table-column prop="requestParams" label="请求参数" min-width="80px">
          <template slot-scope="{row}">
            <ColumnDetail v-if="row.requestParams != null" :content="row.requestParams" :limit="0"/>
          </template>
        </el-table-column>
        <el-table-column prop="requestResult" label="请求结果" min-width="80px">
          <template slot-scope="{row}">
            <ColumnDetail v-if="row.requestResult != null" :content="row.requestResult"/>
          </template>
        </el-table-column>
        <el-table-column prop="exceptionLevel" label="异常等级" sortable="custom" sort-by="EXCEPTION_LEVEL" min-width="100px">
          <template slot-scope="{row}">
            {{row.exceptionLevel | exceptionLevelText}}
          </template>
        </el-table-column>
        <el-table-column prop="exceptionStack" label="异常信息" min-width="170px">
          <template slot-scope="{row}">
            <ColumnDetail v-if="row.exceptionStack != null" :content="row.exceptionStack" :button-type="getExceptionButtonType(row.exceptionLevel)"/>
          </template>
        </el-table-column>
        <el-table-column prop="operaSpendTime" label="请求耗时(ms)" sortable="custom" sort-by="OPERA_SPEND_TIME" min-width="120px"></el-table-column>
        <el-table-column prop="userRealname" label="操作人" min-width="100px"></el-table-column>
        <el-table-column prop="operaTime" label="操作时间" sortable="custom" sort-by="OPERA_TIME" min-width="140px"></el-table-column>
        <el-table-column prop="platform" label="操作平台" min-width="100px"></el-table-column>
        <el-table-column prop="systemVersion" label="系统版本" min-width="80px"></el-table-column>
        <el-table-column prop="serverIp" label="处理服务器IP" min-width="100px"></el-table-column>
        <el-table-column prop="ip" label="用户IP" min-width="100px"></el-table-column>
        <el-table-column prop="clientInfo" label="用户客户端" min-width="200px"></el-table-column>
        <el-table-column prop="osInfo" label="用户操作系统" min-width="100px"></el-table-column>
      </el-table>
      <pagination
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
        :pagination="tableData.pagination"
      ></pagination>
    </template>
  </TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import ColumnDetail from '../../components/common/ColumnDetail'
export default {
  name: 'SystemTraceLog',
  extends: BaseTable,
  components: { ColumnDetail, TableLayout, Pagination },
  data () {
    return {
      // æœç´¢æ—¶é—´åèƒƒ
      searchDateRange: [],
      // æœç´¢
      searchForm: {
        userRealname: '',
        operaModule: '',
        requestUri: '',
        status: null,
        exceptionLevel: null,
        startTime: null,
        endTime: null
      }
    }
  },
  filters: {
    // çŠ¶æ€
    statusText (value) {
      if (value === 1) {
        return '成功'
      }
      if (value === 0) {
        return '失败'
      }
      return '未处理'
    },
    // å¼‚常等级
    exceptionLevelText (value) {
      if (value == null) {
        return ''
      }
      if (value === 0) {
        return '低'
      }
      if (value === 5) {
        return '中'
      }
      if (value === 10) {
        return '高'
      }
      return '未知'
    }
  },
  methods: {
    // æœç´¢æ¡†é‡ç½®
    reset () {
      this.$refs.searchForm.resetFields()
      this.searchDateRange = []
      this.searchForm.startTime = null
      this.searchForm.endTime = null
      this.search()
    },
    // æ ‡è®°è¡Œclass
    tableRowClassName ({ row }) {
      if (row.exceptionLevel === 5 || row.status === -1) {
        return 'warning-log'
      } else if (row.exceptionLevel === 10) {
        return 'danger-log'
      }
      return ''
    },
    // èŽ·å–å¼‚å¸¸æŸ¥çœ‹æŒ‰é’®ç±»åž‹
    getExceptionButtonType (level) {
      if (level === 5) {
        return 'warning'
      }
      if (level === 10) {
        return 'danger'
      }
      return null
    },
    // æ—¶é—´æœç´¢èŒƒå›´å˜åŒ–
    handleSearchTimeChange (value) {
      this.searchForm.startTime = null
      this.searchForm.endTime = null
      if (value != null) {
        this.searchForm.startTime = value[0]
        this.searchForm.endTime = value[1]
      }
      this.search()
    }
  },
  created () {
    this.config({
      api: '/system/traceLog',
      sorts: [{
        property: 'OPERA_TIME',
        direction: 'DESC'
      }]
    })
    this.search()
  }
}
</script>
<style scoped lang="scss">
// çŠ¶æ€æ 
.status-bar {
  padding: 0 16px;
  [class^=status-] {
    font-size: 13px;
    margin-right: 12px;
    line-height: 40px;
    &::before {
      position: relative;
      top: 2px;
      display: inline-block;
      content: '';
      width: 12px;
      height: 12px;
      border: 1px solid #ccc;
      background: #fff;
      margin-right: 6px;
    }
  }
  .status-warn::before {
    background-color: oldlace;
    border-color: orange;
  }
  .status-danger::before {
    background-color: #fdf0f0;
    border-color: indianred;
  }
}
/deep/ .table-content {
  margin-top: 0;
}
// è­¦å‘Šçº§æ—¥å¿—
/deep/ .warning-log td {
  background-color: oldlace !important;
}
// å±é™©çº§æ—¥å¿—
/deep/ .danger-log td {
  background-color: #fdf0f0 !important;
}
</style>
admin/src/views/system/user.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
<template>
  <TableLayout :permissions="['system:user:query']">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="80px" inline>
      <el-form-item label="用户名" prop="username">
        <el-input v-model="searchForm.username" v-trim placeholder="请输入用户名" @keypress.enter.native="search"/>
      </el-form-item>
      <el-form-item label="姓名" prop="realname">
        <el-input v-model="searchForm.realname" v-trim placeholder="请输入姓名" @keypress.enter.native="search"/>
      </el-form-item>
      <el-form-item label="手机号码" prop="mobile">
        <el-input v-model="searchForm.mobile" v-trim placeholder="请输入手机号码" @keypress.enter.native="search"/>
      </el-form-item>
            <el-form-item label="所属部门" prop="rootDeptId">
        <DepartmentSelect v-model="searchForm.rootDeptId" placeholder="请选择所属部门" clearable/>
      </el-form-item>
      <el-form-item label="岗位" prop="positionId">
        <PositionSelect v-model="searchForm.positionId" placeholder="请选择岗位" clearable/>
      </el-form-item>
      <section>
        <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
        <el-button @click="reset">重置</el-button>
      </section>
    </el-form>
    <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
    <template v-slot:table-wrap>
      <ul class="toolbar" v-permissions="['system:user:create', 'system:user:delete']">
        <li v-permissions="['system:user:create']"><el-button icon="el-icon-plus" type="primary" @click="$refs.operaUserWindow.open('新建用户')">新建</el-button></li>
        <li v-permissions="['system:user:delete']"><el-button icon="el-icon-delete" @click="deleteByIdInBatch">删除</el-button></li>
      </ul>
      <el-table
        v-loading="isWorking.search"
        :data="tableData.list"
        :default-sort = "{prop: 'createTime', order: 'descending'}"
        stripe
        @selection-change="handleSelectionChange"
        @sort-change="handleSortChange"
      >
        <el-table-column type="selection" width="55"></el-table-column>
        <el-table-column prop="avatar" label="头像" width="80px" class-name="table-column-avatar" fixed="left">
          <template slot-scope="{row}">
            <img :src="row.avatar == null ? '/static/avatar/man.png' : row.avatar">
          </template>
        </el-table-column>
        <el-table-column prop="realname" label="姓名" min-width="100px" fixed="left"></el-table-column>
        <el-table-column prop="username" label="用户名" min-width="120px"></el-table-column>
        <el-table-column prop="empNo" label="工号" sortable="custom" sort-by="EMP_NO" min-width="80px"></el-table-column>
        <el-table-column prop="department" label="部门" min-width="120px">
          <template slot-scope="{row}">{{row.department == null ? '' : row.department.name}}</template>
        </el-table-column>
        <el-table-column prop="position" label="岗位" min-width="160px" class-name="table-column-strings">
          <template slot-scope="{row}">
            <ul>
              <li v-for="position in row.positions" :key="position.id">{{position.name}}</li>
            </ul>
          </template>
        </el-table-column>
        <el-table-column prop="sex" label="性别" sortable="custom" sort-by="SEX" min-width="80px">
          <template slot-scope="{row}">
            {{row.sex | sex}}
          </template>
        </el-table-column>
        <el-table-column prop="mobile" label="手机号码" min-width="100px"></el-table-column>
        <el-table-column prop="email" label="邮箱" min-width="180px"></el-table-column>
        <el-table-column prop="birthday" label="生日" sortable="custom" sort-by="BIRTHDAY" min-width="100px"></el-table-column>
        <el-table-column prop="roles" label="角色" min-width="160px" class-name="table-column-strings">
          <template slot-scope="{row}">
            <ul>
              <li v-for="role in row.roles" :key="role.id">{{role.name}}</li>
            </ul>
          </template>
        </el-table-column>
        <el-table-column prop="createUser" label="创建人" min-width="100px">
          <template slot-scope="{row}">{{row.createUserInfo == null ? '' : row.createUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" sortable="custom" sort-by="CREATE_TIME" min-width="140px"></el-table-column>
        <el-table-column prop="updateUser" label="更新人" min-width="100px">
          <template slot-scope="{row}">{{row.updateUserInfo == null ? '' : row.updateUserInfo.username}}</template>
        </el-table-column>
        <el-table-column prop="updateTime" label="更新时间" sortable="custom" sort-by="UPDATE_TIME" min-width="140px"></el-table-column>
        <el-table-column
          v-if="containPermissions(['system:user:update', 'system:user:createUserRole', 'system:user:resetPwd', 'system:user:delete'])"
          label="操作"
          width="270"
          fixed="right"
        >
          <template v-if="isAdmin || (row.id !== userInfo.id && row.roles.findIndex(r => r.code === adminCode) === -1)" slot-scope="{row}">
            <el-button type="text" icon="el-icon-edit" @click="$refs.operaUserWindow.open('编辑用户', row)" v-permissions="['system:user:update']">编辑</el-button>
            <el-button type="text" icon="el-icon-s-custom" @click="$refs.roleConfigWindow.open(row)" v-permissions="['system:user:createUserRole']">配置角色</el-button>
            <el-button type="text" @click="$refs.resetPwdWindow.open(row)" v-permissions="['system:user:resetPwd']">重置密码</el-button>
            <el-button v-if="!row.fixed" type="text" icon="el-icon-delete" @click="deleteById(row)" v-permissions="['system:user:delete']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
        :pagination="tableData.pagination"
      ></pagination>
    </template>
    <!-- æ–°å»º/修改 -->
    <OperaUserWindow ref="operaUserWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
    <!-- é…ç½®è§’色 -->
    <RoleConfigWindow ref="roleConfigWindow" @success="handlePageChange(tableData.pagination.pageIndex)"/>
    <!-- é‡ç½®å¯†ç  -->
    <ResetPwdWindow ref="resetPwdWindow"/>
  </TableLayout>
</template>
<script>
import Pagination from '@/components/common/Pagination'
import TableLayout from '@/layouts/TableLayout'
import BaseTable from '@/components/base/BaseTable'
import OperaUserWindow from '@/components/system/user/OperaUserWindow'
import RoleConfigWindow from '@/components/system/user/RoleConfigWindow'
import ResetPwdWindow from '@/components/system/user/ResetPwdWindow'
import DepartmentSelect from '@/components/common/DepartmentSelect'
import PositionSelect from '@/components/common/PositionSelect'
export default {
  name: 'SystemUser',
  extends: BaseTable,
  components: { PositionSelect, DepartmentSelect, ResetPwdWindow, RoleConfigWindow, OperaUserWindow, TableLayout, Pagination },
  data () {
    return {
      // æœç´¢
      searchForm: {
        username: '', // åå­—
        realname: '', // å§“名
        rootDeptId: null, // éƒ¨é—¨ID
        positionId: null, // å²—位ID
        mobile: '' // æ‰‹æœºå·ç 
      }
    }
  },
  created () {
    this.config({
      module: '用户',
      api: '/system/user',
      'field.main': 'realname',
      sorts: [{
        property: 'CREATE_TIME',
        direction: 'DESC'
      }]
    })
    this.search()
  }
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables.scss";
// åˆ—表头像处理
.table-column-avatar {
  img {
    width: 48px;
  }
}
</style>
admin/vue.config.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
// è¯¦ç»†é…ç½®è¯·å‚考https://cli.vuejs.org/zh/config/#vue-config-js
const outputDir = process.env.VUE_APP_CONTEXT_PATH.substring(1, process.env.VUE_APP_CONTEXT_PATH.length - 1)
module.exports = {
  publicPath: process.env.VUE_APP_CONTEXT_PATH,
  outputDir: outputDir === '/' ? 'dist' : outputDir,
  assetsDir: 'static',
  lintOnSave: false,
  devServer: {
    host: '0.0.0.0',
    port: 10012,
    proxy: {
      [process.env.VUE_APP_API_PREFIX]: {
        target: 'http://192.168.0.197:10013',
        changeOrigin: true,
        pathRewrite: {
          [`^${[process.env.VUE_APP_API_PREFIX]}`]: ''
        }
      }
    }
  }
}