Эх сурвалжийг харах

fix:创建掌柜控制台

刘恒 2 жил өмнө
parent
commit
c4bf087141
100 өөрчлөгдсөн 6774 нэмэгдсэн , 2 устгасан
  1. 12 0
      .env
  2. 12 0
      .env.development
  3. 20 0
      .gitignore
  4. 21 0
      LICENSE
  5. 53 0
      README.en-US.md
  6. 59 2
      README.md
  7. 13 0
      babel.config.js
  8. 80 0
      package.json
  9. BIN
      public/favicon.ico
  10. 29 0
      public/index.html
  11. 73 0
      public/reset.css
  12. 119 0
      src/App.vue
  13. BIN
      src/assets/img/Frame@2x(3).png
  14. BIN
      src/assets/img/MoneyCollect@2x.png
  15. BIN
      src/assets/img/PropertySafety@2x.png
  16. BIN
      src/assets/img/alipay.png
  17. BIN
      src/assets/img/logo.png
  18. BIN
      src/assets/img/money.png
  19. BIN
      src/assets/img/new_logo.png
  20. BIN
      src/assets/img/preview-nine.png
  21. BIN
      src/assets/img/preview.png
  22. BIN
      src/assets/img/title.png
  23. BIN
      src/assets/img/user.png
  24. BIN
      src/assets/img/users.png
  25. BIN
      src/assets/img/wechatpay.png
  26. BIN
      src/assets/img/ws.png
  27. BIN
      src/assets/img/yis.png
  28. BIN
      src/assets/img/ys.png
  29. 25 0
      src/bootstrap.js
  30. 172 0
      src/components/cache/AKeepAlive.js
  31. 78 0
      src/components/card/ChartCard.vue
  32. 59 0
      src/components/chart/Bar.vue
  33. 67 0
      src/components/chart/MiniArea.vue
  34. 59 0
      src/components/chart/MiniBar.vue
  35. 56 0
      src/components/chart/MiniProgress.vue
  36. 80 0
      src/components/chart/Radar.vue
  37. 59 0
      src/components/chart/RankingList.vue
  38. 79 0
      src/components/chart/Trend.vue
  39. 9 0
      src/components/chart/index.less
  40. 157 0
      src/components/checkbox/ColorCheckbox.vue
  41. 161 0
      src/components/checkbox/ImgCheckbox.vue
  42. 7 0
      src/components/checkbox/index.js
  43. 69 0
      src/components/exception/ExceptionPage.vue
  44. 19 0
      src/components/exception/typeConfig.js
  45. 207 0
      src/components/form/FormRow.vue
  46. 66 0
      src/components/input/IInput.vue
  47. 0 0
      src/components/menu/@/assets/img/logo.png
  48. 85 0
      src/components/menu/Contextmenu.vue
  49. 109 0
      src/components/menu/SideMenu.vue
  50. 38 0
      src/components/menu/index.less
  51. 276 0
      src/components/menu/menu.js
  52. 48 0
      src/components/page/header/PageHeader.vue
  53. 40 0
      src/components/page/header/index.less
  54. 64 0
      src/components/result/Result.vue
  55. 224 0
      src/components/setting/Setting.vue
  56. 25 0
      src/components/setting/SettingItem.vue
  57. 117 0
      src/components/setting/i18n.js
  58. 200 0
      src/components/table/StandardTable.vue
  59. 155 0
      src/components/table/advance/ActionColumns.vue
  60. 44 0
      src/components/table/advance/ActionSize.vue
  61. 249 0
      src/components/table/advance/AdvanceTable.vue
  62. 313 0
      src/components/table/advance/SearchArea.vue
  63. 2 0
      src/components/table/advance/index.js
  64. 49 0
      src/components/table/api/ApiTable.vue
  65. 79 0
      src/components/task/TaskGroup.vue
  66. 25 0
      src/components/task/TaskItem.vue
  67. 59 0
      src/components/tool/AStepItem.vue
  68. 69 0
      src/components/tool/AvatarList.vue
  69. 156 0
      src/components/tool/DetailList.vue
  70. 142 0
      src/components/tool/Drawer.vue
  71. 30 0
      src/components/tool/FooterToolBar.vue
  72. 34 0
      src/components/tool/HeadInfo.vue
  73. 83 0
      src/components/tool/TagSelect.vue
  74. 33 0
      src/components/tool/TagSelectOption.vue
  75. 96 0
      src/components/transition/PageToggleTransition.vue
  76. 58 0
      src/components/tree/tree.vue
  77. 35 0
      src/config/config.js
  78. 18 0
      src/config/default/admin.config.js
  79. 21 0
      src/config/default/animate.config.js
  80. 84 0
      src/config/default/antd.config.js
  81. 6 0
      src/config/default/index.js
  82. 35 0
      src/config/default/setting.config.js
  83. 6 0
      src/config/index.js
  84. 10 0
      src/config/replacer/index.js
  85. 67 0
      src/config/replacer/resolve.config.js
  86. 228 0
      src/layouts/AdminLayout.vue
  87. 22 0
      src/layouts/BlankView.vue
  88. 144 0
      src/layouts/PageLayout.vue
  89. 59 0
      src/layouts/PageView.vue
  90. 161 0
      src/layouts/header/AdminHeader.vue
  91. 61 0
      src/layouts/header/HeaderAvatar.vue
  92. 82 0
      src/layouts/header/HeaderExpand.vue
  93. 92 0
      src/layouts/header/HeaderNotice.vue
  94. 95 0
      src/layouts/header/index.less
  95. 174 0
      src/layouts/tabs/TabsHead.vue
  96. 390 0
      src/layouts/tabs/TabsView.vue
  97. 25 0
      src/layouts/tabs/i18n.js
  98. 2 0
      src/layouts/tabs/index.js
  99. 64 0
      src/main.js
  100. 71 0
      src/minins/mixin.js

+ 12 - 0
.env

@@ -0,0 +1,12 @@
+VUE_APP_PUBLIC_PATH=/
+VUE_APP_NAME=Admin
+VUE_APP_ROUTES_KEY=admin.routes
+VUE_APP_PERMISSIONS_KEY=admin.permissions
+VUE_APP_ROLES_KEY=admin.roles
+VUE_APP_USER_KEY=admin.user
+VUE_APP_SETTING_KEY=admin.setting
+VUE_APP_TBAS_KEY=admin.tabs
+VUE_APP_TBAS_TITLES_KEY=admin.tabs.titles
+VUE_APP_API_BASE_URL=https://sxzgkj.baoxianzhanggui.com/web-api
+VUE_APP_BASE_URL=https://sxzgkj.baoxianzhanggui.com
+

+ 12 - 0
.env.development

@@ -0,0 +1,12 @@
+VUE_APP_API_BASE_URL='http://192.168.1.103:8080'
+# VUE_APP_API_BASE_URL='"https://sxzgkj.baoxianzhanggui.com/web-api"'  #//正式服务器
+# VUE_APP_API_BASE_URL='"https://sxzgkj.baoxianzhanggui.com/web-api"'  #//正式服务器
+# VUE_APP_API_BASE_URL= '"http://192.168.0.115:8080/"'                 #//凯森
+# VUE_APP_API_BASE_URL= '"http://192.168.0.106:8080/"'                #//屈晨
+# VUE_APP_API_BASE_URL= '"http://192.168.0.253:8080/"'                 #//田智
+# VUE_APP_API_BASE_URL= 'http://192.168.0.42:8080/'                 #//武鸿鹏
+# VUE_APP_API_BASE_URL= '"http://192.168.0.253:8080/"'                #//田智
+
+# VUE_APP_API_BASE_URL= '"http://192.168.1.101:8080/"'
+# VUE_APP_API_BASE_URL= '"http://192.168.0.250:8080/"'//
+# VUE_APP_API_BASE_URL= 'http://192.168.0.103:8080'                 #//霍续亮

+ 20 - 0
.gitignore

@@ -0,0 +1,20 @@
+.DS_Store
+node_modules/
+dist/
+admindb/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+/test/unit/coverage/
+/test/e2e/reports/
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+package-lock.json
+.env.production.local

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 iczer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 53 - 0
README.en-US.md

@@ -0,0 +1,53 @@
+[简体中文](./README.md) | English
+<h1 align="center">Vue Antd Admin</h1>
+
+<div align="center">
+  
+[Ant Design Pro](https://github.com/ant-design/ant-design-pro)'s implementation with Vue.  
+An out-of-box UI solution for enterprise applications as a React boilerplate.
+
+[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)
+[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)
+[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)
+[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)
+![image](./src/assets/img/preview.png)   
+
+Multiple theme modes available:  
+![image](./src/assets/img/preview-nine.png)
+</div>
+
+- Preview:https://iczer.gitee.io/vue-antd-admin
+- Documentation:https://iczer.gitee.io/vue-antd-admin-docs
+- FAQ:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html
+- Mirror Repo in China:https://gitee.com/iczer/vue-antd-admin
+
+## Browsers support
+Modern browsers and IE10.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
+| --- | --- | --- | --- | --- |
+| IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## Usage
+### clone
+```bash
+$ git clone https://github.com/iczer/vue-antd-admin.git
+```
+### yarn
+```bash
+$ yarn install
+$ yarn serve
+```
+### or npm
+```
+$ npm install
+$ npm run serve
+```
+More instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs).
+
+## Contributing
+Any type of contribution is welcome, here are some examples of how you may contribute to this project: :star2::
+- Use Vue Antd Admin in your daily work.
+- Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions.
+- Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code.
+- Join the community and share your experiences with us. QQ Group:942083829、812277510(full)、610090280(full)

+ 59 - 2
README.md

@@ -1,3 +1,60 @@
-# controlPc
+简体中文 | [English](./README.en-US.md)
+<h1 align="center">Vue Antd Admin</h1>
 
-控制台前端
+<div align="center">
+  
+[Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 实现版本  
+开箱即用的中后台前端/设计解决方案
+
+[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)
+[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)
+[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)
+[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)
+![image](./src/assets/img/preview.png)  
+
+多种主题模式可选:  
+![image](./src/assets/img/preview-nine.png)
+</div>
+
+- 预览地址:https://iczer.gitee.io/vue-antd-admin
+- 使用文档:https://iczer.gitee.io/vue-antd-admin-docs
+- 常见问题:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html
+- 国内镜像:https://gitee.com/iczer/vue-antd-admin
+
+## 浏览器支持
+现代浏览器及 IE10
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
+| --- | --- | --- | --- | --- |
+| IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## 使用
+### clone
+```bash
+$ git clone https://github.com/iczer/vue-antd-admin.git
+```
+### yarn
+```bash
+$ yarn install
+$ yarn serve
+```
+### or npm
+```
+$ npm install
+$ npm run serve
+```
+更多信息参考 [使用文档](https://iczer.gitee.io/vue-antd-admin-docs)
+
+## 参与贡献
+我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2::
+- 在你的公司或个人项目中使用 Vue Antd Admin。
+- 通过 [Issue](https://github.com/iczer/vue-antd-admin/issues) 报告:bug:或进行咨询。
+- 提交 [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) 改进 Admin 的代码。
+- 加入社群,与小伙伴们一同交流心得。QQ群:942083829、 812277510(已满)、610090280(已满)
+
+## 打赏
+如果该项目对您有所帮助,可以请作者喝一杯咖啡。
+<p>
+  <img src="./src/assets/img/alipay.png" width="320px" style="display: inline-block;" />
+  <img src="./src/assets/img/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px;" />
+</p>

+ 13 - 0
babel.config.js

@@ -0,0 +1,13 @@
+const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
+
+const plugins = []
+if (IS_PROD) {
+  plugins.push('transform-remove-console')
+}
+
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ],
+  plugins
+}

+ 80 - 0
package.json

@@ -0,0 +1,80 @@
+{
+  "name": "vue-antd-admin",
+  "version": "0.7.4",
+  "homepage": "https://iczer.github.io/vue-antd-admin",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint",
+    "predeploy": "yarn build",
+    "deploy": "gh-pages -d dist -b pages -r https://gitee.com/iczer/vue-antd-admin.git",
+    "docs:dev": "vuepress dev docs",
+    "docs:build": "vuepress build docs",
+    "docs:deploy": "vuepress build docs && gh-pages -d docs/.vuepress/dist -b master -r https://gitee.com/iczer/vue-antd-admin-docs.git"
+  },
+  "dependencies": {
+    "@antv/data-set": "^0.11.4",
+    "animate.css": "^4.1.0",
+    "ant-design-vue": "1.7.2",
+    "axios": "^0.19.2",
+    "clipboard": "^2.0.6",
+    "core-js": "^3.6.5",
+    "crypto-js": "^4.2.0",
+    "date-fns": "^2.14.0",
+    "enquire.js": "^2.1.6",
+    "highlight.js": "^10.2.1",
+    "js-cookie": "^2.2.1",
+    "mockjs": "^1.1.0",
+    "nprogress": "^0.2.0",
+    "viser-vue": "^2.4.8",
+    "vue": "~2.6.11",
+    "vue-i18n": "^8.18.2",
+    "vue-router": "^3.3.4",
+    "vue-template-compiler": "^2.6.11",
+    "vuedraggable": "^2.23.2",
+    "vuex": "^3.4.0"
+  },
+  "devDependencies": {
+    "@ant-design/colors": "^4.0.1",
+    "@vue/cli-plugin-babel": "^4.4.0",
+    "@vue/cli-plugin-eslint": "^4.4.0",
+    "@vue/cli-service": "^4.4.0",
+    "@vuepress/plugin-back-to-top": "^1.5.2",
+    "babel-eslint": "^10.1.0",
+    "babel-plugin-transform-remove-console": "^6.9.4",
+    "babel-polyfill": "^6.26.0",
+    "compression-webpack-plugin": "^2.0.0",
+    "deepmerge": "^4.2.2",
+    "eslint": "^6.7.2",
+    "eslint-plugin-vue": "^6.2.2",
+    "fast-deep-equal": "^3.1.3",
+    "gh-pages": "^3.1.0",
+    "less-loader": "^6.1.1",
+    "normalize.css": "^8.0.1",
+    "style-resources-loader": "^1.3.2",
+    "vue-cli-plugin-style-resources-loader": "^0.1.4",
+    "vuepress": "^1.5.2",
+    "webpack-theme-color-replacer": "1.3.18",
+    "whatwg-fetch": "^3.0.0"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ]
+}

BIN
public/favicon.ico


+ 29 - 0
public/index.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en" class="beauty-scroll">
+  <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">
+    <link  rel="stylesheet" href="./reset.css">
+
+    <title><%= process.env.VUE_APP_NAME %></title>
+    <!-- require cdn assets css -->
+    <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
+      <link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
+    <% } %>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="popContainer" class="beauty-scroll" style="height: 100vh;">
+      <div id="app"></div>
+    </div>
+    <!-- require cdn assets js -->
+    <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
+      <script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
+    <% } %>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 73 - 0
public/reset.css

@@ -0,0 +1,73 @@
+/* 清除内外边距 */
+body, h1, h2, h3, h4, h5, h6, hr, p, blockquote,
+dl, dt, dd, ul, ol, li,
+pre,
+fieldset, lengend, button, input, textarea,
+th, td {
+    margin: 0;
+    padding: 0;
+}
+ 
+/* 设置默认字体 */
+body,
+button, input, select, textarea { /* for ie */
+    /*font: 12px/1 Tahoma, Helvetica, Arial, "宋体", sans-serif;*/
+    font: 12px/1.3 "Microsoft YaHei",Tahoma, Helvetica, Arial, "\5b8b\4f53", sans-serif; /* 用 ascii 字符表示,使得在任何编码下都无问题 */
+    color: #333;
+}
+ 
+h1 { font-size: 18px; /* 18px / 12px = 1.5 */ }
+h2 { font-size: 16px; }
+h3 { font-size: 14px; }
+h4, h5, h6 { font-size: 100%; }
+ 
+address, cite, dfn, em, var, i{ font-style: normal; } /* 将斜体扶正 */
+b, strong{ font-weight: normal; } /* 将粗体扶细 */
+code, kbd, pre, samp, tt { font-family: "Courier New", Courier, monospace; } /* 统一等宽字体 */
+small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */
+ 
+/* 重置列表元素 */
+ul, ol { list-style: none; }
+ 
+/* 重置文本格式元素 */
+a { text-decoration: none; color: #666;}
+ 
+/* 重置表单元素 */
+legend { color: #000; } /* for ie6 */
+fieldset, img { border: none; }
+button, input, select, textarea {
+    font-size: 100%; /* 使得表单元素在 ie 下能继承字体大小 */
+}
+ 
+/* 重置表格元素 */
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+ 
+/* 重置 hr */
+hr {
+    border: none;
+    height: 1px;
+}
+.clearFix::after{
+    content:"";
+    display: block;
+    clear:both;
+}
+/* 让非ie浏览器默认也显示垂直滚动条,防止因滚动条引起的闪烁 */
+html { overflow-y: scroll; }
+ 
+a:link:hover{
+    color : rgb(79, 76, 212) !important;
+    text-decoration: underline;
+}
+ 
+/* 清除浮动 */
+.clearfix::after {
+    display: block;
+    height: 0;
+    content: "";
+    clear: both;
+    visibility: hidden;
+}

+ 119 - 0
src/App.vue

@@ -0,0 +1,119 @@
+<template>
+  <a-config-provider :locale="locale" :get-popup-container="popContainer">
+    <router-view />
+  </a-config-provider>
+</template>
+
+<script>
+import { enquireScreen } from "./utils/util";
+import { mapState, mapMutations } from "vuex";
+import themeUtil from "@/utils/themeUtil";
+// import { getI18nKey } from "@/utils/routerUtil";
+// import store from "@/store";
+// import { loadRoutes } from "@/utils/routerUtil";
+export default {
+  name: "App",
+  data() {
+    return {
+      locale: {},
+    };
+  },
+  created() {
+    this.setHtmlTitle();
+    this.setLanguage(this.lang);
+    enquireScreen((isMobile) => this.setDevice(isMobile));
+    // this.$API.userService.getRoutesConfig().then((result) => {
+    //   console.log(result);
+    //   const routesConfig = result.data.data;
+    //   loadRoutes(routesConfig);
+    //   this.$router.push("/form/agreement");
+    //   this.$message.success(loginRes.message, 3);
+    // });
+    // this.$API.sysManage.getMenuList().then((res) => {
+    //   if (res.data.code === 200) {
+    //     console.log(res.data.data);
+    //     store.commit("setting/setMenuData", res.data.data);
+    //   }
+    // });
+  },
+  mounted() {
+    this.setWeekModeTheme(this.weekMode);
+  },
+  watch: {
+    weekMode(val) {
+      this.setWeekModeTheme(val);
+    },
+    lang(val) {
+      this.setLanguage(val);
+      this.setHtmlTitle();
+    },
+    $route() {
+      this.setHtmlTitle();
+    },
+    "theme.mode": function (val) {
+      let closeMessage = this.$message.loading(
+        `您选择了主题模式 ${val}, 正在切换...`
+      );
+      themeUtil.changeThemeColor(this.theme.color, val).then(closeMessage);
+    },
+    "theme.color": function (val) {
+      let closeMessage = this.$message.loading(
+        `您选择了主题色 ${val}, 正在切换...`
+      );
+      themeUtil.changeThemeColor(val, this.theme.mode).then(closeMessage);
+    },
+    layout: function () {
+      window.dispatchEvent(new Event("resize"));
+    },
+  },
+  computed: {
+    ...mapState("setting", ["layout", "theme", "weekMode", "lang"]),
+  },
+  methods: {
+    ...mapMutations("setting", ["setDevice"]),
+    setWeekModeTheme(weekMode) {
+      if (weekMode) {
+        document.body.classList.add("week-mode");
+      } else {
+        document.body.classList.remove("week-mode");
+      }
+    },
+    setLanguage(lang) {
+      this.$i18n.locale = lang;
+      switch (lang) {
+        case "CN":
+          this.locale =
+            require("ant-design-vue/es/locale-provider/zh_CN").default;
+          break;
+        case "HK":
+          this.locale =
+            require("ant-design-vue/es/locale-provider/zh_TW").default;
+          break;
+        case "US":
+        default:
+          this.locale =
+            require("ant-design-vue/es/locale-provider/en_US").default;
+          break;
+      }
+    },
+    setHtmlTitle() {
+      const route = this.$route;
+      // console.log(route, 123);
+
+      // const key =
+      //   route.path === "/"
+      //     ? "home.name"
+      //     : getI18nKey(route.matched[route.matched.length - 1]?.path);
+      document.title = route.name;
+    },
+    popContainer() {
+      return document.getElementById("popContainer");
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+#id {
+}
+</style>

BIN
src/assets/img/Frame@2x(3).png


BIN
src/assets/img/MoneyCollect@2x.png


BIN
src/assets/img/PropertySafety@2x.png


BIN
src/assets/img/alipay.png


BIN
src/assets/img/logo.png


BIN
src/assets/img/money.png


BIN
src/assets/img/new_logo.png


BIN
src/assets/img/preview-nine.png


BIN
src/assets/img/preview.png


BIN
src/assets/img/title.png


BIN
src/assets/img/user.png


BIN
src/assets/img/users.png


BIN
src/assets/img/wechatpay.png


BIN
src/assets/img/ws.png


BIN
src/assets/img/yis.png


BIN
src/assets/img/ys.png


+ 25 - 0
src/bootstrap.js

@@ -0,0 +1,25 @@
+import {loadRoutes, loadGuards, setAppOptions} from '@/utils/routerUtil'
+import {loadInterceptors} from '@/utils/request'
+import guards from '@/router/guards'
+import interceptors from '@/utils/axios-interceptors'
+
+/**
+ * 启动引导方法
+ * 应用启动时需要执行的操作放在这里
+ * @param router 应用的路由实例
+ * @param store 应用的 vuex.store 实例
+ * @param i18n 应用的 vue-i18n 实例
+ * @param i18n 应用的 message 实例
+ */
+function bootstrap({router, store, i18n, message}) {
+  // 设置应用配置
+  setAppOptions({router, store, i18n})
+  // 加载 axios 拦截器
+  loadInterceptors(interceptors, {router, store, i18n, message})
+  // 加载路由
+  loadRoutes()
+  // 加载路由守卫
+  loadGuards(guards, {router, store, i18n, message})
+}
+
+export default bootstrap

+ 172 - 0
src/components/cache/AKeepAlive.js

@@ -0,0 +1,172 @@
+import {isDef, isRegExp, remove} from '@/utils/util'
+
+const patternTypes = [String, RegExp, Array]
+
+function matches (pattern, name) {
+  if (Array.isArray(pattern)) {
+    if (pattern.indexOf(name) > -1) {
+      return true
+    } else {
+      for (let item of pattern) {
+        if (isRegExp(item) && item.test(name)) {
+          return true
+        }
+      }
+      return false
+    }
+  } else if (typeof pattern === 'string') {
+    return pattern.split(',').indexOf(name) > -1
+  } else if (isRegExp(pattern)) {
+    return pattern.test(name)
+  }
+  /* istanbul ignore next */
+  return false
+}
+
+function getComponentName (opts) {
+  return opts && (opts.Ctor.options.name || opts.tag)
+}
+
+function getComponentKey (vnode) {
+  const {componentOptions, key} = vnode
+  return key == null
+    ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
+    : key + componentOptions.Ctor.cid
+}
+
+function getFirstComponentChild (children) {
+  if (Array.isArray(children)) {
+    for (let i = 0; i < children.length; i++) {
+      const c = children[i]
+      if (isDef(c) && (isDef(c.componentOptions) || c.isAsyncPlaceholder)) {
+        return c
+      }
+    }
+  }
+}
+
+function pruneCache (keepAliveInstance, filter) {
+  const { cache, keys, _vnode } = keepAliveInstance
+  for (const key in cache) {
+    const cachedNode = cache[key]
+    if (cachedNode) {
+      const name = getComponentName(cachedNode.componentOptions)
+      const componentKey = getComponentKey(cachedNode)
+      if (name && !filter(name, componentKey)) {
+        pruneCacheEntry(cache, key, keys, _vnode)
+      }
+    }
+  }
+}
+
+function pruneCacheEntry2(cache, key, keys) {
+  const cached = cache[key]
+  if (cached) {
+    cached.componentInstance.$destroy()
+  }
+  cache[key] = null
+  remove(keys, key)
+}
+
+function pruneCacheEntry (cache, key, keys, current) {
+  const cached = cache[key]
+  if (cached && (!current || cached.tag !== current.tag)) {
+    cached.componentInstance.$destroy()
+  }
+  cache[key] = null
+  remove(keys, key)
+}
+
+export default {
+  name: 'AKeepAlive',
+  abstract: true,
+  model: {
+    prop: 'clearCaches',
+    event: 'clear',
+  },
+  props: {
+    include: patternTypes,
+    exclude: patternTypes,
+    excludeKeys: patternTypes,
+    max: [String, Number],
+    clearCaches: Array
+  },
+  watch: {
+    clearCaches: function(val) {
+      if (val && val.length > 0) {
+        const {cache, keys} = this
+        val.forEach(key => {
+          pruneCacheEntry2(cache, key, keys)
+        })
+        this.$emit('clear', [])
+      }
+    }
+  },
+
+  created() {
+    this.cache = Object.create(null)
+    this.keys = []
+  },
+
+  destroyed () {
+    for (const key in this.cache) {
+      pruneCacheEntry(this.cache, key, this.keys)
+    }
+  },
+
+  mounted () {
+    this.$watch('include', val => {
+      pruneCache(this, (name) => matches(val, name))
+    })
+    this.$watch('exclude', val => {
+      pruneCache(this, (name) => !matches(val, name))
+    })
+    this.$watch('excludeKeys', val => {
+      pruneCache(this, (name, key) => !matches(val, key))
+    })
+  },
+
+  render () {
+    const slot = this.$slots.default
+    const vnode = getFirstComponentChild(slot)
+    const componentOptions = vnode && vnode.componentOptions
+    if (componentOptions) {
+      // check pattern
+      const name = getComponentName(componentOptions)
+      const componentKey = getComponentKey(vnode)
+      const { include, exclude, excludeKeys } = this
+      if (
+        // not included
+        (include && (!name || !matches(include, name))) ||
+        // excluded
+        (exclude && name && matches(exclude, name)) ||
+        (excludeKeys && componentKey && matches(excludeKeys, componentKey))
+      ) {
+        return vnode
+      }
+
+      const { cache, keys } = this
+      const key = vnode.key == null
+        // same constructor may get registered as different local components
+        // so cid alone is not enough (#3269)
+        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
+        : vnode.key + componentOptions.Ctor.cid
+      if (cache[key]) {
+        vnode.componentInstance = cache[key].componentInstance
+        // make current key freshest
+        remove(keys, key)
+        keys.push(key)
+      } else {
+        cache[key] = vnode
+        keys.push(key)
+        // prune oldest entry
+        if (this.max && keys.length > parseInt(this.max)) {
+          pruneCacheEntry(cache, keys[0], keys, this._vnode)
+        }
+      }
+
+      vnode.data.keepAlive = true
+    }
+    return vnode || (slot && slot[0])
+  }
+}

+ 78 - 0
src/components/card/ChartCard.vue

@@ -0,0 +1,78 @@
+<template>
+  <a-card :loading="loading" :body-style="{padding: '20px 24px 8px'}" :bordered="false">
+    <div class="chart-card-header">
+      <div class="meta">
+        <span class="chart-card-title">{{title}}</span>
+        <span class="chart-card-action">
+        <slot name="action"></slot>
+      </span>
+      </div>
+      <div class="total"><span>{{total}}</span></div>
+    </div>
+    <div class="chart-card-content">
+      <div class="content-fix">
+        <slot></slot>
+      </div>
+    </div>
+    <div class="chart-card-footer">
+      <slot name="footer"></slot>
+    </div>
+  </a-card>
+</template>
+
+<script>
+export default {
+  name: 'ChartCard',
+  props: ['title', 'total', 'loading']
+}
+</script>
+
+<style scoped lang="less">
+  .chart-card-header{
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+  }
+  .chart-card-header .meta{
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+    color: @text-color-second;
+    font-size: 14px;
+    line-height: 22px;
+  }
+  .chart-card-action{
+    cursor: pointer;
+    position: absolute;
+    top: 0;
+    right: 0;
+  }
+  .total {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    white-space: nowrap;
+    margin-top: 4px;
+    margin-bottom: 0;
+    font-size: 30px;
+    line-height: 38px;
+    height: 38px;
+  }
+  .chart-card-footer{
+    border-top: 1px solid @border-color-base;
+    padding-top: 9px;
+    margin-top: 8px;
+  }
+  .chart-card-content{
+    margin-bottom: 12px;
+    position: relative;
+    height: 46px;
+    width: 100%;
+  }
+  .chart-card-content .content-fix{
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    width: 100%;
+  }
+</style>

+ 59 - 0
src/components/chart/Bar.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="bar">
+    <h4>{{title}}</h4>
+    <div class="chart">
+      <v-chart :force-fit="true" height="312" :data="data" :padding="[24, 0, 0, 0]">
+        <v-tooltip />
+        <v-axis />
+        <v-bar position="x*y"/>
+      </v-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+
+const data = []
+for (let i = 0; i < 12; i += 1) {
+  data.push({
+    x: `${i + 1}月`,
+    y: Math.floor(Math.random() * 1000) + 200
+  })
+}
+const tooltip = [
+  'x*y',
+  (x, y) => ({
+    name: x,
+    value: y
+  })
+]
+
+const scale = [{
+  dataKey: 'x',
+  min: 2
+}, {
+  dataKey: 'y',
+  title: '时间',
+  min: 1,
+  max: 22
+}]
+export default {
+  name: 'Bar',
+  props: ['title'],
+  data () {
+    return {
+      data,
+      scale,
+      tooltip
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+  .bar{
+    position: relative;
+    .chart{
+    }
+  }
+</style>

+ 67 - 0
src/components/chart/MiniArea.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="mini-chart">
+    <div class="chart-content" :style="{height: 46}">
+      <v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
+        <v-tooltip />
+        <v-smooth-area position="x*y" />
+      </v-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+import {format} from 'date-fns'
+
+const data = []
+const beginDay = new Date().getTime()
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]
+for (let i = 0; i < fakeY.length; i += 1) {
+  data.push({
+    x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
+    y: fakeY[i]
+  })
+}
+
+const tooltip = [
+  'x*y',
+  (x, y) => ({
+    name: x,
+    value: y
+  })
+]
+
+const scale = [{
+  dataKey: 'x',
+  min: 2
+}, {
+  dataKey: 'y',
+  title: '时间',
+  min: 1,
+  max: 22
+}]
+
+export default {
+  name: 'MiniArea',
+  data () {
+    return {
+      data,
+      scale,
+      tooltip,
+      height: 100
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .mini-chart {
+    position: relative;
+    width: 100%
+  }
+  .mini-chart .chart-content{
+    position: absolute;
+    bottom: -28px;
+    width: 100%;
+  }
+</style>

+ 59 - 0
src/components/chart/MiniBar.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="mini-chart">
+    <div class="chart-content" :style="{height: 46}">
+      <v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
+        <v-tooltip />
+        <v-bar position="x*y" />
+      </v-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+import {format} from 'date-fns'
+
+const data = []
+const beginDay = new Date().getTime()
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]
+for (let i = 0; i < fakeY.length; i += 1) {
+  data.push({
+    x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
+    y: fakeY[i]
+  })
+}
+
+const tooltip = [
+  'x*y',
+  (x, y) => ({
+    name: x,
+    value: y
+  })
+]
+
+const scale = [{
+  dataKey: 'x',
+  min: 2
+}, {
+  dataKey: 'y',
+  title: '时间',
+  min: 1,
+  max: 22
+}]
+
+export default {
+  name: 'MiniBar',
+  data () {
+    return {
+      data,
+      scale,
+      tooltip,
+      height: 100
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+@import "index.less";
+</style>

+ 56 - 0
src/components/chart/MiniProgress.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="mini-progress">
+    <a-tooltip :title="'目标值:' + target + '%'">
+      <div class="target" :style="{left: target + '%'}">
+        <span :style="{backgroundColor: color}" />
+        <span :style="{backgroundColor: color}" />
+      </div>
+    </a-tooltip>
+    <div class="wrap">
+      <div class="progress" :style="{backgroundColor: color, width: percent + '%', height: height}" />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MiniProgress',
+  props: ['target', 'color', 'percent', 'height']
+}
+</script>
+
+<style lang="less"  scoped>
+  .mini-progress {
+    padding: 5px 0;
+    position: relative;
+    width: 100%;
+    .wrap {
+      background-color: @layout-bg-color;
+      position: relative;
+    }
+    .progress {
+      transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
+      border-radius: 1px 0 0 1px;
+      background-color: #13C2C2;
+      width: 0;
+      height: 100%;
+    }
+    .target {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      span {
+        border-radius: 100px;
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 4px;
+        width: 2px;
+      }
+      span:last-child {
+        top: auto;
+        bottom: 0;
+      }
+    }
+  }
+</style>

+ 80 - 0
src/components/chart/Radar.vue

@@ -0,0 +1,80 @@
+<template>
+    <v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
+      <v-tooltip  />
+      <v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
+      <v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
+      <v-legend dataKey="user" marker="circle" :offset="30" />
+      <v-coord type="polar" radius="0.8" />
+      <v-line position="item*score" color="user" :size="2" />
+      <v-point position="item*score" color="user" :size="4" shape="circle" />
+    </v-chart>
+</template>
+
+<script>
+const DataSet = require('@antv/data-set')
+
+const sourceData = [
+  {item: '引用', a: 70, b: 30, c: 40},
+  {item: '口碑', a: 60, b: 70, c: 40},
+  {item: '产量', a: 50, b: 60, c: 40},
+  {item: '贡献', a: 40, b: 50, c: 40},
+  {item: '热度', a: 60, b: 70, c: 40},
+  {item: '引用', a: 70, b: 50, c: 40}
+]
+
+const dv = new DataSet.View().source(sourceData)
+dv.transform({
+  type: 'fold',
+  fields: ['a', 'b', 'c'],
+  key: 'user',
+  value: 'score'
+})
+
+const scale = [{
+  dataKey: 'score',
+  min: 0,
+  max: 80
+}]
+
+const data = dv.rows
+
+const axis1Opts = {
+  dataKey: 'item',
+  line: null,
+  tickLine: null,
+  grid: {
+    lineStyle: {
+      lineDash: null
+    },
+    hideFirstLine: false
+  }
+}
+const axis2Opts = {
+  dataKey: 'score',
+  line: null,
+  tickLine: null,
+  grid: {
+    type: 'polygon',
+    lineStyle: {
+      lineDash: null
+    }
+  }
+}
+
+export default {
+  name: 'Radar',
+  data () {
+    return {
+      sourceData,
+      data,
+      axis1Opts,
+      axis2Opts,
+      scale
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 59 - 0
src/components/chart/RankingList.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="rank">
+    <h4 class="title">{{title}}</h4>
+    <ul class="list">
+      <li :key="index" v-for="(item, index) in list">
+        <span :class="index < 3 ? 'active' : null">{{index + 1}}</span>
+        <span >{{item.name}}</span>
+        <span >{{item.total}}</span>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'RankingList',
+  props: ['title', 'list']
+}
+</script>
+
+<style lang="less" scoped>
+  .rank{
+    padding: 0 32px 32px 72px;
+    .title{
+    }
+    .list{
+      margin: 25px 0 0;
+      padding: 0;
+      list-style: none;
+      li {
+        margin-top: 16px;
+        span {
+          color: @text-color-second;
+          font-size: 14px;
+          line-height: 22px;
+        }
+        span:first-child {
+          background-color: @layout-bg-color;
+          border-radius: 20px;
+          display: inline-block;
+          font-size: 12px;
+          font-weight: 600;
+          margin-right: 24px;
+          height: 20px;
+          line-height: 20px;
+          width: 20px;
+          text-align: center;
+        }
+        span.active {
+          background-color: #314659 !important;
+          color: @text-color-inverse !important;
+        }
+        span:last-child {
+          float: right;
+        }
+      }
+    }
+  }
+</style>

+ 79 - 0
src/components/chart/Trend.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="chart-trend">
+    {{term}}
+    <span>{{rate}}%</span>
+    <span :class="['chart-trend-icon', trend]" style=""><a-icon :type="'caret-' + trend" /></span>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Trend',
+  props: {
+    term: {
+      type: String,
+      required: true
+    },
+    target: {
+      type: Number,
+      required: false,
+      default: 0
+    },
+    value: {
+      type: Number,
+      required: false,
+      default: 0
+    },
+    isIncrease: {
+      type: Boolean,
+      required: false,
+      default: null
+    },
+    percent: {
+      type: Number,
+      required: false,
+      default: null
+    },
+    scale: {
+      type: Number,
+      required: false,
+      default: 2
+    }
+  },
+  data () {
+    return {
+      trend: this.isIncrease ? 'up' : 'down',
+      rate: this.percent
+    }
+  },
+  created () {
+    this.trend = this.caulateTrend()
+    this.rate = this.caulateRate()
+  },
+  methods: {
+    caulateRate () {
+      return (this.percent === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percent).toFixed(this.scale)
+    },
+    caulateTrend () {
+      let isIncrease = this.isIncrease === null ? this.value >= this.target : this.isIncrease
+      return isIncrease ? 'up' : 'down'
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .chart-trend{
+    display: inline-block;
+    font-size: 14px;
+    .chart-trend-icon{
+      font-size: 12px;
+      &.up{
+        color: @red-6;
+      }
+      &.down{
+        color: @green-6;
+      }
+    }
+  }
+</style>

+ 9 - 0
src/components/chart/index.less

@@ -0,0 +1,9 @@
+.mini-chart{
+  position: relative;
+  width: 100%;
+  .chart-content{
+    position: absolute;
+    bottom: -28px;
+    width: 100%;
+  }
+}

+ 157 - 0
src/components/checkbox/ColorCheckbox.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="theme-color" :style="{backgroundColor: color}" @click="toggle">
+    <a-icon v-if="sChecked" type="check" />
+  </div>
+</template>
+
+<script>
+const Group = {
+  name: 'ColorCheckboxGroup',
+  props: {
+    defaultValues: {
+      type: Array,
+      required: false,
+      default: () => []
+    },
+    multiple: {
+      type: Boolean,
+      required: false,
+      default: false
+    }
+  },
+  data () {
+    return {
+      values: [],
+      options: []
+    }
+  },
+  computed: {
+    colors () {
+      let colors = []
+      this.options.forEach(item => {
+        if (item.sChecked) {
+          colors.push(item.color)
+        }
+      })
+      return colors
+    }
+  },
+  provide () {
+    return {
+      groupContext: this
+    }
+  },
+  watch: {
+    values(value) {
+      this.$emit('change', value, this.colors)
+    }
+  },
+  methods: {
+    handleChange (option) {
+      if (!option.checked) {
+        if (this.values.indexOf(option.value) > -1) {
+          this.values = this.values.filter(item => item != option.value)
+        }
+      } else {
+        if (!this.multiple) {
+          this.values = [option.value]
+          this.options.forEach(item => {
+            if (item.value != option.value) {
+              item.sChecked = false
+            }
+          })
+        } else {
+          this.values.push(option.value)
+        }
+      }
+    }
+  },
+  render (h) {
+    const clear = h('div', {attrs: {style: 'clear: both'}})
+    return h(
+      'div',
+      {},
+      [this.$slots.default, clear]
+    )
+  }
+}
+
+export default {
+  name: 'ColorCheckbox',
+  Group: Group,
+  props: {
+    color: {
+      type: String,
+      required: true
+    },
+    value: {
+      type: [String, Number],
+      required: true
+    },
+    checked: {
+      type: Boolean,
+      required: false,
+      default: false
+    }
+  },
+  data () {
+    return {
+      sChecked: this.initChecked()
+    }
+  },
+  computed: {
+  },
+  inject: ['groupContext'],
+  watch: {
+    'sChecked': function () {
+      const value = {
+        value: this.value,
+        color: this.color,
+        checked: this.sChecked
+      }
+      this.$emit('change', value)
+      const groupContext = this.groupContext
+      if (groupContext) {
+        groupContext.handleChange(value)
+      }
+    }
+  },
+  created () {
+    const groupContext = this.groupContext
+    if (groupContext) {
+      groupContext.options.push(this)
+    }
+  },
+  methods: {
+    toggle () {
+      if (this.groupContext.multiple || !this.sChecked) {
+        this.sChecked = !this.sChecked
+      }
+    },
+    initChecked() {
+      let groupContext = this.groupContext
+      if (!groupContext) {
+        return this.checked
+      }else if (groupContext.multiple) {
+        return groupContext.defaultValues.indexOf(this.value) > -1
+      } else {
+        return groupContext.defaultValues[0] == this.value
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .theme-color{
+    float: left;
+    width: 20px;
+    height: 20px;
+    border-radius: 2px;
+    cursor: pointer;
+    margin-right: 8px;
+    text-align: center;
+    color: @base-bg-color;
+    font-weight: bold;
+  }
+</style>

+ 161 - 0
src/components/checkbox/ImgCheckbox.vue

@@ -0,0 +1,161 @@
+<template>
+  <a-tooltip :title="title" :overlayStyle="{zIndex: 2001}">
+    <div class="img-check-box" @click="toggle">
+      <img :src="img" />
+      <div v-if="sChecked" class="check-item">
+        <a-icon type="check" />
+      </div>
+    </div>
+  </a-tooltip>
+</template>
+
+<script>
+const Group = {
+  name: 'ImgCheckboxGroup',
+  props: {
+    multiple: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    defaultValues: {
+      type: Array,
+      required: false,
+      default: () => []
+    }
+  },
+  data () {
+    return {
+      values: [],
+      options: []
+    }
+  },
+  provide () {
+    return {
+      groupContext: this
+    }
+  },
+  watch: {
+    'values': function (value) {
+      this.$emit('change', value)
+      // // 此条件是为解决单选时,触发两次chang事件问题
+      // if (!(newVal.length === 1 && oldVal.length === 1 && newVal[0] === oldVal[0])) {
+      //   this.$emit('change', this.values)
+      // }
+    }
+  },
+  methods: {
+    handleChange (option) {
+      if (!option.checked) {
+        if (this.values.indexOf(option.value) > -1) {
+          this.values = this.values.filter(item => item != option.value)
+        }
+      } else {
+        if (!this.multiple) {
+          this.values = [option.value]
+          this.options.forEach(item => {
+            if (item.value != option.value) {
+              item.sChecked = false
+            }
+          })
+        } else {
+          this.values.push(option.value)
+        }
+      }
+    }
+  },
+  render (h) {
+    return h(
+      'div',
+      {
+        attrs: {style: 'display: flex'}
+      },
+      [this.$slots.default]
+    )
+  }
+}
+
+export default {
+  name: 'ImgCheckbox',
+  Group,
+  props: {
+    checked: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    img: {
+      type: String,
+      required: true
+    },
+    value: {
+      required: true
+    },
+    title: String
+  },
+  data () {
+    return {
+      sChecked: this.initChecked()
+    }
+  },
+  inject: ['groupContext'],
+  watch: {
+    'sChecked': function () {
+      const option = {
+        value: this.value,
+        checked: this.sChecked
+      }
+      this.$emit('change', option)
+      const groupContext = this.groupContext
+      if (groupContext) {
+        groupContext.handleChange(option)
+      }
+    }
+  },
+  created () {
+    const groupContext = this.groupContext
+    if (groupContext) {
+      this.sChecked = groupContext.defaultValues.length > 0 ? groupContext.defaultValues.indexOf(this.value) >= 0 : this.sChecked
+      groupContext.options.push(this)
+    }
+  },
+  methods: {
+    toggle () {
+      if (this.groupContext.multiple || !this.sChecked) {
+        this.sChecked = !this.sChecked
+      }
+    },
+    initChecked() {
+      let groupContext = this.groupContext
+      if (!groupContext) {
+        return this.checked
+      }else if (groupContext.multiple) {
+        return groupContext.defaultValues.indexOf(this.value) > -1
+      } else {
+        return groupContext.defaultValues[0] == this.value
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .img-check-box{
+    margin-right: 16px;
+    position: relative;
+    border-radius: 4px;
+    cursor: pointer;
+    .check-item{
+      position: absolute;
+      top: 0;
+      right: 0;
+      width: 100%;
+      padding-top: 15px;
+      padding-left: 24px;
+      height: 100%;
+      color: @primary-color;
+      font-size: 14px;
+      font-weight: bold;
+    }
+  }
+</style>

+ 7 - 0
src/components/checkbox/index.js

@@ -0,0 +1,7 @@
+import ColorCheckbox from '@/components/checkbox/ColorCheckbox'
+import ImgCheckbox from '@/components/checkbox/ImgCheckbox'
+
+export {
+  ColorCheckbox,
+  ImgCheckbox
+}

+ 69 - 0
src/components/exception/ExceptionPage.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="exception-page">
+    <div class="img">
+      <img :src="config[type].img" />
+    </div>
+    <div class="content">
+      <h1>{{config[type].title}}</h1>
+      <div class="desc">{{config[type].desc}}</div>
+      <div class="action">
+        <a-button type="primary" @click="backHome">返回首页</a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Config from './typeConfig'
+
+export default {
+  name: 'ExceptionPage',
+  props: ['type', 'homeRoute'],
+  data () {
+    return {
+      config: Config
+    }
+  },
+  methods: {
+    backHome() {
+      if (this.homeRoute) {
+        this.$router.push(this.homeRoute)
+      }
+      this.$emit('backHome', this.type)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .exception-page{
+    border-radius: 4px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: @base-bg-color;
+    .img{
+      padding-right: 52px;
+      zoom: 1;
+      img{
+        max-width: 430px;
+      }
+    }
+    .content{
+      h1{
+        color: #434e59;
+        font-size: 72px;
+        font-weight: 600;
+        line-height: 72px;
+        margin-bottom: 24px;
+      }
+      .desc{
+        color: @text-color-second;
+        font-size: 20px;
+        line-height: 28px;
+        margin-bottom: 16px;
+      }
+    }
+  }
+
+</style>

+ 19 - 0
src/components/exception/typeConfig.js

@@ -0,0 +1,19 @@
+const config = {
+  403: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
+    title: '403',
+    desc: '抱歉,你无权访问该页面'
+  },
+  404: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
+    title: '404',
+    desc: '抱歉,你访问的页面不存在或仍在开发中'
+  },
+  500: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
+    title: '500',
+    desc: '抱歉,服务器出错了'
+  }
+}
+
+export default config

+ 207 - 0
src/components/form/FormRow.vue

@@ -0,0 +1,207 @@
+<template>
+  <a-form layout="inline" :class="className" :model="formState">
+    <a-form-item label="总机构" :labelCol="{ span: 5 }" :colon="false">
+      <a-select
+        style="width: 100%"
+        v-model="formState.whole"
+        placeholder="请选择总机构"
+        @change="wholeHandleChange"
+        allowClear
+      >
+        <a-select-option
+          v-for="item in wholeList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        >
+          {{ item.name }}
+        </a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item label="省机构" :labelCol="{ span: 5 }" :colon="false">
+      <a-select
+        style="width: 100%"
+        v-model="formState.provinceId"
+        placeholder="请选择省机构"
+        @change="provinceHandleChange"
+        allowClear
+      >
+        <a-select-option
+          v-for="item in provinceList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        >
+          {{ item.name }}
+        </a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item label="市机构" :labelCol="{ span: 5 }" :colon="false">
+      <a-select
+        style="width: 100%"
+        v-model="formState.cityId"
+        placeholder="请选择市机构"
+        @change="cityHandleChange"
+        allowClear
+      >
+        <a-select-option
+          v-for="item in cityList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        >
+          {{ item.name }}
+        </a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item
+      label="区机构"
+      :labelCol="{ span: 5 }"
+      :colon="false"
+      v-show="showTab > 3"
+    >
+      <a-select
+        style="width: 100%"
+        v-model="formState.countyId"
+        placeholder="请选择区机构"
+        @change="countyHandleChange"
+        allowClear
+      >
+        <a-select-option
+          v-for="item in areaList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        >
+          {{ item.name }}
+        </a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item
+      label="门店机构"
+      :labelCol="{ span: 5 }"
+      :colon="false"
+      v-show="showTab > 3"
+    >
+      <a-select
+        style="width: 100%"
+        v-model="formState.houseId"
+        placeholder="请选择门店机构"
+        allowClear
+      >
+        <a-select-option
+          v-for="item in storeList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        >
+          {{ item.name }}
+        </a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item
+      label="姓名"
+      :labelCol="{ span: 5 }"
+      :colon="false"
+      v-show="className"
+    >
+      <a-input placeholder="请输入姓名" v-model="formState.name"></a-input>
+    </a-form-item>
+    <a-form-item
+      label="工号"
+      :labelCol="{ span: 5 }"
+      :colon="false"
+      v-show="className"
+    >
+      <a-input placeholder="请输入工号" v-model="formState.userCode"></a-input>
+    </a-form-item>
+    <a-form-item
+      label="手机号"
+      :labelCol="{ span: 5 }"
+      :colon="false"
+      v-show="className"
+    >
+      <a-input
+        placeholder="请输入手机号"
+        v-model="formState.phoneNumber"
+      ></a-input>
+    </a-form-item>
+    <a-form-item
+      label="             "
+      :labelCol="{ span: 5 }"
+      :colon="false"
+      class="form-item-search"
+      v-show="search"
+    >
+      <a-button type="primary" @click="getList">查询</a-button>
+    </a-form-item>
+  </a-form>
+</template>
+
+<script>
+import mixin from "../../minins/mixin";
+export default {
+  name: "FormRow",
+  mixins: [mixin],
+  props: {
+    formState: {},
+    search: {
+      default: true,
+    },
+    className: {
+      default: "",
+    },
+    showTab: {
+      default: 5,
+    },
+  },
+  methods: {
+    wholeHandleChange() {
+      this.getProvinceList(this.formState.whole);
+    },
+    provinceHandleChange() {
+      this.getCityList(this.formState.provinceId);
+    },
+    cityHandleChange() {
+      this.getAreaList(this.formState.cityId);
+    },
+    countyHandleChange() {
+      this.getStoreList(this.formState.countyId);
+    },
+    getList() {
+      this.$emit("getList", this.formState);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.ant-form {
+  display: flex;
+  > .ant-form-item {
+    flex: 1;
+    /deep/.ant-form-item-control-wrapper {
+      width: 100%;
+    }
+  }
+}
+
+.form-item-search {
+  // display: flex;
+  // justify-content: flex-end;
+  /deep/ .ant-form-item-control {
+    display: flex;
+    justify-content: flex-end;
+  }
+}
+.item-4 {
+  display: flex;
+  > .ant-form-item {
+    flex: none;
+    width: 23%;
+    /deep/.ant-form-item-control-wrapper {
+      width: 100%;
+    }
+  }
+}
+</style>

+ 66 - 0
src/components/input/IInput.vue

@@ -0,0 +1,66 @@
+<template>
+  <a-input
+    :addon-after="addonAfter"
+    :addon-before="addonBefore"
+    :default-value="defaultValue"
+    :disabled="disabled"
+    :id="id"
+    :max-length="maxLength"
+    :prefix="prefix"
+    :size="size"
+    :suffix="suffix || lenSuffix"
+    :type="type"
+    :allow-clear="allowClear"
+    v-model="sValue"
+    :value="value"
+    @change="onChange"
+    @input="onInput"
+    @pressEnter="onPressEnter"
+    @keydown="onKeydown"
+  >
+    <template :slot="slot" v-for="slot in Object.keys($slots)">
+      <slot :name="slot"></slot>
+    </template>
+  </a-input>
+</template>
+
+<script>
+  export default {
+    name: 'IInput',
+    model: {
+      prop: 'value',
+      event: 'change.value'
+    },
+    props: ['addonAfter', 'addonBefore', 'defaultValue', 'disabled', 'id', 'maxLength', 'prefix', 'size', 'suffix', 'type', 'value', 'allowClear'],
+    data() {
+      return {
+        sValue: this.value || this.defaultValue || ''
+      }
+    },
+    watch: {
+      value(val) {
+        this.sValue = val
+      }
+    },
+    computed: {
+      lenSuffix() {
+        return this.maxLength && `${(this.sValue + '').length}/${this.maxLength}`
+      }
+    },
+    methods: {
+      onChange(e) {
+        this.$emit('change', e)
+        this.$emit('change.value', e.target.value)
+      },
+      onInput(e) {
+        this.$emit('input', e)
+      },
+      onPressEnter(e) {
+        this.$emit('pressEnter', e)
+      },
+      onKeydown(e) {
+        this.$emit('keydown', e)
+      }
+    }
+  }
+</script>

+ 0 - 0
src/components/menu/@/assets/img/logo.png


+ 85 - 0
src/components/menu/Contextmenu.vue

@@ -0,0 +1,85 @@
+<template>
+  <a-menu
+    v-show="visible"
+    class="contextmenu"
+    :style="style"
+    :selectedKeys="selectedKeys"
+    @click="handleClick"
+  >
+    <a-menu-item :key="item.key" v-for="item in itemList">
+      {{ 123456 }}
+      <a-icon v-if="item.icon" :type="item.icon" />
+      <span>{{ item.text }}</span>
+    </a-menu-item>
+  </a-menu>
+</template>
+
+<script>
+export default {
+  name: 'Contextmenu',
+  props: {
+    visible: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    itemList: {
+      type: Array,
+      required: true,
+      default: () => []
+    }
+  },
+  data () {
+    return {
+      left: 0,
+      top: 0,
+      target: null,
+      meta: null,
+      selectedKeys: []
+    }
+  },
+  computed: {
+    style () {
+      return {
+        left: this.left + 'px',
+        top: this.top + 'px'
+      }
+    }
+  },
+  created () {
+    window.addEventListener('click', this.closeMenu)
+    window.addEventListener('contextmenu', this.setPosition)
+  },
+  beforeDestroy() {
+    window.removeEventListener('click', this.closeMenu)
+    window.removeEventListener('contextmenu', this.setPosition)
+  },
+  methods: {
+    closeMenu () {
+      this.$emit('update:visible', false)
+    },
+    setPosition (e) {
+      this.left = e.clientX
+      this.top = e.clientY
+      this.target = e.target
+      this.meta = e.meta
+    },
+    handleClick ({ key }) {
+      this.$emit('select', key, this.target, this.meta)
+      this.closeMenu()
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .contextmenu{
+    position: fixed;
+    z-index: 1000;
+    border-radius: 4px;
+    box-shadow: -4px 4px 16px 1px @shadow-color !important;
+  }
+  .ant-menu-item {
+    margin: 0 !important // 菜单项之间的缝隙会影响点击
+  }
+</style>

+ 109 - 0
src/components/menu/SideMenu.vue

@@ -0,0 +1,109 @@
+<template>
+  <a-layout-sider
+    :theme="sideTheme"
+    :class="['side-menu', 'beauty-scroll', isMobile ? null : 'shadow']"
+    width="256px"
+    :collapsible="collapsible"
+    v-model="collapsed"
+    :trigger="null"
+  >
+    <div :class="['logo', theme]">
+      <router-link to="/agreement/agreement">
+        <img src="@/assets/img/title.png" />
+        <h1>晋掌柜控制台</h1>
+      </router-link>
+    </div>
+    <a-menu
+      style="width: 256px"
+      :default-selected-keys="['1']"
+      mode="inline"
+      @click="handleClick"
+    >
+      <a-sub-menu v-for="item in menuData" :key="item.id">
+        <span slot="title"
+          ><a-icon type="appstore" /><span>{{ item.name }}</span></span
+        >
+
+        <a-menu-item
+          v-for="val in item.children"
+          :key="val.id"
+          v-show="val.type == 1"
+        >
+          <div @click="routerChange(val, item)">{{ val.name }}</div>
+        </a-menu-item>
+        <a-sub-menu
+          v-for="(val, index) in item.children"
+          :key="index"
+          v-show="val.type == 0"
+        >
+          <span slot="title"
+            ><a-icon type="appstore" /><span>{{ val.name }}</span></span
+          >
+          <a-menu-item v-for="three in val.children" :key="three.id">
+            <div @click="routerChange(three, val)">{{ three.name }}</div>
+          </a-menu-item>
+        </a-sub-menu>
+      </a-sub-menu>
+    </a-menu>
+  </a-layout-sider>
+</template>
+
+<script>
+// import IMenu from "./menu";
+import { mapState, mapGetters } from "vuex";
+export default {
+  name: "SideMenu",
+  // components: { IMenu },
+  props: {
+    collapsible: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    collapsed: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    menuData: {
+      type: Array,
+      required: true,
+    },
+    theme: {
+      type: String,
+      required: false,
+      default: "dark",
+    },
+  },
+  computed: {
+    sideTheme() {
+      return this.theme == "light" ? this.theme : "dark";
+    },
+    ...mapState("setting", ["isMobile", "systemName"]),
+    ...mapGetters("setting", ["firstMenu", "subMenu", "menuData"]),
+  },
+  created() {
+    console.log(this.menuData);
+  },
+  methods: {
+    handleClick(e) {
+      console.log("click", e);
+    },
+
+    onSelect(obj) {
+      this.$emit("menuSelect", obj);
+    },
+    routerChange(val, parent) {
+      console.log(val, parent);
+      // console.log(this.$router);
+      this.$router.replace({
+        path: "/" + parent.url + "/" + val.url,
+      });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+@import "index";
+</style>

+ 38 - 0
src/components/menu/index.less

@@ -0,0 +1,38 @@
+.shadow{
+  box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
+}
+.side-menu{
+  min-height: 100vh;
+  overflow-y: auto;
+  z-index: 10;
+  .logo{
+    height: 64px;
+    position: relative;
+    line-height: 64px;
+    padding-left: 24px;
+    -webkit-transition: all .3s;
+    transition: all .3s;
+    overflow: hidden;
+    // background-color: @layout-trigger-background;
+    &.light{
+      background-color: #fff;
+      h1{
+        // color: @primary-color;
+      }
+    }
+    h1{
+      // color: @menu-dark-highlight-color;
+      font-size: 20px;
+      margin: 0 0 0 12px;
+      display: inline-block;
+      vertical-align: middle;
+    }
+    img{
+      width: 32px;
+      vertical-align: middle;
+    }
+  }
+}
+.menu{
+  padding: 16px 0;
+}

+ 276 - 0
src/components/menu/menu.js

@@ -0,0 +1,276 @@
+/**
+ * 该插件可根据菜单配置自动生成 ANTD menu组件
+ * menuOptions示例:
+ * [
+ *  {
+ *    name: '菜单名称',
+ *    path: '菜单路由',
+ *    meta: {
+ *      icon: '菜单图标',
+ *      invisible: 'boolean, 是否不可见, 默认 false',
+ *    },
+ *    children: [子菜单配置]
+ *  },
+ *  {
+ *    name: '菜单名称',
+ *    path: '菜单路由',
+ *    meta: {
+ *      icon: '菜单图标',
+ *      invisible: 'boolean, 是否不可见, 默认 false',
+ *    },
+ *    children: [子菜单配置]
+ *  }
+ * ]
+ *
+ * i18n: 国际化配置。系统默认会根据 options route配置的 path 和 name 生成英文以及中文的国际化配置,如需自定义或增加其他语言,配置
+ * 此项即可。如:
+ * i18n: {
+ *   messages: {
+ *     CN: {dashboard: {name: '监控中心'}}
+ *     HK: {dashboard: {name: '監控中心'}}
+ *   }
+ * }
+ **/
+import Menu from 'ant-design-vue/es/menu'
+import Icon from 'ant-design-vue/es/icon'
+import fastEqual from 'fast-deep-equal'
+import { getI18nKey } from '@/utils/routerUtil'
+
+const { Item, SubMenu } = Menu
+
+const resolvePath = (path, params = {}) => {
+  let _path = path
+  Object.entries(params).forEach(([key, value]) => {
+    _path = _path.replace(new RegExp(`:${key}`, 'g'), value)
+  })
+  return _path
+}
+
+const toRoutesMap = (routes) => {
+
+  const map = {}
+  routes.forEach(route => {
+    map[route.fullPath] = route
+    if (route.children && route.children.length > 0) {
+      const childrenMap = toRoutesMap(route.children)
+      Object.assign(map, childrenMap)
+    }
+  })
+  return map
+}
+
+export default {
+  name: 'IMenu',
+  props: {
+    options: {
+      type: Array,
+      required: true
+    },
+    theme: {
+      type: String,
+      required: false,
+      default: 'dark'
+    },
+    mode: {
+      type: String,
+      required: false,
+      default: 'inline'
+    },
+    collapsed: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    i18n: Object,
+    openKeys: Array
+  },
+  data() {
+    return {
+      selectedKeys: [],
+      sOpenKeys: [],
+      cachedOpenKeys: []
+    }
+  },
+  computed: {
+    menuTheme() {
+      return this.theme == 'light' ? this.theme : 'dark'
+    },
+    routesMap() {
+
+      return toRoutesMap(this.options)
+    }
+  },
+  created() {
+    this.updateMenu()
+    if (this.options.length > 0 && !this.options[0].fullPath) {
+      this.formatOptions(this.options, '')
+    }
+    // 自定义国际化配置
+    if (this.i18n && this.i18n.messages) {
+      const messages = this.i18n.messages
+      Object.keys(messages).forEach(key => {
+        this.$i18n.mergeLocaleMessage(key, messages[key])
+      })
+    }
+  },
+  watch: {
+    options(val) {
+      if (val.length > 0 && !val[0].fullPath) {
+        this.formatOptions(this.options, '')
+      }
+    },
+    i18n(val) {
+      if (val && val.messages) {
+        const messages = this.i18n.messages
+        Object.keys(messages).forEach(key => {
+          this.$i18n.mergeLocaleMessage(key, messages[key])
+        })
+      }
+    },
+    collapsed(val) {
+      if (val) {
+        this.cachedOpenKeys = this.sOpenKeys
+        this.sOpenKeys = []
+      } else {
+        this.sOpenKeys = this.cachedOpenKeys
+      }
+    },
+    '$route': function () {
+      this.updateMenu()
+    },
+    sOpenKeys(val) {
+      this.$emit('openChange', val)
+      this.$emit('update:openKeys', val)
+    }
+  },
+  methods: {
+    renderIcon: function (h, icon, key) {
+      if (this.$scopedSlots.icon && icon && icon !== 'none') {
+        const vnodes = this.$scopedSlots.icon({ icon, key })
+        vnodes.forEach(vnode => {
+          vnode.data.class = vnode.data.class ? vnode.data.class : []
+          vnode.data.class.push('anticon')
+        })
+        return vnodes
+      }
+      return !icon || icon == 'none' ? null : h(Icon, { props: { type: icon } })
+    },
+    renderMenuItem: function (h, menu) {
+      let tag = 'router-link'
+      const path = resolvePath(menu.fullPath, menu.meta.params)
+      let config = { props: { to: { path, query: menu.meta.query }, }, attrs: { style: 'overflow:hidden;white-space:normal;text-overflow:clip;' } }
+      if (menu.meta && menu.meta.link) {
+        tag = 'a'
+        config = { attrs: { style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank' } }
+      }
+      return h(
+        Item, { key: menu.fullPath },
+        [
+          h(tag, config,
+            [
+              this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
+              this.$t(getI18nKey(menu.fullPath))
+            ]
+          )
+        ]
+      )
+    },
+    renderSubMenu: function (h, menu) {
+      let this_ = this
+      let subItem = [h('span', { slot: 'title', attrs: { style: 'overflow:hidden;white-space:normal;text-overflow:clip;' } },
+        [
+          this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
+          this.$t(getI18nKey(menu.fullPath))
+        ]
+      )]
+      let itemArr = []
+      menu.children.forEach(function (item) {
+        itemArr.push(this_.renderItem(h, item))
+      })
+      return h(SubMenu, { key: menu.fullPath },
+        subItem.concat(itemArr)
+      )
+    },
+    renderItem: function (h, menu) {
+      const meta = menu.meta
+      // console.log(meta.invisible)
+      if (!meta || !meta.invisible) {
+        let renderChildren = false
+        const children = menu.children
+        if (children != undefined) {
+          for (let i = 0; i < children.length; i++) {
+            const childMeta = children[i].meta
+            if (!childMeta || !childMeta.invisible) {
+              renderChildren = true
+              break
+            }
+          }
+        }
+        return (menu.children && renderChildren) ? this.renderSubMenu(h, menu) : this.renderMenuItem(h, menu)
+      }
+    },
+    renderMenu: function (h, menuTree) {
+      let this_ = this
+      let menuArr = []
+      menuTree.forEach(function (menu, i) {
+        // console.log(menu)
+        menuArr.push(this_.renderItem(h, menu, '0', i))
+      })
+      return menuArr
+    },
+    formatOptions(options, parentPath) {
+      options.forEach(route => {
+
+        let isFullPath = route.path.substring(0, 1) == '/'
+        route.fullPath = isFullPath ? route.path : parentPath + '/' + route.path
+        if (route.children) {
+          this.formatOptions(route.children, route.fullPath)
+        }
+      })
+    },
+    updateMenu() {
+      this.selectedKeys = this.getSelectedKeys()
+      let openKeys = this.selectedKeys.filter(item => item !== '')
+      openKeys = openKeys.slice(0, openKeys.length - 1)
+      if (!fastEqual(openKeys, this.sOpenKeys)) {
+        this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys
+      }
+    },
+    getSelectedKeys() {
+      let matches = this.$route.matched
+      const route = matches[matches.length - 1]
+
+      let chose = this.routesMap[route.path]
+
+      if (chose.meta && chose.meta.highlight) {
+        chose = this.routesMap[chose.meta.highlight]
+        const resolve = this.$router.resolve({ path: chose.fullPath })
+        matches = (resolve.resolved && resolve.resolved.matched) || matches
+      }
+      return matches.map(item => item.path)
+    }
+  },
+  render(h) {
+    // console.log(this.options)
+    return h(
+      Menu,
+      {
+        props: {
+          theme: this.menuTheme,
+          mode: this.$props.mode,
+          selectedKeys: this.selectedKeys,
+          openKeys: this.openKeys ? this.openKeys : this.sOpenKeys
+        },
+        on: {
+          'update:openKeys': (val) => {
+            this.sOpenKeys = val
+          },
+          click: (obj) => {
+            obj.selectedKeys = [obj.key]
+            this.$emit('select', obj)
+          }
+        }
+      }, this.renderMenu(h, this.options)
+    )
+  }
+}

+ 48 - 0
src/components/page/header/PageHeader.vue

@@ -0,0 +1,48 @@
+<template>
+  <div :class="['page-header', layout, pageWidth]">
+    <div class="page-header-wide">
+     
+      <div class="detail">
+        <div class="main">
+          <div class="row">
+            <h1 v-if="showPageTitle && title" class="title">{{title}}</h1>
+           
+          </div>
+          
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {mapState} from 'vuex'
+export default {
+  name: 'PageHeader',
+  props: {
+    title: {
+      type: [String, Boolean],
+      required: false
+    },
+    breadcrumb: {
+      type: Array,
+      required: false
+    },
+    logo: {
+      type: String,
+      required: false
+    },
+    avatar: {
+      type: String,
+      required: false
+    },
+  },
+  computed: {
+    ...mapState('setting', ['layout', 'showPageTitle', 'pageWidth'])
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  @import "index";
+</style>

+ 40 - 0
src/components/page/header/index.less

@@ -0,0 +1,40 @@
+.page-header{
+  background: @base-bg-color;
+  padding: 0px 24px;
+  &.head.fixed{
+    margin: auto;
+    max-width: 1400px;
+  }
+  .page-header-wide{
+    .breadcrumb{
+      margin-bottom: 20px;
+    }
+    .detail{
+      display: flex;
+      .row {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-between;
+      }
+      .avatar {
+        margin:0 24px 0 0;
+      }
+      .main{
+        width: 100%;
+        .title{
+          font-size: 20px;
+          color: @title-color;
+          margin-bottom: 16px;
+        }
+        .content{
+          display: flex;
+          flex-wrap: wrap;
+          color: @text-color-second;
+        }
+        .extra{
+          display: flex;
+        }
+      }
+    }
+  }
+}

+ 64 - 0
src/components/result/Result.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="result">
+    <div >
+      <a-icon :class="[isSuccess ? 'success' : 'error' ,'icon']" :type="isSuccess ? 'check-circle' : 'close-circle'" />
+    </div>
+    <div class="title" v-if="title">{{title}}</div>
+    <div class="desc" v-if="description">{{description}}</div>
+    <div class="content">
+      <slot></slot>
+    </div>
+    <div class="action">
+      <slot name="action"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Result',
+  props: ['isSuccess', 'title', 'description']
+}
+</script>
+
+<style lang="less" scoped>
+  .result{
+    text-align: center;
+    width: 72%;
+    margin: 0 auto;
+    .icon{
+      font-size: 72px;
+      line-height: 72px;
+      margin-bottom: 24px;
+    }
+    .success {
+      color: @success-color;
+    }
+    .error {
+      color: @error-color;
+    }
+    .title{
+      font-size: 24px;
+      color: @title-color;
+      font-weight: 500;
+      line-height: 32px;
+      margin-bottom: 16px;
+    }
+    .desc{
+      font-size: 14px;
+      line-height: 22px;
+      color: @text-color-second;
+      margin-bottom: 24px;
+    }
+    .content{
+      background-color: @background-color-light;
+      padding: 24px 40px;
+      border-radius: 2px;
+      text-align: left;
+    }
+    .action{
+      margin-top: 32px;
+    }
+  }
+
+</style>

+ 224 - 0
src/components/setting/Setting.vue

@@ -0,0 +1,224 @@
+<template>
+  <div class="side-setting">
+    <setting-item>
+      <a-button @click="saveSetting" type="primary" icon="save">{{$t('save')}}</a-button>
+      <a-button @click="resetSetting" type="dashed" icon="redo" style="float: right">{{$t('reset')}}</a-button>
+    </setting-item>
+    <setting-item :title="$t('theme.title')">
+      <img-checkbox-group
+        @change="values => setTheme({...theme, mode: values[0]})"
+        :default-values="[theme.mode]"
+      >
+        <img-checkbox :title="$t('theme.dark')" img="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" value="dark"/>
+        <img-checkbox :title="$t('theme.light')" img="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" value="light"/>
+        <img-checkbox :title="$t('theme.night')" img="https://gw.alipayobjects.com/zos/antfincdn/hmKaLQvmY2/LCkqqYNmvBEbokSDscrm.svg" value="night"/>
+      </img-checkbox-group>
+    </setting-item>
+    <setting-item :title="$t('theme.color')">
+      <color-checkbox-group
+        @change="(values, colors) => setTheme({...theme, color: colors[0]})"
+        :defaultValues="[palettes.indexOf(theme.color)]" :multiple="false"
+      >
+        <color-checkbox v-for="(color, index) in palettes" :key="index" :color="color" :value="index" />
+      </color-checkbox-group>
+    </setting-item>
+    <a-divider/>
+    <setting-item :title="$t('navigate.title')">
+      <img-checkbox-group
+        @change="values => setLayout(values[0])"
+        :default-values="[layout]"
+      >
+        <img-checkbox :title="$t('navigate.side')" img="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" value="side"/>
+        <img-checkbox :title="$t('navigate.head')" img="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" value="head"/>
+        <img-checkbox :title="$t('navigate.mix')" img="https://gw.alipayobjects.com/zos/antfincdn/x8Ob%26B8cy8/LCkqqYNmvBEbokSDscrm.svg" value="mix"/>
+      </img-checkbox-group>
+    </setting-item>
+    <setting-item>
+      <a-list :split="false">
+        <a-list-item>
+          {{$t('navigate.content.title')}}
+          <a-select
+            :getPopupContainer="getPopupContainer"
+            :value="pageWidth"
+            @change="setPageWidth"
+            class="select-item" size="small" slot="actions"
+          >
+            <a-select-option value="fluid">{{$t('navigate.content.fluid')}}</a-select-option>
+            <a-select-option value="fixed">{{$t('navigate.content.fixed')}}</a-select-option>
+          </a-select>
+        </a-list-item>
+        <a-list-item>
+          {{$t('navigate.fixedHeader')}}
+          <a-switch :checked="fixedHeader" slot="actions" size="small" @change="setFixedHeader" />
+        </a-list-item>
+        <a-list-item>
+          {{$t('navigate.fixedSideBar')}}
+          <a-switch :checked="fixedSideBar" slot="actions" size="small" @change="setFixedSideBar" />
+        </a-list-item>
+      </a-list>
+    </setting-item>
+    <a-divider />
+    <setting-item :title="$t('other.title')">
+      <a-list :split="false">
+        <a-list-item>
+          {{$t('other.weekMode')}}
+          <a-switch :checked="weekMode" slot="actions" size="small" @change="setWeekMode" />
+        </a-list-item>
+        <a-list-item>
+          {{$t('other.multiPages')}}
+          <a-switch :checked="multiPage" slot="actions" size="small" @change="setMultiPage" />
+        </a-list-item>
+        <a-list-item>
+          {{$t('other.hideSetting')}}
+          <a-switch :checked="hideSetting" slot="actions" size="small" @change="setHideSetting" />
+        </a-list-item>
+      </a-list>
+    </setting-item>
+    <a-divider />
+    <setting-item :title="$t('animate.title')">
+      <a-list :split="false">
+        <a-list-item>
+          {{$t('animate.disable')}}
+          <a-switch :checked="animate.disabled" slot="actions" size="small" @change="val => setAnimate({...animate, disabled: val})" />
+        </a-list-item>
+        <a-list-item>
+          {{$t('animate.effect')}}
+          <a-select
+            :value="animate.name"
+            :getPopupContainer="getPopupContainer"
+            @change="val => setAnimate({...animate, name: val})"
+            class="select-item" size="small" slot="actions"
+          >
+            <a-select-option :key="index" :value="item.name" v-for="(item, index) in animates">{{item.alias}}</a-select-option>
+          </a-select>
+        </a-list-item>
+        <a-list-item>
+          {{$t('animate.direction')}}
+          <a-select
+            :value="animate.direction"
+            :getPopupContainer="getPopupContainer"
+            @change="val => setAnimate({...animate, direction: val})"
+            class="select-item" size="small" slot="actions"
+          >
+            <a-select-option :key="index" :value="item" v-for="(item, index) in directions">{{item}}</a-select-option>
+          </a-select>
+        </a-list-item>
+      </a-list>
+    </setting-item>
+    <a-alert
+      v-if="isDev"
+      style="max-width: 240px; margin: -16px 0 8px; word-break: break-all"
+      type="warning"
+      :message="$t('alert')"
+    >
+    </a-alert>
+    <a-button v-if="isDev" id="copyBtn" :data-clipboard-text="copyConfig" @click="copyCode" style="width: 100%" icon="copy" >{{$t('copy')}}</a-button>
+  </div>
+</template>
+
+<script>
+import SettingItem from './SettingItem'
+import {ColorCheckbox, ImgCheckbox} from '@/components/checkbox'
+import Clipboard from 'clipboard'
+import { mapState, mapMutations } from 'vuex'
+import {formatConfig} from '@/utils/formatter'
+import {setting} from '@/config/default'
+import sysConfig from '@/config/config'
+import fastEqual from 'fast-deep-equal'
+import deepMerge from 'deepmerge'
+
+const ColorCheckboxGroup = ColorCheckbox.Group
+const ImgCheckboxGroup = ImgCheckbox.Group
+export default {
+  name: 'Setting',
+  i18n: require('./i18n'),
+  components: {ImgCheckboxGroup, ImgCheckbox, ColorCheckboxGroup, ColorCheckbox, SettingItem},
+  data() {
+    return {
+      copyConfig: 'Sorry, you have copied nothing O(∩_∩)O~',
+      isDev: process.env.NODE_ENV === 'development'
+    }
+  },
+  computed: {
+    directions() {
+      return this.animates.find(item => item.name == this.animate.name).directions
+    },
+    ...mapState('setting', ['theme', 'layout', 'animate', 'animates', 'palettes', 'multiPage', 'weekMode', 'fixedHeader', 'fixedSideBar', 'hideSetting', 'pageWidth'])
+  },
+  watch: {
+    'animate.name': function(val) {
+      this.setAnimate({name: val, direction: this.directions[0]})
+    }
+  },
+  methods: {
+    getPopupContainer() {
+      return this.$el.parentNode
+    },
+    copyCode () {
+      let config = this.extractConfig(false)
+      this.copyConfig = `// 自定义配置,参考 ./default/setting.config.js,需要自定义的属性在这里配置即可
+      module.exports = ${formatConfig(config)}
+      `
+      let clipboard = new Clipboard('#copyBtn')
+      clipboard.on('success', () => {
+        this.$message.success(`复制成功,覆盖文件 src/config/config.js 然后重启项目即可生效`).then(() => {
+          const localConfig = localStorage.getItem(process.env.VUE_APP_SETTING_KEY)
+          if (localConfig) {
+            console.warn('检测到本地有历史保存的主题配置,想要要拷贝的配置代码生效,您可能需要先重置配置')
+            this.$message.warn('检测到本地有历史保存的主题配置,想要要拷贝的配置代码生效,您可能需要先重置配置', 5)
+          }
+        })
+        clipboard.destroy()
+      })
+    },
+    saveSetting() {
+      const closeMessage = this.$message.loading('正在保存到本地,请稍后...', 0)
+      const config = this.extractConfig(true)
+      localStorage.setItem(process.env.VUE_APP_SETTING_KEY, JSON.stringify(config))
+      setTimeout(closeMessage, 800)
+    },
+    resetSetting() {
+      this.$confirm({
+        title: '重置主题会刷新页面,当前页面内容不会保留,确认重置?',
+        onOk() {
+          localStorage.removeItem(process.env.VUE_APP_SETTING_KEY)
+          window.location.reload()
+        }
+      })
+    },
+    //提取配置
+    extractConfig(local = false) {
+      let config = {}
+      let mySetting = this.$store.state.setting
+      let dftSetting = local ? deepMerge(setting, sysConfig) : setting
+      Object.keys(mySetting).forEach(key => {
+        const dftValue = dftSetting[key], myValue = mySetting[key]
+        if (dftValue != undefined && !fastEqual(dftValue, myValue)) {
+          config[key] = myValue
+        }
+      })
+      return config
+    },
+    ...mapMutations('setting', ['setTheme', 'setLayout', 'setMultiPage', 'setWeekMode',
+      'setFixedSideBar', 'setFixedHeader', 'setAnimate', 'setHideSetting', 'setPageWidth'])
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .side-setting{
+    min-height: 100%;
+    background-color: @base-bg-color;
+    padding: 24px;
+    font-size: 14px;
+    line-height: 1.5;
+    word-wrap: break-word;
+    position: relative;
+    .flex{
+      display: flex;
+    }
+    .select-item{
+      width: 80px;
+    }
+  }
+</style>

+ 25 - 0
src/components/setting/SettingItem.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="setting-item">
+    <h3 v-if="title" class="title">{{title}}</h3>
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SettingItem',
+  props: ['title']
+}
+</script>
+
+<style lang="less" scoped>
+  .setting-item{
+    margin-bottom: 24px;
+    .title{
+      font-size: 14px;
+      color: @title-color;
+      line-height: 22px;
+      margin-bottom: 12px;
+    }
+  }
+</style>

+ 117 - 0
src/components/setting/i18n.js

@@ -0,0 +1,117 @@
+module.exports = {
+  messages: {
+    CN: {
+      theme: {
+        title: '整体风格设置',
+        light: '亮色菜单风格',
+        dark: '暗色菜单风格',
+        night: '深夜模式',
+        color: '主题色'
+      },
+      navigate: {
+        title: '导航设置',
+        side: '侧边导航',
+        head: '顶部导航',
+        mix: '混合导航',
+        content: {
+          title: '内容区域宽度',
+          fluid: '流式',
+          fixed: '定宽'
+        },
+        fixedHeader: '固定Header',
+        fixedSideBar: '固定侧边栏',
+      },
+      other: {
+        title: '其他设置',
+        weekMode: '色弱模式',
+        multiPages: '多页签模式',
+        hideSetting: '隐藏设置抽屉'
+      },
+      animate: {
+        title: '页面切换动画',
+        disable: '禁用动画',
+        effect: '动画效果',
+        direction: '动画方向'
+      },
+      alert: '拷贝配置后,直接覆盖文件 src/config/config.js 中的全部内容,然后重启即可。(注意:仅会拷贝与默认配置不同的项)',
+      copy: '拷贝配置',
+      save: '保存配置',
+      reset: '重置配置',
+    },
+    HK: {
+      theme: {
+        title: '整體風格設置',
+        light: '亮色菜單風格',
+        dark: '暗色菜單風格',
+        night: '深夜模式',
+        color: '主題色'
+      },
+      navigate: {
+        title: '導航設置',
+        side: '側邊導航',
+        head: '頂部導航',
+        content: {
+          title: '內容區域寬度',
+          fluid: '流式',
+          fixed: '定寬'
+        },
+        fixedHeader: '固定Header',
+        fixedSideBar: '固定側邊欄',
+      },
+      other: {
+        title: '其他設置',
+        weekMode: '色弱模式',
+        multiPages: '多頁簽模式',
+        hideSetting: '隱藏設置抽屜'
+      },
+      animate: {
+        title: '頁面切換動畫',
+        disable: '禁用動畫',
+        effect: '動畫效果',
+        direction: '動畫方向'
+      },
+      alert: '拷貝配置后,直接覆蓋文件 src/config/config.js 中的全部內容,然後重啟即可。(注意:僅會拷貝與默認配置不同的項)',
+      copy: '拷貝配置',
+      save: '保存配置',
+      reset: '重置配置',
+    },
+    US: {
+      theme: {
+        title: 'Page Style Setting',
+        light: 'Light Style',
+        dark: 'Dark Style',
+        night: 'Night Style',
+        color: 'Theme Color'
+      },
+      navigate: {
+        title: 'Navigation Mode',
+        side: 'Side Menu Layout',
+        head: 'Top Menu Layout',
+        mix: 'Mix Menu Layout',
+        content: {
+          title: 'Content Width',
+          fluid: 'Fluid',
+          fixed: 'Fixed'
+        },
+        fixedHeader: 'Fixed Header',
+        fixedSideBar: 'Fixed SideBar',
+      },
+      other: {
+        title: 'Other Setting',
+        weekMode: 'Week Mode',
+        multiPages: 'Multi Pages',
+        hideSetting: 'Hide Setting Drawer'
+      },
+      animate: {
+        title: 'Page Toggle Animation',
+        disable: 'Disable',
+        effect: 'Effect',
+        direction: 'Direction'
+      },
+      alert: 'After copying the configuration code, directly cover all contents in the file src/config/config.js, then restart the server. (Note: only items that are different from the default configuration will be copied)',
+      copy: 'Copy Setting',
+      save: 'Save',
+      reset: 'Reset',
+    }
+  }
+}

+ 200 - 0
src/components/table/StandardTable.vue

@@ -0,0 +1,200 @@
+<template>
+  <div class="standard-table">
+    <div class="alert">
+      <a-alert type="info" :show-icon="true" v-if="selectedRows">
+        <div class="message" slot="message">
+          已选择&nbsp;<a>{{ selectedRows.length }}</a
+          >&nbsp;项 <a class="clear" @click="onClear">清空</a>
+          <template v-for="(item, index) in needTotalList">
+            <div v-if="item.needTotal" :key="index">
+              {{ item.title }}总计&nbsp;
+              <a>{{
+                item.customRender ? item.customRender(item.total) : item.total
+              }}</a>
+            </div>
+          </template>
+        </div>
+      </a-alert>
+    </div>
+    <a-table
+      :bordered="bordered"
+      :loading="loading"
+      :columns="columns"
+      :dataSource="dataSource"
+      :rowKey="rowKey"
+      :pagination="pagination"
+      :expandedRowKeys="expandedRowKeys"
+      :expandedRowRender="expandedRowRender"
+      @change="onChange"
+      :rowSelection="
+        selectedRows
+          ? { selectedRowKeys: selectedRowKeys, onChange: updateSelect }
+          : undefined
+      "
+      :rowClassName="rowClassName"
+      :custom-row="onClickRow"
+    >
+      <template
+        slot-scope="text, record, index"
+        :slot="slot"
+        v-for="slot in Object.keys($scopedSlots).filter(
+          (key) => key !== 'expandedRowRender'
+        )"
+      >
+        <slot :name="slot" v-bind="{ text, record, index }"></slot>
+      </template>
+      <template :slot="slot" v-for="slot in Object.keys($slots)">
+        <slot :name="slot"></slot>
+      </template>
+      <template
+        slot-scope="record, index, indent, expanded"
+        :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"
+      >
+        <slot
+          v-bind="{ record, index, indent, expanded }"
+          :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"
+        ></slot>
+      </template>
+    </a-table>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "StandardTable",
+  props: {
+    bordered: Boolean,
+    loading: [Boolean, Object],
+    columns: Array,
+    dataSource: Array,
+    rowKey: {
+      type: [String, Function],
+      default: "key",
+    },
+    pagination: {
+      type: [Object, Boolean],
+      default: () => {
+        return {
+          current: 1,
+          defaultPageSize: 10,
+          total: 0,
+          pagesize0ptions: ["10", "20", "30"],
+          hideOnSinglePage: false,
+          showQuickJumper: true, //
+          showsizeChanger: true, //是否可以改变pagesize
+        };
+      },
+    },
+    selectedRows: Array,
+    expandedRowKeys: Array,
+    expandedRowRender: Function,
+  },
+  data() {
+    return {
+      needTotalList: [],
+      selectRow: -1,
+    };
+  },
+  methods: {
+    onClickRow(record, index) {
+      return {
+        on: {
+          click: () => {
+            this.$emit("onClickRow", record);
+            this.selectRow = index;
+          },
+        },
+      };
+    },
+
+    rowClassName(record, index) {
+      // 根据是否是选中行来设置不同的class
+      // console.log(this.selectRow, index);
+      return this.selectRow === index ? "highlight-row" : "";
+    },
+
+    updateSelect(selectedRowKeys, selectedRows) {
+      console.log(selectedRowKeys, selectedRows);
+      this.$emit("update:selectedRows", selectedRows);
+      this.$emit("selectedRowChange", selectedRowKeys, selectedRows);
+    },
+    initTotalList(columns) {
+      const totalList = columns
+        .filter((item) => item.needTotal)
+        .map((item) => {
+          return {
+            ...item,
+            total: 0,
+          };
+        });
+      return totalList;
+    },
+    onClear() {
+      this.updateSelect([], []);
+      this.$emit("clear");
+    },
+    onChange(pagination, filters, sorter, { currentDataSource }) {
+      this.$emit("change", pagination, filters, sorter, { currentDataSource });
+    },
+  },
+  created() {
+    this.needTotalList = this.initTotalList(this.columns);
+  },
+  watch: {
+    selectedRows(selectedRows) {
+      this.needTotalList = this.needTotalList.map((item) => {
+        return {
+          ...item,
+          total: selectedRows.reduce((sum, val) => {
+            let v;
+            try {
+              v = val[item.dataIndex]
+                ? val[item.dataIndex]
+                : eval(`val.${item.dataIndex}`);
+            } catch (_) {
+              v = val[item.dataIndex];
+            }
+            v = !isNaN(parseFloat(v)) ? parseFloat(v) : 0;
+            return sum + v;
+          }, 0),
+        };
+      });
+    },
+  },
+  computed: {
+    selectedRowKeys() {
+      return this.selectedRows.map((record) => {
+        return typeof this.rowKey === "function"
+          ? this.rowKey(record)
+          : record[this.rowKey];
+      });
+    },
+  },
+};
+</script>
+
+<style scoped lang="less">
+.standard-table {
+  background: #fff;
+  padding: 0 15px;
+  box-sizing: border-box;
+  .alert {
+    margin-bottom: 16px;
+    .message {
+      a {
+        font-weight: 600;
+      }
+    }
+    .clear {
+      float: right;
+    }
+  }
+  /deep/ .ant-table-thead > tr > th {
+    background: #fff;
+  }
+  /deep/ .ant-table-tbody > tr.highlight-row {
+    background: #dee9f9;
+    // opacity: ;
+  }
+}
+</style>

+ 155 - 0
src/components/table/advance/ActionColumns.vue

@@ -0,0 +1,155 @@
+<template>
+  <div class="action-columns" ref="root">
+      <a-popover v-model="visible" placement="bottomRight" trigger="click" :get-popup-container="() => $refs.root">
+        <div slot="title">
+          <a-checkbox :indeterminate="indeterminate" :checked="checkAll" @change="onCheckAllChange" class="check-all" />列展示
+          <a-button @click="resetColumns" style="float: right" type="link" size="small">重置</a-button>
+        </div>
+        <a-list style="width: 100%" size="small" :key="i" v-for="(col, i) in columns" slot="content">
+          <a-list-item>
+            <a-checkbox v-model="col.visible" @change="e => onCheckChange(e, col)"/>
+            <template v-if="col.title">
+              {{col.title}}:
+            </template>
+            <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
+            <template slot="actions">
+              <a-tooltip title="固定在列头" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
+                <a-icon :class="['left', {active: col.fixed === 'left'}]" @click="fixColumn('left', col)" type="vertical-align-top" />
+              </a-tooltip>
+              <a-tooltip title="固定在列尾" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
+                <a-icon :class="['right', {active: col.fixed === 'right'}]" @click="fixColumn('right', col)" type="vertical-align-bottom" />
+              </a-tooltip>
+              <a-tooltip title="添加搜索" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
+                <a-icon :class="{active: col.searchAble}" @click="setSearch(col)" type="search" />
+              </a-tooltip>
+            </template>
+          </a-list-item>
+        </a-list>
+        <a-icon class="action" type="setting" />
+      </a-popover>
+  </div>
+</template>
+
+<script>
+  import cloneDeep from 'lodash.clonedeep'
+
+  export default {
+    name: 'ActionColumns',
+    props: ['columns', 'visibleColumns'],
+    data() {
+      return {
+        visible: false,
+        indeterminate: false,
+        checkAll: true,
+        checkedCounts: this.columns.length,
+        backColumns: cloneDeep(this.columns)
+      }
+    },
+    watch: {
+      checkedCounts(val) {
+        this.checkAll = val === this.columns.length
+        this.indeterminate = val > 0 && val < this.columns.length
+      },
+      columns(newVal, oldVal) {
+        if (newVal != oldVal) {
+          this.checkedCounts = newVal.length
+          this.formatColumns(newVal)
+        }
+      }
+    },
+    created() {
+      this.formatColumns(this.columns)
+    },
+    methods: {
+      onCheckChange(e, col) {
+        if (!col.visible) {
+          this.checkedCounts -= 1
+        } else {
+          this.checkedCounts += 1
+        }
+      },
+      fixColumn(fixed, col) {
+        if (fixed !== col.fixed) {
+          this.$set(col, 'fixed', fixed)
+        } else {
+          this.$set(col, 'fixed', undefined)
+        }
+      },
+      setSearch(col) {
+        this.$set(col, 'searchAble', !col.searchAble)
+        if (!col.searchAble && col.search) {
+          this.resetSearch(col)
+        }
+      },
+      resetSearch(col) {
+        // col.search.value = col.dataType === 'boolean' ? false : undefined
+        col.search.value = undefined
+        col.search.backup = undefined
+      },
+      resetColumns() {
+        const {columns, backColumns} = this
+        let counts = columns.length
+        backColumns.forEach((back, index) => {
+          const column = columns[index]
+          column.visible = back.visible === undefined || back.visible
+          if (!column.visible) {
+            counts -= 1
+          }
+          if (back.fixed !== undefined) {
+            column.fixed = back.fixed
+          } else {
+            this.$set(column, 'fixed', undefined)
+          }
+          this.$set(column, 'searchAble', back.searchAble)
+          // column.searchAble = back.searchAble
+          this.resetSearch(column)
+        })
+        this.checkedCounts = counts
+        this.visible = false
+        this.$emit('reset', this.getConditions(columns))
+      },
+      onCheckAllChange(e) {
+        if (e.target.checked) {
+          this.checkedCounts = this.columns.length
+          this.columns.forEach(col => col.visible = true)
+        } else {
+          this.checkedCounts = 0
+          this.columns.forEach(col => col.visible = false)
+        }
+      },
+      getConditions(columns) {
+        const conditions = {}
+        columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
+          .forEach(col => {
+            conditions[col.dataIndex] = col.search.value
+          })
+        return conditions
+      },
+      formatColumns(columns) {
+        for (let col of columns) {
+          if (col.visible === undefined) {
+            this.$set(col, 'visible', true)
+          }
+          if (!col.visible) {
+            this.checkedCounts -= 1
+          }
+        }
+      }
+    }
+  }
+</script>
+
+<style scoped lang="less">
+.action-columns{
+  display: inline-block;
+  .check-all{
+    margin-right: 8px;
+  }
+  .left,.right{
+    transform: rotate(-90deg);
+  }
+  .active{
+    color: @primary-color;
+  }
+}
+</style>

+ 44 - 0
src/components/table/advance/ActionSize.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="action-size" ref="root">
+    <a-tooltip title="密度">
+      <a-dropdown placement="bottomCenter" :trigger="['click']" :get-popup-container="() => $refs.root">
+        <a-icon class="action" type="column-height" />
+        <a-menu :selected-keys="[value]" slot="overlay" @click="onClick">
+          <a-menu-item key="default">
+            默认
+          </a-menu-item>
+          <a-menu-item key="middle">
+            中等
+          </a-menu-item>
+          <a-menu-item key="small">
+            紧密
+          </a-menu-item>
+        </a-menu>
+      </a-dropdown>
+    </a-tooltip>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'ActionSize',
+    props: ['value'],
+    inject: ['table'],
+    data() {
+      return {
+        selectedKeys: ['middle']
+      }
+    },
+    methods: {
+      onClick({key}) {
+        this.$emit('input', key)
+      }
+    }
+  }
+</script>
+
+<style scoped lang="less">
+.action-size{
+  display: inline-block;
+}
+</style>

+ 249 - 0
src/components/table/advance/AdvanceTable.vue

@@ -0,0 +1,249 @@
+<template>
+  <div ref="table" :id="id" class="advanced-table">
+    <a-spin :spinning="loading">
+    <div :class="['header-bar', size]">
+      <div class="title">
+        <template v-if="title">{{title}}</template>
+        <slot v-else-if="$slots.title" name="title"></slot>
+        <template v-else>高级表格</template>
+      </div>
+      <div class="search">
+        <search-area :format-conditions="formatConditions" @change="onSearchChange" :columns="columns" >
+          <template :slot="slot" v-for="slot in slots">
+            <slot :name="slot"></slot>
+          </template>
+        </search-area>
+      </div>
+      <div class="actions">
+        <a-tooltip title="刷新">
+          <a-icon @click="refresh" class="action" :type="loading ? 'loading' : 'reload'" />
+        </a-tooltip>
+        <action-size v-model="sSize" class="action" />
+        <a-tooltip title="列配置">
+          <action-columns :columns="columns" @reset="onColumnsReset" class="action">
+            <template :slot="slot" v-for="slot in slots">
+              <slot :name="slot"></slot>
+            </template>
+          </action-columns>
+        </a-tooltip>
+        <a-tooltip title="全屏">
+          <a-icon @click="toggleScreen" class="action" :type="fullScreen ? 'fullscreen-exit' : 'fullscreen'" />
+        </a-tooltip>
+      </div>
+    </div>
+    <a-table
+      v-bind="{...$props, columns: visibleColumns, title: undefined, loading: false}"
+      :size="sSize"
+      @expandedRowsChange="onExpandedRowsChange"
+      @change="onChange"
+      @expand="onExpand"
+    >
+      <template slot-scope="text, record, index" :slot="slot" v-for="slot in scopedSlots ">
+        <slot :name="slot" v-bind="{text, record, index}"></slot>
+      </template>
+      <template :slot="slot" v-for="slot in slots">
+        <slot :name="slot"></slot>
+      </template>
+      <template slot-scope="record, index, indent, expanded" :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''">
+        <slot v-bind="{record, index, indent, expanded}" :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"></slot>
+      </template>
+    </a-table>
+    </a-spin>
+  </div>
+</template>
+
+<script>
+  import ActionSize from '@/components/table/advance/ActionSize'
+  import ActionColumns from '@/components/table/advance/ActionColumns'
+  import SearchArea from '@/components/table/advance/SearchArea'
+  export default {
+    name: 'AdvanceTable',
+    components: {SearchArea, ActionColumns, ActionSize},
+    props: {
+      tableLayout: String,
+      bordered: Boolean,
+      childrenColumnName: {type: String, default: 'children'},
+      columns: Array,
+      components: Object,
+      dataSource: Array,
+      defaultExpandAllRows: Array[String],
+      expandedRowKeys: Array[String],
+      expandedRowRender: Function,
+      expandIcon: Function,
+      expandRowByClick: Boolean,
+      expandIconColumnIndex: Number,
+      footer: Function,
+      indentSize: Number,
+      loading: Boolean,
+      locale: Object,
+      pagination: [Object, Boolean],
+      rowClassName: Function,
+      rowKey: [String, Function],
+      rowSelection: Object,
+      scroll: Object,
+      showHeader: {type: Boolean, default: true},
+      size: String,
+      title: String,
+      customHeaderRow: Function,
+      customRow: Function,
+      getPopupContainer: Function,
+      transformCellText: Function,
+      formatConditions: Boolean
+    },
+    provide() {
+      return {
+        table: this
+      }
+    },
+    data() {
+      return {
+        id: `${new Date().getTime()}-${Math.floor(Math.random() * 10)}`,
+        sSize: this.size || 'default',
+        fullScreen: false,
+        conditions: {}
+      }
+    },
+    computed: {
+      slots() {
+        return Object.keys(this.$slots).filter(slot => slot !== 'title')
+      },
+      scopedSlots() {
+        return Object.keys(this.$scopedSlots).filter(slot => slot !== 'expandedRowRender' && slot !== 'title')
+      },
+      visibleColumns(){
+        return this.columns.filter(col => col.visible)
+      }
+    },
+    created() {
+      this.addListener()
+    },
+    beforeDestroy() {
+      this.removeListener()
+    },
+    methods: {
+      refresh() {
+        this.$emit('refresh', this.conditions)
+      },
+      onSearchChange(conditions, searchOptions) {
+        this.conditions = conditions
+        this.$emit('search', conditions, searchOptions)
+      },
+      toggleScreen() {
+        if (this.fullScreen) {
+          this.outFullScreen()
+        } else {
+          this.inFullScreen()
+        }
+      },
+      inFullScreen() {
+        const el = this.$refs.table
+        el.classList.add('beauty-scroll')
+        if (el.requestFullscreen) {
+          el.requestFullscreen()
+          return true
+        } else if (el.webkitRequestFullScreen) {
+          el.webkitRequestFullScreen()
+          return true
+        } else if (el.mozRequestFullScreen) {
+          el.mozRequestFullScreen()
+          return true
+        } else if (el.msRequestFullscreen) {
+          el.msRequestFullscreen()
+          return true
+        }
+        this.$message.warn('对不起,您的浏览器不支持全屏模式')
+        el.classList.remove('beauty-scroll')
+        return false
+      },
+      outFullScreen() {
+        if (document.exitFullscreen) {
+          document.exitFullscreen()
+        } else if (document.webkitCancelFullScreen) {
+          document.webkitCancelFullScreen();
+        } else if (document.mozCancelFullScreen) {
+          document.mozCancelFullScreen()
+        } else if (document.msExitFullscreen) {
+          document.msExitFullscreen()
+        }
+        this.$refs.table.classList.remove('beauty-scroll')
+      },
+      onColumnsReset(conditions) {
+        this.$emit('reset', conditions)
+      },
+      onExpandedRowsChange(expandedRows) {
+        this.$emit('expandedRowsChange', expandedRows)
+      },
+      onChange(pagination, filters, sorter, options) {
+        this.$emit('change', pagination, filters, sorter, options)
+      },
+      onExpand(expanded, record) {
+        this.$emit('expand', expanded, record)
+      },
+      addListener() {
+        document.addEventListener('fullscreenchange', this.fullScreenListener)
+        document.addEventListener('webkitfullscreenchange', this.fullScreenListener)
+        document.addEventListener('mozfullscreenchange', this.fullScreenListener)
+        document.addEventListener('msfullscreenchange', this.fullScreenListener)
+      },
+      removeListener() {
+        document.removeEventListener('fullscreenchange', this.fullScreenListener)
+        document.removeEventListener('webkitfullscreenchange', this.fullScreenListener)
+        document.removeEventListener('mozfullscreenchange', this.fullScreenListener)
+        document.removeEventListener('msfullscreenchange', this.fullScreenListener)
+      },
+      fullScreenListener(e) {
+        if (e.target.id === this.id) {
+          this.fullScreen = !this.fullScreen
+        }
+      }
+    }
+  }
+</script>
+
+<style scoped lang="less">
+.advanced-table{
+  overflow-y: auto;
+  background-color: @component-background;
+  .header-bar{
+    padding: 16px 24px;
+    display: flex;
+    align-items: center;
+    border-radius: 4px;
+    transition: all 0.3s;
+    &.middle{
+      padding: 12px 16px;
+    }
+    &.small{
+      padding: 8px 12px;
+      border: 1px solid @border-color;
+      border-bottom: 0;
+      .title{
+        font-size: 16px;
+      }
+    }
+    .title{
+      transition: all 0.3s;
+      font-size: 18px;
+      color: @title-color;
+      font-weight: 700;
+    }
+    .search{
+      flex: 1;
+      text-align: right;
+      margin: 0 24px;
+    }
+    .actions{
+      text-align: right;
+      font-size: 17px;
+      color: @text-color;
+      .action{
+        margin: 0 8px;
+        cursor: pointer;
+        &:hover{
+          color: @primary-color;
+        }
+      }
+    }
+  }
+}
+</style>

+ 313 - 0
src/components/table/advance/SearchArea.vue

@@ -0,0 +1,313 @@
+<template>
+  <div class="search-area" ref="root">
+    <div class="select-root" ref="selectRoot"></div>
+    <div class="search-item" :key="index" v-for="(col, index) in searchCols">
+      <div v-if="col.dataType === 'boolean'" :class="['title', {active: col.search.value !== undefined}]">
+        <template v-if="col.title">
+          {{col.title}}:
+        </template>
+        <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
+        <a-switch @change="onSwitchChange(col)" class="switch" v-model="col.search.value" size="small"
+                  :checked-children="(col.search.switchOptions && col.search.switchOptions.checkedText) || '是'"
+                  :un-checked-children="(col.search.switchOptions && col.search.switchOptions.uncheckedText) || '否'"
+        />
+        <a-icon v-if="col.search.value !== undefined" class="close" @click="e => onCloseClick(e, col)" type="close-circle" theme="filled" />
+      </div>
+      <div v-else-if="col.dataType === 'time'" :class="['title', {active: col.search.value}]">
+        <template v-if="col.title">
+          {{col.title}}:
+        </template>
+        <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
+        <a-time-picker :format="col.search.format" v-model="col.search.value" placeholder="选择时间" @change="(time, timeStr) => onCalendarChange(time, timeStr, col)" @openChange="open => onCalendarOpenChange(open, col)" class="time-picker" size="small" :get-popup-container="() => $refs.root"/>
+      </div>
+      <div v-else-if="col.dataType === 'date'" :class="['title', {active: col.search.value}]">
+        <template v-if="col.title">
+          {{col.title}}:
+        </template>
+        <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
+        <a-date-picker :format="col.search.format" v-model="col.search.value" @change="onDateChange(col)" class="date-picker" size="small" :getCalendarContainer="() => $refs.root"/>
+      </div>
+      <div v-else-if="col.dataType === 'datetime'" class="title datetime active">
+        <template v-if="col.title">
+          {{col.title}}:
+        </template>
+        <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
+        <a-date-picker :format="col.search.format" v-model="col.search.value" @change="(date, dateStr) => onCalendarChange(date, dateStr, col)" @openChange="open => onCalendarOpenChange(open, col)" class="datetime-picker" size="small" show-time :getCalendarContainer="() => $refs.root"/>
+      </div>
+      <div v-else-if="col.dataType === 'select'" :class="['title', {active: col.search.value !== undefined}]">
+        <template v-if="col.title">
+          {{col.title}}:
+        </template>
+        <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
+        <a-select :allowClear="true" :options="col.search.selectOptions" v-model="col.search.value" placeholder="请选择..." @change="onSelectChange(col)" class="select" slot="content" size="small" :get-popup-container="() => $refs.selectRoot">
+        </a-select>
+      </div>
+      <div v-else :class="['title', {active: col.search.value}]">
+        <a-popover @visibleChange="onVisibleChange(col, index)" v-model="col.search.visible" placement="bottom" :trigger="['click']" :get-popup-container="() => $refs.root">
+          <template v-if="col.title">
+            {{col.title}}
+          </template>
+          <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
+          <div class="value " v-if="col.search.value">:&nbsp;&nbsp;{{col.search.format && typeof col.search.format === 'function' ? col.search.format(col.search.value) : col.search.value}}</div>
+          <a-icon v-if="!col.search.value" class="icon-down" type="down"/>
+          <div class="operations" slot="content">
+            <a-button @click="onCancel(col)" class="btn" size="small" type="link">取消</a-button>
+            <a-button @click="onConfirm(col)" class="btn" size="small" type="primary">确认</a-button>
+          </div>
+          <div class="search-overlay" slot="title">
+            <a-input :id="`${searchIdPrefix}${index}`" :allow-clear="true" @keyup.esc="onCancel(col)" @keyup.enter="onConfirm(col)" v-model="col.search.value" size="small" />
+          </div>
+        </a-popover>
+        <a-icon v-if="col.search.value" @click="e => onCloseClick(e, col)" class="close" type="close-circle" theme="filled"/>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  import fastEqual from 'fast-deep-equal'
+  import moment from 'moment'
+
+  export default {
+    name: 'SearchArea',
+    props: ['columns', 'formatConditions'],
+    inject: ['table'],
+    created() {
+      this.formatColumns(this.columns)
+    },
+    watch: {
+      columns(newVal, oldVal) {
+        if (newVal != oldVal) {
+          this.formatColumns(newVal)
+        }
+      },
+      searchCols(newVal, oldVal) {
+        if (newVal.length != oldVal.length) {
+          const newConditions = this.getConditions(newVal)
+          const newSearchOptions = this.getSearchOptions(newVal)
+          if (!fastEqual(newConditions, this.conditions)) {
+            this.conditions = newConditions
+            this.searchOptions = newSearchOptions
+            this.$emit('change', this.conditions, this.searchOptions)
+          }
+        }
+      }
+    },
+    data() {
+      return {
+        conditions: {},
+        searchOptions: []
+      }
+    },
+    computed: {
+      searchCols() {
+        return this.columns.filter(item => item.searchAble)
+      },
+      searchIdPrefix() {
+        return this.table.id + '-ipt-'
+      }
+    },
+    methods: {
+      onCloseClick(e, col) {
+        e.preventDefault()
+        e.stopPropagation()
+        col.search.value = undefined
+        const {backup, value} = col.search
+        if (backup !== value) {
+          this.backupAndEmitChange(col)
+        }
+      },
+      onCancel(col) {
+        col.search.value = col.search.backup
+        col.search.visible = false
+      },
+      onConfirm(col) {
+        const {backup, value} = col.search
+        col.search.visible = false
+        if (backup !== value) {
+          this.backupAndEmitChange(col)
+        }
+      },
+      onSwitchChange(col) {
+        const {backup, value} = col.search
+        if (backup !== value) {
+          this.backupAndEmitChange(col)
+        }
+      },
+      onSelectChange(col) {
+        this.backupAndEmitChange(col)
+      },
+      onCalendarOpenChange(open, col) {
+        col.search.visible = open
+        const {momentEqual, backupAndEmitChange} = this
+        const {value, backup, format} = col.search
+        if (!open && !momentEqual(value, backup, format)) {
+          backupAndEmitChange(col, moment(value))
+        }
+      },
+      onCalendarChange(date, dateStr, col) {
+        const {momentEqual, backupAndEmitChange} = this
+        const {value, backup, format} = col.search
+        if (!col.search.visible && !momentEqual(value, backup, format)) {
+          backupAndEmitChange(col, moment(value))
+        }
+      },
+      onDateChange(col) {
+        const {momentEqual, backupAndEmitChange} = this
+        const {value, backup, format} = col.search
+        if (!momentEqual(value, backup, format)) {
+          backupAndEmitChange(col, moment(value))
+        }
+      },
+      getFormat(col) {
+        if (col.search && col.search.format) {
+          return col.search.format
+        }
+        const dataType = col.dataType
+        switch(dataType) {
+          case 'time': return 'HH:mm:ss'
+          case 'date': return 'YYYY-MM-DD'
+          case 'datetime': return 'YYYY-MM-DD HH:mm:ss'
+          default: return undefined
+        }
+      },
+      backupAndEmitChange(col, backValue = col.search.value) {
+        const {getConditions, getSearchOptions} = this
+        col.search.backup = backValue
+        this.conditions = getConditions(this.searchCols)
+        this.searchOptions = getSearchOptions(this.searchCols)
+        this.$emit('change', this.conditions, this.searchOptions)
+      },
+      getConditions(columns) {
+        const conditions = {}
+        columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
+          .forEach(col => {
+            const {value, format} = col.search
+            if (this.formatConditions && format) {
+              if (typeof format === 'function') {
+                conditions[col.dataIndex] = format(col.search.value)
+              } else if (typeof format === 'string' && value.constructor.name === 'Moment') {
+                conditions[col.dataIndex] = value.format(format)
+              } else {
+                conditions[col.dataIndex] = value
+              }
+            } else {
+              conditions[col.dataIndex] = value
+            }
+          })
+        return conditions
+      },
+      getSearchOptions(columns) {
+        return columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
+          .map(({dataIndex, search}) => ({field: dataIndex, value: search.value, format: search.format}))
+      },
+      onVisibleChange(col, index) {
+        if (!col.search.visible) {
+          col.search.value = col.search.backup
+        } else {
+          let input = document.getElementById(`${this.searchIdPrefix}${index}`)
+          if (input) {
+            setTimeout(() => {input.focus()}, 0)
+          } else {
+            this.$nextTick(() => {
+              input = document.getElementById(`${this.searchIdPrefix}${index}`)
+              input.focus()
+            })
+          }
+        }
+      },
+      momentEqual(target, source, format) {
+        if (target === source) {
+          return true
+        } else if (target && source && target.format(format) === source.format(format)) {
+          return true
+        }
+        return false
+      },
+      formatColumns(columns) {
+        columns.forEach(item => {
+          this.$set(item, 'search', {...item.search, visible: false, value: undefined, format: this.getFormat(item)})
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped lang="less">
+.search-area{
+  .select-root{
+    text-align: left;
+  }
+  margin: -4px 0;
+  .search-item{
+    margin: 4px 4px;
+    display: inline-block;
+    .title{
+      padding: 4px 8px;
+      cursor: pointer;
+      border-radius: 4px;
+      user-select: none;
+      display: inline-flex;
+      align-items: center;
+      .close{
+        color: @text-color-second;
+        margin-left: 4px;
+        font-size: 12px;
+        vertical-align: middle;
+        :hover{
+          color: @text-color;
+        }
+      }
+      .switch{
+        margin-left: 4px;
+      }
+      .time-picker{
+        margin-left: 4px;
+        width: 96px;
+      }
+      .date-picker{
+        margin-left: 4px;
+        width: 120px;
+      }
+      .datetime-picker{
+        margin-left: 4px;
+        width: 195px;
+      }
+      .value{
+        display: inline-block;
+        overflow: hidden;
+        flex:1;
+        vertical-align: middle;
+        max-width: 144px;
+        text-overflow: ellipsis;
+        word-break: break-all;
+        white-space: nowrap;
+      }
+      &.active{
+        background-color: @layout-bg-color;
+      }
+    }
+    .icon-down{
+      vertical-align: middle;
+      font-size: 12px;
+    }
+  }
+  .search-overlay{
+    padding: 8px 0px;
+    text-align: center;
+  }
+  .select{
+    margin-left: 4px;
+    max-width: 144px;
+    min-width: 96px;
+    text-align: left;
+  }
+  .operations{
+    display: flex;
+    margin: -6px 0;
+    justify-content: space-between;
+    .btn{
+    }
+  }
+}
+</style>

+ 2 - 0
src/components/table/advance/index.js

@@ -0,0 +1,2 @@
+import AdvanceTable from './AdvanceTable'
+export default AdvanceTable

+ 49 - 0
src/components/table/api/ApiTable.vue

@@ -0,0 +1,49 @@
+<template>
+  <a-table :data-source="apiSource" :pagination="false">
+    <h2 v-if="title" style="margin: 0 16px 0" slot="title">{{title}}</h2>
+    <a-table-column width="20%" data-index="param" title="参数">
+      <div slot-scope="text" v-html="text"></div>
+    </a-table-column>
+    <a-table-column width="50%" data-index="desc" title="说明">
+      <div slot-scope="text" v-html="text"></div>
+    </a-table-column>
+    <a-table-column v-if="isApi" width="15%" data-index="type" title="类型">
+      <div slot-scope="text" v-html="text"></div>
+    </a-table-column>
+    <a-table-column v-if="isApi" width="15%" data-index="default" title="默认值">
+      <div slot-scope="text" v-html="text"></div>
+    </a-table-column>
+    <a-table-column v-if="!isApi" width="30%" data-index="callback" title="回调函数">
+      <div slot-scope="text" v-html="text"></div>
+    </a-table-column>
+  </a-table>
+</template>
+
+<script>
+  export default {
+    name: 'ApiTable',
+    props: {
+      title: {
+        type: String,
+        default: 'API'
+      },
+      type: {
+        type: String,
+        default: 'api',
+        validator(value) {
+          return ['api', 'event'].includes(value)
+        }
+      },
+      apiSource: Array
+    },
+    computed: {
+      isApi() {
+        return this.type === 'api'
+      }
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>

+ 79 - 0
src/components/task/TaskGroup.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="task-group">
+    <div class="task-head">
+      <h3 class="title"><span v-if="count">{{count}}</span>{{title}}</h3>
+      <div class="actions" style="float: right">
+        <a-icon class="add" type="plus" draggable="true"/>
+        <a-icon class="more" style="margin-left: 8px" type="ellipsis" />
+      </div>
+    </div>
+    <div class="task-content">
+      <draggable :options="dragOptions">
+        <slot></slot>
+      </draggable>
+    </div>
+  </div>
+</template>
+
+<script>
+import Draggable from 'vuedraggable'
+
+const dragOptions = {
+  sort: true,
+  scroll: true,
+  scrollSpeed: 2,
+  animation: 150,
+  ghostClass: 'dragable-ghost',
+  chosenClass: 'dragable-chose',
+  dragClass: 'dragable-drag'
+}
+
+export default {
+  name: 'TaskGroup',
+  components: {Draggable},
+  props: ['title', 'group'],
+  data () {
+    return {
+      dragOptions: {...dragOptions, group: this.group}
+    }
+  },
+  computed: {
+    count () {
+      return this.$slots.default.length
+    }
+  }
+}
+</script>
+
+<style lang="less">
+  .task-group{
+    width: 33.33%;
+    padding: 8px 8px;
+    background-color: @background-color-light;
+    border-radius: 6px;
+    border: 1px solid @shadow-color;
+    .task-head{
+      margin-bottom: 8px;
+      .title{
+        display: inline-block;
+        span{
+          display: inline-block;
+          border-radius: 10px;
+          margin: 0 8px;
+          font-size: 12px;
+          padding: 2px 6px;
+          background-color: @base-bg-color;
+        }
+      }
+      .actions{
+        display: inline-block;
+        float: right;
+        font-size: 18px;
+        font-weight: bold;
+        i{
+          cursor: pointer;
+        }
+      }
+    }
+  }
+</style>

+ 25 - 0
src/components/task/TaskItem.vue

@@ -0,0 +1,25 @@
+<template>
+  <a-card class="task-item" type="inner">
+    {{content}}
+  </a-card>
+</template>
+
+<script>
+export default {
+  name: 'TaskItem',
+  props: ['content']
+}
+</script>
+
+<style lang="less" scoped>
+  .task-item{
+    margin-bottom: 16px;
+    box-shadow: 0 1px 1px @shadow-color;
+    border-radius: 6px;
+    & :hover{
+      cursor: move;
+      box-shadow: 0 1px 2px @shadow-color;
+      border-radius: 6px;
+    }
+  }
+</style>

+ 59 - 0
src/components/tool/AStepItem.vue

@@ -0,0 +1,59 @@
+<template>
+  <div
+    :class="['step-item', link ? 'linkable' : null]"
+    @click="go"
+  >
+    <span :style="titleStyle">{{title}}</span>
+    <a-icon v-if="icon" :style="iconStyle" :type="icon" />
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+const Group = {
+  name: 'AStepItemGroup',
+  props: {
+    align: {
+      type: String,
+      default: 'center',
+      validator(value) {
+        return ['left', 'center', 'right'].indexOf(value) != -1
+      }
+    }
+  },
+  render (h) {
+    return h(
+      'div',
+      {attrs: {style: `text-align: ${this.align}; margin-top: 8px`}},
+      [h('div', {attrs: {style: 'text-align: left; display: inline-block;'}}, [this.$slots.default])]
+    )
+  }
+}
+
+export default {
+  name: 'AStepItem',
+  Group: Group,
+  props: ['title', 'icon', 'link', 'titleStyle', 'iconStyle'],
+  methods: {
+    go () {
+      const link = this.link
+      if (link) {
+        this.$router.push(link)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .step-item{
+    cursor: pointer;
+  }
+  :global{
+    .ant-steps-item-process{
+      .linkable{
+        color: @primary-color;
+      }
+    }
+  }
+</style>

+ 69 - 0
src/components/tool/AvatarList.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="avatar-list">
+    <slot>
+    </slot>
+  </div>
+</template>
+
+<script>
+import AAvatar from 'ant-design-vue/es/avatar/Avatar'
+import ATooltip from 'ant-design-vue/es/tooltip/Tooltip'
+const Item = {
+  name: 'AvatarListItem',
+  props: {
+    size: {
+      type: String,
+      required: false,
+      default: 'small'
+    },
+    src: {
+      type: String,
+      required: true
+    },
+    tips: {
+      type: String,
+      required: false
+    }
+  },
+  methods: {
+    renderAvatar (h, size, src) {
+      return h(AAvatar, {props: {size: size, src: src}}, [])
+    }
+  },
+  render (h) {
+    const avatar = this.renderAvatar(h, this.$props.size, this.$props.src)
+    return h(
+      'li',
+      {class: 'avatar-item'},
+      [this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar]
+    )
+  }
+}
+export default {
+  name: 'AvatarList',
+  Item: Item
+}
+</script>
+
+<style lang="less" scoped>
+  .avatar-list {
+    display: inline-block;
+      display: inline-block;
+      margin-left: 8px;
+      font-size: 0;
+      .avatar-item {
+        display: inline-block;
+        font-size: 14px;
+        margin-left: -8px;
+        width: 20px;
+        height: 20px;
+        :global {
+          .ant-avatar {
+            border: 1px solid #fff;
+            width: 20px;
+            height: 20px;
+          }
+        }
+      }
+  }
+</style>

+ 156 - 0
src/components/tool/DetailList.vue

@@ -0,0 +1,156 @@
+<template>
+  <div :class="['detail-list', size === 'small' ? 'small' : 'large', layout === 'vertical' ? 'vertical': 'horizontal']">
+    <div v-if="title" class="title">{{title}}</div>
+    <a-row>
+      <slot></slot>
+    </a-row>
+  </div>
+</template>
+
+<script>
+import ACol from 'ant-design-vue/es/grid/Col'
+const Item = {
+  name: 'DetailListItem',
+  props: {
+    term: {
+      type: String,
+      required: false
+    }
+  },
+  inject: {
+    col: {
+      type: Number
+    }
+  },
+  methods: {
+    renderTerm (h, term) {
+      return term ? h(
+        'div',
+        {
+          attrs: {
+            class: 'term'
+          }
+        },
+        [term]
+      ) : null
+    },
+    renderContent (h, content) {
+      return h(
+        'div',
+        {
+          attrs: {
+            class: 'content'
+          }
+        },
+        [content]
+      )
+    }
+  },
+  render (h) {
+    const term = this.renderTerm(h, this.$props.term)
+    const content = this.renderContent(h, this.$slots.default)
+    return h(
+      ACol,
+      {
+        props: responsive[this.col]
+      },
+      [term, content]
+    )
+  }
+}
+
+const responsive = {
+  1: { xs: 24 },
+  2: { xs: 24, sm: 12 },
+  3: { xs: 24, sm: 12, md: 8 },
+  4: { xs: 24, sm: 12, md: 6 }
+}
+
+export default {
+  name: 'DetailList',
+  Item: Item,
+  props: {
+    title: {
+      type: String,
+      required: false
+    },
+    col: {
+      type: Number,
+      required: false,
+      default: 3
+    },
+    size: {
+      type: String,
+      required: false,
+      default: 'large'
+    },
+    layout: {
+      type: String,
+      required: false,
+      default: 'horizontal'
+    }
+  },
+  provide () {
+    return {
+      col: this.col > 4 ? 4 : this.col
+    }
+  }
+}
+</script>
+
+<style lang="less">
+  .detail-list{
+    .title {
+      font-size: 16px;
+      color: @title-color;
+      font-weight: bolder;
+      margin-bottom: 16px;
+    }
+    .term {
+      // Line-height is 22px IE dom height will calculate error
+      line-height: 20px;
+      padding-bottom: 16px;
+      margin-right: 8px;
+      color: @title-color;
+      white-space: nowrap;
+      display: table-cell;
+      &:after {
+        content: ':';
+        margin: 0 8px 0 2px;
+        position: relative;
+        top: -0.5px;
+      }
+    }
+    .content{
+      line-height: 22px;
+      width: 100%;
+      padding-bottom: 16px;
+      color: @text-color;
+      display: table-cell;
+    }
+    &.small{
+      .title{
+        font-size: 14px;
+        color: @text-color;
+        font-weight: normal;
+        margin-bottom: 12px;
+      }
+      .term,.content{
+        padding-bottom: 8px;
+      }
+    }
+    &.large{
+      .term,.content{
+        padding-bottom: 16px;
+      }
+    }
+    &.vertical{
+      .term {
+        padding-bottom: 8px;
+      }
+      .term,.content{
+        display: block;
+      }
+    }
+  }
+</style>

+ 142 - 0
src/components/tool/Drawer.vue

@@ -0,0 +1,142 @@
+<template>
+  <div >
+    <div :class="['mask', visible ? 'open' : 'close']" @click="close"></div>
+    <div :class="['drawer', placement, visible ? 'open' : 'close']">
+      <div ref="drawer" class="content beauty-scroll">
+        <slot></slot>
+      </div>
+      <div v-if="showHandler" :class="['handler-container', placement, visible ? 'open' : 'close']" ref="handler" @click="toggle">
+        <slot v-if="$slots.handler" name="handler"></slot>
+        <div v-else class="handler">
+          <a-icon :type="visible ? 'close'  : 'bars'" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Drawer',
+  data () {
+    return {
+    }
+  },
+  model: {
+    prop: 'visible',
+    event: 'change'
+  },
+  props: {
+    visible: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    placement: {
+      type: String,
+      required: false,
+      default: 'left'
+    },
+    showHandler: {
+      type: Boolean,
+      required: false,
+      default: true
+    }
+  },
+  methods: {
+    open () {
+      this.$emit('change', true)
+    },
+    close () {
+      this.$emit('change', false)
+    },
+    toggle () {
+      this.$emit('change', !this.visible)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .mask{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    top: 0;
+    background-color: @shadow-color;
+    transition: all 0.5s;
+    z-index: 100;
+    &.open{
+      display: inline-block;
+    }
+    &.close{
+      display: none;
+    }
+  }
+  .drawer{
+    position: fixed;
+    transition: all 0.5s;
+    height: 100vh;
+    z-index: 100;
+    &.left{
+      left: 0px;
+      &.open{
+        .content{
+          box-shadow: 2px 0 8px @shadow-color;
+        }
+      }
+      &.close{
+        transform: translateX(-100%);
+      }
+    }
+    &.right{
+      right: 0px;
+      .content{
+        float: right;
+      }
+      &.open{
+        .content{
+          box-shadow: -2px 0 8px @shadow-color;
+        }
+      }
+      &.close{
+        transform: translateX(100%);
+      }
+    }
+  }
+  .content{
+    display: inline-block;
+    height: 100vh;
+    overflow-y: auto;
+  }
+  .handler-container{
+    position: absolute;
+    display: inline-block;
+    text-align: center;
+    transition: all 0.5s;
+    cursor: pointer;
+    top: 200px;
+    z-index: 100;
+    .handler {
+      height: 40px;
+      width: 40px;
+      background-color: @base-bg-color;
+      font-size: 26px;
+      box-shadow: 0 2px 8px @shadow-color;
+      line-height: 40px;
+    }
+    &.left{
+      right: -40px;
+      .handler{
+        border-radius: 0 5px 5px 0;
+      }
+    }
+    &.right{
+      left: -40px;
+      .handler{
+        border-radius: 5px 0 0 5px;
+      }
+    }
+  }
+</style>

+ 30 - 0
src/components/tool/FooterToolBar.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="toolbar">
+    <div style="float: left">
+      <slot name="extra"></slot>
+    </div>
+    <div style="float: right">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FooterToolBar'
+}
+</script>
+
+<style lang="less" scoped>
+  .toolbar{
+    position: fixed;
+    width: 100%;
+    bottom: 0;
+    right: 0;
+    box-shadow: 0 -1px 2px @shadow-color;
+    background: @base-bg-color;
+    border-top: 1px solid @border-color-split;
+    padding: 12px 24px;
+    z-index: 9;
+  }
+</style>

+ 34 - 0
src/components/tool/HeadInfo.vue

@@ -0,0 +1,34 @@
+<template>
+  <div class="head-info">
+    <span>{{title}}</span>
+    <p>{{content}}</p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HeadInfo',
+  props: ['title', 'content', 'bordered']
+}
+</script>
+
+<style lang="less" scoped>
+  .head-info{
+    text-align: center;
+    padding: 0 24px;
+    flex-grow: 1;
+    flex-shrink: 0;
+    align-self: center;
+    span{
+      color: @text-color-second;
+      display: inline-block;
+      font-size: 14px;
+      margin-bottom: 4px;
+    }
+    p{
+      color: @text-color;
+      font-size: 24px;
+      margin: 0;
+    }
+  }
+</style>

+ 83 - 0
src/components/tool/TagSelect.vue

@@ -0,0 +1,83 @@
+<template>
+  <div class="tag-select">
+    <tag-select-option @click="toggleCheck">全部</tag-select-option>
+    <slot></slot>
+    <a @click="toggle" v-show="showTrigger" ref="trigger" class="trigger">展开<a-icon style="margin-left: 5px" :type="collapsed ? 'down' : 'up'"/></a>
+  </div>
+</template>
+
+<script>
+import TagSelectOption from './TagSelectOption'
+export default {
+  name: 'TagSelect',
+  Option: TagSelectOption,
+  components: {TagSelectOption},
+  data () {
+    return {
+      showTrigger: false,
+      collapsed: true,
+      screenWidth: document.body.clientWidth,
+      checkAll: false
+    }
+  },
+  watch: {
+    screenWidth: function () {
+      this.showTrigger = this.needTrigger()
+    },
+    collapsed: function (val) {
+      this.$el.style.maxHeight = val ? '39px' : '78px'
+    }
+  },
+  mounted () {
+    let _this = this
+    // 此处延迟执行,是为解决mouted未完全完成情况下引发的trigger显示bug
+    setTimeout(() => {
+      _this.showTrigger = _this.needTrigger()
+      _this.$refs.trigger.style.display = _this.showTrigger ? 'inline' : 'none'
+    }, 1)
+    window.onresize = () => {
+      return (() => {
+        window.screenWidth = document.body.clientWidth
+        _this.screenWidth = window.screenWidth
+      })()
+    }
+  },
+  methods: {
+    needTrigger () {
+      return this.$el.clientHeight < this.$el.scrollHeight || this.$el.scrollHeight > 39
+    },
+    toggle () {
+      this.collapsed = !this.collapsed
+    },
+    getAllTags () {
+      const tagList = this.$children.filter((item) => {
+        return item.isTagSelectOption
+      })
+      return tagList
+    },
+    toggleCheck () {
+      this.checkAll = !this.checkAll
+      const tagList = this.getAllTags()
+      tagList.forEach((item) => {
+        item.checked = this.checkAll
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .tag-select{
+    user-select: none;
+    position: relative;
+    overflow: hidden;
+    max-height: 39px;
+    padding-right: 50px;
+    display: inline-block;
+  }
+  .trigger{
+    position: absolute;
+    top: 0;
+    right: 0;
+  }
+</style>

+ 33 - 0
src/components/tool/TagSelectOption.vue

@@ -0,0 +1,33 @@
+<template>
+  <a-checkable-tag @change="$emit('click')" class="tag-default" v-model="checked">
+    <slot></slot>
+  </a-checkable-tag>
+</template>
+
+<script>
+export default {
+  name: 'TagSelectOption',
+  props: {
+    size: {
+      type: String,
+      required: false,
+      default: 'default'
+    }
+  },
+  data () {
+    return {
+      checked: false,
+      isTagSelectOption: true
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .tag-default{
+    font-size: 14px;
+    padding: 0 8px;
+    height: auto;
+    margin-right: 24px;
+  }
+</style>

+ 96 - 0
src/components/transition/PageToggleTransition.vue

@@ -0,0 +1,96 @@
+<template>
+  <transition
+    v-if="!disabled"
+    :enter-active-class="`animated ${enterAnimate} page-toggle-enter-active`"
+    :leave-active-class="`animated ${leaveAnimate} page-toggle-leave-active`"
+  >
+    <slot></slot>
+  </transition>
+  <div v-else><slot></slot></div>
+</template>
+
+<script>
+  import {preset as animates} from '@/config/default/animate.config'
+
+  export default {
+    name: 'PageToggleTransition',
+    props: {
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      animate: {
+        type: String,
+        validator(value) {
+          return animates.findIndex(item => item.name == value) != -1
+        },
+        default: 'bounce'
+      },
+      direction: {
+        type: String,
+        validator(value) {
+          return ['x', 'y', 'left', 'right', 'up', 'down', 'downLeft', 'upRight', 'downRight', 'upLeft', 'downBig',
+            'upBig', 'downLeft', 'downRight', 'topRight', 'bottomLeft', 'topLeft', 'bottomRight', 'default'].indexOf(value) > -1
+        }
+      },
+      reverse: {
+        type: Boolean,
+        default: true
+      }
+    },
+    computed: {
+      enterAnimate() {
+        return this.activeClass(false)
+      },
+      leaveAnimate() {
+        return this.activeClass(true)
+      }
+    },
+    methods: {
+      activeClass(isLeave) {
+        let animate = animates.find(item => this.animate == item.name)
+        if (animate == undefined) {
+          return ''
+        }
+        let direction = ''
+        if (this.direction == undefined) {
+          direction = animate.directions[0]
+        } else {
+          direction = animate.directions.find(item => item == this.direction)
+        }
+        direction = (direction == undefined || direction === 'default') ? '' : direction
+        if (direction != '') {
+          direction = isLeave && this.reverse ? this.reversePosition(direction, animate.directions) : direction
+          direction = direction[0].toUpperCase() + direction.substring(1)
+        }
+        let t = isLeave ? 'Out' : 'In'
+        return animate.name + t + direction
+      },
+      reversePosition(direction, directions) {
+        if(direction.length == 0 || direction == 'x' || direction == 'y') {
+          return direction
+        }
+        let index = directions.indexOf(direction)
+        index = (index % 2 == 1) ? index - 1 : index + 1
+        return directions[index]
+      }
+    }
+  }
+</script>
+
+<style lang="less">
+  .page-toggle-enter-active{
+    position: absolute !important;
+    animation-duration: 0.8s !important;
+    width: calc(100%) !important;
+  }
+  .page-toggle-leave-active{
+    position: absolute !important;
+    animation-duration: 0.8s !important;
+    width: calc(100%) !important;
+  }
+  .page-toggle-enter{
+  }
+  .page-toggle-leave-to{
+  }
+</style>

+ 58 - 0
src/components/tree/tree.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="tree">
+    <a-tree
+      :tree-data="treeData"
+      :title="'11'"
+      :replaceFields="replaceFields"
+      @select="onSelect"
+    >
+      <template slot="custom" slot-scope="item">
+        <span>{{ item }}</span>
+        <!-- <span style="color: #f50">({{ item.num }})</span> -->
+      </template>
+    </a-tree>
+  </div>
+</template>
+  
+  <script>
+// import mixin from "../../minins/mixin";
+export default {
+  name: "FormRow",
+  //   mixins: [mixin],
+  props: {},
+  data() {
+    return {
+      treeData: [],
+      replaceFields: {
+        children: "children",
+        title: "name",
+        key: "id",
+      },
+    };
+  },
+  mounted() {
+    this.getMechanismTree();
+  },
+  methods: {
+    getMechanismTree() {
+      this.$API.mechanism.getMechanismTree({ type: 0, id: 9 }).then((res) => {
+        if (res.data.code === 200) {
+          this.treeData = res.data.data;
+        }
+      });
+    },
+
+    onSelect(keys) {
+      this.$emit("getList", keys[0]);
+    },
+  },
+};
+</script>
+  
+<style lang="less" scoped>
+.tree {
+  height: 900px;
+  overflow-y: auto;
+}
+</style>
+  

+ 35 - 0
src/config/config.js

@@ -0,0 +1,35 @@
+// 自定义配置,参考 ./default/setting.config.js,需要自定义的属性在这里配置即可
+module.exports = {
+  theme: {
+    color: '#2A55E5',
+    mode: 'light',
+    success: '#52c41a',
+    warning: '#faad14',
+    error: '#f5222f'
+  },
+  multiPage: true,
+  animate: {
+    disabled: true,
+    name: 'lightSpeed',
+    direction: 'left'
+  }
+}
+
+// // 自定义配置,参考 ./default/setting.config.js,需要自定义的属性在这里配置即可
+// module.exports = {
+//   theme: {
+//     color: '#fa541c',
+//     mode: 'light',
+//     success: '#52c41a',
+//     warning: '#faad14',
+//     error: '#f5222f'
+//   },
+//   multiPage: true,
+//   animate: {
+//     disabled: true,
+//     name: 'lightSpeed',
+//     direction: 'left'
+//   }
+// }
+       
+      

+ 18 - 0
src/config/default/admin.config.js

@@ -0,0 +1,18 @@
+// admin 配置
+const ADMIN = {
+  palettes: ['#f5222d', '#fa541c', '#fadb14', '#3eaf7c', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96'],
+  animates: require('./animate.config').preset,
+  theme: {
+    mode: {
+      DARK: 'dark',
+      LIGHT: 'light',
+      NIGHT: 'night'
+    }
+  },
+  layout: {
+    SIDE: 'side',
+    HEAD: 'head'
+  }
+}
+
+module.exports = ADMIN

+ 21 - 0
src/config/default/animate.config.js

@@ -0,0 +1,21 @@
+const direct_s = ['left', 'right']
+const direct_1 = ['left', 'right', 'down', 'up']
+const direct_1_b = ['downBig', 'upBig', 'leftBig', 'rightBig']
+const direct_2 = ['topLeft', 'bottomRight', 'topRight', 'bottomLeft']
+const direct_3 = ['downLeft', 'upRight', 'downRight', 'upLeft']
+
+// animate.css 配置
+const ANIMATE = {
+  preset: [ //预设动画配置
+    {name: 'back', alias: '渐近', directions: direct_1},
+    {name: 'bounce', alias: '弹跳', directions: direct_1.concat('default')},
+    {name: 'fade', alias: '淡化', directions: direct_1.concat(direct_1_b).concat(direct_2).concat('default')},
+    {name: 'flip', alias: '翻转', directions: ['x', 'y']},
+    {name: 'lightSpeed', alias: '光速', directions: direct_s},
+    {name: 'rotate', alias: '旋转', directions: direct_3.concat('default')},
+    {name: 'roll', alias: '翻滚', directions: ['default']},
+    {name: 'zoom', alias: '缩放', directions: direct_1.concat('default')},
+    {name: 'slide', alias: '滑动', directions: direct_1},
+  ]
+}
+module.exports = ANIMATE

+ 84 - 0
src/config/default/antd.config.js

@@ -0,0 +1,84 @@
+// antd 配置
+const ANTD = {
+  primary: {
+    color: '#1890ff',
+    warning: '#faad14',
+    success: '#52c41a',
+    error: '#f5222d',
+    light: {
+      menuColors: ['#000c17', '#001529', '#002140']
+    },
+    dark: {
+      menuColors: ['#000c17', '#001529', '#002140']
+    },
+    night: {
+      menuColors: ['#151515', '#1f1f1f', '#1e1e1e'],
+    }
+  },
+  theme: {
+    dark: {
+      'layout-body-background': '#f0f2f5',
+      'body-background': '#fff',
+      'component-background': '#fff',
+      'heading-color': 'rgba(0, 0, 0, 0.85)',
+      'text-color': 'rgba(0, 0, 0, 0.65)',
+      'text-color-inverse': '#fff',
+      'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
+      'shadow-color': 'rgba(0, 0, 0, 0.15)',
+      'border-color-split': '#f0f0f0',
+      'background-color-light': '#fafafa',
+      'background-color-base': '#f5f5f5',
+      'table-selected-row-bg': '#fafafa',
+      'table-expanded-row-bg': '#fbfbfb',
+      'checkbox-check-color': '#fff',
+      'disabled-color': 'rgba(0, 0, 0, 0.25)',
+      'menu-dark-color': 'rgba(254, 254, 254, 0.65)',
+      'menu-dark-highlight-color': '#fefefe',
+      'menu-dark-arrow-color': '#fefefe',
+      'btn-primary-color': '#fff',
+    },
+    light: {
+      'layout-body-background': '#f0f2f5',
+      'body-background': '#fff',
+      'component-background': '#fff',
+      'heading-color': 'rgba(0, 0, 0, 0.85)',
+      'text-color': 'rgba(0, 0, 0, 0.65)',
+      'text-color-inverse': '#fff',
+      'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
+      'shadow-color': 'rgba(0, 0, 0, 0.15)',
+      'border-color-split': '#f0f0f0',
+      'background-color-light': '#fafafa',
+      'background-color-base': '#f5f5f5',
+      'table-selected-row-bg': '#fafafa',
+      'table-expanded-row-bg': '#fbfbfb',
+      'checkbox-check-color': '#fff',
+      'disabled-color': 'rgba(0, 0, 0, 0.25)',
+      'menu-dark-color': 'rgba(1, 1, 1, 0.65)',
+      'menu-dark-highlight-color': '#fefefe',
+      'menu-dark-arrow-color': '#fefefe',
+      'btn-primary-color': '#fff',
+    },
+    night: {
+      'layout-body-background': '#000',
+      'body-background': '#141414',
+      'component-background': '#141414',
+      'heading-color': 'rgba(255, 255, 255, 0.85)',
+      'text-color': 'rgba(255, 255, 255, 0.85)',
+      'text-color-inverse': '#141414',
+      'text-color-secondary': 'rgba(255, 255, 255, 0.45)',
+      'shadow-color': 'rgba(255, 255, 255, 0.15)',
+      'border-color-split': '#303030',
+      'background-color-light': '#ffffff0a',
+      'background-color-base': '#2a2a2a',
+      'table-selected-row-bg': '#ffffff0a',
+      'table-expanded-row-bg': '#ffffff0b',
+      'checkbox-check-color': '#141414',
+      'disabled-color': 'rgba(255, 255, 255, 0.25)',
+      'menu-dark-color': 'rgba(254, 254, 254, 0.65)',
+      'menu-dark-highlight-color': '#fefefe',
+      'menu-dark-arrow-color': '#fefefe',
+      'btn-primary-color': '#141414',
+    }
+  }
+}
+module.exports = ANTD

+ 6 - 0
src/config/default/index.js

@@ -0,0 +1,6 @@
+const ANTD = require('./antd.config')
+const ADMIN = require('./admin.config')
+const ANIMATE = require('./animate.config')
+const setting = require('./setting.config')
+
+module.exports = {ANTD, ADMIN, ANIMATE, setting}

+ 35 - 0
src/config/default/setting.config.js

@@ -0,0 +1,35 @@
+// 此配置为系统默认设置,需修改的设置项,在src/config/config.js中添加修改项即可。也可直接在此文件中修改。
+module.exports = {
+  lang: 'CN',                           //语言,可选 CN(简体)、HK(繁体)、US(英语),也可扩展其它语言
+  theme: {                              //主题
+    color: '#1890ff',                   //主题色
+    mode: 'dark',                       //主题模式 可选 dark、 light 和 night
+    success: '#52c41a',                 //成功色
+    warning: '#faad14',                 //警告色
+    error: '#f5222f',                   //错误色
+  },
+  layout: 'side',                       //导航布局,可选 side 和 head,分别为侧边导航和顶部导航
+  fixedHeader: false,                   //固定头部状态栏,true:固定,false:不固定
+  fixedSideBar: true,                   //固定侧边栏,true:固定,false:不固定
+  fixedTabs: false,                      //固定页签头,true:固定,false:不固定
+  pageWidth: 'fixed',                   //内容区域宽度,fixed:固定宽度,fluid:流式宽度
+  weekMode: false,                      //色弱模式,true:开启,false:不开启
+  multiPage: false,                     //多页签模式,true:开启,false:不开启
+  cachePage: true,                      //是否缓存页面数据,仅多页签模式下生效,true 缓存, false 不缓存
+  hideSetting: true,                   //隐藏设置抽屉,true:隐藏,false:不隐藏
+  systemName: 'Vue Antd Admin',         //系统名称
+  copyright: '2018 ICZER 工作室出品',     //copyright
+  asyncRoutes: true,                   //异步加载路由,true:开启,false:不开启
+  showPageTitle: true,                  //是否显示页面标题(PageLayout 布局中的页面标题),true:显示,false:不显示
+  filterMenu: true,                    //根据权限过滤菜单,true:过滤,false:不过滤
+  animate: {                            //动画设置
+    disabled: false,                    //禁用动画,true:禁用,false:启用
+    name: 'bounce',                     //动画效果,支持的动画效果可参考 ./animate.config.js
+    direction: 'left'                   //动画方向,切换页面时动画的方向,参考 ./animate.config.js
+  },
+  footerLinks: [                        //页面底部链接,{link: '链接地址', name: '名称/显示文字', icon: '图标,支持 ant design vue 图标库'}
+    { link: 'https://pro.ant.design', name: 'Pro首页' },
+    { link: 'https://github.com/iczer/vue-antd-admin', icon: 'github' },
+    { link: 'https://ant.design', name: 'Ant Design' }
+  ],
+}

+ 6 - 0
src/config/index.js

@@ -0,0 +1,6 @@
+const deepMerge = require('deepmerge')
+const _config = require('./config')
+const {setting} = require('./default')
+const config = deepMerge(setting, _config)
+
+module.exports = config

+ 10 - 0
src/config/replacer/index.js

@@ -0,0 +1,10 @@
+/**
+ * webpack-theme-color-replacer 配置
+ * webpack-theme-color-replacer 是一个高效的主题色替换插件,可以实现系统运行时动态切换主题功能。
+ * 但有些情景下,我们需要为 webpack-theme-color-replacer 配置一些规则,以达到我们的个性化需求的目的
+ *
+ * @cssResolve: css处理规则,在 webpack-theme-color-replacer 提取 需要替换主题色的 css 后,应用此规则。一般在
+ *              webpack-theme-color-replacer 默认规则无法达到我们的要求时使用。
+ */
+const cssResolve = require('./resolve.config')
+module.exports = {cssResolve}

+ 67 - 0
src/config/replacer/resolve.config.js

@@ -0,0 +1,67 @@
+/**
+ * webpack-theme-color-replacer 插件的 resolve 配置
+ * 为特定的 css 选择器(selector)配置 resolve 规则。
+ *
+ * key 为 css selector 值或合法的正则表达式字符串
+ * 当 key 设置 css selector 值时,会匹配对应的 css
+ * 当 key 设置为正则表达式时,会匹配所有满足此正则表达式的的 css
+ *
+ * value 可以设置为 boolean 值 false 或 一个对象
+ * 当 value 为 false 时,则会忽略此 css,即此 css 不纳入 webpack-theme-color-replacer 管理
+ * 当 value 为 对象时,会调用该对象的 resolve 函数,并传入 cssText(原始的 css文本) 和 cssObj(css对象)参数; resolve函数应该返
+ * 回一个处理后的、合法的 css字符串(包含 selector)
+ * 注意: value 不能设置为 true
+ */
+const cssResolve = {
+  '.ant-checkbox-checked .ant-checkbox-inner::after': {
+    resolve(cssText, cssObj) {
+      cssObj.rules.push('border-top:0', 'border-left:0')
+      return cssObj.toText()
+    }
+  },
+  '.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after': {
+    resolve(cssText, cssObj) {
+      cssObj.rules.push('border-top:0', 'border-left:0')
+      return cssObj.toText()
+    }
+  },
+  '.ant-checkbox-checked .ant-checkbox-inner:after': {
+    resolve(cssText, cssObj) {
+      cssObj.rules.push('border-top:0', 'border-left:0')
+      return cssObj.toText()
+    }
+  },
+  '.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after': {
+    resolve(cssText, cssObj) {
+      cssObj.rules.push('border-top:0', 'border-left:0')
+      return cssObj.toText()
+    }
+  },
+  '.ant-menu-dark .ant-menu-inline.ant-menu-sub': {
+    resolve(cssText, cssObj) {
+      cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1)
+      return cssObj.toText()
+    }
+  },
+  '.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu:hover,.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-submenu-selected': {
+    resolve(cssText, cssObj) {
+      cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')
+      return cssObj.toText()
+    }
+  },
+  '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': {
+    resolve(cssText, cssObj) {
+      cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')
+      return cssObj.toText()
+    }
+  },
+  '.ant-layout-sider': {
+    resolve(cssText, cssObj) {
+      cssObj.selector = '.ant-layout-sider-dark'
+      return cssObj.toText()
+    }
+  },
+  '/keyframes/': false
+}
+
+module.exports = cssResolve

+ 228 - 0
src/layouts/AdminLayout.vue

@@ -0,0 +1,228 @@
+<template>
+  <a-layout :class="['admin-layout', 'beauty-scroll']">
+    <drawer v-if="isMobile" v-model="drawerOpen">
+      <side-menu
+        :theme="theme.mode"
+        :menuData="menuData"
+        :collapsed="false"
+        :collapsible="false"
+        @menuSelect="onMenuSelect"
+      />
+    </drawer>
+    <side-menu
+      :class="[fixedSideBar ? 'fixed-side' : '']"
+      :theme="theme.mode"
+      v-else-if="layout === 'side' || layout === 'mix'"
+      :menuData="sideMenuData"
+      :collapsed="collapsed"
+      :collapsible="true"
+    />
+
+    <div
+      v-if="fixedSideBar && !isMobile"
+      :style="`width: ${sideMenuWidth}; min-width: ${sideMenuWidth};max-width: ${sideMenuWidth};`"
+      class="virtual-side"
+    ></div>
+    <drawer v-if="!hideSetting" v-model="showSetting" placement="right">
+      <div class="setting" slot="handler">
+        <a-icon :type="showSetting ? 'close' : 'setting'" />
+      </div>
+      <setting />
+    </drawer>
+    <a-layout class="admin-layout-main beauty-scroll">
+      <admin-header
+        :class="[
+          {
+            'fixed-tabs': fixedTabs,
+            'fixed-header': fixedHeader,
+            'multi-page': multiPage,
+          },
+        ]"
+        :style="headerStyle"
+        :menuData="headMenuData"
+        :collapsed="collapsed"
+        @toggleCollapse="toggleCollapse"
+      />
+      <!-- <a-layout-header :class="['virtual-header', {'fixed-tabs' : fixedTabs, 'fixed-header': fixedHeader, 'multi-page': multiPage}]" v-show="fixedHeader"></a-layout-header> -->
+      <a-layout-content
+        class="admin-layout-content"
+        :style="`min-height: ${minHeight}px;`"
+      >
+        <div style="position: relative" class="admin-layout-content-subject">
+          <slot></slot>
+        </div>
+      </a-layout-content>
+      <!-- <a-layout-footer style="padding: 0px">
+        <page-footer :link-list="footerLinks" :copyright="copyright" />
+      </a-layout-footer> -->
+    </a-layout>
+  </a-layout>
+</template>
+
+<script>
+import AdminHeader from "./header/AdminHeader";
+// import PageFooter from './footer/PageFooter'
+import Drawer from "../components/tool/Drawer";
+import SideMenu from "../components/menu/SideMenu";
+import Setting from "../components/setting/Setting";
+import { mapState, mapMutations, mapGetters } from "vuex";
+
+// const minHeight = window.innerHeight - 64 - 122
+
+export default {
+  name: "AdminLayout",
+  components: { Setting, SideMenu, Drawer, AdminHeader },
+  data() {
+    return {
+      minHeight: window.innerHeight - 64 - 122,
+      collapsed: false,
+      showSetting: false,
+      drawerOpen: false,
+    };
+  },
+  provide() {
+    return {
+      adminLayout: this,
+    };
+  },
+  watch: {
+    $route(val) {
+      this.setActivated(val);
+    },
+    layout() {
+      this.setActivated(this.$route);
+    },
+    isMobile(val) {
+      if (!val) {
+        this.drawerOpen = false;
+      }
+    },
+  },
+  computed: {
+    ...mapState("setting", [
+      "isMobile",
+      "theme",
+      "layout",
+      "footerLinks",
+      "copyright",
+      "fixedHeader",
+      "fixedSideBar",
+      "fixedTabs",
+      "hideSetting",
+      "multiPage",
+    ]),
+    ...mapGetters("setting", ["firstMenu", "subMenu", "menuData"]),
+    sideMenuWidth() {
+      return this.collapsed ? "80px" : "256px";
+    },
+    headerStyle() {
+      let width =
+        this.fixedHeader && this.layout !== "head" && !this.isMobile
+          ? `calc(100% - ${this.sideMenuWidth})`
+          : "100%";
+      let position = this.fixedHeader ? "fixed" : "static";
+      // console.log(width);
+      // console.log(position);
+      return `width: ${width}; position: ${position};`;
+    },
+    headMenuData() {
+      const { layout, menuData, firstMenu } = this;
+      return layout === "mix" ? firstMenu : menuData;
+    },
+    sideMenuData() {
+      const { layout, menuData, subMenu } = this;
+      console.log(menuData, 666, this);
+
+      return layout === "mix" ? subMenu : menuData;
+    },
+  },
+  methods: {
+    ...mapMutations("setting", ["correctPageMinHeight", "setActivatedFirst"]),
+    toggleCollapse() {
+      this.collapsed = !this.collapsed;
+    },
+    onMenuSelect() {
+      this.toggleCollapse();
+    },
+    setActivated(route) {
+      if (this.layout === "mix") {
+        let matched = route.matched;
+        matched = matched.slice(0, matched.length - 1);
+        const { firstMenu } = this;
+        for (let menu of firstMenu) {
+          if (matched.findIndex((item) => item.path === menu.fullPath) !== -1) {
+            this.setActivatedFirst(menu.fullPath);
+            break;
+          }
+        }
+      }
+    },
+  },
+  created() {
+    this.correctPageMinHeight(this.minHeight - 24);
+    this.setActivated(this.$route);
+  },
+  beforeDestroy() {
+    this.correctPageMinHeight(-this.minHeight + 24);
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.admin-layout {
+  height: 100%;
+  .side-menu {
+    &.fixed-side {
+      position: fixed;
+      height: 100vh;
+      left: 0;
+      top: 0;
+    }
+  }
+  .virtual-side {
+    transition: all 0.2s;
+  }
+  .virtual-header {
+    transition: all 0.2s;
+    opacity: 0;
+    &.fixed-tabs.multi-page:not(.fixed-header) {
+      height: 0;
+    }
+  }
+  .admin-layout-main {
+    // min-height: 100vh;
+    height: 100%;
+    .admin-header {
+      top: 0;
+      right: 0;
+      overflow: hidden;
+      transition: all 0.2s;
+      &.fixed-tabs.multi-page:not(.fixed-header) {
+        height: 0;
+      }
+    }
+  }
+  .admin-layout-content {
+    padding: 24px 24px 0;
+    box-sizing: border-box;
+    // background: #fff;
+    /*overflow-x: hidden;*/
+    /*min-height: calc(100vh - 64px - 122px);*/
+    .admin-layout-content-subject {
+      height: 100%;
+      padding-top: 24px;
+      box-sizing: border-box;
+    }
+  }
+  .setting {
+    background-color: @primary-color;
+    color: @base-bg-color;
+    border-radius: 5px 0 0 5px;
+    line-height: 40px;
+    font-size: 22px;
+    width: 40px;
+    height: 40px;
+    box-shadow: -2px 0 8px @shadow-color;
+  }
+}
+</style>

+ 22 - 0
src/layouts/BlankView.vue

@@ -0,0 +1,22 @@
+<template>
+  <page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
+    <router-view />
+  </page-toggle-transition>
+</template>
+
+<script>
+import PageToggleTransition from '../components/transition/PageToggleTransition';
+import {mapState} from 'vuex'
+
+export default {
+  name: 'BlankView',
+  components: {PageToggleTransition},
+  computed: {
+    ...mapState('setting', ['multiPage', 'animate'])
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 144 - 0
src/layouts/PageLayout.vue

@@ -0,0 +1,144 @@
+<template>
+  <div class="page-layout">
+    <!-- <page-header ref="pageHeader" :style="`margin-top: ${multiPage ? 0 : -24}px`" :breadcrumb="breadcrumb" :title="pageTitle" :logo="logo" :avatar="avatar">
+      <slot name="action"  slot="action"></slot>
+      <slot slot="content" name="headerContent"></slot>
+      <div slot="content" v-if="!this.$slots.headerContent && desc">
+        <p>{{desc}}</p>
+        <div v-if="this.linkList" class="link">
+          <template  v-for="(link, index) in linkList">
+            <a :key="index" :href="link.href"><a-icon :type="link.icon" />{{link.title}}</a>
+          </template>
+        </div>
+      </div>
+      <slot v-if="this.$slots.extra" slot="extra" name="extra"></slot>
+    </page-header> -->
+    <div ref="page" :class="['page-content', layout, pageWidth]" >
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+// import PageHeader from '@/components/page/header/PageHeader'
+import {mapState, mapMutations} from 'vuex'
+import {getI18nKey} from '@/utils/routerUtil'
+
+export default {
+  name: 'PageLayout',
+  components: {},
+  props: ['desc', 'logo', 'title', 'avatar', 'linkList', 'extraImage'],
+  data () {
+    return {
+      page: {},
+      pageHeaderHeight: 0,
+    }
+  },
+  watch: {
+    $route() {
+      this.page = this.$route.meta.page
+    }
+  },
+  updated() {
+    if (!this._inactive) {
+      this.updatePageHeight()
+    }
+  },
+  activated() {
+    this.updatePageHeight()
+  },
+  deactivated() {
+    this.updatePageHeight(0)
+  },
+  mounted() {
+    this.updatePageHeight()
+  },
+  created() {
+    this.page = this.$route.meta.page
+  },
+  beforeDestroy() {
+    this.updatePageHeight(0)
+  },
+  computed: {
+    ...mapState('setting', ['layout', 'multiPage', 'pageMinHeight', 'pageWidth', 'customTitles']),
+    pageTitle() {
+      let pageTitle = this.page && this.page.title
+      return this.customTitle || (pageTitle && this.$t(pageTitle)) || this.title || this.routeName
+    },
+    routeName() {
+      const route = this.$route
+      return this.$t(getI18nKey(route.matched[route.matched.length - 1].path))
+    },
+    breadcrumb() {
+      let page = this.page
+      let breadcrumb = page && page.breadcrumb
+      if (breadcrumb) {
+        let i18nBreadcrumb = []
+        breadcrumb.forEach(item => {
+          i18nBreadcrumb.push(this.$t(item))
+        })
+        return i18nBreadcrumb
+      } else {
+        return this.getRouteBreadcrumb()
+      }
+    },
+    marginCorrect() {
+      return this.multiPage ? 24 : 0
+    }
+  },
+  methods: {
+    ...mapMutations('setting', ['correctPageMinHeight']),
+    getRouteBreadcrumb() {
+      let routes = this.$route.matched
+      const path = this.$route.path
+      let breadcrumb = []
+      routes.filter(item => path.includes(item.path))
+        .forEach(route => {
+        const path = route.path.length === 0 ? '/home' : route.path
+        breadcrumb.push(this.$t(getI18nKey(path)))
+      })
+      let pageTitle = this.page && this.page.title
+      if (this.customTitle || pageTitle) {
+        breadcrumb[breadcrumb.length - 1] = this.customTitle || pageTitle
+      }
+      return breadcrumb
+    },
+    /**
+     * 用于计算页面内容最小高度
+     * @param newHeight
+     */
+    updatePageHeight(newHeight =  this.marginCorrect) {
+      // this.correctPageMinHeight(this.pageHeaderHeight - newHeight)
+      this.pageHeaderHeight = newHeight
+    }
+  }
+}
+</script>
+
+<style lang="less">
+  .page-header{
+    margin: 0 -24px 0;
+  }
+  .link{
+    /*margin-top: 16px;*/
+    line-height: 24px;
+    a{
+      font-size: 14px;
+      margin-right: 32px;
+      i{
+        font-size: 22px;
+        margin-right: 8px;
+      }
+    }
+  }
+  .page-content{
+    position: relative;
+    // padding: 24px 0 0;
+    &.side{
+    }
+    &.head.fixed{
+      margin: 0 auto;
+      max-width: 1400px;
+    }
+  }
+</style>

+ 59 - 0
src/layouts/PageView.vue

@@ -0,0 +1,59 @@
+<template>
+  <page-layout :desc="''" :linkList="[]">
+    <div slot="extra" class="extraImg">
+      <img :src="extraImage" />
+    </div>
+    <page-toggle-transition
+      :disabled="animate.disabled"
+      :animate="animate.name"
+      :direction="animate.direction"
+    >
+      <router-view ref="page" />
+    </page-toggle-transition>
+  </page-layout>
+</template>
+
+<script>
+import PageLayout from "./PageLayout";
+import PageToggleTransition from "../components/transition/PageToggleTransition";
+import { mapState } from "vuex";
+
+export default {
+  name: "PageView",
+  components: { PageToggleTransition, PageLayout },
+  data() {
+    return {
+      page: {},
+    };
+  },
+  computed: {
+    ...mapState("setting", ["isMobile", "multiPage", "animate"]),
+    desc() {
+      return this.page.desc;
+    },
+    linkList() {
+      return this.page.linkList;
+    },
+    extraImage() {
+      return this.page?.extraImage;
+    },
+  },
+  mounted() {
+    this.page = this.$refs.page;
+  },
+  updated() {
+    this.page = this.$refs.page;
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.extraImg {
+  margin-top: -60px;
+  text-align: center;
+  width: 195px;
+  img {
+    width: 100%;
+  }
+}
+</style>

+ 161 - 0
src/layouts/header/AdminHeader.vue

@@ -0,0 +1,161 @@
+<template>
+  <a-layout-header :class="[headerTheme, 'admin-header']">
+    <div :class="['admin-header-wide', layout, pageWidth]">
+      <router-link
+        v-if="isMobile || layout === 'head'"
+        to="/"
+        :class="['logo', isMobile ? null : 'pc', headerTheme]"
+      >
+        <img width="32" src="@/assets/img/title.png" />
+        <h1 v-if="!isMobile">{{ systemName }}</h1>
+      </router-link>
+      <a-divider v-if="isMobile" type="vertical" />
+      <div class="breadcrumb">
+        <a-breadcrumb>
+          <a-breadcrumb-item :key="index" v-for="(item, index) in breadcrumb">
+            <router-link :to="item.path">{{ item.name }}</router-link>
+          </a-breadcrumb-item>
+        </a-breadcrumb>
+      </div>
+      <!-- <a-icon v-if="layout !== 'head'" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/> -->
+      <!-- <div v-if="layout !== 'side' && !isMobile" class="admin-header-menu" :style="`width: ${menuWidth};`">
+        <i-menu class="head-menu" :theme="headerTheme" mode="horizontal" :options="menuData" @select="onSelect"/>
+      </div> -->
+      <div :class="['admin-header-right', headerTheme]">
+        <!-- <header-search class="header-item" @active="val => searchActive = val" /> -->
+        <!-- <a-tooltip class="header-item" title="帮助文档" placement="bottom" >
+            <a href="https://iczer.gitee.io/vue-antd-admin-docs/" target="_blank">
+              <a-icon type="question-circle-o" />
+            </a>
+          </a-tooltip> -->
+        <!-- <ExpandOutlined /> -->
+
+        <!-- <a-icon :class="['header-notice-icon']" type="scan" /> -->
+
+        <HeaderExpand class="header-item"></HeaderExpand>
+        <header-notice class="header-item" />
+        <header-avatar class="header-item" />
+        <a-dropdown class="lang header-item">
+          <div><a-icon type="global" /> {{ langAlias }}</div>
+          <a-menu
+            @click="(val) => setLang(val.key)"
+            :selected-keys="[lang]"
+            slot="overlay"
+          >
+            <a-menu-item v-for="lang in langList" :key="lang.key">{{
+              lang.key.toLowerCase() + " " + lang.name
+            }}</a-menu-item>
+          </a-menu>
+        </a-dropdown>
+      </div>
+    </div>
+  </a-layout-header>
+</template>
+
+<script>
+// import HeaderSearch from './HeaderSearch'
+import HeaderNotice from "./HeaderNotice";
+import HeaderAvatar from "./HeaderAvatar";
+import HeaderExpand from "./HeaderExpand";
+
+// import IMenu from '@/components/menu/menu'
+import { mapState, mapMutations } from "vuex";
+// import { getI18nKey } from "@/utils/routerUtil";
+
+export default {
+  name: "AdminHeader",
+  components: { HeaderAvatar, HeaderNotice, HeaderExpand },
+  props: ["collapsed", "menuData"],
+  data() {
+    return {
+      langList: [
+        { key: "CN", name: "简体中文", alias: "简体" },
+        { key: "HK", name: "繁體中文", alias: "繁體" },
+        { key: "US", name: "English", alias: "English" },
+      ],
+      searchActive: false,
+      page: {},
+    };
+  },
+  created() {
+    this.page = this.$route.meta.page;
+  },
+  computed: {
+    ...mapState("setting", [
+      "theme",
+      "isMobile",
+      "layout",
+      "systemName",
+      "lang",
+      "pageWidth",
+    ]),
+    headerTheme() {
+      if (
+        this.layout == "side" &&
+        this.theme.mode == "dark" &&
+        !this.isMobile
+      ) {
+        return "light";
+      }
+      return this.theme.mode;
+    },
+    langAlias() {
+      let lang = this.langList.find((item) => item.key == this.lang);
+      return lang.alias;
+    },
+    menuWidth() {
+      const { layout, searchActive } = this;
+      const headWidth = layout === "head" ? "100% - 188px" : "100%";
+      const extraWidth = searchActive ? "600px" : "400px";
+      return `calc(${headWidth} - ${extraWidth})`;
+    },
+    breadcrumb() {
+      let page = this.page;
+      let breadcrumb = page && page.matched;
+      console.log(page);
+      if (breadcrumb) {
+        let i18nBreadcrumb = [];
+        breadcrumb.forEach((item) => {
+          i18nBreadcrumb.push(item.name);
+        });
+
+        return i18nBreadcrumb;
+      } else {
+        return this.getRouteBreadcrumb();
+      }
+    },
+  },
+  methods: {
+    toggleCollapse() {
+      this.$emit("toggleCollapse");
+    },
+    onSelect(obj) {
+      this.$emit("menuSelect", obj);
+    },
+    ...mapMutations("setting", ["setLang", "correctPageMinHeight"]),
+    // ...mapMutations('setting', ['correctPageMinHeight']),
+    getRouteBreadcrumb() {
+      let routes = this.$route.matched;
+      const path = this.$route.path;
+      let breadcrumb = [];
+      routes
+        .filter((item) => path.includes(item.path))
+        .forEach((route) => {
+          const path = route.path;
+
+          breadcrumb.push({ path: path, name: route.name });
+        });
+      let pageTitle = this.page && this.page.title;
+      if (this.customTitle || pageTitle) {
+        breadcrumb[breadcrumb.length - 1] = this.customTitle || pageTitle;
+      }
+      // console.log(breadcrumb)
+      return breadcrumb;
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+@import "index";
+</style>

+ 61 - 0
src/layouts/header/HeaderAvatar.vue

@@ -0,0 +1,61 @@
+<template>
+  <a-dropdown>
+    <div class="header-avatar" style="cursor: pointer">
+      <a-avatar class="avatar" size="small" shape="circle" :src="user.avatar" />
+      <span class="name">{{ user.name }}</span>
+    </div>
+    <a-menu :class="['avatar-menu']" slot="overlay">
+      <a-menu-item>
+        <a-icon type="user" />
+        <span>个人中心</span>
+      </a-menu-item>
+      <a-menu-item>
+        <a-icon type="setting" />
+        <span>设置</span>
+      </a-menu-item>
+      <a-menu-divider />
+      <a-menu-item @click="logout">
+        <a-icon style="margin-right: 8px" type="poweroff" />
+        <span>退出登录</span>
+      </a-menu-item>
+    </a-menu>
+  </a-dropdown>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import { logout } from "@/services/user";
+
+export default {
+  name: "HeaderAvatar",
+  computed: {
+    ...mapGetters("account", ["user"]),
+  },
+  methods: {
+    logout() {
+      logout();
+      // this.$router.push('/login')
+      window.location.href = "https://test.baoxianzhanggui.com/#/login";
+    },
+  },
+};
+</script>
+
+<style lang="less">
+.header-avatar {
+  display: inline-flex;
+  .avatar,
+  .name {
+    align-self: center;
+  }
+  .avatar {
+    margin-right: 8px;
+  }
+  .name {
+    font-weight: 500;
+  }
+}
+.avatar-menu {
+  width: 150px;
+}
+</style>

+ 82 - 0
src/layouts/header/HeaderExpand.vue

@@ -0,0 +1,82 @@
+<template>
+  
+    <div >
+      <span @click="fullscreen" class="header-notice">
+        <a-icon :class="['header-notice-icon']" type="scan" />
+      </span>
+    </div>
+    
+ 
+</template>
+
+<script>
+export default {
+  name: 'HeaderNotice',
+  data () {
+    return {
+      loading: false,
+      show: false
+    }
+  },
+  computed: {
+  },
+  methods: {
+    fullscreen () {
+      if (!document.fullscreenElement) {
+        this.enterFullScreen();
+      } else {
+        this.exitFullScreen();
+      }
+    },
+    enterFullScreen() {
+      let element = document.documentElement;
+      if (element.requestFullscreen) {
+        element.requestFullscreen();
+      } else if (element.mozRequestFullScreen) { /* Firefox */
+        element.mozRequestFullScreen();
+      } else if (element.webkitRequestFullscreen) { /* Chrome, Safari & Opera */
+        element.webkitRequestFullscreen();
+      } else if (element.msRequestFullscreen) { /* IE/Edge */
+        element.msRequestFullscreen();
+      }
+    },
+    exitFullScreen() {
+      if (document.exitFullscreen) {
+        document.exitFullscreen();
+      } else if (document.mozCancelFullScreen) { /* Firefox */
+        document.mozCancelFullScreen();
+      } else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
+        document.webkitExitFullscreen();
+      } else if (document.msExitFullscreen) { /* IE/Edge */
+        document.msExitFullscreen();
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less">
+  .header-notice{
+    display: inline-block;
+    transition: all 0.3s;
+    span {
+      vertical-align: initial;
+    }
+    .notice-badge{
+      color: inherit;
+      .header-notice-icon{
+        font-size: 16px;
+        padding: 4px;
+      }
+    }
+  }
+  .dropdown-tabs{
+    background-color: @base-bg-color;
+    box-shadow: 0 2px 8px @shadow-color;
+    border-radius: 4px;
+    .tab-pane{
+      padding: 0 24px 12px;
+      min-height: 250px;
+    }
+  }
+</style>

+ 92 - 0
src/layouts/header/HeaderNotice.vue

@@ -0,0 +1,92 @@
+<template>
+  <a-dropdown :trigger="['click']" v-model="show">
+    <div slot="overlay">
+      <a-spin :spinning="loading">
+        <a-tabs class="dropdown-tabs" :tabBarStyle="{textAlign: 'center'}" :style="{width: '297px'}">
+          <a-tab-pane tab="通知" key="1">
+            <a-list class="tab-pane">
+              <a-list-item>
+                <a-list-item-meta title="你收到了 14 份新周报" description="一年前">
+                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
+                </a-list-item-meta>
+              </a-list-item>
+              <a-list-item>
+                <a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
+                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
+                </a-list-item-meta>
+              </a-list-item>
+              <a-list-item>
+                <a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
+                  <a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
+                </a-list-item-meta>
+              </a-list-item>
+            </a-list>
+          </a-tab-pane>
+          <a-tab-pane tab="消息" key="2">
+            <a-list class="tab-pane"></a-list>
+          </a-tab-pane>
+          <a-tab-pane tab="待办" key="3">
+            <a-list class="tab-pane"></a-list>
+          </a-tab-pane>
+        </a-tabs>
+      </a-spin>
+    </div>
+    <span @click="fetchNotice" class="header-notice">
+      <a-badge class="notice-badge" count="12">
+        <a-icon :class="['header-notice-icon']" type="bell" />
+      </a-badge>
+    </span>
+  </a-dropdown>
+</template>
+
+<script>
+export default {
+  name: 'HeaderNotice',
+  data () {
+    return {
+      loading: false,
+      show: false
+    }
+  },
+  computed: {
+  },
+  methods: {
+    fetchNotice () {
+      if (this.loading) {
+        this.loading = false
+        return
+      }
+      this.loadding = true
+      setTimeout(() => {
+        this.loadding = false
+      }, 1000)
+    }
+  }
+}
+</script>
+
+<style lang="less">
+  .header-notice{
+    display: inline-block;
+    transition: all 0.3s;
+    span {
+      vertical-align: initial;
+    }
+    .notice-badge{
+      color: inherit;
+      .header-notice-icon{
+        font-size: 16px;
+        padding: 4px;
+      }
+    }
+  }
+  .dropdown-tabs{
+    background-color: @base-bg-color;
+    box-shadow: 0 2px 8px @shadow-color;
+    border-radius: 4px;
+    .tab-pane{
+      padding: 0 24px 12px;
+      min-height: 250px;
+    }
+  }
+</style>

+ 95 - 0
src/layouts/header/index.less

@@ -0,0 +1,95 @@
+.admin-header{
+  padding: 0;
+  z-index: 2;
+  box-shadow: @shadow-down;
+  // position: relative;
+  background: @base-bg-color;
+  // .head-menu{
+  //   height: 64px;
+  //   line-height: 64px;
+  //   vertical-align: middle;
+  //   box-shadow: none;
+  // }
+  &.dark{
+    background: @header-bg-color-dark;
+    color: white;
+  }
+  &.night{
+    .head-menu{
+      background: @base-bg-color;
+    }
+  }
+  .admin-header-wide{
+    padding-left: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    &.head.fixed{
+      max-width: 1400px;
+      margin: auto;
+      padding-left: 0;
+    }
+    &.side{
+      padding-right: 12px;
+    }
+    .logo {
+      height: 64px;
+      line-height: 58px;
+      vertical-align: top;
+      display: inline-block;
+      padding: 0 12px 0 24px;
+      cursor: pointer;
+      font-size: 20px;
+      color: inherit;
+      &.pc{
+        padding: 0 12px 0 0;
+      }
+      img {
+        vertical-align: middle;
+      }
+      h1{
+        color: inherit;
+        display: inline-block;
+        font-size: 16px;
+      }
+    }
+    .trigger {
+      font-size: 20px;
+      line-height: 64px;
+      padding: 0 24px;
+      cursor: pointer;
+      transition: color .3s;
+      &:hover{
+        color: @primary-color;
+      }
+    }
+    .admin-header-menu{
+      display: inline-block;
+    }
+    .admin-header-right{
+      float: right;
+      display: flex;
+      color: inherit;
+      .header-item{
+        color: inherit;
+        padding: 0 12px;
+        cursor: pointer;
+        align-self: center;
+        a{
+          color: inherit;
+          i{
+            font-size: 16px;
+          }
+        }
+      }
+      each(@theme-list, {
+        &.@{value} .header-item{
+          &:hover{
+            @class: ~'hover-bg-color-@{value}';
+            background-color: @@class;
+          }
+        }
+      })
+    }
+  }
+}

+ 174 - 0
src/layouts/tabs/TabsHead.vue

@@ -0,0 +1,174 @@
+<template>
+  <div :class="['tabs-head', layout, pageWidth]">
+   
+    <!-- <div v-if="affixed" class="virtual-tabs"></div> -->
+  </div>
+</template>
+
+<script>
+  import {mapState, mapMutations} from 'vuex'
+  import {getI18nKey} from '@/utils/routerUtil'
+
+  export default {
+    name: 'TabsHead',
+    i18n: {
+      messages: {
+        CN: {
+          lock: '点击锁定页签头',
+          unlock: '点击解除锁定',
+        },
+        HK: {
+          lock: '點擊鎖定頁簽頭',
+          unlock: '點擊解除鎖定',
+        },
+        US: {
+          lock: 'click to lock the tabs head',
+          unlock: 'click to unlock',
+        }
+      }
+    },
+    props: {
+      pageList: Array,
+      active: String,
+      fixed: Boolean
+    },
+    data() {
+      return {
+        affixed: false,
+      }
+    },
+    inject:['adminLayout'],
+    created() {
+      this.affixed = this.fixedTabs
+    },
+    computed: {
+      ...mapState('setting', ['layout', 'pageWidth', 'fixedHeader', 'fixedTabs', 'customTitles']),
+      lockTitle() {
+        return this.$t(this.fixedTabs ? 'unlock' : 'lock')
+      }
+    },
+    methods: {
+      ...mapMutations('setting', ['setFixedTabs']),
+      onLockClick() {
+        this.setFixedTabs(!this.fixedTabs)
+        if (this.fixedTabs) {
+          setTimeout(() => {
+            this.affixed = true
+          }, 200)
+        } else {
+          this.affixed = false
+        }
+      },
+      onTabClick(key) {
+        console.log(this.pageList)
+        if (this.active !== key) {
+          this.$emit('change', key)
+        }
+      },
+      onClose(key) {
+        console.log(key)
+
+        this.$emit('close', key)
+      },
+      onRefresh(page) {
+        console.log(page)
+
+        this.$emit('refresh', page.path, page)
+      },
+      onContextmenu(pageKey, e) {
+        console.log(pageKey,e)
+        
+        this.$emit('contextmenu', pageKey, e)
+      },
+      pageName(page) {
+        const pagePath = page.fullPath.split('?')[0]
+        const custom = this.customTitles.find(item => item.path === pagePath)
+        return (custom && custom.title) || page.title || this.$t(getI18nKey(page.keyPath))
+      }
+    }
+  }
+</script>
+
+<style scoped lang="less">
+  .tab{
+    margin: 0 -16px;
+    padding: 0 16px;
+    font-size: 14px;
+    user-select: none;
+    transition: all 0.2s;
+    .title{
+      display: inline-block;
+      height: 100%;
+    }
+    .icon-close{
+      font-size: 12px;
+      margin-left: 6px;
+      margin-right: -4px !important;
+      color: @text-color-second;
+      &:hover{
+        color: @text-color;
+      }
+    }
+    .icon-sync{
+      margin-left: -4px;
+      color: @primary-4;
+      transition: all 0.3s ease-in-out;
+      &:hover{
+        color: @primary-color;
+      }
+      font-size: 14px;
+      &.hide{
+        font-size: 0;
+      }
+    }
+  }
+  .tabs-head{
+    margin: 0 auto;
+    &.head.fixed{
+      width: 1400px;
+    }
+  }
+  .tabs-container{
+    margin: -16px auto 8px;
+    transition: top,left 0.2s;
+    .header-lock{
+      font-size: 18px;
+      cursor: pointer;
+      color: @primary-3;
+      &:hover{
+        color: @primary-color;
+      }
+    }
+    &.affixed{
+      margin: 0 auto;
+      top: 0px;
+      padding: 8px 24px 0;
+      position: fixed;
+      height: 48px;
+      z-index: 1;
+      background-color: @layout-body-background;
+      &.side,&.mix{
+        right: 0;
+        left: 256px;
+        &.collapsed{
+          left: 80px;
+        }
+      }
+      &.head{
+        width: inherit;
+        padding: 8px 0 0;
+        &.fluid{
+          left: 0;
+          right: 0;
+          padding: 8px 24px 0;
+        }
+      }
+      &.fixed-header{
+        top: 64px;
+      }
+    }
+  }
+  .virtual-tabs{
+    height: 48px;
+  }
+</style>

+ 390 - 0
src/layouts/tabs/TabsView.vue

@@ -0,0 +1,390 @@
+<template>
+  <admin-layout>
+    <contextmenu
+      :itemList="menuItemList"
+      :visible.sync="menuVisible"
+      @select="onMenuSelect"
+    />
+    <tabs-head
+      v-if="multiPage"
+      :active="activePage"
+      :page-list="pageList"
+      @change="changePage"
+      @close="remove"
+      @refresh="refresh"
+      @contextmenu="onContextmenu"
+    />
+    <div
+      :class="['tabs-view-content', layout, pageWidth]"
+      :style="`margin-top: ${multiPage ? -24 : 0}px`"
+    >
+      <page-toggle-transition
+        :disabled="animate.disabled"
+        :animate="animate.name"
+        :direction="animate.direction"
+      >
+        <!-- <a-keep-alive :exclude-keys="excludeKeys" v-if="multiPage && cachePage" v-model="clearCaches"> -->
+        <router-view ref="tabContent" :key="$route.path" />
+        <!-- </a-keep-alive> -->
+        <!-- <router-view ref="tabContent" v-else-if="!refreshing" /> -->
+      </page-toggle-transition>
+    </div>
+  </admin-layout>
+</template>
+
+<script>
+import AdminLayout from "@/layouts/AdminLayout";
+import Contextmenu from "@/components/menu/Contextmenu";
+import PageToggleTransition from "@/components/transition/PageToggleTransition";
+import { mapState, mapMutations } from "vuex";
+import { getI18nKey } from "@/utils/routerUtil";
+// import AKeepAlive from "@/components/cache/AKeepAlive";
+import TabsHead from "@/layouts/tabs/TabsHead";
+
+export default {
+  name: "TabsView",
+  i18n: require("./i18n"),
+  components: {
+    TabsHead,
+    PageToggleTransition,
+    Contextmenu,
+    AdminLayout,
+    // AKeepAlive,
+  },
+  data() {
+    return {
+      clearCaches: [],
+      pageList: [],
+      activePage: "",
+      menuVisible: false,
+      refreshing: false,
+      excludeKeys: [],
+    };
+  },
+  computed: {
+    ...mapState("setting", [
+      "multiPage",
+      "cachePage",
+      "animate",
+      "layout",
+      "pageWidth",
+    ]),
+    menuItemList() {
+      return [
+        { key: "1", icon: "vertical-right", text: this.$t("closeLeft") },
+        { key: "2", icon: "vertical-left", text: this.$t("closeRight") },
+        { key: "3", icon: "close", text: this.$t("closeOthers") },
+        { key: "4", icon: "sync", text: this.$t("refresh") },
+      ];
+    },
+    tabsOffset() {
+      return this.multiPage ? 24 : 0;
+    },
+  },
+  created() {
+    this.loadCacheConfig(this.$router?.options?.routes);
+    this.loadCachedTabs();
+    const route = this.$route;
+    if (this.pageList.findIndex((item) => item.path === route.path) === -1) {
+      this.pageList.push(this.createPage(route));
+    }
+    this.activePage = route.path;
+    if (this.multiPage) {
+      this.$nextTick(() => {
+        console.log(route);
+        this.setCachedKey(route);
+      });
+      this.addListener();
+    }
+  },
+  mounted() {
+    this.correctPageMinHeight(-this.tabsOffset);
+  },
+  beforeDestroy() {
+    this.removeListener();
+    this.correctPageMinHeight(this.tabsOffset);
+  },
+  watch: {
+    "$router.options.routes": function (val) {
+      this.excludeKeys = [];
+      this.loadCacheConfig(val);
+    },
+    $route: function (newRoute) {
+      this.activePage = newRoute.path;
+      const page = this.pageList.find((item) => item.path === newRoute.path);
+      if (!this.multiPage) {
+        this.pageList = [this.createPage(newRoute)];
+      } else if (page) {
+        page.fullPath = newRoute.fullPath;
+      } else if (!page) {
+        this.pageList.push(this.createPage(newRoute));
+      }
+      if (this.multiPage) {
+        this.$nextTick(() => {
+          console.log(newRoute);
+          this.setCachedKey(newRoute);
+        });
+      }
+    },
+    multiPage: function (newVal) {
+      if (!newVal) {
+        this.pageList = [this.createPage(this.$route)];
+        this.removeListener();
+      } else {
+        this.addListener();
+      }
+    },
+    tabsOffset(newVal, oldVal) {
+      this.correctPageMinHeight(oldVal - newVal);
+    },
+  },
+  methods: {
+    changePage(key) {
+      this.activePage = key;
+      const page = this.pageList.find((item) => item.path === key);
+      this.$router.push(page.fullPath);
+    },
+    remove(key, next) {
+      if (this.pageList.length === 1) {
+        return this.$message.warning(this.$t("warn"));
+      }
+      //清除缓存
+      let index = this.pageList.findIndex((item) => item.path === key);
+      this.clearCaches = this.pageList
+        .splice(index, 1)
+        .map((page) => page.cachedKey);
+      if (next) {
+        this.$router.push(next);
+      } else if (key === this.activePage) {
+        index =
+          index >= this.pageList.length ? this.pageList.length - 1 : index;
+        this.activePage = this.pageList[index].path;
+        this.$router.push(this.activePage);
+      }
+    },
+    refresh(key, page) {
+      page = page || this.pageList.find((item) => item.path === key);
+      page.loading = true;
+      this.clearCache(page);
+      if (key === this.activePage) {
+        this.reloadContent(() => (page.loading = false));
+      } else {
+        // 其实刷新很快,加这个延迟纯粹为了 loading 状态多展示一会儿,让用户感知刷新这一过程
+        setTimeout(() => (page.loading = false), 500);
+      }
+    },
+    onContextmenu(pageKey, e) {
+      if (pageKey) {
+        e.preventDefault();
+        e.meta = pageKey;
+        this.menuVisible = true;
+      }
+    },
+    onMenuSelect(key, target, pageKey) {
+      console.log(key);
+      switch (key) {
+        case "1":
+          this.closeLeft(pageKey);
+          break;
+        case "2":
+          this.closeRight(pageKey);
+          break;
+        case "3":
+          this.closeOthers(pageKey);
+          break;
+        case "4":
+          this.refresh(pageKey);
+          break;
+        default:
+          break;
+      }
+    },
+    closeOthers(pageKey) {
+      // 清除缓存
+      const clearPages = this.pageList.filter(
+        (item) => item.path !== pageKey && !item.unclose
+      );
+      this.clearCaches = clearPages.map((item) => item.cachedKey);
+      this.pageList = this.pageList.filter(
+        (item) => !clearPages.includes(item)
+      );
+      // 判断跳转
+      if (this.activePage != pageKey) {
+        this.activePage = pageKey;
+        this.$router.push(this.activePage);
+      }
+    },
+    closeLeft(pageKey) {
+      const index = this.pageList.findIndex((item) => item.path === pageKey);
+      // 清除缓存
+      const clearPages = this.pageList.filter(
+        (item, i) => i < index && !item.unclose
+      );
+      this.clearCaches = clearPages.map((item) => item.cachedKey);
+      this.pageList = this.pageList.filter(
+        (item) => !clearPages.includes(item)
+      );
+      // 判断跳转
+      if (!this.pageList.find((item) => item.path === this.activePage)) {
+        this.activePage = pageKey;
+        this.$router.push(this.activePage);
+      }
+    },
+    closeRight(pageKey) {
+      // 清除缓存
+      const index = this.pageList.findIndex((item) => item.path === pageKey);
+      const clearPages = this.pageList.filter(
+        (item, i) => i > index && !item.unclose
+      );
+      this.clearCaches = clearPages.map((item) => item.cachedKey);
+      this.pageList = this.pageList.filter(
+        (item) => !clearPages.includes(item)
+      );
+      // 判断跳转
+      if (!this.pageList.find((item) => item.path === this.activePage)) {
+        this.activePage = pageKey;
+        this.$router.push(this.activePage);
+      }
+    },
+    clearCache(page) {
+      page._init_ = false;
+      this.clearCaches = [page.cachedKey];
+    },
+    reloadContent(onLoaded) {
+      this.refreshing = true;
+      setTimeout(() => {
+        this.refreshing = false;
+        this.$nextTick(() => {
+          console.log(this.$route);
+          this.setCachedKey(this.$route);
+          if (typeof onLoaded === "function") {
+            onLoaded.apply(this, []);
+          }
+        });
+      }, 200);
+    },
+    pageName(page) {
+      return this.$t(getI18nKey(page.keyPath));
+    },
+    /**
+     * 添加监听器
+     */
+    addListener() {
+      window.addEventListener("page:close", this.closePageListener);
+      window.addEventListener("page:refresh", this.refreshPageListener);
+      window.addEventListener("unload", this.unloadListener);
+    },
+    /**
+     * 移出监听器
+     */
+    removeListener() {
+      window.removeEventListener("page:close", this.closePageListener);
+      window.removeEventListener("page:refresh", this.refreshPageListener);
+      window.removeEventListener("unload", this.unloadListener);
+    },
+    /**
+     * 页签关闭事件监听
+     * @param event 页签关闭事件
+     */
+    closePageListener(event) {
+      const { closeRoute, nextRoute } = event.detail;
+      const closePath =
+        typeof closeRoute === "string" ? closeRoute : closeRoute.path;
+      const path = closePath && closePath.split("?")[0];
+      this.remove(path, nextRoute);
+    },
+    /**
+     * 页面刷新事件监听
+     * @param event 页签关闭事件
+     */
+    refreshPageListener(event) {
+      const { pageKey } = event.detail;
+      const path = pageKey && pageKey.split("?")[0];
+      this.refresh(path);
+    },
+    /**
+     * 页面 unload 事件监听器,添加页签到 session 缓存,用于刷新时保留页签
+     */
+    unloadListener() {
+      const tabs = this.pageList.map((item) => ({ ...item, _init_: false }));
+      sessionStorage.setItem(
+        process.env.VUE_APP_TBAS_KEY,
+        JSON.stringify(tabs)
+      );
+    },
+    createPage(route) {
+      return {
+        keyPath: route.matched[route.matched.length - 1].path,
+        fullPath: route.fullPath,
+        loading: false,
+        path: route.path,
+        title: route.meta && route.meta.page && route.meta.page.title,
+        unclose:
+          route.meta && route.meta.page && route.meta.page.closable === false,
+      };
+    },
+    /**
+     * 设置页面缓存的key
+     * @param route 页面对应的路由
+     */
+    setCachedKey(route) {
+      const page = this.pageList.find((item) => item.path === route.path);
+      page.unclose =
+        route.meta && route.meta.page && route.meta.page.closable === false;
+      if (!page._init_) {
+        const vnode = this.$refs.tabContent.$vnode;
+        page.cachedKey = vnode.key + vnode.componentOptions.Ctor.cid;
+        page._init_ = true;
+      }
+    },
+    /**
+     * 加载缓存的 tabs
+     */
+    loadCachedTabs() {
+      const cachedTabsStr = sessionStorage.getItem(
+        process.env.VUE_APP_TBAS_KEY
+      );
+      if (cachedTabsStr) {
+        try {
+          const cachedTabs = JSON.parse(cachedTabsStr);
+          if (cachedTabs.length > 0) {
+            this.pageList = cachedTabs;
+          }
+        } catch (e) {
+          console.warn("failed to load cached tabs, got exception:", e);
+        } finally {
+          sessionStorage.removeItem(process.env.VUE_APP_TBAS_KEY);
+        }
+      }
+    },
+    loadCacheConfig(routes, pCache = true) {
+      routes.forEach((item) => {
+        const cacheAble = item.meta?.page?.cacheAble ?? pCache ?? true;
+        if (!cacheAble) {
+          this.excludeKeys.push(new RegExp(`${item.path}\\d+$`));
+        }
+        if (item.children) {
+          this.loadCacheConfig(item.children, cacheAble);
+        }
+      });
+    },
+    ...mapMutations("setting", ["correctPageMinHeight"]),
+  },
+};
+</script>
+
+<style scoped lang="less">
+.tabs-view {
+  margin: -16px auto 8px;
+  &.head.fixed {
+    max-width: 1400px;
+  }
+}
+.tabs-view-content {
+  position: relative;
+  &.head.fixed {
+    width: 1400px;
+    margin: 0 auto;
+  }
+}
+</style>

+ 25 - 0
src/layouts/tabs/i18n.js

@@ -0,0 +1,25 @@
+module.exports = {
+  messages: {
+    CN: {
+      closeLeft: '关闭左侧',
+      closeRight: '关闭右侧',
+      closeOthers: '关闭其它',
+      refresh: '刷新页面',
+      warn: '这是最后一页,不能再关闭了',
+    },
+    HK: {
+      closeLeft: '關閉左側',
+      closeRight: '關閉右側',
+      closeOthers: '關閉其它',
+      refresh: '刷新頁面',
+      warn: '這是最後一頁,不能再關閉了',
+    },
+    US: {
+      closeLeft: 'close left',
+      closeRight: 'close right',
+      closeOthers: 'close others',
+      refresh: 'refresh the page',
+      warn: 'This is the last page, you can\'t close it',
+    },
+  }
+}

+ 2 - 0
src/layouts/tabs/index.js

@@ -0,0 +1,2 @@
+import TabsView from './TabsView'
+export default TabsView

+ 64 - 0
src/main.js

@@ -0,0 +1,64 @@
+import Vue from 'vue'
+import App from './App.vue'
+import router from './router/config.js'
+import './theme/index.less'
+import Antd from 'ant-design-vue'
+import Viser from 'viser-vue'
+import '@/mock'
+import store from './store'
+import 'animate.css/source/animate.css'
+import Plugins from '@/plugins'
+import { initI18n } from '@/utils/i18n'
+// import bootstrap from '@/bootstrap'
+import 'moment/locale/zh-cn'
+// import Cookies from "js-cookie";
+import '@/services'
+// import { api } from "@/services/index"
+Vue.use(router)
+Vue.config.devtools = true
+// const router = initRouter(store.state.setting.asyncRoutes)
+const i18n = initI18n('CN', 'US')
+import "normalize.css"
+import { setAuthorization } from "@/utils/request";
+Vue.use(Antd)
+Vue.config.productionTip = false
+Vue.use(Viser)
+Vue.use(Plugins)
+//App.vue
+import { message } from 'ant-design-vue';
+window.$message = message  //window上挂载
+
+
+//ts申明window $message属性
+
+
+
+
+// bootstrap({ router, store, i18n, message: Vue.prototype.$message })
+import CryptoJS from "crypto-js";
+
+
+if (window.location.href.includes('token')) {
+  const decryptedBytes = CryptoJS.AES.decrypt(window.location.href.split("=")[1],
+    'token')
+  const token = decryptedBytes.toString(CryptoJS.enc.Utf8);
+
+
+  setAuthorization(token,)
+}
+
+// api.userService.getLoginUser().then((res) => {
+//   // console.log(res)
+//   if (res.data.code === 200) {
+//     sessionStorage.setItem("user", res.data.data.id);
+//     Cookies.set("user", res.data.data.id);
+//   }
+// })
+
+
+new Vue({
+  router,
+  store,
+  i18n,
+  render: h => h(App),
+}).$mount('#app')

+ 71 - 0
src/minins/mixin.js

@@ -0,0 +1,71 @@
+
+
+// 创建一个混入对象
+export default {
+    data() {
+        return {
+            wholeList: [],
+            provinceList: [],
+            cityList: [],
+            areaList: [],
+            storeList: []
+        }
+    },
+    created() {
+        // console.log('Mixin created!')
+        this.getWholeList()
+        // this.getMechanismTree()
+
+    },
+    methods: {
+
+        async getWholeList() {
+
+
+            this.$API.mechanism.getMechanism({ type: 0, id: 9 }).then(res => {
+
+                if (res.data.code === 200) {
+
+                    this.wholeList = res.data.data
+                }
+            })
+
+
+
+
+        },
+        getProvinceList(id) {
+            this.$API.mechanism.getMechanism({ type: 1, id: id }).then(res => {
+
+                if (res.data.code === 200) {
+
+                    this.provinceList = res.data.data
+                }
+            })
+        },
+        getCityList(id) {
+            this.$API.mechanism.getMechanism({ type: 2, id: id }).then(res => {
+                if (res.data.code === 200) {
+
+                    this.cityList = res.data.data
+                }
+            })
+        },
+        getAreaList(id) {
+            this.$API.mechanism.getMechanism({ type: 3, id: id }).then(res => {
+                if (res.data.code === 200) {
+
+                    this.areaList = res.data.data
+                }
+            })
+        },
+        getStoreList(id) {
+            this.$API.mechanism.getMechanism({ type: 4, id: id }).then(res => {
+                if (res.data.code === 200) {
+
+                    this.storeList = res.data.data
+                }
+            })
+        }
+    }
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно