萤火科技 2 gadi atpakaļ
vecāks
revīzija
f26f74c44b
100 mainītis faili ar 7314 papildinājumiem un 0 dzēšanām
  1. 3 0
      .browserslistrc
  2. 3 0
      .env
  3. 75 0
      .eslintrc.js
  4. 1 0
      .gitattributes
  5. 21 0
      .gitignore
  6. 5 0
      .prettierrc
  7. 7 0
      .travis.yml
  8. 201 0
      LICENSE
  9. 54 0
      README.md
  10. 28 0
      babel.config.js
  11. 46 0
      config/plugin.config.js
  12. 31 0
      docs/add-page-loading-animate.md
  13. 95 0
      docs/load-on-demand.md
  14. 28 0
      docs/multi-tabs.md
  15. 40 0
      docs/webpack-bundle-analyzer.md
  16. 23 0
      jest.config.js
  17. 11 0
      jsconfig.json
  18. 59 0
      package.json
  19. 5 0
      postcss.config.js
  20. 11 0
      public/config.js
  21. 32 0
      public/index.html
  22. 1 0
      public/loading/loading.css
  23. 1 0
      public/loading/loading.html
  24. 2 0
      public/loading/option2/html_code_segment.html
  25. 1 0
      public/loading/option2/loading.css
  26. 0 0
      public/loading/option2/loading.svg
  27. 24 0
      src/App.vue
  28. 27 0
      src/api/admin/user.js
  29. 75 0
      src/api/api.config.js
  30. 27 0
      src/api/login.js
  31. 14 0
      src/api/setting/cache.js
  32. 14 0
      src/api/setting/queue.js
  33. 13 0
      src/api/setting/science.js
  34. 14 0
      src/api/setting/timer.js
  35. 86 0
      src/api/store.js
  36. 50 0
      src/api/store/api.js
  37. 74 0
      src/api/store/menu.js
  38. 69 0
      src/assets/background.svg
  39. 0 0
      src/assets/icons/access.svg
  40. 1 0
      src/assets/icons/bx-analyse.svg
  41. 1 0
      src/assets/icons/menu.svg
  42. 0 0
      src/assets/icons/setting.svg
  43. 0 0
      src/assets/icons/shop.svg
  44. BIN
      src/assets/logo.png
  45. 29 0
      src/assets/logo.svg
  46. 52 0
      src/components/ContentHeader/ContentHeader.vue
  47. 2 0
      src/components/ContentHeader/index.js
  48. 113 0
      src/components/Dialog.js
  49. 130 0
      src/components/Exception/ExceptionPage.vue
  50. 2 0
      src/components/Exception/index.js
  51. 19 0
      src/components/Exception/type.js
  52. 50 0
      src/components/GlobalFooter/GlobalFooter.vue
  53. 2 0
      src/components/GlobalFooter/index.js
  54. 133 0
      src/components/GlobalHeader/GlobalHeader.vue
  55. 2 0
      src/components/GlobalHeader/index.js
  56. 62 0
      src/components/Menu/SideMenu.vue
  57. 2 0
      src/components/Menu/index.js
  58. 180 0
      src/components/Menu/menu.js
  59. 156 0
      src/components/Menu/menu.render.js
  60. 162 0
      src/components/MultiTab/MultiTab.vue
  61. 2 0
      src/components/MultiTab/events.js
  62. 40 0
      src/components/MultiTab/index.js
  63. 25 0
      src/components/MultiTab/index.less
  64. 76 0
      src/components/NProgress/nprogress.less
  65. 106 0
      src/components/PageLoading/index.jsx
  66. 341 0
      src/components/Table/README.md
  67. 319 0
      src/components/Table/index.js
  68. 46 0
      src/components/_util/util.js
  69. 23 0
      src/components/index.js
  70. 6 0
      src/components/index.less
  71. 45 0
      src/components/tools/Breadcrumb.vue
  72. 31 0
      src/components/tools/Logo.vue
  73. 56 0
      src/components/tools/UserMenu.vue
  74. 0 0
      src/components/tools/index.js
  75. 237 0
      src/components/tree/DirectoryTree.jsx
  76. 230 0
      src/components/tree/Tree.jsx
  77. 15 0
      src/components/tree/index.jsx
  78. 95 0
      src/components/tree/style/directory.less
  79. 2 0
      src/components/tree/style/index.js
  80. 314 0
      src/components/tree/style/index.less
  81. 29 0
      src/components/tree/style/mixin.less
  82. 101 0
      src/components/tree/util.js
  83. BIN
      src/components/vc-tree/assets/icons.png
  84. 73 0
      src/components/vc-tree/assets/index.less
  85. BIN
      src/components/vc-tree/assets/line.gif
  86. BIN
      src/components/vc-tree/assets/loading.gif
  87. 4 0
      src/components/vc-tree/index.js
  88. 701 0
      src/components/vc-tree/src/Tree.jsx
  89. 571 0
      src/components/vc-tree/src/TreeNode.jsx
  90. 7 0
      src/components/vc-tree/src/index.js
  91. 428 0
      src/components/vc-tree/src/util.js
  92. 33 0
      src/config/defaultSettings.js
  93. 133 0
      src/config/router.config.js
  94. 32 0
      src/core/bootstrap.js
  95. 34 0
      src/core/directives/action.js
  96. 16 0
      src/core/icons.js
  97. 103 0
      src/core/lazy_lib/components_use.js
  98. 25 0
      src/core/lazy_use.js
  99. 28 0
      src/core/use.js
  100. 618 0
      src/global.less

+ 3 - 0
.browserslistrc

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

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+NODE_ENV=production
+VUE_APP_PREVIEW=false
+VUE_APP_API_BASE_URL=/api

+ 75 - 0
.eslintrc.js

@@ -0,0 +1,75 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  'extends': [
+    'plugin:vue/strongly-recommended',
+    '@vue/standard'
+  ],
+  rules: {
+    'no-console': 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'generator-star-spacing': 'off',
+    'no-mixed-operators': 0,
+    'vue/max-attributes-per-line': [
+      2,
+      {
+        'singleline': 5,
+        'multiline': {
+          'max': 1,
+          'allowFirstLine': false
+        }
+      }
+    ],
+    'vue/attribute-hyphenation': 0,
+    'vue/html-self-closing': 0,
+    'vue/component-name-in-template-casing': 0,
+    'vue/html-closing-bracket-spacing': 0,
+    'vue/singleline-html-element-content-newline': 0,
+    'vue/no-unused-components': 0,
+    'vue/multiline-html-element-content-newline': 0,
+    'vue/no-use-v-if-with-v-for': 0,
+    'vue/html-closing-bracket-newline': 0,
+    'vue/no-parsing-error': 0,
+    'no-tabs': 0,
+    'quotes': [
+      2,
+      'single',
+      {
+        'avoidEscape': true,
+        'allowTemplateLiterals': true
+      }
+    ],
+    'semi': [
+      2,
+      'never',
+      {
+        'beforeStatementContinuationChars': 'never'
+      }
+    ],
+    'no-delete-var': 2,
+    'prefer-const': [
+      2,
+      {
+        'ignoreReadBeforeAssign': false
+      }
+    ],
+    'template-curly-spacing': 'off',
+    'indent': 'off'
+  },
+  parserOptions: {
+    parser: 'babel-eslint'
+  },
+  overrides: [
+    {
+      files: [
+        '**/__tests__/*.{j,t}s?(x)',
+        '**/tests/unit/**/*.spec.{j,t}s?(x)'
+      ],
+      env: {
+        jest: true
+      }
+    }
+  ]
+}

+ 1 - 0
.gitattributes

@@ -0,0 +1 @@
+public/* linguist-vendored

+ 21 - 0
.gitignore

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

+ 5 - 0
.prettierrc

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

+ 7 - 0
.travis.yml

@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+  - 10.15.0
+cache: yarn
+script:
+  - yarn
+  - yarn run lint --no-fix && yarn run build

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 54 - 0
README.md

@@ -0,0 +1,54 @@
+
+
+项目简述
+----
+
+基于 [Ant Design Pro](https://pro.ant.design/) 实现的 [萤火商城V2.0-admin端]
+
+环境和依赖
+----
+
+- node — v18.12.1
+- yarn — v8.19.2
+- npm — v1.22.19
+
+- [ant-design-vue](https://github.com/vueComponent/ant-design-vue) - Ant Design Of Vue 实现
+
+
+项目运行和发布
+----
+
+- 修改配置文件
+```
+1. 打开文件:/public/config.js 
+2. 修改服务端API信息
+```
+
+- 安装依赖
+```
+yarn config set ignore-engines true
+yarn install
+```
+
+- 开发模式运行
+```
+yarn serve
+```
+
+- 编译项目
+```
+yarn build
+```
+
+- 发布项目
+```
+将编译完成的所有文件,复制替换到商城后端项目的/public/admin目录下
+```
+
+## 浏览器兼容
+
+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" /></br>IE / Edge | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" /></br>Firefox | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" /></br>Chrome | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" /></br>Safari | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" /></br>Opera |
+| --- | --- | --- | --- | --- |
+| IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |

+ 28 - 0
babel.config.js

@@ -0,0 +1,28 @@
+const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
+
+const plugins = []
+if (IS_PROD) {
+  plugins.push('transform-remove-console')
+}
+
+// lazy load ant-design-vue
+// if your use import on Demand, Use this code
+plugins.push(['import', {
+  'libraryName': 'ant-design-vue',
+  'libraryDirectory': 'es',
+  'style': true // `style: true` 会加载 less 文件
+}])
+
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset',
+    [
+      '@babel/preset-env',
+      {
+        'useBuiltIns': 'entry',
+        'corejs': 3
+      }
+    ]
+  ],
+  plugins
+}

+ 46 - 0
config/plugin.config.js

@@ -0,0 +1,46 @@
+const ThemeColorReplacer = require('webpack-theme-color-replacer')
+const generate = require('@ant-design/colors/lib/generate').default
+
+const getAntdSerials = (color) => {
+  // 淡化(即less的tint)
+  const lightens = new Array(9).fill().map((t, i) => {
+    return ThemeColorReplacer.varyColor.lighten(color, i / 10)
+  })
+  const colorPalettes = generate(color)
+  const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',')
+  return lightens.concat(colorPalettes).concat(rgb)
+}
+
+const themePluginOption = {
+  fileName: 'css/theme-colors-[contenthash:8].css',
+  matchColors: getAntdSerials('#1890ff'), // 主色系列
+  // 改变样式选择器,解决样式覆盖问题
+  changeSelector (selector) {
+    switch (selector) {
+      case '.ant-calendar-today .ant-calendar-date':
+        return ':not(.ant-calendar-selected-date):not(.ant-calendar-selected-day)' + selector
+      case '.ant-btn:focus,.ant-btn:hover':
+        return '.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger)'
+      case '.ant-btn.active,.ant-btn:active':
+        return '.ant-btn.active:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:active:not(.ant-btn-primary):not(.ant-btn-danger)'
+      case '.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon':
+      case '.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon':
+        return ':not(.ant-steps-item-process)' + selector
+      case '.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':
+      case '.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':
+        return '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu:hover'
+      case '.ant-menu-horizontal > .ant-menu-item-selected > a':
+      case '.ant-menu-horizontal>.ant-menu-item-selected>a':
+        return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item-selected > a'
+      case '.ant-menu-horizontal > .ant-menu-item > a:hover':
+      case '.ant-menu-horizontal>.ant-menu-item>a:hover':
+        return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item > a:hover'
+      default :
+        return selector
+    }
+  }
+}
+
+const createThemeColorReplacerPlugin = () => new ThemeColorReplacer(themePluginOption)
+
+module.exports = createThemeColorReplacerPlugin

+ 31 - 0
docs/add-page-loading-animate.md

@@ -0,0 +1,31 @@
+为首屏增加 加载动画
+====
+
+
+
+## 需求
+
+> 为了缓解用户第一次访问时,加载 JS 过大所导致用户等待白屏时间过长导致的用户体验不好,进行的一个优化动效。
+
+
+
+## 实现方案
+
+1. 将 动画加载 dom 元素放在 #app 内,Vue 生命周期开始时,会自动清掉 #app 下的所有元素。
+2. 将 动画加载 dom 元素放在 body 下,Vue 生命周期开始时 App.vue (created, mounted) 调用 `@/utils/utll` 下的 removeLoadingAnimate(#id, timeout) 则会移除加载动画
+
+最后一步:
+​	将样式插入到 `public/index.html` 文件的 `<head></head>` 最好写成内联 `<style>动画样式</style>` 
+
+
+
+----
+
+目前提供有两个样式,均在 `public/loading` 文件夹内。且 pro 已经默认使用了一套 loading 动画方案,可以直接参考 `public/index.html`
+
+
+## 写在最后
+
+目前 pro 有页面 overflow 显示出浏览器滚动条时,页面会抖动一下的问题。
+
+欢迎各位提供能解决的方案和实现 demo。如果在条件允许的情况下,建议请直接使用 pro 进行改造,也欢迎直接 PR 到 pro 的仓库

+ 95 - 0
docs/load-on-demand.md

@@ -0,0 +1,95 @@
+按需加载 减小打包
+====
+
+
+
+## 按需引入组件依赖
+
+`Ant Design Pro Vue` 默认编码工作并不支持按需引入,不过可以通过以下操作结合 [Ant Design Of Vue](https://vuecomponent.github.io/ant-design-vue/docs/vue/introduce-cn/) 官方文档来进行按需引入。
+
+- 增加项目按需引入依赖
+- 修改引入组件方式
+
+
+
+1. 增加按需引入所需依赖  `babel-plugin-import` 
+并且修改文件 `babel.config.js` 
+   ```ecmascript 6
+   module.exports = {
+     presets: [
+       '@vue/app'
+     ],
+     plugins: [
+       [ "import", {
+         "libraryName": "ant-design-vue",
+         "libraryDirectory": "es",
+         "style": "css"
+       } ]
+     ]
+   }
+   ```
+
+
+2. 修改引入组件方式 (注意,这只是一个例子,请完整引入你所需要的组件)
+
+   文件 `@/core/lazy_lib/component_use.js`
+
+   ```javascript
+   import Vue from 'vue'
+   import {
+       Input, 
+       Button, 
+       Select, 
+       Card, 
+       Form, 
+       Row, 
+       Col, 
+       Modal, 
+       Table, 
+       notification
+   } from 'ant-design-vue'
+   
+   Vue.use(Input)
+   Vue.use(Button)
+   Vue.use(Select)
+   Vue.use(Card)
+   Vue.use(Form)
+   Vue.use(Row)
+   Vue.use(Col)
+   Vue.use(Modal)
+   Vue.use(Table)
+   Vue.use(notification)
+   
+   Vue.prototype.$notification = notification;
+   ```
+
+
+3. 最后在 `main.js` 中引入 `@/core/lazy_use.js` 文件即可,如下
+
+   ```javascript
+   
+   import Vue from 'vue'
+   import App from './App'
+   
+   // 引入 按需组件的统一引入文件
+   import './core/lazy_use'
+   
+   import './style/index.less'
+   
+   
+   Vue.config.productionTip = false
+   
+   new Vue({
+     render: h => h(App),
+   }).$mount('#app')
+
+   ```
+
+
+
+## 其他 减少打包大小
+
+
+
+1.   Ant Design Vue 1.2.x 版本起,采用的 ant-design 官方方案 svg Icon 组件,整个项目打包会变大很多,图标进行按需加载可参考 https://github.com/HeskeyBaozi/reduce-antd-icons-bundle-demo
+2. moment 按需加载 可参考 https://github.com/jmblog/how-to-optimize-momentjs-with-webpack

+ 28 - 0
docs/multi-tabs.md

@@ -0,0 +1,28 @@
+多(页签)标签 模式
+====
+
+
+## 让框架支持打开的页面增加多标签,可随时切换
+
+### 关于如何移除该功能 组件
+  1. 移除 `/src/layouts/BasicLayout.vue` L44, L69, L80
+      ```vue
+      // L44
+      <multi-tab v-if="multiTab"></multi-tab>
+      
+      // L69
+      import MultiTab from '@/components/MultiTab'
+      
+      // L80
+      MultiTab,
+      ```
+  2. 移除 `/src/config/defaultSettings.js` L25
+
+  3. 移除 `src/store/modules/app.js` L27, L76-L79, L118-L120
+  
+  4. 移除 `src/utils/mixin.js` L21
+  
+  5. 删除组件目录 `src/components/MultiTab` 
+
+> 以上 `L x` 均代表行N ,如 L3 = 行3 
+

+ 40 - 0
docs/webpack-bundle-analyzer.md

@@ -0,0 +1,40 @@
+先增加依赖
+
+```bash
+// npm
+$ npm install --save-dev webpack-bundle-analyzer
+
+// or yarn
+$ yarn add webpack-bundle-analyzer -D
+```
+
+配置文件 `vue.config.js` 增加 `configureWebpack.plugins` 参数
+
+```
+const path = require('path')
+const webpack = require('webpack')
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+
+function resolve (dir) {
+  return path.join(__dirname, dir)
+}
+
+// vue.config.js
+module.exports = {
+  configureWebpack: {
+    plugins: [
+      // Ignore all locale files of moment.js
+      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
+      // 依赖大小分析工具
+      new BundleAnalyzerPlugin(),
+    ]
+  },
+  
+  
+  ...
+}
+```
+
+
+
+启动 `cli` 的 `build` 命令进行项目编译,编译完成时,会自动运行一个 http://localhost:8888 的地址,完整显示了支持库依赖

+ 23 - 0
jest.config.js

@@ -0,0 +1,23 @@
+module.exports = {
+  moduleFileExtensions: [
+    'js',
+    'jsx',
+    'json',
+    'vue'
+  ],
+  transform: {
+    '^.+\\.vue$': 'vue-jest',
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
+    '^.+\\.jsx?$': 'babel-jest'
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  snapshotSerializers: [
+    'jest-serializer-vue'
+  ],
+  testMatch: [
+    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
+  ],
+  testURL: 'http://localhost/'
+}

+ 11 - 0
jsconfig.json

@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "target": "es6",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"],
+  "include": ["src/**/*"]
+}

+ 59 - 0
package.json

@@ -0,0 +1,59 @@
+{
+  "name": "vue-antd-pro",
+  "version": "3.0.0",
+  "private": true,
+  "scripts": {
+    "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
+    "build": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
+    "test:unit": "vue-cli-service test:unit",
+    "lint": "vue-cli-service lint",
+    "build:preview": "vue-cli-service build --mode preview",
+    "lint:nofix": "vue-cli-service lint --no-fix"
+  },
+  "dependencies": {
+    "@antv/data-set": "^0.10.2",
+    "ant-design-vue": "1.7.8",
+    "axios": "^0.19.0",
+    "core-js": "^3.1.2",
+    "enquire.js": "^2.1.6",
+    "lodash.clonedeep": "^4.5.0",
+    "lodash.get": "^4.4.2",
+    "lodash.pick": "^4.4.0",
+    "md5": "^2.2.1",
+    "moment": "^2.24.0",
+    "nprogress": "^0.2.0",
+    "viser-vue": "^2.4.6",
+    "vue": "^2.6.10",
+    "vue-clipboard2": "^0.2.1",
+    "vue-ls": "^3.2.1",
+    "vue-router": "^3.1.2",
+    "vue-svg-component-runtime": "^1.0.1",
+    "vuex": "^3.1.1"
+  },
+  "devDependencies": {
+    "@ant-design/colors": "^3.2.1",
+    "@vue/cli-plugin-babel": "^4.0.4",
+    "@vue/cli-plugin-eslint": "^4.0.4",
+    "@vue/cli-plugin-router": "^4.0.4",
+    "@vue/cli-plugin-unit-jest": "^4.0.4",
+    "@vue/cli-plugin-vuex": "^4.0.4",
+    "@vue/cli-service": "^4.0.4",
+    "@vue/eslint-config-standard": "^4.0.0",
+    "@vue/test-utils": "^1.0.0-beta.29",
+    "babel-eslint": "^10.0.1",
+    "babel-plugin-import": "^1.12.2",
+    "babel-plugin-transform-class-properties": "^6.24.1",
+    "babel-plugin-transform-remove-console": "^6.9.4",
+    "eslint": "^5.16.0",
+    "eslint-plugin-html": "^5.0.0",
+    "eslint-plugin-vue": "^5.2.3",
+    "git-revision-webpack-plugin": "^3.0.6",
+    "less": "^3.0.4",
+    "less-loader": "^5.0.0",
+    "opencollective": "^1.0.3",
+    "opencollective-postinstall": "^2.0.2",
+    "vue-svg-icon-loader": "^2.1.1",
+    "vue-template-compiler": "^2.6.10",
+    "webpack-theme-color-replacer": "^1.3.12"
+  }
+}

+ 5 - 0
postcss.config.js

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

+ 11 - 0
public/config.js

@@ -0,0 +1,11 @@
+window.serverConfig = {
+  // 系统名称
+  APP_NAME: '萤火商城系统2.0',
+  // 必填: api地址, 换成自己的域名即可
+  // 例如: https://www.你的域名.com/index.php?s=/admin
+  // BASE_API: '../index.php?s=/admin',
+  BASE_API: 'http://yoshop2-feel.cn/index.php?s=/admin',
+  // 必填: store模块的入口地址
+  // 例如: https://www.你的域名.com/store
+  STORE_URL: '../store'
+}

+ 32 - 0
public/index.html

@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="zh-cmn-Hans">
+  <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">
+    <title>超级管理后台</title>
+    <style>#loading-mask{position:fixed;left:0;top:0;height:100%;width:100%;background:#fff;user-select:none;z-index:9999;overflow:hidden}.loading-wrapper{position:absolute;top:50%;left:50%;transform:translate(-50%,-100%)}.loading-dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:64px;width:64px;height:64px;box-sizing:border-box}.loading-dot i{width:22px;height:22px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.loading-dot i:nth-child(1){top:0;left:0}.loading-dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.loading-dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.loading-dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
+    <!-- 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 vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app">
+      <div id="loading-mask">
+          <div class="loading-wrapper">
+            <span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
+          </div>
+        </div>
+    </div>
+    <script type="text/javascript" src="config.js"></script>
+    <!-- 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>

+ 1 - 0
public/loading/loading.css

@@ -0,0 +1 @@
+#preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;}  @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}

+ 1 - 0
public/loading/loading.html

@@ -0,0 +1 @@
+<div id="preloadingAnimation"><div class=lds-roller><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div class=load-tips>Loading</div></div>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2 - 0
public/loading/option2/html_code_segment.html


+ 1 - 0
public/loading/option2/loading.css

@@ -0,0 +1 @@
+.preloading-animate{background:#ffffff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:299;}.preloading-animate .preloading-wrapper{position:absolute;width:5rem;height:5rem;left:50%;top:50%;transform:translate(-50%,-50%);}.preloading-animate .preloading-wrapper .preloading-balls{font-size:5rem;}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
public/loading/option2/loading.svg


+ 24 - 0
src/App.vue

@@ -0,0 +1,24 @@
+<template>
+  <a-config-provider :locale="locale">
+    <div id="app">
+      <router-view/>
+    </div>
+  </a-config-provider>
+</template>
+
+<script>
+import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
+import { AppDeviceEnquire } from '@/utils/mixin'
+
+export default {
+  mixins: [AppDeviceEnquire],
+  data () {
+    return {
+      locale: zhCN
+    }
+  },
+  mounted () {
+
+  }
+}
+</script>

+ 27 - 0
src/api/admin/user.js

@@ -0,0 +1,27 @@
+import api from '../api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 获取超管用户信息
+ */
+export function info () {
+  return axios({
+    url: api.admin.user.detail,
+    method: 'get',
+    headers: {
+      'Content-Type': 'application/json; charset=utf-8'
+    }
+  })
+}
+
+/**
+ * 更新当前管理员信息api
+ * @param {*} data
+ */
+export function renew (data = {}) {
+  return axios({
+    url: api.admin.user.renew,
+    method: 'post',
+    data: data
+  })
+}

+ 75 - 0
src/api/api.config.js

@@ -0,0 +1,75 @@
+export default {
+
+  // 用户认证
+  passport: {
+    login: '/passport/login'
+    // logout: '/passport/logout'
+  },
+
+  // 超管模块
+  admin: {
+    user: {
+      detail: '/admin.user/detail',
+      renew: 'admin.user/renew'
+    }
+  },
+
+  // 商家模块
+  store: {
+    list: 'store/index',
+    superLogin: 'store/superLogin',
+    recycle: 'store/recycle',
+    add: 'store/add',
+    recovery: 'store/recovery',
+    delete: 'store/delete',
+    move: 'store/move',
+
+    // // 商家功能模块管理
+    // module: {
+    //   default: 'store.module/default',
+    //   detail: 'store.module/detail',
+    //   edit: 'store.module/edit',
+    // },
+
+    // 商家后台api权限
+    api: {
+      list: 'store.api/index',
+      add: 'store.api/add',
+      edit: 'store.api/edit',
+      delete: 'store.api/delete'
+    },
+
+    // 商家后台菜单
+    menu: {
+      list: 'store.menu/index',
+      info: 'store.menu/info',
+      add: 'store.menu/add',
+      edit: 'store.menu/edit',
+      delete: 'store.menu/delete',
+      setApis: 'store.menu/setApis',
+
+      // 商家后台菜单操作权限
+      action: {
+        list: 'store.menu.action/index',
+        add: 'store.menu.action/add',
+        edit: 'store.menu.action/edit',
+        delete: 'store.menu.action/delete'
+      }
+    },
+  },
+
+  // 系统管理
+  setting: {
+    cache: {
+      clear: 'setting.cache/clear'
+    },
+    science: 'setting.science/info',
+    queue: {
+      test: 'setting.queue/test'
+    },
+    timer: {
+      test: 'setting.timer/test'
+    }
+  },
+
+}

+ 27 - 0
src/api/login.js

@@ -0,0 +1,27 @@
+import api from './api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 超管用户登录
+ * @param {*} parameter
+ */
+export function login (parameter) {
+  return axios({
+    url: api.passport.login,
+    method: 'post',
+    data: parameter
+  })
+}
+
+/**
+ * 超管用户退出登录
+ */
+// export function logout () {
+//   return axios({
+//     url: api.passport.logout,
+//     method: 'post',
+//     headers: {
+//       'Content-Type': 'application/json; charset=utf-8'
+//     }
+//   })
+// }

+ 14 - 0
src/api/setting/cache.js

@@ -0,0 +1,14 @@
+import api from '../api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 清理缓存api
+ * @param {*} data
+ */
+export function clear (data) {
+  return axios({
+    url: api.setting.cache.clear,
+    method: 'post',
+    data: data
+  })
+}

+ 14 - 0
src/api/setting/queue.js

@@ -0,0 +1,14 @@
+import api from '../api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 测试队列服务是否开启
+ * @param {*} data
+ */
+export function test (data) {
+  return axios({
+    url: api.setting.queue.test,
+    method: 'post',
+    data
+  })
+}

+ 13 - 0
src/api/setting/science.js

@@ -0,0 +1,13 @@
+import api from '../api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 清理缓存api
+ * @param {*} data
+ */
+export function info () {
+  return axios({
+    url: api.setting.science,
+    method: 'get'
+  })
+}

+ 14 - 0
src/api/setting/timer.js

@@ -0,0 +1,14 @@
+import api from '../api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 测试定时任务是否开启
+ * @param {*} data
+ */
+export function test (data) {
+  return axios({
+    url: api.setting.timer.test,
+    method: 'post',
+    data
+  })
+}

+ 86 - 0
src/api/store.js

@@ -0,0 +1,86 @@
+import api from './api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 获取列表
+ * @param {*} params
+ */
+export function list (params) {
+  return axios({
+    url: api.store.list,
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 获取商城登录token
+ * @param {*} params
+ */
+export function superLogin ({ storeId }) {
+  return axios({
+    url: api.store.superLogin,
+    method: 'get',
+    params: { storeId }
+  })
+}
+
+/**
+ * 回收站列表
+ * @param {*} params
+ */
+export function recycle (params) {
+  return axios({
+    url: api.store.recycle,
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 新增记录
+ * @param {*} data
+ */
+export function add (data) {
+  return axios({
+    url: api.store.add,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 移入回收站
+ * @param {*} data
+ */
+export function recovery (data) {
+  return axios({
+    url: api.store.recovery,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 删除记录
+ * @param {*} data
+ */
+export function del (data) {
+  return axios({
+    url: api.store.delete,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 移出回收站
+ * @param {*} data
+ */
+export function move (data) {
+  return axios({
+    url: api.store.move,
+    method: 'post',
+    data: data
+  })
+}

+ 50 - 0
src/api/store/api.js

@@ -0,0 +1,50 @@
+import api from '../api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 获取列表
+ * @param {*} params
+ */
+export function list (params) {
+  return axios({
+    url: api.store.api.list,
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 新增记录
+ * @param {*} data
+ */
+export function add (data) {
+  return axios({
+    url: api.store.api.add,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 编辑记录
+ * @param {*} data
+ */
+export function edit (data) {
+  return axios({
+    url: api.store.api.edit,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 删除记录
+ * @param {*} data
+ */
+export function deleted (data) {
+  return axios({
+    url: api.store.api.delete,
+    method: 'post',
+    data: data
+  })
+}

+ 74 - 0
src/api/store/menu.js

@@ -0,0 +1,74 @@
+import api from '../api.config'
+import { axios } from '@/utils/request'
+
+/**
+ * 获取列表
+ * @param {*} params
+ */
+export function list (params) {
+  return axios({
+    url: api.store.menu.list,
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 获取单条记录信息
+ * @param {*} params
+ */
+export function info (params) {
+  return axios({
+    url: api.store.menu.info,
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 新增记录
+ * @param {*} data
+ */
+export function add (data) {
+  return axios({
+    url: api.store.menu.add,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 编辑记录
+ * @param {*} data
+ */
+export function edit (data) {
+  return axios({
+    url: api.store.menu.edit,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 编辑记录
+ * @param {*} data
+ */
+export function setApis (data) {
+  return axios({
+    url: api.store.menu.setApis,
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 删除记录
+ * @param {*} data
+ */
+export function deleted (data) {
+  return axios({
+    url: api.store.menu.delete,
+    method: 'post',
+    data: data
+  })
+}

+ 69 - 0
src/assets/background.svg

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+    <title>Group 21</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
+            <g id="Group-21" transform="translate(77.000000, 73.000000)">
+                <g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
+                    <ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
+                    <ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
+                    <path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
+                    <path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
+                    <path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
+                    <path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
+                    <g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
+                        <ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
+                        <path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
+                    </g>
+                </g>
+                <g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
+                    <ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
+                    <ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
+                    <ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
+                    <ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
+                    <path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
+                    <g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
+                        <ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
+                        <path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
+                    </g>
+                    <ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
+                    <ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
+                    <ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
+                    <path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
+                </g>
+                <g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
+                    <ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
+                    <g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
+                        <ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
+                        <path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
+                    </g>
+                    <path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
+                    <ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
+                    <ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
+                    <path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
+                </g>
+                <g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
+                    <g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
+                        <circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
+                        <path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
+                    </g>
+                    <circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
+                    <path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
+                    <path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
+                    <path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
+                    <circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
+                    <circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
+                    <circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
+                    <circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
+                    <circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/assets/icons/access.svg


+ 1 - 0
src/assets/icons/bx-analyse.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1551058675966" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7872" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M85.333333 512h85.333334a340.736 340.736 0 0 1 99.712-241.621333 337.493333 337.493333 0 0 1 108.458666-72.96 346.453333 346.453333 0 0 1 261.546667-1.749334A106.154667 106.154667 0 0 0 746.666667 298.666667C805.802667 298.666667 853.333333 251.136 853.333333 192S805.802667 85.333333 746.666667 85.333333c-29.397333 0-55.978667 11.776-75.221334 30.933334-103.722667-41.514667-222.848-40.874667-325.76 2.517333a423.594667 423.594667 0 0 0-135.68 91.264 423.253333 423.253333 0 0 0-91.306666 135.637333A426.88 426.88 0 0 0 85.333333 512z m741.248 133.205333c-17.109333 40.618667-41.685333 77.141333-72.96 108.416s-67.797333 55.850667-108.458666 72.96a346.453333 346.453333 0 0 1-261.546667 1.749334A106.154667 106.154667 0 0 0 277.333333 725.333333C218.197333 725.333333 170.666667 772.864 170.666667 832S218.197333 938.666667 277.333333 938.666667c29.397333 0 55.978667-11.776 75.221334-30.933334A425.173333 425.173333 0 0 0 512 938.666667a425.941333 425.941333 0 0 0 393.258667-260.352A426.325333 426.325333 0 0 0 938.666667 512h-85.333334a341.034667 341.034667 0 0 1-26.752 133.205333z" p-id="7873"></path><path d="M512 318.378667c-106.752 0-193.621333 86.869333-193.621333 193.621333S405.248 705.621333 512 705.621333s193.621333-86.869333 193.621333-193.621333S618.752 318.378667 512 318.378667z m0 301.909333c-59.690667 0-108.288-48.597333-108.288-108.288S452.309333 403.712 512 403.712s108.288 48.597333 108.288 108.288-48.597333 108.288-108.288 108.288z" p-id="7874"></path></svg>

+ 1 - 0
src/assets/icons/menu.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1593669930802" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5011" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500"><defs><style type="text/css"></style></defs><path d="M380.744 480.94H100.196A100.196 100.196 0 0 1 0 380.743V100.196A100.196 100.196 0 0 1 100.196 0h280.548a100.196 100.196 0 0 1 100.195 100.196v280.548a100.196 100.196 0 0 1-100.195 100.195zM100.196 80.156a20.04 20.04 0 0 0-20.04 20.039v280.548a20.04 20.04 0 0 0 20.04 20.039h280.548a20.04 20.04 0 0 0 20.039-20.04V100.197a20.04 20.04 0 0 0-20.04-20.04z m823.608 400.782H643.256a100.196 100.196 0 0 1-100.195-100.195V100.196A100.196 100.196 0 0 1 643.256 0h280.548A100.196 100.196 0 0 1 1024 100.196v280.548a100.196 100.196 0 0 1-100.196 100.195zM643.256 80.157a20.04 20.04 0 0 0-20.039 20.039v280.548a20.04 20.04 0 0 0 20.04 20.039h280.547a20.04 20.04 0 0 0 20.04-20.04V100.197a20.04 20.04 0 0 0-20.04-20.04zM380.744 1024H100.196A100.196 100.196 0 0 1 0 923.804V643.256a100.196 100.196 0 0 1 100.196-100.195h280.548a100.196 100.196 0 0 1 100.195 100.195v280.548A100.196 100.196 0 0 1 380.744 1024zM100.196 623.217a20.04 20.04 0 0 0-20.04 20.04v280.547a20.04 20.04 0 0 0 20.04 20.04h280.548a20.04 20.04 0 0 0 20.039-20.04V643.256a20.04 20.04 0 0 0-20.04-20.039zM827.616 1024h-184.36a100.196 100.196 0 0 1-100.195-100.196V643.256a100.196 100.196 0 0 1 100.195-100.195h280.548A100.196 100.196 0 0 1 1024 643.256V818.6a40.078 40.078 0 0 1-80.157 0V643.256a20.04 20.04 0 0 0-20.039-20.039H643.256a20.04 20.04 0 0 0-20.039 20.04v280.547a20.04 20.04 0 0 0 20.04 20.04h184.36a40.078 40.078 0 0 1 0 80.156z" p-id="5012"></path></svg>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/assets/icons/setting.svg


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/assets/icons/shop.svg


BIN
src/assets/logo.png


+ 29 - 0
src/assets/logo.svg

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
+  <title>Vue</title>
+  <desc>Created with Sketch.</desc>
+  <defs>
+    <linearGradient x1="69.644116%" y1="0%" x2="69.644116%" y2="100%" id="linearGradient-1">
+      <stop stop-color="#29CDFF" offset="0%"></stop>
+      <stop stop-color="#148EFF" offset="37.8600687%"></stop>
+      <stop stop-color="#0A60FF" offset="100%"></stop>
+    </linearGradient>
+    <linearGradient x1="-19.8191553%" y1="-36.7931464%" x2="138.57919%" y2="157.637507%" id="linearGradient-2">
+      <stop stop-color="#29CDFF" offset="0%"></stop>
+      <stop stop-color="#0F78FF" offset="100%"></stop>
+    </linearGradient>
+    <linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-3">
+      <stop stop-color="#FA8E7D" offset="0%"></stop>
+      <stop stop-color="#F74A5C" offset="51.2635191%"></stop>
+      <stop stop-color="#F51D2C" offset="100%"></stop>
+    </linearGradient>
+  </defs>
+  <g id="Vue" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="Group" transform="translate(19.000000, 9.000000)">
+      <path d="M89.96,90.48 C78.58,93.48 68.33,83.36 67.62,82.48 L46.6604487,62.2292258 C45.5023849,61.1103236 44.8426845,59.5728835 44.8296987,57.9626396 L44.5035564,17.5209948 C44.4948861,16.4458744 44.0537714,15.4195095 43.2796864,14.6733517 L29.6459999,1.53153737 C28.055475,-0.00160504005 25.5232423,0.0449126588 23.9900999,1.63543756 C23.2715121,2.38092066 22.87,3.37600834 22.87,4.41143746 L22.87,64.3864751 C22.87,67.0807891 23.9572233,69.6611067 25.885409,71.5429748 L63.6004615,108.352061 C65.9466323,110.641873 69.6963584,110.624605 72.0213403,108.313281" id="Path-Copy" fill="url(#linearGradient-1)" fill-rule="nonzero" transform="translate(56.415000, 54.831157) scale(-1, 1) translate(-56.415000, -54.831157) "></path>
+      <path d="M68,90.1163122 C56.62,93.1163122 45.46,83.36 44.75,82.48 L23.7904487,62.2292258 C22.6323849,61.1103236 21.9726845,59.5728835 21.9596987,57.9626396 L21.6335564,17.5209948 C21.6248861,16.4458744 21.1837714,15.4195095 20.4096864,14.6733517 L6.7759999,1.53153737 C5.185475,-0.00160504005 2.65324232,0.0449126588 1.12009991,1.63543756 C0.401512125,2.38092066 3.90211878e-13,3.37600834 3.90798505e-13,4.41143746 L3.94351218e-13,64.3864751 C3.94681177e-13,67.0807891 1.08722326,69.6611067 3.01540903,71.5429748 L40.7807092,108.401101 C43.1069304,110.671444 46.8180151,110.676525 49.1504445,108.412561" id="Path" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
+      <path d="M43.2983488,19.0991931 L27.5566079,3.88246244 C26.7624281,3.11476967 26.7409561,1.84862177 27.5086488,1.05444194 C27.8854826,0.664606611 28.4044438,0.444472651 28.9466386,0.444472651 L60.3925021,0.444472651 C61.4970716,0.444472651 62.3925021,1.33990315 62.3925021,2.44447265 C62.3925021,2.9858375 62.1730396,3.50407742 61.7842512,3.88079942 L46.0801285,19.0975301 C45.3051579,19.8484488 44.0742167,19.8491847 43.2983488,19.0991931 Z" id="Path" fill="url(#linearGradient-3)"></path>
+    </g>
+  </g>
+</svg>

+ 52 - 0
src/components/ContentHeader/ContentHeader.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="content-header">
+    <div class="widget-head">
+      <div class="widget-title">{{ title }}</div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'ContentHeader',
+  props: {
+    title: {
+      type: String,
+      default: null
+    }
+  },
+  data () {
+    return {}
+  },
+  mounted () {
+  },
+  methods: {
+  }
+}
+</script>
+
+<style lang="less" scoped>
+@import '../index.less';
+
+.widget-head {
+  width: 100%;
+  padding: 5px 0 14px 20px;
+  border-bottom: 1px solid #eef1f5;
+  margin-bottom: 20px;
+  .widget-title {
+    position: relative;
+    font-size: 15px;
+    &::before {
+      content: '';
+      position: absolute;
+      width: 4px;
+      height: 16px;
+      // background: @primary-color;
+      background: #46a6ff;
+      top: 4px;
+      left: -15px;
+    }
+  }
+}
+</style>

+ 2 - 0
src/components/ContentHeader/index.js

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

+ 113 - 0
src/components/Dialog.js

@@ -0,0 +1,113 @@
+import Modal from 'ant-design-vue/es/modal'
+export default (Vue) => {
+  function dialog (component, componentProps, modalProps) {
+    const _vm = this
+    modalProps = modalProps || {}
+    if (!_vm || !_vm._isVue) {
+      return
+    }
+    let dialogDiv = document.querySelector('body>div[type=dialog]')
+    if (!dialogDiv) {
+      dialogDiv = document.createElement('div')
+      dialogDiv.setAttribute('type', 'dialog')
+      document.body.appendChild(dialogDiv)
+    }
+
+    const handle = function (checkFunction, afterHandel) {
+      if (checkFunction instanceof Function) {
+        const res = checkFunction()
+        if (res instanceof Promise) {
+          res.then(c => {
+            c && afterHandel()
+          })
+        } else {
+          res && afterHandel()
+        }
+      } else {
+        // checkFunction && afterHandel()
+        checkFunction || afterHandel()
+      }
+    }
+
+    const dialogInstance = new Vue({
+      data () {
+        return {
+          visible: true
+        }
+      },
+      router: _vm.$router,
+      store: _vm.$store,
+      mounted () {
+        this.$on('close', (v) => {
+          this.handleClose()
+        })
+      },
+      methods: {
+        handleClose () {
+          handle(this.$refs._component.onCancel, () => {
+            this.visible = false
+            this.$refs._component.$emit('close')
+            this.$refs._component.$emit('cancel')
+            dialogInstance.$destroy()
+          })
+        },
+        handleOk () {
+          handle(this.$refs._component.onOK || this.$refs._component.onOk, () => {
+            this.visible = false
+            this.$refs._component.$emit('close')
+            this.$refs._component.$emit('ok')
+            dialogInstance.$destroy()
+          })
+        }
+      },
+      render: function (h) {
+        const that = this
+        const modalModel = modalProps && modalProps.model
+        if (modalModel) {
+          delete modalProps.model
+        }
+        const ModalProps = Object.assign({}, modalModel && { model: modalModel } || {}, {
+          attrs: Object.assign({}, {
+            ...(modalProps.attrs || modalProps)
+          }, {
+            visible: this.visible
+          }),
+          on: Object.assign({}, {
+            ...(modalProps.on || modalProps)
+          }, {
+            ok: () => {
+              that.handleOk()
+            },
+            cancel: () => {
+              that.handleClose()
+            }
+          })
+        })
+
+        const componentModel = componentProps && componentProps.model
+        if (componentModel) {
+          delete componentProps.model
+        }
+        const ComponentProps = Object.assign({}, componentModel && { model: componentModel } || {}, {
+          ref: '_component',
+          attrs: Object.assign({}, {
+            ...((componentProps && componentProps.attrs) || componentProps)
+          }),
+          on: Object.assign({}, {
+            ...((componentProps && componentProps.on) || componentProps)
+          })
+        })
+
+        return h(Modal, ModalProps, [h(component, ComponentProps)])
+      }
+    }).$mount(dialogDiv)
+  }
+
+  Object.defineProperty(Vue.prototype, '$dialog', {
+    get: () => {
+      return function () {
+        dialog.apply(this, arguments)
+      }
+    }
+  })
+}

+ 130 - 0
src/components/Exception/ExceptionPage.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="exception">
+    <div class="imgBlock">
+      <div class="imgEle" :style="{backgroundImage: `url(${config[type].img})`}">
+      </div>
+    </div>
+    <div class="content">
+      <h1>{{ config[type].title }}</h1>
+      <div class="desc">{{ config[type].desc }}</div>
+      <div class="actions">
+        <a-button type="primary" @click="handleToHome">返回首页</a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import types from './type'
+
+export default {
+  name: 'Exception',
+  props: {
+    type: {
+      type: String,
+      default: '404'
+    }
+  },
+  data () {
+    return {
+      config: types
+    }
+  },
+  methods: {
+    handleToHome () {
+      this.$router.push({ name: 'Store' })
+    }
+  }
+}
+</script>
+<style lang="less">
+@import "~ant-design-vue/lib/style/index";
+
+.exception {
+  display: flex;
+  align-items: center;
+  height: 80%;
+  min-height: 500px;
+
+  .imgBlock {
+    flex: 0 0 62.5%;
+    width: 62.5%;
+    padding-right: 152px;
+    zoom: 1;
+    &::before,
+    &::after {
+      content: ' ';
+      display: table;
+    }
+    &::after {
+      clear: both;
+      height: 0;
+      font-size: 0;
+      visibility: hidden;
+    }
+  }
+
+  .imgEle {
+    float: right;
+    width: 100%;
+    max-width: 430px;
+    height: 360px;
+    background-repeat: no-repeat;
+    background-position: 50% 50%;
+    background-size: contain;
+  }
+
+  .content {
+    flex: auto;
+
+    h1 {
+      margin-bottom: 24px;
+      color: #434e59;
+      font-weight: 600;
+      font-size: 72px;
+      line-height: 72px;
+    }
+
+    .desc {
+      margin-bottom: 16px;
+      color: @text-color-secondary;
+      font-size: 20px;
+      line-height: 28px;
+    }
+
+    .actions {
+      button:not(:last-child) {
+        margin-right: 8px;
+      }
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xl) {
+  .exception {
+    .imgBlock {
+      padding-right: 88px;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-sm) {
+  .exception {
+    display: block;
+    text-align: center;
+    .imgBlock {
+      margin: 0 auto 24px;
+      padding-right: 0;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .exception {
+    .imgBlock {
+      margin-bottom: -24px;
+      overflow: hidden;
+    }
+  }
+}
+</style>

+ 2 - 0
src/components/Exception/index.js

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

+ 19 - 0
src/components/Exception/type.js

@@ -0,0 +1,19 @@
+const types = {
+  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 types

+ 50 - 0
src/components/GlobalFooter/GlobalFooter.vue

@@ -0,0 +1,50 @@
+<template>
+  <div class="footer">
+    <div class="content">
+      <span class="text">{{ content }}</span>
+      <a :href="link.url" target="_blank">{{ link.text }}</a>
+    </div>
+  </div>
+</template>
+
+<script>
+const uncompileStr = (code) => {
+  code = unescape(code)
+  var c = String.fromCharCode(code.charCodeAt(0) - code.length)
+  for (var i = 1; i < code.length; i++) {
+    c += String.fromCharCode(code.charCodeAt(i) - c.charCodeAt(i - 1))
+  }
+  return c
+}
+
+export default {
+  name: 'GlobalFooter',
+  data () {
+    return {
+      visibility: true,
+      content: uncompileStr('%5C%B2%DF%E9%EB%DB%D0%CF%DC%94%C9%C9RbbfT%u8444%uF48F%uC5B1%uAD14%u5824%88%60%5E'),
+      link: {
+        text: uncompileStr('b%A2%98%A5%A5%7Dq%92%9C'),
+        url: uncompileStr('%7D%DC%E8%E4%E3%ADi%5E%A6%EE%EE%A5%A7%E2%D8%E5%E5%9D%91%D2%DC')
+      }
+    }
+  },
+}
+</script>
+
+<style lang="less" scoped>
+.footer {
+  padding: 0 16px;
+  margin: 48px 0 24px;
+  text-align: center;
+  user-select: none;
+
+  .content {
+    color: rgba(0, 0, 0, 0.45);
+    font-size: 14px;
+    .text {
+      margin-right: 6px;
+    }
+  }
+}
+</style>

+ 2 - 0
src/components/GlobalFooter/index.js

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

+ 133 - 0
src/components/GlobalHeader/GlobalHeader.vue

@@ -0,0 +1,133 @@
+<template>
+  <transition name="showHeader">
+    <div v-if="visible" class="header-animat">
+      <a-layout-header
+        v-if="visible"
+        :class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
+        :style="{ padding: '0' }"
+      >
+        <div v-if="mode === 'sidemenu'" class="header">
+          <div class="content clearfix">
+            <span class="title">系统管理中心</span>
+            <user-menu></user-menu>
+          </div>
+        </div>
+        <div v-else :class="['top-nav-header-index', theme]">
+          <div class="header-index-wide">
+            <div class="header-index-left">
+              <logo class="top-nav-header" :show-title="device !== 'mobile'" />
+              <s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme" />
+              <a-icon
+                v-else
+                class="trigger"
+                :type="collapsed ? 'menu-fold' : 'menu-unfold'"
+                @click="toggle"
+              />
+            </div>
+            <user-menu class="header-index-right"></user-menu>
+          </div>
+        </div>
+      </a-layout-header>
+    </div>
+  </transition>
+</template>
+
+<script>
+import UserMenu from '../tools/UserMenu'
+import SMenu from '../Menu/'
+import Logo from '../tools/Logo'
+import { mixin } from '@/utils/mixin'
+
+export default {
+  name: 'GlobalHeader',
+  components: {
+    UserMenu,
+    SMenu,
+    Logo
+  },
+  mixins: [mixin],
+  props: {
+    mode: {
+      type: String,
+      // sidemenu, topmenu
+      default: 'sidemenu'
+    },
+    menus: {
+      type: Array,
+      required: true
+    },
+    theme: {
+      type: String,
+      required: false,
+      default: 'dark'
+    },
+    collapsed: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    device: {
+      type: String,
+      required: false,
+      default: 'desktop'
+    }
+  },
+  data () {
+    return {
+      visible: true,
+      oldScrollTop: 0
+    }
+  },
+  mounted () {
+    document.addEventListener('scroll', this.handleScroll, { passive: true })
+  },
+  methods: {
+    handleScroll () {
+      if (!this.autoHideHeader) {
+        return
+      }
+
+      const scrollTop = document.body.scrollTop + document.documentElement.scrollTop
+      if (!this.ticking) {
+        this.ticking = true
+        requestAnimationFrame(() => {
+          if (this.oldScrollTop > scrollTop) {
+            this.visible = true
+          } else if (scrollTop > 300 && this.visible) {
+            this.visible = false
+          } else if (scrollTop < 300 && !this.visible) {
+            this.visible = true
+          }
+          this.oldScrollTop = scrollTop
+          this.ticking = false
+        })
+      }
+    },
+    toggle () {
+      this.$emit('toggle')
+    }
+  },
+  beforeDestroy () {
+    document.body.removeEventListener('scroll', this.handleScroll, true)
+  }
+}
+</script>
+
+<style lang="less">
+@import '../index.less';
+
+.header-animat {
+  position: relative;
+  z-index: @ant-global-header-zindex;
+}
+.showHeader-enter-active {
+  transition: all 0.25s ease;
+}
+.showHeader-leave-active {
+  transition: all 0.5s ease;
+}
+.showHeader-enter,
+.showHeader-leave-to {
+  opacity: 0;
+}
+</style>

+ 2 - 0
src/components/GlobalHeader/index.js

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

+ 62 - 0
src/components/Menu/SideMenu.vue

@@ -0,0 +1,62 @@
+<template>
+  <a-layout-sider
+    :class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null ]"
+    width="256px"
+    style="margin-top: 24px"
+    :collapsible="collapsible"
+    v-model="collapsed"
+    :trigger="null"
+  >
+    <s-menu
+      :collapsed="collapsed"
+      :menu="menus"
+      :theme="theme"
+      :mode="mode"
+      @select="onSelect"
+      style="padding: 16px 0px;"
+    ></s-menu>
+  </a-layout-sider>
+</template>
+
+<script>
+import Logo from '@/components/tools/Logo'
+import SMenu from './index'
+import { mixin, mixinDevice } from '@/utils/mixin'
+
+export default {
+  name: 'SideMenu',
+  components: { Logo, SMenu },
+  mixins: [mixin, mixinDevice],
+  props: {
+    mode: {
+      type: String,
+      required: false,
+      default: 'inline'
+    },
+    theme: {
+      type: String,
+      required: false,
+      default: 'dark'
+    },
+    collapsible: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    collapsed: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    menus: {
+      type: Array,
+      required: true
+    }
+  },
+  methods: {
+    onSelect (obj) {
+      this.$emit('menuSelect', obj)
+    }
+  }
+}
+</script>

+ 2 - 0
src/components/Menu/index.js

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

+ 180 - 0
src/components/Menu/menu.js

@@ -0,0 +1,180 @@
+import Menu from 'ant-design-vue/es/menu'
+import Icon from 'ant-design-vue/es/icon'
+
+export default {
+  name: 'SMenu',
+  props: {
+    menu: {
+      type: Array,
+      required: true
+    },
+    theme: {
+      type: String,
+      required: false,
+      default: 'light'
+    },
+    mode: {
+      type: String,
+      required: false,
+      default: 'inline'
+    },
+    collapsed: {
+      type: Boolean,
+      required: false,
+      default: false
+    }
+  },
+  data () {
+    return {
+      openKeys: [],
+      selectedKeys: [],
+      cachedOpenKeys: []
+    }
+  },
+  computed: {
+    rootSubmenuKeys: vm => {
+      const keys = []
+      vm.menu.forEach(item => keys.push(item.path))
+      return keys
+    }
+  },
+  mounted () {
+    this.updateMenu()
+  },
+  watch: {
+    collapsed (val) {
+      if (val) {
+        this.cachedOpenKeys = this.openKeys.concat()
+        this.openKeys = []
+      } else {
+        this.openKeys = this.cachedOpenKeys
+      }
+    },
+    $route: function () {
+      this.updateMenu()
+    }
+  },
+  methods: {
+    // select menu item
+    onOpenChange (openKeys) {
+      this.openKeys = openKeys
+
+      // // 在水平模式下时执行,并且不再执行后续
+      // if (this.mode === 'horizontal') {
+      //   this.openKeys = openKeys
+      //   return
+      // }
+      // // 非水平模式时
+      // const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
+      // if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
+      //   this.openKeys = openKeys
+      // } else {
+      //   this.openKeys = latestOpenKey ? [latestOpenKey] : []
+      // }
+      // console.log(this.openKeys)
+    },
+    onSelect ({ item, key, selectedKeys }) {
+      this.selectedKeys = selectedKeys
+      this.$emit('select', { item, key, selectedKeys })
+    },
+    updateMenu () {
+      const routes = this.$route.matched.concat()
+      const { hidden } = this.$route.meta
+      if (routes.length >= 3 && hidden) {
+        routes.pop()
+        this.selectedKeys = [routes[routes.length - 1].path]
+      } else {
+        this.selectedKeys = [routes.pop().path]
+      }
+      const openKeys = []
+      if (this.mode === 'inline') {
+        routes.forEach(item => {
+          openKeys.push(item.path)
+        })
+      }
+
+      this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
+    },
+
+    // render
+    renderItem (menu) {
+      if (!menu.hidden) {
+        return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
+      }
+      return null
+    },
+    renderMenuItem (menu) {
+      const target = menu.meta.target || null
+      const CustomTag = target && 'a' || 'router-link'
+      const props = { to: { path: menu.path } }
+      const attrs = { href: menu.path, target: menu.meta.target }
+
+      if (menu.children && menu.hideChildrenInMenu) {
+        // 把有子菜单的 并且 父菜单是要隐藏子菜单的
+        // 都给子菜单增加一个 hidden 属性
+        // 用来给刷新页面时, selectedKeys 做控制用
+        menu.children.forEach(item => {
+          item.meta = Object.assign(item.meta, { hidden: true })
+        })
+      }
+
+      return (
+        <Menu.Item {...{ key: menu.path }}>
+          <CustomTag {...{ props, attrs }}>
+            {this.renderIcon(menu.meta.icon)}
+            <span>{menu.meta.title}</span>
+          </CustomTag>
+        </Menu.Item>
+      )
+    },
+    renderSubMenu (menu) {
+      const itemArr = []
+      if (!menu.hideChildrenInMenu) {
+        menu.children.forEach(item => itemArr.push(this.renderItem(item)))
+      }
+      return (
+        <Menu.SubMenu {...{ key: menu.path }}>
+          <span slot="title">
+            {this.renderIcon(menu.meta.icon)}
+            <span>{menu.meta.title}</span>
+          </span>
+          {itemArr}
+        </Menu.SubMenu>
+      )
+    },
+    renderIcon (icon) {
+      if (icon === 'none' || icon === undefined) {
+        return null
+      }
+      const props = {}
+      typeof (icon) === 'object' ? props.component = icon : props.type = icon
+      return (
+        <Icon {... { props }} />
+      )
+    }
+  },
+
+  render () {
+    const dynamicProps = {
+      props: {
+        mode: this.mode,
+        theme: this.theme,
+        defaultOpenKeys: this.rootSubmenuKeys,
+        // openKeys: this.openKeys,
+        selectedKeys: this.selectedKeys
+      },
+      on: {
+        openChange: this.onOpenChange,
+        select: this.onSelect
+      }
+    }
+    const menuTree = this.menu.map(item => {
+      if (item.hidden) {
+        return null
+      }
+      return this.renderItem(item)
+    })
+
+    return (<Menu {...dynamicProps}>{menuTree}</Menu>)
+  }
+}

+ 156 - 0
src/components/Menu/menu.render.js

@@ -0,0 +1,156 @@
+import Menu from 'ant-design-vue/es/menu'
+import Icon from 'ant-design-vue/es/icon'
+
+const { Item, SubMenu } = Menu
+
+export default {
+  name: 'SMenu',
+  props: {
+    menu: {
+      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
+    }
+  },
+  data () {
+    return {
+      openKeys: [],
+      selectedKeys: [],
+      cachedOpenKeys: []
+    }
+  },
+  computed: {
+    rootSubmenuKeys: vm => {
+      const keys = []
+      vm.menu.forEach(item => keys.push(item.path))
+      return keys
+    }
+  },
+  created () {
+    this.updateMenu()
+  },
+  watch: {
+    collapsed (val) {
+      if (val) {
+        this.cachedOpenKeys = this.openKeys.concat()
+        this.openKeys = []
+      } else {
+        this.openKeys = this.cachedOpenKeys
+      }
+    },
+    $route: function () {
+      this.updateMenu()
+    }
+  },
+  methods: {
+    renderIcon: function (h, icon) {
+      if (icon === 'none' || icon === undefined) {
+        return null
+      }
+      const props = {}
+      typeof (icon) === 'object' ? props.component = icon : props.type = icon
+      return h(Icon, { props: { ...props } })
+    },
+    renderMenuItem: function (h, menu, pIndex, index) {
+      const target = menu.meta.target || null
+      return h(Item, { key: menu.path ? menu.path : 'item_' + pIndex + '_' + index }, [
+        h('router-link', { attrs: { to: { name: menu.name }, target: target } }, [
+          this.renderIcon(h, menu.meta.icon),
+          h('span', [menu.meta.title])
+        ])
+      ])
+    },
+    renderSubMenu: function (h, menu, pIndex, index) {
+      const this2_ = this
+      const subItem = [h('span', { slot: 'title' }, [this.renderIcon(h, menu.meta.icon), h('span', [menu.meta.title])])]
+      const itemArr = []
+      const pIndex_ = pIndex + '_' + index
+      console.log('menu', menu)
+      if (!menu.hideChildrenInMenu) {
+        menu.children.forEach(function (item, i) {
+          itemArr.push(this2_.renderItem(h, item, pIndex_, i))
+        })
+      }
+      return h(SubMenu, { key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index }, subItem.concat(itemArr))
+    },
+    renderItem: function (h, menu, pIndex, index) {
+      if (!menu.hidden) {
+        return menu.children && !menu.hideChildrenInMenu
+          ? this.renderSubMenu(h, menu, pIndex, index)
+          : this.renderMenuItem(h, menu, pIndex, index)
+      }
+    },
+    renderMenu: function (h, menuTree) {
+      const this2_ = this
+      const menuArr = []
+      menuTree.forEach(function (menu, i) {
+        if (!menu.hidden) {
+          menuArr.push(this2_.renderItem(h, menu, '0', i))
+        }
+      })
+      return menuArr
+    },
+    onOpenChange (openKeys) {
+      const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
+      if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
+        this.openKeys = openKeys
+      } else {
+        this.openKeys = latestOpenKey ? [latestOpenKey] : []
+      }
+    },
+    updateMenu () {
+      const routes = this.$route.matched.concat()
+
+      if (routes.length >= 4 && this.$route.meta.hidden) {
+        routes.pop()
+        this.selectedKeys = [routes[2].path]
+      } else {
+        this.selectedKeys = [routes.pop().path]
+      }
+
+      const openKeys = []
+      if (this.mode === 'inline') {
+        routes.forEach(item => {
+          openKeys.push(item.path)
+        })
+      }
+
+      this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
+    }
+  },
+  render (h) {
+    return h(
+      Menu,
+      {
+        props: {
+          theme: this.$props.theme,
+          mode: this.$props.mode,
+          openKeys: this.openKeys,
+          selectedKeys: this.selectedKeys
+        },
+        on: {
+          openChange: this.onOpenChange,
+          select: obj => {
+            this.selectedKeys = obj.selectedKeys
+            this.$emit('select', obj)
+          }
+        }
+      },
+      this.renderMenu(h, this.menu)
+    )
+  }
+}

+ 162 - 0
src/components/MultiTab/MultiTab.vue

@@ -0,0 +1,162 @@
+<script>
+import events from './events'
+
+export default {
+  name: 'MultiTab',
+  data () {
+    return {
+      fullPathList: [],
+      pages: [],
+      activeKey: '',
+      newTabIndex: 0
+    }
+  },
+  created () {
+    // bind event
+    events.$on('open', val => {
+      if (!val) {
+        throw new Error(`multi-tab: open tab ${val} err`)
+      }
+      this.activeKey = val
+    }).$on('close', val => {
+      if (!val) {
+        this.closeThat(this.activeKey)
+        return
+      }
+      this.closeThat(val)
+    }).$on('rename', ({ key, name }) => {
+      console.log('rename', key, name)
+      try {
+        const item = this.pages.find(item => item.path === key)
+        item.meta.customTitle = name
+        this.$forceUpdate()
+      } catch (e) {
+      }
+    })
+
+    this.pages.push(this.$route)
+    this.fullPathList.push(this.$route.fullPath)
+    this.selectedLastPath()
+  },
+  methods: {
+    onEdit (targetKey, action) {
+      this[action](targetKey)
+    },
+    remove (targetKey) {
+      this.pages = this.pages.filter(page => page.fullPath !== targetKey)
+      this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
+      // 判断当前标签是否关闭,若关闭则跳转到最后一个还存在的标签页
+      if (!this.fullPathList.includes(this.activeKey)) {
+        this.selectedLastPath()
+      }
+    },
+    selectedLastPath () {
+      this.activeKey = this.fullPathList[this.fullPathList.length - 1]
+    },
+
+    // content menu
+    closeThat (e) {
+      // 判断是否为最后一个标签页,如果是最后一个,则无法被关闭
+      if (this.fullPathList.length > 1) {
+        this.remove(e)
+      } else {
+        this.$message.info('这是最后一个标签了, 无法被关闭')
+      }
+    },
+    closeLeft (e) {
+      const currentIndex = this.fullPathList.indexOf(e)
+      if (currentIndex > 0) {
+        this.fullPathList.forEach((item, index) => {
+          if (index < currentIndex) {
+            this.remove(item)
+          }
+        })
+      } else {
+        this.$message.info('左侧没有标签')
+      }
+    },
+    closeRight (e) {
+      const currentIndex = this.fullPathList.indexOf(e)
+      if (currentIndex < (this.fullPathList.length - 1)) {
+        this.fullPathList.forEach((item, index) => {
+          if (index > currentIndex) {
+            this.remove(item)
+          }
+        })
+      } else {
+        this.$message.info('右侧没有标签')
+      }
+    },
+    closeAll (e) {
+      const currentIndex = this.fullPathList.indexOf(e)
+      this.fullPathList.forEach((item, index) => {
+        if (index !== currentIndex) {
+          this.remove(item)
+        }
+      })
+    },
+    closeMenuClick (key, route) {
+      this[key](route)
+    },
+    renderTabPaneMenu (e) {
+      return (
+        <a-menu {...{ on: { click: ({ key, item, domEvent }) => { this.closeMenuClick(key, e) } } }}>
+          <a-menu-item key="closeThat">关闭当前标签</a-menu-item>
+          <a-menu-item key="closeRight">关闭右侧</a-menu-item>
+          <a-menu-item key="closeLeft">关闭左侧</a-menu-item>
+          <a-menu-item key="closeAll">关闭全部</a-menu-item>
+        </a-menu>
+      )
+    },
+    // render
+    renderTabPane (title, keyPath) {
+      const menu = this.renderTabPaneMenu(keyPath)
+
+      return (
+        <a-dropdown overlay={menu} trigger={['contextmenu']}>
+          <span style={{ userSelect: 'none' }}>{ title }</span>
+        </a-dropdown>
+      )
+    }
+  },
+  watch: {
+    '$route': function (newVal) {
+      this.activeKey = newVal.fullPath
+      if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
+        this.fullPathList.push(newVal.fullPath)
+        this.pages.push(newVal)
+      }
+    },
+    activeKey: function (newPathKey) {
+      this.$router.push({ path: newPathKey })
+    }
+  },
+  render () {
+    const { onEdit, $data: { pages } } = this
+    const panes = pages.map(page => {
+      return (
+        <a-tab-pane
+          style={{ height: 0 }}
+          tab={this.renderTabPane(page.meta.customTitle || page.meta.title, page.fullPath)}
+          key={page.fullPath} closable={pages.length > 1}
+        >
+        </a-tab-pane>)
+    })
+
+    return (
+      <div class="ant-pro-multi-tab">
+        <div class="ant-pro-multi-tab-wrapper">
+          <a-tabs
+            hideAdd
+            type={'editable-card'}
+            v-model={this.activeKey}
+            tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
+            {...{ on: { edit: onEdit } }}>
+            {panes}
+          </a-tabs>
+        </div>
+      </div>
+    )
+  }
+}
+</script>

+ 2 - 0
src/components/MultiTab/events.js

@@ -0,0 +1,2 @@
+import Vue from 'vue'
+export default new Vue()

+ 40 - 0
src/components/MultiTab/index.js

@@ -0,0 +1,40 @@
+import events from './events'
+import MultiTab from './MultiTab'
+import './index.less'
+
+const api = {
+  /**
+   * open new tab on route fullPath
+   * @param config
+   */
+  open: function (config) {
+    events.$emit('open', config)
+  },
+  rename: function (key, name) {
+    events.$emit('rename', { key: key, name: name })
+  },
+  /**
+   * close current page
+   */
+  closeCurrentPage: function () {
+    this.close()
+  },
+  /**
+   * close route fullPath tab
+   * @param config
+   */
+  close: function (config) {
+    events.$emit('close', config)
+  }
+}
+
+MultiTab.install = function (Vue) {
+  if (Vue.prototype.$multiTab) {
+    return
+  }
+  api.instance = events
+  Vue.prototype.$multiTab = api
+  Vue.component('multi-tab', MultiTab)
+}
+
+export default MultiTab

+ 25 - 0
src/components/MultiTab/index.less

@@ -0,0 +1,25 @@
+@import '../index';
+
+@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
+@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
+
+/*
+.topmenu .@{multi-tab-prefix-cls} {
+  max-width: 1200px;
+  margin: -23px auto 24px auto;
+}
+*/
+.@{multi-tab-prefix-cls} {
+  margin: -23px -24px 24px -24px;
+  background: #fff;
+}
+
+.topmenu .@{multi-tab-wrapper-prefix-cls} {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} {
+  max-width: 100%;
+  margin: 0 auto;
+}

+ 76 - 0
src/components/NProgress/nprogress.less

@@ -0,0 +1,76 @@
+@import url('../index.less');
+
+/* Make clicks pass-through */
+#nprogress {
+  pointer-events: none;
+}
+
+#nprogress .bar {
+  background: @primary-color;
+
+  position: fixed;
+  z-index: 1031;
+  top: 0;
+  left: 0;
+
+  width: 100%;
+  height: 2px;
+}
+
+/* Fancy blur effect */
+#nprogress .peg {
+  display: block;
+  position: absolute;
+  right: 0px;
+  width: 100px;
+  height: 100%;
+  box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color;
+  opacity: 1.0;
+
+  -webkit-transform: rotate(3deg) translate(0px, -4px);
+  -ms-transform: rotate(3deg) translate(0px, -4px);
+  transform: rotate(3deg) translate(0px, -4px);
+}
+
+/* Remove these to get rid of the spinner */
+#nprogress .spinner {
+  display: block;
+  position: fixed;
+  z-index: 1031;
+  top: 15px;
+  right: 15px;
+}
+
+#nprogress .spinner-icon {
+  width: 18px;
+  height: 18px;
+  box-sizing: border-box;
+
+  border: solid 2px transparent;
+  border-top-color: @primary-color;
+  border-left-color: @primary-color;
+  border-radius: 50%;
+
+  -webkit-animation: nprogress-spinner 400ms linear infinite;
+  animation: nprogress-spinner 400ms linear infinite;
+}
+
+.nprogress-custom-parent {
+  overflow: hidden;
+  position: relative;
+}
+
+.nprogress-custom-parent #nprogress .spinner,
+.nprogress-custom-parent #nprogress .bar {
+  position: absolute;
+}
+
+@-webkit-keyframes nprogress-spinner {
+  0%   { -webkit-transform: rotate(0deg); }
+  100% { -webkit-transform: rotate(360deg); }
+}
+@keyframes nprogress-spinner {
+  0%   { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+

+ 106 - 0
src/components/PageLoading/index.jsx

@@ -0,0 +1,106 @@
+import { Spin } from 'ant-design-vue'
+
+export const PageLoading = {
+  name: 'PageLoading',
+  props: {
+    tip: {
+      type: String,
+      default: 'Loading..'
+    },
+    size: {
+      type: String,
+      default: 'large'
+    }
+  },
+  render () {
+    const style = {
+      textAlign: 'center',
+      background: 'rgba(0,0,0,0.6)',
+      position: 'fixed',
+      top: 0,
+      bottom: 0,
+      left: 0,
+      right: 0,
+      zIndex: 1100
+    }
+    const spinStyle = {
+      position: 'absolute',
+      left: '50%',
+      top: '40%',
+      transform: 'translate(-50%, -50%)'
+    }
+    return (<div style={style}>
+      <Spin size={this.size} style={spinStyle} tip={this.tip} />
+    </div>)
+  }
+}
+
+const version = '0.0.1'
+const loading = {}
+
+loading.newInstance = (Vue, options) => {
+  let loadingElement = document.querySelector('body>div[type=loading]')
+  if (!loadingElement) {
+    loadingElement = document.createElement('div')
+    loadingElement.setAttribute('type', 'loading')
+    loadingElement.setAttribute('class', 'ant-loading-wrapper')
+    document.body.appendChild(loadingElement)
+  }
+
+  const cdProps = Object.assign({ visible: false, size: 'large', tip: 'Loading...' }, options)
+
+  const instance = new Vue({
+    data () {
+      return {
+        ...cdProps
+      }
+    },
+    render () {
+      const { tip } = this
+      const props = {}
+      this.tip && (props.tip = tip)
+      if (this.visible) {
+        return <PageLoading { ...{ props } } />
+      }
+      return null
+    }
+  }).$mount(loadingElement)
+
+  function update (config) {
+    const { visible, size, tip } = { ...cdProps, ...config }
+    instance.$set(instance, 'visible', visible)
+    if (tip) {
+      instance.$set(instance, 'tip', tip)
+    }
+    if (size) {
+      instance.$set(instance, 'size', size)
+    }
+  }
+
+  return {
+    instance,
+    update
+  }
+}
+
+const api = {
+  show: function (options) {
+    this.instance.update({ ...options, visible: true })
+  },
+  hide: function () {
+    this.instance.update({ visible: false })
+  }
+}
+
+const install = function (Vue, options) {
+  if (Vue.prototype.$loading) {
+    return
+  }
+  api.instance = loading.newInstance(Vue, options)
+  Vue.prototype.$loading = api
+}
+
+export default {
+  version,
+  install
+}

+ 341 - 0
src/components/Table/README.md

@@ -0,0 +1,341 @@
+Table 重封装组件说明
+====
+
+
+封装说明
+----
+
+>  基础的使用方式与 API 与 [官方版(Table)](https://vuecomponent.github.io/ant-design-vue/components/table-cn/) 本一致,在其基础上,封装了加载数据的方法。
+>
+> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 Table 组件传递绑定 `:data="Promise"` 对象即可
+
+该 `table` 由 [@Saraka](https://github.com/saraka-tsukai) 完成封装
+
+
+例子1
+----
+(基础使用)
+
+```vue
+
+<template>
+  <s-table
+    ref="table"
+    size="default"
+    :rowKey="(record) => record.data.id"
+    :columns="columns"
+    :data="loadData"
+    :rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
+  >
+  </s-table>
+</template>
+
+<script>
+  import STable from '@/components'
+
+  export default {
+    components: {
+      STable
+    },
+    data() {
+      return {
+        columns: [
+          {
+            title: '规则编号',
+            dataIndex: 'no'
+          },
+          {
+            title: '描述',
+            dataIndex: 'description'
+          },
+          {
+            title: '服务调用次数',
+            dataIndex: 'callNo',
+            sorter: true,
+            needTotal: true,
+            customRender: (text) => text + ' 次'
+          },
+          {
+            title: '状态',
+            dataIndex: 'status',
+            needTotal: true
+          },
+          {
+            title: '更新时间',
+            dataIndex: 'updatedAt',
+            sorter: true
+          }
+        ],
+        // 查询条件参数
+        queryParam: {},
+        // 加载数据方法 必须为 Promise 对象
+        loadData: parameter => {
+          return this.$http.get('/service', {
+            params: Object.assign(parameter, this.queryParam)
+          }).then(res => {
+            return res.result
+          })
+        },
+        selectedRowKeys: [],
+        selectedRows: []
+      }
+    },
+    methods: {
+      onSelectChange (selectedRowKeys, selectedRows) {
+         this.selectedRowKeys = selectedRowKeys
+         this.selectedRows = selectedRows
+       }
+    }
+  }
+</script>
+
+```
+
+
+
+例子2
+----
+
+(简单的表格,最后一列是各种操作)
+
+```vue
+<template>
+  <s-table
+    ref="table"
+    size="default"
+    :columns="columns"
+    :data="loadData"
+  >
+    <span slot="action" slot-scope="text, record">
+      <a>编辑</a>
+      <a-divider type="vertical"/>
+      <a-dropdown>
+        <a class="ant-dropdown-link">
+          更多 <a-icon type="down"/>
+        </a>
+        <a-menu slot="overlay">
+          <a-menu-item>
+            <a href="javascript:;">1st menu item</a>
+          </a-menu-item>
+          <a-menu-item>
+            <a href="javascript:;">2nd menu item</a>
+          </a-menu-item>
+          <a-menu-item>
+            <a href="javascript:;">3rd menu item</a>
+          </a-menu-item>
+        </a-menu>
+      </a-dropdown>
+    </span>
+  </s-table>
+</template>
+
+<script>
+  import STable from '@/components/table/'
+
+  export default {
+    components: {
+      STable
+    },
+    data() {
+      return {
+        columns: [
+          {
+            title: '规则编号',
+            dataIndex: 'no'
+          },
+          {
+            title: '描述',
+            dataIndex: 'description'
+          },
+          {
+            title: '服务调用次数',
+            dataIndex: 'callNo',
+          },
+          {
+            title: '状态',
+            dataIndex: 'status',
+          },
+          {
+            title: '更新时间',
+            dataIndex: 'updatedAt',
+          },
+          {
+            table: '操作',
+            dataIndex: 'action',
+            scopedSlots: {customRender: 'action'},
+          }
+        ],
+        // 查询条件参数
+        queryParam: {},
+        // 加载数据方法 必须为 Promise 对象
+        loadData: parameter => {
+          return this.$http.get('/service', {
+            params: Object.assign(parameter, this.queryParam)
+          }).then(res => {
+            return res.result
+          })
+        },
+      }
+    },
+    methods: {
+      edit(row) {
+        // axios 发送数据到后端 修改数据成功后
+        // 调用 refresh() 重新加载列表数据
+        // 这里 setTimeout 模拟发起请求的网络延迟..
+        setTimeout(() => {
+          this.$refs.table.refresh() // refresh() 不传参默认值 false 不刷新到分页第一页
+        }, 1500)
+
+      }
+    }
+  }
+</script>
+```
+
+
+
+内置方法
+----
+
+通过 `this.$refs.table` 调用
+
+`this.$refs.table.refresh(true)` 刷新列表 (用户新增/修改数据后,重载列表数据)
+
+> 注意:要调用 `refresh(bool)` 需要给表格组件设定 `ref` 值
+>
+> `refresh()` 方法可以传一个 `bool` 值,当有传值 或值为 `true` 时,则刷新时会强制刷新到第一页(常用户页面 搜索 按钮进行搜索时,结果从第一页开始分页)
+
+
+内置属性
+----
+> 除去 `a-table` 自带属性外,还而外提供了一些额外属性属性  
+
+
+| 属性           | 说明                                            | 类型              | 默认值 |
+| -------------- | ----------------------------------------------- | ----------------- | ------ |
+| alert          | 设置是否显示表格信息栏                          | [object, boolean] | null   |
+| showPagination | 显示分页选择器,可传 'auto' \| boolean          | [string, boolean] | 'auto' |
+| data           | 加载数据方法 必须为 `Promise` 对象 **必须绑定** | Promise           | -      |
+
+
+`alert` 属性对象:
+
+```javascript
+alert: {
+  show: Boolean, 
+  clear: [Function, Boolean]
+}
+```
+
+注意事项
+----
+
+> 你可能需要为了与后端提供的接口返回结果一致而去修改以下代码:
+> (需要注意的是,这里的修改是全局性的,意味着整个项目所有使用该 table 组件都需要遵守这个返回结果定义的字段。)
+>
+> 文档中的结构有可能由于组件 bug 进行修正而改动。实际修改请以当时最新版本为准
+
+修改 `@/components/table/index.js`  第 156 行起
+
+
+
+```javascript
+result.then(r => {
+          this.localPagination = this.showPagination && Object.assign({}, this.localPagination, {
+            current: r.pageNo, // 返回结果中的当前分页数
+            total: r.totalCount, // 返回结果中的总记录数
+            showSizeChanger: this.showSizeChanger,
+            pageSize: (pagination && pagination.pageSize) ||
+              this.localPagination.pageSize
+          }) || false
+          // 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
+          if (r.data.length === 0 && this.showPagination && this.localPagination.current > 1) {
+            this.localPagination.current--
+            this.loadData()
+            return
+          }
+
+          // 这里用于判断接口是否有返回 r.totalCount 且 this.showPagination = true 且 pageNo 和 pageSize 存在 且 totalCount 小于等于 pageNo * pageSize 的大小
+          // 当情况满足时,表示数据不满足分页大小,关闭 table 分页功能
+          try {
+            if ((['auto', true].includes(this.showPagination) && r.totalCount <= (r.pageNo * this.localPagination.pageSize))) {
+              this.localPagination.hideOnSinglePage = true
+            }
+          } catch (e) {
+            this.localPagination = false
+          }
+          console.log('loadData -> this.localPagination', this.localPagination)
+          this.localDataSource = r.data // 返回结果中的数组数据
+          this.localLoading = false
+        })
+```
+返回 JSON 例子:
+```json
+{
+  "message": "",
+  "result": {
+    "data": [{
+        id: 1,
+        cover: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
+        title: 'Alipay',
+        description: '那是一种内在的东西, 他们到达不了,也无法触及的',
+        status: 1,
+        updatedAt: '2018-07-26 00:00:00'
+      },
+      {
+        id: 2,
+        cover: 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png',
+        title: 'Angular',
+        description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+        status: 1,
+        updatedAt: '2018-07-26 00:00:00'
+      },
+      {
+        id: 3,
+        cover: 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png',
+        title: 'Ant Design',
+        description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+        status: 1,
+        updatedAt: '2018-07-26 00:00:00'
+      },
+      {
+        id: 4,
+        cover: 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png',
+        title: 'Ant Design Pro',
+        description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+        status: 1,
+        updatedAt: '2018-07-26 00:00:00'
+      },
+      {
+        id: 5,
+        cover: 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png',
+        title: 'Bootstrap',
+        description: '凛冬将至',
+        status: 1,
+        updatedAt: '2018-07-26 00:00:00'
+      },
+      {
+        id: 6,
+        cover: 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png',
+        title: 'Vue',
+        description: '生命就像一盒巧克力,结果往往出人意料',
+        status: 1,
+        updatedAt: '2018-07-26 00:00:00'
+      }
+    ],
+    "pageSize": 10,
+    "pageNo": 0,
+    "totalPage": 6,
+    "totalCount": 57
+  },
+  "status": 200,
+  "timestamp": 1534955098193
+}
+```
+
+
+
+更新时间
+----
+
+该文档最后更新于: 2019-06-23 PM 17:19

+ 319 - 0
src/components/Table/index.js

@@ -0,0 +1,319 @@
+import T from 'ant-design-vue/es/table/Table'
+import get from 'lodash.get'
+
+export default {
+  data () {
+    return {
+      needTotalList: [],
+
+      selectedRows: [],
+      selectedRowKeys: [],
+
+      localLoading: false,
+      localDataSource: [],
+      localPagination: Object.assign({}, this.pagination)
+    }
+  },
+  props: Object.assign({}, T.props, {
+    rowKey: {
+      type: [String, Function],
+      default: 'key'
+    },
+    data: {
+      type: Function,
+      required: true
+    },
+    pageNum: {
+      type: Number,
+      default: 1
+    },
+    pageSize: {
+      type: Number,
+      default: 10
+    },
+    showSizeChanger: {
+      type: Boolean,
+      default: false
+    },
+    size: {
+      type: String,
+      default: 'default'
+    },
+    // 展开的图标显示在哪一列
+    expandIconColumnIndex: {
+      type: Number,
+      default: 0
+    },
+    /**
+     * alert: {
+     *   show: true,
+     *   clear: Function
+     * }
+     */
+    alert: {
+      type: [Object, Boolean],
+      default: null
+    },
+    rowSelection: {
+      type: Object,
+      default: null
+    },
+    /** @Deprecated */
+    showAlertInfo: {
+      type: Boolean,
+      default: false
+    },
+    showPagination: {
+      type: String | Boolean,
+      default: 'auto'
+    },
+    /**
+     * enable page URI mode
+     *
+     * e.g:
+     * /users/1
+     * /users/2
+     * /users/3?queryParam=test
+     * ...
+     */
+    pageURI: {
+      type: Boolean,
+      default: false
+    }
+  }),
+  watch: {
+    'localPagination.current' (val) {
+      this.pageURI && this.$router.push({
+        ...this.$route,
+        name: this.$route.name,
+        params: Object.assign({}, this.$route.params, {
+          page: val
+        })
+      })
+    },
+    pageNum (val) {
+      Object.assign(this.localPagination, {
+        current: val
+      })
+    },
+    pageSize (val) {
+      Object.assign(this.localPagination, {
+        pageSize: val
+      })
+    },
+    showSizeChanger (val) {
+      Object.assign(this.localPagination, {
+        showSizeChanger: val
+      })
+    }
+  },
+  created () {
+    const { page } = this.$route.params
+    const localPageNum = this.pageURI && (page && parseInt(page)) || this.pageNum
+    this.localPagination = ['auto', true].includes(this.showPagination) && Object.assign({}, this.localPagination, {
+      current: localPageNum,
+      pageSize: this.pageSize,
+      showSizeChanger: this.showSizeChanger
+    }) || false
+    // console.log('this.localPagination', this.localPagination)
+    this.needTotalList = this.initTotalList(this.columns)
+    this.loadData()
+  },
+  methods: {
+    /**
+     * 表格重新加载方法
+     * 如果参数为 true, 则强制刷新到第一页
+     * @param Boolean bool
+     */
+    refresh (bool = false) {
+      bool && (this.localPagination = Object.assign({}, {
+        current: 1, pageSize: this.pageSize
+      }))
+      this.loadData()
+    },
+    /**
+     * 加载数据方法
+     * @param {Object} pagination 分页选项器
+     * @param {Object} filters 过滤条件
+     * @param {Object} sorter 排序条件
+     */
+    loadData (pagination, filters, sorter) {
+      this.localLoading = true
+      const parameter = Object.assign({
+        page: (pagination && pagination.current) ||
+          this.showPagination && this.localPagination.current || this.pageNum
+        // pageSize: (pagination && pagination.pageSize) ||
+        //   this.showPagination && this.localPagination.pageSize || this.pageSize
+      },
+        (sorter && sorter.field && {
+          sortField: sorter.field
+        }) || {},
+        (sorter && sorter.order && {
+          sortOrder: sorter.order
+        }) || {}, {
+        ...filters
+      }
+      )
+      // console.log('parameter', parameter)
+      const result = this.data(parameter)
+      // 对接自己的通用数据接口需要修改下方代码中的 r.page, r.totalCount, r.data
+      // eslint-disable-next-line
+      if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
+        result.then(r => {
+          this.localPagination = this.showPagination && Object.assign({}, this.localPagination, {
+            current: r.current_page, // 返回结果中的当前分页数
+            total: r.total, // 返回结果中的总记录数
+            showSizeChanger: this.showSizeChanger,
+            pageSize: (pagination && pagination.pageSize) ||
+              this.localPagination.pageSize
+          }) || false
+          // 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
+          if (r.data.length === 0 && this.showPagination && this.localPagination.current > 1) {
+            this.localPagination.current--
+            this.loadData()
+            return
+          }
+
+          // 这里用于判断接口是否有返回 r.totalCount 且 this.showPagination = true 且 page 和 pageSize 存在 且 totalCount 小于等于 page * pageSize 的大小
+          // 当情况满足时,表示数据不满足分页大小,关闭 table 分页功能
+          try {
+            if ((['auto', true].includes(this.showPagination) && r.total <= (r.current_page * this.localPagination.pageSize))) {
+              this.localPagination.hideOnSinglePage = true
+            }
+          } catch (e) {
+            this.localPagination = false
+          }
+          // console.log('loadData -> this.localPagination', this.localPagination)
+          this.localDataSource = r.data // 返回结果中的数组数据
+          this.localLoading = false
+        })
+      }
+    },
+    initTotalList (columns) {
+      const totalList = []
+      columns && columns instanceof Array && columns.forEach(column => {
+        if (column.needTotal) {
+          totalList.push({
+            ...column,
+            total: 0
+          })
+        }
+      })
+      return totalList
+    },
+    /**
+     * 用于更新已选中的列表数据 total 统计
+     * @param selectedRowKeys
+     * @param selectedRows
+     */
+    updateSelect (selectedRowKeys, selectedRows) {
+      this.selectedRows = selectedRows
+      this.selectedRowKeys = selectedRowKeys
+      const list = this.needTotalList
+      this.needTotalList = list.map(item => {
+        return {
+          ...item,
+          total: selectedRows.reduce((sum, val) => {
+            const total = sum + parseInt(get(val, item.dataIndex))
+            return isNaN(total) ? 0 : total
+          }, 0)
+        }
+      })
+    },
+    /**
+     * 清空 table 已选中项
+     */
+    clearSelected () {
+      if (this.rowSelection) {
+        this.rowSelection.onChange([], [])
+        this.updateSelect([], [])
+      }
+    },
+    /**
+     * 处理交给 table 使用者去处理 clear 事件时,内部选中统计同时调用
+     * @param callback
+     * @returns {*}
+     */
+    renderClear (callback) {
+      if (this.selectedRowKeys.length <= 0) return null
+      return (
+        <a style="margin-left: 24px" onClick={() => {
+          callback()
+          this.clearSelected()
+        }}>清空</a>
+      )
+    },
+    renderAlert () {
+      // 绘制统计列数据
+      const needTotalItems = this.needTotalList.map((item) => {
+        return (<span style="margin-right: 12px">
+          {item.title}总计 <a style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
+        </span>)
+      })
+
+      // 绘制 清空 按钮
+      const clearItem = (typeof this.alert.clear === 'boolean' && this.alert.clear) ? (
+        this.renderClear(this.clearSelected)
+      ) : (this.alert !== null && typeof this.alert.clear === 'function') ? (
+        this.renderClear(this.alert.clear)
+      ) : null
+
+      // 绘制 alert 组件
+      return (
+        <a-alert showIcon={true} style="margin-bottom: 16px">
+          <template slot="message">
+            <span style="margin-right: 12px">已选择: <a style="font-weight: 600">{this.selectedRows.length}</a></span>
+            {needTotalItems}
+            {clearItem}
+          </template>
+        </a-alert>
+      )
+    }
+  },
+
+  render () {
+    const props = {}
+    const localKeys = Object.keys(this.$data)
+    const showAlert = (typeof this.alert === 'object' && this.alert !== null && this.alert.show) && typeof this.rowSelection.selectedRowKeys !== 'undefined' || this.alert
+    Object.keys(T.props).forEach(k => {
+      const localKey = `local${k.substring(0, 1).toUpperCase()}${k.substring(1)}`
+      if (localKeys.includes(localKey)) {
+        props[k] = this[localKey]
+        return props[k]
+      }
+      if (k === 'rowSelection') {
+        if (showAlert && this.rowSelection) {
+          // 如果需要使用alert,则重新绑定 rowSelection 事件
+          // console.log('this.rowSelection', this.rowSelection)
+          props[k] = {
+            ...this.rowSelection,
+            selectedRows: this.selectedRows,
+            selectedRowKeys: this.selectedRowKeys,
+            onChange: (selectedRowKeys, selectedRows) => {
+              this.updateSelect(selectedRowKeys, selectedRows)
+              typeof this[k].onChange !== 'undefined' && this[k].onChange(selectedRowKeys, selectedRows)
+            }
+          }
+          return props[k]
+        } else if (!this.rowSelection) {
+          // 如果没打算开启 rowSelection 则清空默认的选择项
+          props[k] = null
+          return props[k]
+        }
+      }
+      this[k] && (props[k] = this[k])
+      return props[k]
+    })
+    const table = (
+      <a-table {...{ props, scopedSlots: { ...this.$scopedSlots } }} onChange={this.loadData} onExpand={(expanded, record) => { this.$emit('expand', expanded, record) }}>
+        {Object.keys(this.$slots).map(name => (<template slot={name}>{this.$slots[name]}</template>))}
+      </a-table>
+    )
+    return (
+      <div class="table-wrapper">
+        {showAlert ? this.renderAlert() : null}
+        {table}
+      </div>
+    )
+  }
+}

+ 46 - 0
src/components/_util/util.js

@@ -0,0 +1,46 @@
+/**
+ * components util
+ */
+
+/**
+ * 清理空值,对象
+ * @param children
+ * @returns {*[]}
+ */
+export function filterEmpty (children = []) {
+  return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
+}
+
+/**
+ * 获取字符串长度,英文字符 长度1,中文字符长度2
+ * @param {*} str
+ */
+export const getStrFullLength = (str = '') =>
+  str.split('').reduce((pre, cur) => {
+    const charCode = cur.charCodeAt(0)
+    if (charCode >= 0 && charCode <= 128) {
+      return pre + 1
+    }
+    return pre + 2
+  }, 0)
+
+/**
+ * 截取字符串,根据 maxLength 截取后返回
+ * @param {*} str
+ * @param {*} maxLength
+ */
+export const cutStrByFullLength = (str = '', maxLength) => {
+  let showLength = 0
+  return str.split('').reduce((pre, cur) => {
+    const charCode = cur.charCodeAt(0)
+    if (charCode >= 0 && charCode <= 128) {
+      showLength += 1
+    } else {
+      showLength += 2
+    }
+    if (showLength <= maxLength) {
+      return pre + cur
+    }
+    return pre
+  }, '')
+}

+ 23 - 0
src/components/index.js

@@ -0,0 +1,23 @@
+// pro components
+// import Tree from '@/components/Tree/Tree'
+import STable from '@/components/Table'
+import MultiTab from '@/components/MultiTab'
+import ExceptionPage from '@/components/Exception'
+
+import Dialog from '@/components/Dialog'
+import ContentHeader from '@/components/ContentHeader'
+
+// ant design vue
+import Tree from '@/components/tree/Tree'
+
+export {
+  // Tree,
+  STable,
+  MultiTab,
+  ExceptionPage,
+
+  Dialog,
+  ContentHeader,
+
+  Tree
+}

+ 6 - 0
src/components/index.less

@@ -0,0 +1,6 @@
+@import "~ant-design-vue/lib/style/index";
+
+// The prefix to use on all css classes from ant-pro.
+@ant-pro-prefix             : ant-pro;
+@ant-global-sider-zindex    : 106;
+@ant-global-header-zindex   : 105;

+ 45 - 0
src/components/tools/Breadcrumb.vue

@@ -0,0 +1,45 @@
+<template>
+  <a-breadcrumb class="breadcrumb">
+    <a-breadcrumb-item v-for="(item, index) in breadList" :key="item.name">
+      <router-link
+        v-if="item.name != name && index != 1"
+        :to="{ path: item.path === '' ? '/' : item.path }"
+      >{{ item.meta.title }}</router-link>
+      <span v-else>{{ item.meta.title }}</span>
+    </a-breadcrumb-item>
+  </a-breadcrumb>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      name: '',
+      breadList: []
+    }
+  },
+  created () {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb () {
+      this.breadList = []
+      // this.breadList.push({name: 'index', path: '/dashboard/', meta: {title: '首页'}})
+
+      this.name = this.$route.name
+      this.$route.matched.forEach(item => {
+        // item.name !== 'index' && this.breadList.push(item)
+        this.breadList.push(item)
+      })
+    }
+  },
+  watch: {
+    $route () {
+      this.getBreadcrumb()
+    }
+  }
+}
+</script>
+
+<style scoped>
+</style>

+ 31 - 0
src/components/tools/Logo.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="logo">
+    <router-link :to="{name:'Store'}">
+      <LogoSvg alt="logo" />
+      <h1 v-if="showTitle">{{ title }}</h1>
+    </router-link>
+  </div>
+</template>
+
+<script>
+import LogoSvg from '@/assets/logo.svg?inline'
+
+export default {
+  name: 'Logo',
+  components: {
+    LogoSvg
+  },
+  props: {
+    title: {
+      type: String,
+      default: 'Ant Design Pro',
+      required: false
+    },
+    showTitle: {
+      type: Boolean,
+      default: true,
+      required: false
+    }
+  }
+}
+</script>

+ 56 - 0
src/components/tools/UserMenu.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="user-wrapper">
+    <div class="content-box">
+      <a-dropdown>
+        <span class="action ant-dropdown-link user-dropdown-menu">
+          <a-icon type="user" :style="{ fontSize: '16px', marginRight: '5px' }"></a-icon>
+          <span :style="{ fontSize: '15px' }">{{ nickname }}</span>
+        </span>
+        <a-menu slot="overlay" class="user-dropdown-menu-wrapper">
+          <a-menu-item key="0">
+            <router-link :to="{ name: 'Manage' }">
+              <a-icon type="setting" />
+              <span :style="{ fontSize: '13px' }">账户设置</span>
+            </router-link>
+          </a-menu-item>
+          <a-menu-item key="1">
+            <a href="javascript:void(0);" @click="handleLogout">
+              <a-icon type="logout" />
+              <span :style="{ fontSize: '13px' }">退出登录</span>
+            </a>
+          </a-menu-item>
+        </a-menu>
+      </a-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex'
+
+export default {
+  name: 'UserMenu',
+  computed: {
+    ...mapGetters(['nickname', 'avatar'])
+  },
+  methods: {
+    ...mapActions(['Logout']),
+    handleLogout () {
+      this.$confirm({
+        title: '提示',
+        content: '真的要注销登录吗 ?',
+        onOk: () => {
+          return this.Logout({})
+            .then(() => {
+              setTimeout(() => {
+                window.location.reload()
+              }, 200)
+            })
+        },
+        onCancel () {
+        }
+      })
+    }
+  }
+}
+</script>

+ 0 - 0
src/components/tools/index.js


+ 237 - 0
src/components/tree/DirectoryTree.jsx

@@ -0,0 +1,237 @@
+/* eslint-disable */
+import omit from 'omit.js'
+import debounce from 'lodash/debounce'
+import PropTypes from 'ant-design-vue/es/_util/vue-types'
+import warning from 'ant-design-vue/es/_util/warning'
+import { conductExpandParent, convertTreeToEntities } from '../vc-tree/src/util'
+import Tree, { TreeProps } from './Tree'
+import {
+  calcRangeKeys,
+  getFullKeyList,
+  convertDirectoryKeysToNodes,
+  getFullKeyListByTreeData
+} from './util'
+import Icon from 'ant-design-vue/es/icon'
+import BaseMixin from 'ant-design-vue/es/_util/BaseMixin'
+import {
+  initDefaultProps,
+  getOptionProps,
+  getListeners,
+  getComponentFromProp
+} from 'ant-design-vue/es/_util/props-util'
+import { ConfigConsumerProps } from 'ant-design-vue/es/config-provider/configConsumerProps'
+
+// export type ExpandAction = false | 'click' | 'dblclick'; export interface
+// DirectoryTreeProps extends TreeProps {   expandAction?: ExpandAction; }
+// export interface DirectoryTreeState {   expandedKeys?: string[];
+// selectedKeys?: string[]; }
+
+function getIcon (props, h) {
+  const { isLeaf, expanded } = props
+  if (isLeaf) {
+    return <Icon type="file" />
+  }
+  return <Icon type={expanded ? 'folder-open' : 'folder'} />
+}
+
+export default {
+  name: 'ADirectoryTree',
+  mixins: [BaseMixin],
+  model: {
+    prop: 'checkedKeys',
+    event: 'check'
+  },
+  props: initDefaultProps(
+    {
+      ...TreeProps(),
+      expandAction: PropTypes.oneOf([false, 'click', 'doubleclick', 'dblclick'])
+    },
+    {
+      showIcon: true,
+      expandAction: 'click'
+    }
+  ),
+
+  // state: DirectoryTreeState; onDebounceExpand: (event, node: AntTreeNode) =>
+  // void; // Shift click usage lastSelectedKey?: string; cachedSelectedKeys?:
+  // string[];
+  inject: {
+    configProvider: {
+      default: () => ConfigConsumerProps
+    }
+  },
+  data () {
+    const props = getOptionProps(this)
+    const { defaultExpandAll, defaultExpandParent, expandedKeys, defaultExpandedKeys } = props
+    const { keyEntities } = convertTreeToEntities(this.$slots.default)
+    const state = {}
+    // Selected keys
+    state._selectedKeys = props.selectedKeys || props.defaultSelectedKeys || []
+
+    // Expanded keys
+    if (defaultExpandAll) {
+      if (props.treeData) {
+        state._expandedKeys = getFullKeyListByTreeData(props.treeData)
+      } else {
+        state._expandedKeys = getFullKeyList(this.$slots.default)
+      }
+    } else if (defaultExpandParent) {
+      state._expandedKeys = conductExpandParent(expandedKeys || defaultExpandedKeys, keyEntities)
+    } else {
+      state._expandedKeys = expandedKeys || defaultExpandedKeys
+    }
+
+    this.onDebounceExpand = debounce(this.expandFolderNode, 200, { leading: true })
+    return {
+      _selectedKeys: [],
+      _expandedKeys: [],
+      ...state
+    }
+  },
+  watch: {
+    expandedKeys (val) {
+      this.setState({ _expandedKeys: val })
+    },
+    selectedKeys (val) {
+      this.setState({ _selectedKeys: val })
+    }
+  },
+  methods: {
+    onExpand (expandedKeys, info) {
+      this.setUncontrolledState({ _expandedKeys: expandedKeys })
+
+      this.$emit('expand', expandedKeys, info)
+
+      return undefined
+    },
+
+    onClick (event, node) {
+      const { expandAction } = this.$props
+
+      // Expand the tree
+      if (expandAction === 'click') {
+        this.onDebounceExpand(event, node)
+      }
+      this.$emit('click', event, node)
+    },
+
+    onDoubleClick (event, node) {
+      const { expandAction } = this.$props
+
+      // Expand the tree
+      if (expandAction === 'dblclick' || expandAction === 'doubleclick') {
+        this.onDebounceExpand(event, node)
+      }
+
+      this.$emit('doubleclick', event, node)
+      this.$emit('dblclick', event, node)
+    },
+
+    onSelect (keys, event) {
+      const { multiple } = this.$props
+      const children = this.$slots.default || []
+      const { _expandedKeys: expandedKeys = [] } = this.$data
+      const { node, nativeEvent } = event
+      const { eventKey = '' } = node
+
+      const newState = {}
+
+      // We need wrap this event since some value is not same
+      const newEvent = {
+        ...event,
+        selected: true // Directory selected always true
+      }
+
+      // Windows / Mac single pick
+      const ctrlPick = nativeEvent.ctrlKey || nativeEvent.metaKey
+      const shiftPick = nativeEvent.shiftKey
+
+      // Generate new selected keys
+      let newSelectedKeys
+      if (multiple && ctrlPick) {
+        // Control click
+        newSelectedKeys = keys
+        this.lastSelectedKey = eventKey
+        this.cachedSelectedKeys = newSelectedKeys
+        newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys)
+      } else if (multiple && shiftPick) {
+        // Shift click
+        newSelectedKeys = Array.from(
+          new Set([
+            ...(this.cachedSelectedKeys || []),
+            ...calcRangeKeys(children, expandedKeys, eventKey, this.lastSelectedKey)
+          ])
+        )
+        newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys)
+      } else {
+        // Single click
+        newSelectedKeys = [eventKey]
+        this.lastSelectedKey = eventKey
+        this.cachedSelectedKeys = newSelectedKeys
+        newEvent.selectedNodes = [event.node]
+      }
+      newState._selectedKeys = newSelectedKeys
+
+      this.$emit('update:selectedKeys', newSelectedKeys)
+      this.$emit('select', newSelectedKeys, newEvent)
+
+      this.setUncontrolledState(newState)
+    },
+
+    expandFolderNode (event, node) {
+      const { isLeaf } = node
+
+      if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) {
+        return
+      }
+
+      if (this.$refs.tree.$refs.tree) {
+        // Get internal vc-tree
+        const internalTree = this.$refs.tree.$refs.tree
+
+        // Call internal rc-tree expand function
+        // https://github.com/ant-design/ant-design/issues/12567
+        internalTree.onNodeExpand(event, node)
+      }
+    },
+
+    setUncontrolledState (state) {
+      const newState = omit(
+        state,
+        Object.keys(getOptionProps(this)).map(p => `_${p}`)
+      )
+      if (Object.keys(newState).length) {
+        this.setState(newState)
+      }
+    }
+  },
+
+  render () {
+    const { prefixCls: customizePrefixCls, ...props } = getOptionProps(this)
+    const getPrefixCls = this.configProvider.getPrefixCls
+    const prefixCls = getPrefixCls('tree', customizePrefixCls)
+    const { _expandedKeys: expandedKeys, _selectedKeys: selectedKeys } = this.$data
+    const listeners = getListeners(this)
+    warning(!listeners.doubleclick, '`doubleclick` is deprecated. please use `dblclick` instead.')
+    const treeProps = {
+      props: {
+        icon: getIcon,
+        ...props,
+        prefixCls,
+        expandedKeys,
+        selectedKeys,
+        switcherIcon: getComponentFromProp(this, 'switcherIcon')
+      },
+      ref: 'tree',
+      class: `${prefixCls}-directory`,
+      on: {
+        ...omit(listeners, ['update:selectedKeys']),
+        select: this.onSelect,
+        click: this.onClick,
+        dblclick: this.onDoubleClick,
+        expand: this.onExpand
+      }
+    }
+    return <Tree {...treeProps}>{this.$slots.default}</Tree>
+  }
+}

+ 230 - 0
src/components/tree/Tree.jsx

@@ -0,0 +1,230 @@
+/* eslint-disable */
+import warning from 'warning'
+import { Tree as VcTree, TreeNode } from '../vc-tree'
+import animation from 'ant-design-vue/es/_util/openAnimation'
+import PropTypes from 'ant-design-vue/es/_util/vue-types'
+import {
+  initDefaultProps,
+  getOptionProps,
+  filterEmpty,
+  getComponentFromProp,
+  getListeners
+} from 'ant-design-vue/es/_util/props-util'
+import { cloneElement } from 'ant-design-vue/es/_util/vnode'
+import { ConfigConsumerProps } from 'ant-design-vue/es/config-provider/configConsumerProps'
+import Icon from 'ant-design-vue/es/icon'
+import './style';
+
+function TreeProps () {
+  return {
+    showLine: PropTypes.bool,
+    /** 是否支持多选 */
+    multiple: PropTypes.bool,
+    /** 是否自动展开父节点 */
+    autoExpandParent: PropTypes.bool,
+    /** checkable状态下节点选择完全受控(父子节点选中状态不再关联) */
+    checkStrictly: PropTypes.bool,
+    /** 是否支持选中 */
+    checkable: PropTypes.bool,
+    /** 是否禁用树 */
+    disabled: PropTypes.bool,
+    /** 默认展开所有树节点 */
+    defaultExpandAll: PropTypes.bool,
+    /** 默认展开对应树节点 */
+    defaultExpandParent: PropTypes.bool,
+    /** 默认展开指定的树节点 */
+    defaultExpandedKeys: PropTypes.array,
+    /** (受控)展开指定的树节点 */
+    expandedKeys: PropTypes.array,
+    /** (受控)选中复选框的树节点 */
+    checkedKeys: PropTypes.oneOfType([
+      PropTypes.array,
+      PropTypes.shape({
+        checked: PropTypes.array,
+        halfChecked: PropTypes.array
+      }).loose
+    ]),
+    /** 默认选中复选框的树节点 */
+    defaultCheckedKeys: PropTypes.array,
+    /** (受控)设置选中的树节点 */
+    selectedKeys: PropTypes.array,
+    /** 默认选中的树节点 */
+    defaultSelectedKeys: PropTypes.array,
+    selectable: PropTypes.bool,
+    /** 展开/收起节点时触发 */
+    // onExpand: (expandedKeys: string[], info: AntTreeNodeExpandedEvent) => void | PromiseLike<any>,
+    /** 点击复选框触发 */
+    // onCheck: (checkedKeys: string[] | { checked: string[]; halfChecked: string[] }, e: AntTreeNodeCheckedEvent) => void,
+    /** 点击树节点触发 */
+    // onSelect: (selectedKeys: string[], e: AntTreeNodeSelectedEvent) => void,
+    /** 单击树节点触发 */
+    // onClick: (e: React.MouseEvent<HTMLElement>, node: AntTreeNode) => void,
+    /** 双击树节点触发 */
+    // onDoubleClick: (e: React.MouseEvent<HTMLElement>, node: AntTreeNode) => void,
+    /** filter some AntTreeNodes as you need. it should return true */
+    filterAntTreeNode: PropTypes.func,
+    /** 异步加载数据 */
+    loadData: PropTypes.func,
+    loadedKeys: PropTypes.array,
+    // onLoaded: (loadedKeys: string[], info: { event: 'load', node: AntTreeNode; }) => void,
+    /** 响应右键点击 */
+    // onRightClick: (options: AntTreeNodeMouseEvent) => void,
+    /** 设置节点可拖拽(IE>8) */
+    draggable: PropTypes.bool,
+    // /** 开始拖拽时调用 */
+    // onDragStart: (options: AntTreeNodeMouseEvent) => void,
+    // /** dragenter 触发时调用 */
+    // onDragEnter: (options: AntTreeNodeMouseEvent) => void,
+    // /** dragover 触发时调用 */
+    // onDragOver: (options: AntTreeNodeMouseEvent) => void,
+    // /** dragleave 触发时调用 */
+    // onDragLeave: (options: AntTreeNodeMouseEvent) => void,
+    // /** drop 触发时调用 */
+    // onDrop: (options: AntTreeNodeMouseEvent) => void,
+    showIcon: PropTypes.bool,
+    icon: PropTypes.func,
+    switcherIcon: PropTypes.any,
+    prefixCls: PropTypes.string,
+    filterTreeNode: PropTypes.func,
+    openAnimation: PropTypes.any,
+    treeNodes: PropTypes.array,
+    treeData: PropTypes.array,
+    /**
+     * @default{title,key,children}
+     * 替换treeNode中 title,key,children字段为treeData中对应的字段
+     */
+    replaceFields: PropTypes.object,
+    blockNode: PropTypes.bool
+  }
+}
+
+export { TreeProps }
+
+export default {
+  name: 'ATree',
+  model: {
+    prop: 'checkedKeys',
+    event: 'check'
+  },
+  props: initDefaultProps(TreeProps(), {
+    checkable: false,
+    showIcon: false,
+    openAnimation: {
+      on: animation,
+      props: { appear: null }
+    },
+    blockNode: false
+  }),
+  inject: {
+    configProvider: { default: () => ConfigConsumerProps }
+  },
+  created () {
+    warning(
+      !('treeNodes' in getOptionProps(this)),
+      '`treeNodes` is deprecated. please use treeData instead.'
+    )
+  },
+  TreeNode,
+  methods: {
+    renderSwitcherIcon (prefixCls, switcherIcon, { isLeaf, expanded, loading }) {
+      const { showLine } = this.$props
+      if (loading) {
+        return <Icon type="loading" class={`${prefixCls}-switcher-loading-icon`} />
+      }
+
+      if (isLeaf) {
+        return showLine ? <Icon type="file" class={`${prefixCls}-switcher-line-icon`} /> : null
+      }
+      const switcherCls = `${prefixCls}-switcher-icon`
+      if (switcherIcon) {
+        return cloneElement(switcherIcon, {
+          class: {
+            [switcherCls]: true
+          }
+        })
+      }
+      return showLine ? (
+        <Icon
+          type={expanded ? 'minus-square' : 'plus-square'}
+          class={`${prefixCls}-switcher-line-icon`}
+          theme="outlined"
+        />
+      ) : (
+          <Icon type="caret-down" class={switcherCls} theme="filled" />
+        )
+    },
+    updateTreeData (treeData) {
+      const { $slots, $scopedSlots } = this
+      const defaultFields = { children: 'children', title: 'title', key: 'key' }
+      const replaceFields = { ...defaultFields, ...this.$props.replaceFields }
+      return treeData.map(item => {
+        const key = item[replaceFields.key]
+        const children = item[replaceFields.children]
+        const { on = {}, slots = {}, scopedSlots = {}, class: cls, style, ...restProps } = item
+        const treeNodeProps = {
+          ...restProps,
+          icon: $scopedSlots[scopedSlots.icon] || $slots[slots.icon] || restProps.icon,
+          switcherIcon:
+            $scopedSlots[scopedSlots.switcherIcon] ||
+            $slots[slots.switcherIcon] ||
+            restProps.switcherIcon,
+          title:
+            $scopedSlots[scopedSlots.title] ||
+            $slots[slots.title] ||
+            restProps[replaceFields.title],
+          dataRef: item,
+          on,
+          key,
+          class: cls,
+          style
+        }
+        if (children) {
+          return { ...treeNodeProps, children: this.updateTreeData(children) }
+        }
+        return treeNodeProps
+      })
+    },
+    getCheckedKeys () {
+      return this.$refs.tree.getCheckedKeys()
+    },
+    clearExpandedKeys () {
+      return this.$refs.tree.clearExpandedKeys()
+    },
+    getHalfCheckedKeys () {
+      return this.$refs.tree.getHalfCheckedKeys()
+    }
+  },
+  render () {
+    const props = getOptionProps(this)
+    const { $slots, $scopedSlots } = this
+    const { prefixCls: customizePrefixCls, showIcon, treeNodes, blockNode } = props
+    const getPrefixCls = this.configProvider.getPrefixCls
+    const prefixCls = getPrefixCls('tree', customizePrefixCls)
+    const switcherIcon = getComponentFromProp(this, 'switcherIcon')
+    const checkable = props.checkable
+    let treeData = props.treeData || treeNodes
+    if (treeData) {
+      treeData = this.updateTreeData(treeData)
+    }
+    const vcTreeProps = {
+      props: {
+        ...props,
+        prefixCls,
+        checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable,
+        children: filterEmpty($scopedSlots.default ? $scopedSlots.default() : $slots.default),
+        __propsSymbol__: Symbol(),
+        switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, switcherIcon, nodeProps)
+      },
+      on: getListeners(this),
+      ref: 'tree',
+      class: {
+        [`${prefixCls}-icon-hide`]: !showIcon,
+        [`${prefixCls}-block-node`]: blockNode
+      }
+    }
+    if (treeData) {
+      vcTreeProps.props.treeData = treeData
+    }
+    return <VcTree {...vcTreeProps} />
+  }
+}

+ 15 - 0
src/components/tree/index.jsx

@@ -0,0 +1,15 @@
+import Tree from './Tree'
+import DirectoryTree from './DirectoryTree'
+import Base from 'ant-design-vue/es/base'
+
+Tree.TreeNode.name = 'ATreeNode'
+Tree.DirectoryTree = DirectoryTree
+/* istanbul ignore next */
+Tree.install = function (Vue) {
+  Vue.use(Base)
+  Vue.component(Tree.name, Tree)
+  Vue.component(Tree.TreeNode.name, Tree.TreeNode)
+  Vue.component(DirectoryTree.name, DirectoryTree)
+}
+
+export default Tree

+ 95 - 0
src/components/tree/style/directory.less

@@ -0,0 +1,95 @@
+// @import 'ant-design-vue/es/style/themes/index';
+
+@tree-prefix-cls: ~'@{ant-prefix}-tree';
+
+.@{tree-prefix-cls} {
+  &.@{tree-prefix-cls}-directory {
+    position: relative;
+
+    // Stretch selector width
+    > li,
+    .@{tree-prefix-cls}-child-tree > li {
+      span {
+        &.@{tree-prefix-cls}-switcher {
+          position: relative;
+          z-index: 1;
+
+          &.@{tree-prefix-cls}-switcher-noop {
+            pointer-events: none;
+          }
+        }
+
+        &.@{tree-prefix-cls}-checkbox {
+          position: relative;
+          z-index: 1;
+        }
+
+        &.@{tree-prefix-cls}-node-content-wrapper {
+          border-radius: 0;
+          user-select: none;
+
+          &:hover {
+            background: transparent;
+
+            &::before {
+              background: @item-hover-bg;
+            }
+          }
+
+          &.@{tree-prefix-cls}-node-selected {
+            color: @tree-directory-selected-color;
+            background: transparent;
+          }
+
+          &::before {
+            position: absolute;
+            right: 0;
+            left: 0;
+            height: @tree-title-height;
+            transition: all 0.3s;
+            content: '';
+          }
+
+          > span {
+            position: relative;
+            z-index: 1;
+          }
+        }
+      }
+
+      &.@{tree-prefix-cls}-treenode-selected {
+        > span {
+          &.@{tree-prefix-cls}-switcher {
+            color: @tree-directory-selected-color;
+          }
+
+          &.@{tree-prefix-cls}-checkbox {
+            .@{tree-prefix-cls}-checkbox-inner {
+              border-color: @primary-color;
+            }
+
+            &.@{tree-prefix-cls}-checkbox-checked {
+              &::after {
+                border-color: @checkbox-check-color;
+              }
+
+              .@{tree-prefix-cls}-checkbox-inner {
+                background: @checkbox-check-color;
+
+                &::after {
+                  border-color: @primary-color;
+                }
+              }
+            }
+          }
+
+          &.@{tree-prefix-cls}-node-content-wrapper {
+            &::before {
+              background: @tree-directory-selected-bg;
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 2 - 0
src/components/tree/style/index.js

@@ -0,0 +1,2 @@
+import 'ant-design-vue/es/style/index.less'
+import './index.less'

+ 314 - 0
src/components/tree/style/index.less

@@ -0,0 +1,314 @@
+@import '~ant-design-vue/es/style/themes/index';
+@import '~ant-design-vue/es/style/mixins/index';
+@import '~ant-design-vue/es/checkbox/style/mixin';
+@import './mixin';
+@import './directory';
+
+@tree-prefix-cls         : ~'@{ant-prefix}-tree';
+@tree-showline-icon-color: @text-color-secondary;
+@tree-node-padding       : 4px;
+
+.antCheckboxFn(@checkbox-prefix-cls: ~'@{ant-prefix}-tree-checkbox');
+
+.@{tree-prefix-cls} {
+
+  /* see https://github.com/ant-design/ant-design/issues/16259 */
+  &-checkbox-checked::after {
+    position: absolute;
+    top     : 16.67%;
+    left    : 0;
+    width   : 100%;
+    height  : 66.67%;
+  }
+
+  .reset-component;
+
+  margin : 0;
+  padding: 0;
+
+  ol,
+  ul {
+    margin    : 0;
+    padding   : 0;
+    list-style: none;
+  }
+
+  li {
+    margin     : 0;
+    padding    : @tree-node-padding 0;
+    white-space: nowrap;
+    list-style : none;
+    outline    : 0;
+
+    span[draggable],
+    span[draggable='true'] {
+      line-height      : @tree-title-height - 4px;
+      border-top       : 2px transparent solid;
+      border-bottom    : 2px transparent solid;
+      user-select      : none;
+      /* Required to make elements draggable in old WebKit */
+      -khtml-user-drag : element;
+      -webkit-user-drag: element;
+    }
+
+    &.drag-over {
+      >span[draggable] {
+        color           : white;
+        background-color: @primary-color;
+        opacity         : 0.8;
+      }
+    }
+
+    &.drag-over-gap-top {
+      >span[draggable] {
+        border-top-color: @primary-color;
+      }
+    }
+
+    &.drag-over-gap-bottom {
+      >span[draggable] {
+        border-bottom-color: @primary-color;
+      }
+    }
+
+    &.filter-node {
+      >span {
+        color      : @highlight-color  !important;
+        font-weight: 500 !important;
+      }
+    }
+
+    // When node is loading
+    &.@{tree-prefix-cls}-treenode-loading {
+      span {
+        &.@{tree-prefix-cls}-switcher {
+
+          &.@{tree-prefix-cls}-switcher_open,
+          &.@{tree-prefix-cls}-switcher_close {
+            .@{tree-prefix-cls}-switcher-loading-icon {
+              position : absolute;
+              left     : 0;
+              display  : inline-block;
+              width    : 24px;
+              height   : @tree-title-height;
+              color    : @primary-color;
+              font-size: 14px;
+              transform: none;
+
+              svg {
+                position: absolute;
+                top     : 0;
+                right   : 0;
+                bottom  : 0;
+                left    : 0;
+                margin  : auto;
+              }
+            }
+
+            :root &::after {
+              opacity: 0;
+            }
+          }
+        }
+      }
+    }
+
+    ul {
+      margin : 0;
+      padding: 0 0 0 @tree-child-padding;
+    }
+
+    .@{tree-prefix-cls}-node-content-wrapper {
+      display        : inline-block;
+      height         : @tree-title-height;
+      margin         : 0;
+      padding        : 0 5px;
+      color          : @text-color;
+      line-height    : @tree-title-height;
+      text-decoration: none;
+      vertical-align : top;
+      border-radius  : @border-radius-sm;
+      cursor         : pointer;
+      transition     : all 0.3s;
+
+      &:hover {
+        background-color: @tree-node-hover-bg;
+      }
+
+      &.@{tree-prefix-cls}-node-selected {
+        background-color: @tree-node-selected-bg;
+      }
+    }
+
+    span {
+      &.@{tree-prefix-cls}-checkbox {
+        top    : initial;
+        height : @tree-title-height;
+        margin : 0 4px 0 2px;
+        padding: ((@tree-title-height - 16px) / 2) 0;
+      }
+
+      &.@{tree-prefix-cls}-switcher,
+      &.@{tree-prefix-cls}-iconEle {
+        display       : inline-block;
+        width         : 24px;
+        height        : @tree-title-height;
+        margin        : 0;
+        line-height   : @tree-title-height;
+        text-align    : center;
+        vertical-align: top;
+        border        : 0 none;
+        outline       : none;
+        cursor        : pointer;
+      }
+
+      &.@{tree-prefix-cls}-iconEle:empty {
+        display: none;
+      }
+
+      &.@{tree-prefix-cls}-switcher {
+        position: relative;
+
+        &.@{tree-prefix-cls}-switcher-noop {
+          cursor: default;
+        }
+
+        &.@{tree-prefix-cls}-switcher_open {
+          .antTreeSwitcherIcon();
+        }
+
+        &.@{tree-prefix-cls}-switcher_close {
+          .antTreeSwitcherIcon();
+
+          .@{tree-prefix-cls}-switcher-icon {
+            svg {
+              transform: rotate(-90deg);
+            }
+          }
+        }
+      }
+    }
+
+    &:last-child>span {
+
+      &.@{tree-prefix-cls}-switcher,
+      &.@{tree-prefix-cls}-iconEle {
+        &::before {
+          display: none;
+        }
+      }
+    }
+  }
+
+  >li {
+    &:first-child {
+      padding-top: 7px;
+    }
+
+    &:last-child {
+      padding-bottom: 7px;
+    }
+  }
+
+  &-child-tree {
+
+    // https://github.com/ant-design/ant-design/issues/14958
+    >li {
+
+      // Provide additional padding between top child node and parent node
+      &:first-child {
+        padding-top: 2 * @tree-node-padding;
+      }
+
+      // Hide additional padding between last child node and next parent node
+      &:last-child {
+        padding-bottom: 0;
+      }
+    }
+  }
+
+  li&-treenode-disabled {
+
+    >span:not(.@{tree-prefix-cls}-switcher),
+    >.@{tree-prefix-cls}-node-content-wrapper,
+    >.@{tree-prefix-cls}-node-content-wrapper span {
+      color : @disabled-color;
+      cursor: not-allowed;
+    }
+
+    >.@{tree-prefix-cls}-node-content-wrapper:hover {
+      background: transparent;
+    }
+  }
+
+  &-icon__open {
+    margin-right  : 2px;
+    vertical-align: top;
+  }
+
+  &-icon__close {
+    margin-right  : 2px;
+    vertical-align: top;
+  }
+
+  // Tree with line
+  &&-show-line {
+    li {
+      position: relative;
+
+      span {
+        &.@{tree-prefix-cls}-switcher {
+          color     : @tree-showline-icon-color;
+          background: @component-background;
+
+          &.@{tree-prefix-cls}-switcher-noop {
+            .antTreeShowLineIcon('tree-doc-icon');
+          }
+
+          &.@{tree-prefix-cls}-switcher_open {
+            .antTreeShowLineIcon('tree-showline-open-icon');
+          }
+
+          &.@{tree-prefix-cls}-switcher_close {
+            .antTreeShowLineIcon('tree-showline-close-icon');
+          }
+        }
+      }
+    }
+
+    li:not(:last-child)::before {
+      position   : absolute;
+      left       : 12px;
+      width      : 1px;
+      height     : 100%;
+      height     : calc(100% - 22px); // Remove additional height if support
+      margin     : 22px 0 0;
+      border-left: 1px solid @border-color-base;
+      content    : ' ';
+    }
+  }
+
+  &.@{tree-prefix-cls}-icon-hide {
+    .@{tree-prefix-cls}-treenode-loading {
+      .@{tree-prefix-cls}-iconEle {
+        display: none;
+      }
+    }
+  }
+
+  &.@{tree-prefix-cls}-block-node {
+    li {
+      .@{tree-prefix-cls}-node-content-wrapper {
+        width: ~'calc(100% - 24px)';
+      }
+
+      span {
+        &.@{tree-prefix-cls}-checkbox {
+          +.@{tree-prefix-cls}-node-content-wrapper {
+            width: ~'calc(100% - 46px)';
+          }
+        }
+      }
+    }
+  }
+}

+ 29 - 0
src/components/tree/style/mixin.less

@@ -0,0 +1,29 @@
+// @import '../../style/mixins/index';
+
+@tree-prefix-cls: ~'@{ant-prefix}-tree';
+@tree-select-prefix-cls: ~'@{ant-prefix}-select';
+
+.antTreeSwitcherIcon(@type: 'tree-default-open-icon') {
+  .@{tree-prefix-cls}-switcher-icon,
+  .@{tree-select-prefix-cls}-switcher-icon {
+    .iconfont-size-under-12px(10px);
+
+    display: inline-block;
+    font-weight: bold;
+    svg {
+      transition: transform 0.3s;
+    }
+  }
+}
+
+.antTreeShowLineIcon(@type) {
+  .@{tree-prefix-cls}-switcher-icon,
+  .@{tree-select-prefix-cls}-switcher-icon {
+    display: inline-block;
+    font-weight: normal;
+    font-size: 12px;
+    svg {
+      transition: transform 0.3s;
+    }
+  }
+}

+ 101 - 0
src/components/tree/util.js

@@ -0,0 +1,101 @@
+import { getNodeChildren, convertTreeToEntities } from '../vc-tree/src/util'
+import { getSlots } from 'ant-design-vue/es/_util/props-util'
+
+const Record = {
+  None: 'node',
+  Start: 'start',
+  End: 'end'
+}
+
+// TODO: Move this logic into `rc-tree`
+function traverseNodesKey (rootChildren, callback) {
+  const nodeList = getNodeChildren(rootChildren) || []
+
+  function processNode (node) {
+    const { key } = node
+    const children = getSlots(node).default
+    if (callback(key, node) !== false) {
+      traverseNodesKey(typeof children === 'function' ? children() : children, callback)
+    }
+  }
+
+  nodeList.forEach(processNode)
+}
+
+export function getFullKeyList (children) {
+  const { keyEntities } = convertTreeToEntities(children)
+  return [...keyEntities.keys()]
+}
+
+/** 计算选中范围,只考虑expanded情况以优化性能 */
+export function calcRangeKeys (rootChildren, expandedKeys, startKey, endKey) {
+  const keys = []
+  let record = Record.None
+
+  if (startKey && startKey === endKey) {
+    return [startKey]
+  }
+  if (!startKey || !endKey) {
+    return []
+  }
+
+  function matchKey (key) {
+    return key === startKey || key === endKey
+  }
+
+  traverseNodesKey(rootChildren, key => {
+    if (record === Record.End) {
+      return false
+    }
+
+    if (matchKey(key)) {
+      // Match test
+      keys.push(key)
+
+      if (record === Record.None) {
+        record = Record.Start
+      } else if (record === Record.Start) {
+        record = Record.End
+        return false
+      }
+    } else if (record === Record.Start) {
+      // Append selection
+      keys.push(key)
+    }
+
+    if (expandedKeys.indexOf(key) === -1) {
+      return false
+    }
+
+    return true
+  })
+
+  return keys
+}
+
+export function convertDirectoryKeysToNodes (rootChildren, keys) {
+  const restKeys = [...keys]
+  const nodes = []
+  traverseNodesKey(rootChildren, (key, node) => {
+    const index = restKeys.indexOf(key)
+    if (index !== -1) {
+      nodes.push(node)
+      restKeys.splice(index, 1)
+    }
+
+    return !!restKeys.length
+  })
+  return nodes
+}
+
+export function getFullKeyListByTreeData (treeData) {
+  let keys = [];
+
+  (treeData || []).forEach(item => {
+    keys.push(item.key)
+    if (item.children) {
+      keys = [...keys, ...getFullKeyListByTreeData(item.children)]
+    }
+  })
+  return keys
+}

BIN
src/components/vc-tree/assets/icons.png


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 73 - 0
src/components/vc-tree/assets/index.less


BIN
src/components/vc-tree/assets/line.gif


BIN
src/components/vc-tree/assets/loading.gif


+ 4 - 0
src/components/vc-tree/index.js

@@ -0,0 +1,4 @@
+// based on rc-tree 2.1.3
+'use strict'
+
+module.exports = require('./src/')

+ 701 - 0
src/components/vc-tree/src/Tree.jsx

@@ -0,0 +1,701 @@
+/* eslint-disable */
+import PropTypes from 'ant-design-vue/es/_util/vue-types'
+import classNames from 'classnames'
+import warning from 'warning'
+import { hasProp, initDefaultProps, getOptionProps, getSlots } from 'ant-design-vue/es/_util/props-util'
+import { cloneElement } from 'ant-design-vue/es/_util/vnode'
+import BaseMixin from 'ant-design-vue/es/_util/BaseMixin'
+import proxyComponent from 'ant-design-vue/es/_util/proxyComponent'
+import {
+  convertTreeToEntities,
+  convertDataToTree,
+  getPosition,
+  getDragNodesKeys,
+  parseCheckedKeys,
+  conductExpandParent,
+  calcSelectedKeys,
+  calcDropPosition,
+  arrAdd,
+  arrDel,
+  posToArr,
+  mapChildren,
+  conductCheck,
+  warnOnlyTreeNode
+} from './util'
+
+/**
+ * Thought we still use `cloneElement` to pass `key`,
+ * other props can pass with context for future refactor.
+ */
+
+function getWatch (keys = []) {
+  const watch = {}
+  keys.forEach(k => {
+    watch[k] = function () {
+      this.needSyncKeys[k] = true
+    }
+  })
+  return watch
+}
+
+const Tree = {
+  name: 'Tree',
+  mixins: [BaseMixin],
+  props: initDefaultProps(
+    {
+      prefixCls: PropTypes.string,
+      tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+      children: PropTypes.any,
+      treeData: PropTypes.array, // Generate treeNode by children
+      showLine: PropTypes.bool,
+      showIcon: PropTypes.bool,
+      icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+      focusable: PropTypes.bool,
+      selectable: PropTypes.bool,
+      disabled: PropTypes.bool,
+      multiple: PropTypes.bool,
+      checkable: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
+      checkStrictly: PropTypes.bool,
+      draggable: PropTypes.bool,
+      defaultExpandParent: PropTypes.bool,
+      autoExpandParent: PropTypes.bool,
+      defaultExpandAll: PropTypes.bool,
+      defaultExpandedKeys: PropTypes.array,
+      expandedKeys: PropTypes.array,
+      defaultCheckedKeys: PropTypes.array,
+      checkedKeys: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+      defaultSelectedKeys: PropTypes.array,
+      selectedKeys: PropTypes.array,
+      // onClick: PropTypes.func,
+      // onDoubleClick: PropTypes.func,
+      // onExpand: PropTypes.func,
+      // onCheck: PropTypes.func,
+      // onSelect: PropTypes.func,
+      loadData: PropTypes.func,
+      loadedKeys: PropTypes.array,
+      // onMouseEnter: PropTypes.func,
+      // onMouseLeave: PropTypes.func,
+      // onRightClick: PropTypes.func,
+      // onDragStart: PropTypes.func,
+      // onDragEnter: PropTypes.func,
+      // onDragOver: PropTypes.func,
+      // onDragLeave: PropTypes.func,
+      // onDragEnd: PropTypes.func,
+      // onDrop: PropTypes.func,
+      filterTreeNode: PropTypes.func,
+      openTransitionName: PropTypes.string,
+      openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+      switcherIcon: PropTypes.any,
+      _propsSymbol: PropTypes.any
+    },
+    {
+      prefixCls: 'rc-tree',
+      showLine: false,
+      showIcon: true,
+      selectable: true,
+      multiple: false,
+      checkable: false,
+      disabled: false,
+      checkStrictly: false,
+      draggable: false,
+      defaultExpandParent: true,
+      autoExpandParent: false,
+      defaultExpandAll: false,
+      defaultExpandedKeys: [],
+      defaultCheckedKeys: [],
+      defaultSelectedKeys: []
+    }
+  ),
+
+  data () {
+    warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__')
+    warning(this.$props.children, 'please use children prop replace slots.default')
+    this.needSyncKeys = {}
+    this.domTreeNodes = {}
+    const state = {
+      _posEntities: new Map(),
+      _keyEntities: new Map(),
+      _expandedKeys: [],
+      _selectedKeys: [],
+      _checkedKeys: [],
+      _halfCheckedKeys: [],
+      _loadedKeys: [],
+      _loadingKeys: [],
+      _treeNode: [],
+      _prevProps: null,
+      _dragOverNodeKey: '',
+      _dropPosition: null,
+      _dragNodesKeys: []
+    }
+    return {
+      ...state,
+      ...this.getDerivedState(getOptionProps(this), state)
+    }
+  },
+  provide () {
+    return {
+      vcTree: this
+    }
+  },
+
+  watch: {
+    ...getWatch([
+      'treeData',
+      'children',
+      'expandedKeys',
+      'autoExpandParent',
+      'selectedKeys',
+      'checkedKeys',
+      'loadedKeys'
+    ]),
+    __propsSymbol__ () {
+      this.setState(this.getDerivedState(getOptionProps(this), this.$data))
+      this.needSyncKeys = {}
+    }
+  },
+
+  methods: {
+    getDerivedState (props, prevState) {
+      const { _prevProps } = prevState
+      const newState = {
+        _prevProps: { ...props }
+      }
+      const self = this
+      function needSync (name) {
+        return (!_prevProps && name in props) || (_prevProps && self.needSyncKeys[name])
+      }
+
+      // ================== Tree Node ==================
+      let treeNode = null
+
+      // Check if `treeData` or `children` changed and save into the state.
+      if (needSync('treeData')) {
+        treeNode = convertDataToTree(this.$createElement, props.treeData)
+      } else if (needSync('children')) {
+        treeNode = props.children
+      }
+
+      // Tree support filter function which will break the tree structure in the vdm.
+      // We cache the treeNodes in state so that we can return the treeNode in event trigger.
+      if (treeNode) {
+        newState._treeNode = treeNode
+
+        // Calculate the entities data for quick match
+        const entitiesMap = convertTreeToEntities(treeNode)
+        newState._keyEntities = entitiesMap.keyEntities
+      }
+
+      const keyEntities = newState._keyEntities || prevState._keyEntities
+
+      // ================ expandedKeys =================
+      if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
+        newState._expandedKeys =
+          props.autoExpandParent || (!_prevProps && props.defaultExpandParent)
+            ? conductExpandParent(props.expandedKeys, keyEntities)
+            : props.expandedKeys
+      } else if (!_prevProps && props.defaultExpandAll) {
+        newState._expandedKeys = [...keyEntities.keys()]
+      } else if (!_prevProps && props.defaultExpandedKeys) {
+        newState._expandedKeys =
+          props.autoExpandParent || props.defaultExpandParent
+            ? conductExpandParent(props.defaultExpandedKeys, keyEntities)
+            : props.defaultExpandedKeys
+      }
+
+      // ================ selectedKeys =================
+      if (props.selectable) {
+        if (needSync('selectedKeys')) {
+          newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props)
+        } else if (!_prevProps && props.defaultSelectedKeys) {
+          newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props)
+        }
+      }
+
+      // ================= checkedKeys =================
+      if (props.checkable) {
+        let checkedKeyEntity
+
+        if (needSync('checkedKeys')) {
+          checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {}
+        } else if (!_prevProps && props.defaultCheckedKeys) {
+          checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {}
+        } else if (treeNode) {
+          // If treeNode changed, we also need check it
+          checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {
+            checkedKeys: prevState._checkedKeys,
+            halfCheckedKeys: prevState._halfCheckedKeys
+          }
+        }
+
+        if (checkedKeyEntity) {
+          let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity
+
+          if (!props.checkStrictly) {
+            const conductKeys = conductCheck(checkedKeys, true, keyEntities);
+            ({ checkedKeys, halfCheckedKeys } = conductKeys)
+          }
+
+          newState._checkedKeys = checkedKeys
+          newState._halfCheckedKeys = halfCheckedKeys
+        }
+      }
+      // ================= loadedKeys ==================
+      if (needSync('loadedKeys')) {
+        newState._loadedKeys = props.loadedKeys
+      }
+
+      return newState
+    },
+    onNodeDragStart (event, node) {
+      const { _expandedKeys } = this.$data
+      const { eventKey } = node
+      const children = getSlots(node).default
+      this.dragNode = node
+
+      this.setState({
+        _dragNodesKeys: getDragNodesKeys(
+          typeof children === 'function' ? children() : children,
+          node
+        ),
+        _expandedKeys: arrDel(_expandedKeys, eventKey)
+      })
+      this.__emit('dragstart', { event, node })
+    },
+
+    /**
+     * [Legacy] Select handler is less small than node,
+     * so that this will trigger when drag enter node or select handler.
+     * This is a little tricky if customize css without padding.
+     * Better for use mouse move event to refresh drag state.
+     * But let's just keep it to avoid event trigger logic change.
+     */
+    onNodeDragEnter (event, node) {
+      const { _expandedKeys: expandedKeys } = this.$data
+      const { pos, eventKey } = node
+
+      if (!this.dragNode || !node.$refs.selectHandle) return
+
+      const dropPosition = calcDropPosition(event, node)
+
+      // Skip if drag node is self
+      if (this.dragNode.eventKey === eventKey && dropPosition === 0) {
+        this.setState({
+          _dragOverNodeKey: '',
+          _dropPosition: null
+        })
+        return
+      }
+
+      // Ref: https://github.com/react-component/tree/issues/132
+      // Add timeout to let onDragLevel fire before onDragEnter,
+      // so that we can clean drag props for onDragLeave node.
+      // Macro task for this:
+      // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
+      setTimeout(() => {
+        // Update drag over node
+        this.setState({
+          _dragOverNodeKey: eventKey,
+          _dropPosition: dropPosition
+        })
+
+        // Side effect for delay drag
+        if (!this.delayedDragEnterLogic) {
+          this.delayedDragEnterLogic = {}
+        }
+        Object.keys(this.delayedDragEnterLogic).forEach(key => {
+          clearTimeout(this.delayedDragEnterLogic[key])
+        })
+        this.delayedDragEnterLogic[pos] = setTimeout(() => {
+          const newExpandedKeys = arrAdd(expandedKeys, eventKey)
+          if (!hasProp(this, 'expandedKeys')) {
+            this.setState({
+              _expandedKeys: newExpandedKeys
+            })
+          }
+          this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys })
+        }, 400)
+      }, 0)
+    },
+    onNodeDragOver (event, node) {
+      const { eventKey } = node
+      const { _dragOverNodeKey, _dropPosition } = this.$data
+      // Update drag position
+      if (this.dragNode && eventKey === _dragOverNodeKey && node.$refs.selectHandle) {
+        const dropPosition = calcDropPosition(event, node)
+
+        if (dropPosition === _dropPosition) return
+
+        this.setState({
+          _dropPosition: dropPosition
+        })
+      }
+      this.__emit('dragover', { event, node })
+    },
+    onNodeDragLeave (event, node) {
+      this.setState({
+        _dragOverNodeKey: ''
+      })
+      this.__emit('dragleave', { event, node })
+    },
+    onNodeDragEnd (event, node) {
+      this.setState({
+        _dragOverNodeKey: ''
+      })
+      this.__emit('dragend', { event, node })
+      this.dragNode = null
+    },
+    onNodeDrop (event, node) {
+      const { _dragNodesKeys = [], _dropPosition } = this.$data
+
+      const { eventKey, pos } = node
+
+      this.setState({
+        _dragOverNodeKey: ''
+      })
+
+      if (_dragNodesKeys.indexOf(eventKey) !== -1) {
+        warning(false, "Can not drop to dragNode(include it's children node)")
+        return
+      }
+
+      const posArr = posToArr(pos)
+
+      const dropResult = {
+        event,
+        node,
+        dragNode: this.dragNode,
+        dragNodesKeys: _dragNodesKeys.slice(),
+        dropPosition: _dropPosition + Number(posArr[posArr.length - 1]),
+        dropToGap: false
+      }
+
+      if (_dropPosition !== 0) {
+        dropResult.dropToGap = true
+      }
+      this.__emit('drop', dropResult)
+      this.dragNode = null
+    },
+
+    onNodeClick (e, treeNode) {
+      this.__emit('click', e, treeNode)
+    },
+
+    onNodeDoubleClick (e, treeNode) {
+      this.__emit('dblclick', e, treeNode)
+    },
+
+    onNodeSelect (e, treeNode) {
+      let { _selectedKeys: selectedKeys } = this.$data
+      const { _keyEntities: keyEntities } = this.$data
+      const { multiple } = this.$props
+      const { selected, eventKey } = getOptionProps(treeNode)
+      const targetSelected = !selected
+      // Update selected keys
+      if (!targetSelected) {
+        selectedKeys = arrDel(selectedKeys, eventKey)
+      } else if (!multiple) {
+        selectedKeys = [eventKey]
+      } else {
+        selectedKeys = arrAdd(selectedKeys, eventKey)
+      }
+
+      // [Legacy] Not found related usage in doc or upper libs
+      const selectedNodes = selectedKeys
+        .map(key => {
+          const entity = keyEntities.get(key)
+          if (!entity) return null
+
+          return entity.node
+        })
+        .filter(node => node)
+
+      this.setUncontrolledState({ _selectedKeys: selectedKeys })
+
+      const eventObj = {
+        event: 'select',
+        selected: targetSelected,
+        node: treeNode,
+        selectedNodes,
+        nativeEvent: e
+      }
+      this.__emit('update:selectedKeys', selectedKeys)
+      this.__emit('select', selectedKeys, eventObj)
+    },
+
+    getCheckedKeys () {
+      // _checkedKeys 是选中的节点
+      return this.$data._checkedKeys
+    },
+    clearExpandedKeys () {
+      this.$data._expandedKeys = []
+    },
+    getHalfCheckedKeys () {
+      // _halfCheckedKeys 是半选中的父节点
+      return this.$data._halfCheckedKeys
+    },
+
+    /**
+     * 复选框选中事件
+     * @param {*} e
+     * @param {*} treeNode
+     * @param {*} checked
+     */
+    onNodeCheck (e, treeNode, checked) {
+      const {
+        _keyEntities: keyEntities,
+        _checkedKeys: oriCheckedKeys,
+        _halfCheckedKeys: oriHalfCheckedKeys
+      } = this.$data
+      const { checkStrictly } = this.$props
+      const { eventKey } = getOptionProps(treeNode)
+
+      // Prepare trigger arguments
+      let checkedObj
+      const eventObj = {
+        event: 'check',
+        node: treeNode,
+        checked,
+        nativeEvent: e
+      }
+
+      if (checkStrictly) {
+        const checkedKeys = checked
+          ? arrAdd(oriCheckedKeys, eventKey)
+          : arrDel(oriCheckedKeys, eventKey)
+        const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey)
+        checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys }
+
+        eventObj.checkedNodes = checkedKeys
+          .map(key => keyEntities.get(key))
+          .filter(entity => entity)
+          .map(entity => entity.node)
+
+        this.setUncontrolledState({ _checkedKeys: checkedKeys })
+      } else {
+        const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
+          checkedKeys: oriCheckedKeys,
+          halfCheckedKeys: oriHalfCheckedKeys
+        })
+
+        checkedObj = checkedKeys
+
+        // [Legacy] This is used for `rc-tree-select`
+        eventObj.checkedNodes = []
+        eventObj.checkedNodesPositions = []
+        eventObj.halfCheckedKeys = halfCheckedKeys
+
+        checkedKeys.forEach(key => {
+          const entity = keyEntities.get(key)
+          if (!entity) return
+
+          const { node, pos } = entity
+
+          eventObj.checkedNodes.push(node)
+          eventObj.checkedNodesPositions.push({ node, pos })
+        })
+
+        this.setUncontrolledState({
+          _checkedKeys: checkedKeys,
+          _halfCheckedKeys: halfCheckedKeys
+        })
+      }
+      this.__emit('check', checkedObj, eventObj)
+    },
+    onNodeLoad (treeNode) {
+      return new Promise(resolve => {
+        // We need to get the latest state of loading/loaded keys
+        this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
+          const { loadData } = this.$props
+          const { eventKey } = getOptionProps(treeNode)
+
+          if (
+            !loadData ||
+            loadedKeys.indexOf(eventKey) !== -1 ||
+            loadingKeys.indexOf(eventKey) !== -1
+          ) {
+            return {}
+          }
+
+          // Process load data
+          const promise = loadData(treeNode)
+          promise.then(() => {
+            const { _loadedKeys: currentLoadedKeys, _loadingKeys: currentLoadingKeys } = this.$data
+            const newLoadedKeys = arrAdd(currentLoadedKeys, eventKey)
+            const newLoadingKeys = arrDel(currentLoadingKeys, eventKey)
+
+            // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
+            // https://github.com/ant-design/ant-design/issues/12464
+            this.__emit('load', newLoadedKeys, {
+              event: 'load',
+              node: treeNode
+            })
+            this.setUncontrolledState({
+              _loadedKeys: newLoadedKeys
+            })
+            this.setState({
+              _loadingKeys: newLoadingKeys
+            })
+            resolve()
+          })
+
+          return {
+            _loadingKeys: arrAdd(loadingKeys, eventKey)
+          }
+        })
+      })
+    },
+
+    onNodeExpand (e, treeNode) {
+      let { _expandedKeys: expandedKeys } = this.$data
+      const { loadData } = this.$props
+      const { eventKey, expanded } = getOptionProps(treeNode)
+
+      // Update selected keys
+      const index = expandedKeys.indexOf(eventKey)
+      const targetExpanded = !expanded
+
+      warning(
+        (expanded && index !== -1) || (!expanded && index === -1),
+        'Expand state not sync with index check'
+      )
+
+      if (targetExpanded) {
+        expandedKeys = arrAdd(expandedKeys, eventKey)
+      } else {
+        expandedKeys = arrDel(expandedKeys, eventKey)
+      }
+
+      this.setUncontrolledState({ _expandedKeys: expandedKeys })
+      this.__emit('expand', expandedKeys, {
+        node: treeNode,
+        expanded: targetExpanded,
+        nativeEvent: e
+      })
+      this.__emit('update:expandedKeys', expandedKeys)
+
+      // Async Load data
+      if (targetExpanded && loadData) {
+        const loadPromise = this.onNodeLoad(treeNode)
+        return loadPromise
+          ? loadPromise.then(() => {
+            // [Legacy] Refresh logic
+            this.setUncontrolledState({ _expandedKeys: expandedKeys })
+          })
+          : null
+      }
+
+      return null
+    },
+
+    onNodeMouseEnter (event, node) {
+      this.__emit('mouseenter', { event, node })
+    },
+
+    onNodeMouseLeave (event, node) {
+      this.__emit('mouseleave', { event, node })
+    },
+
+    onNodeContextMenu (event, node) {
+      event.preventDefault()
+      this.__emit('rightClick', { event, node })
+    },
+
+    /**
+     * Only update the value which is not in props
+     */
+    setUncontrolledState (state) {
+      let needSync = false
+      const newState = {}
+      const props = getOptionProps(this)
+      Object.keys(state).forEach(name => {
+        if (name.replace('_', '') in props) return
+        needSync = true
+        newState[name] = state[name]
+      })
+
+      if (needSync) {
+        this.setState(newState)
+      }
+    },
+
+    registerTreeNode (key, node) {
+      if (node) {
+        this.domTreeNodes[key] = node
+      } else {
+        delete this.domTreeNodes[key]
+      }
+    },
+
+    isKeyChecked (key) {
+      const { _checkedKeys: checkedKeys = [] } = this.$data
+      return checkedKeys.indexOf(key) !== -1
+    },
+
+    /**
+     * [Legacy] Original logic use `key` as tracking clue.
+     * We have to use `cloneElement` to pass `key`.
+     */
+    renderTreeNode (child, index, level = 0) {
+      const {
+        _keyEntities: keyEntities,
+        _expandedKeys: expandedKeys = [],
+        _selectedKeys: selectedKeys = [],
+        _halfCheckedKeys: halfCheckedKeys = [],
+        _loadedKeys: loadedKeys = [],
+        _loadingKeys: loadingKeys = [],
+        _dragOverNodeKey: dragOverNodeKey,
+        _dropPosition: dropPosition
+      } = this.$data
+      const pos = getPosition(level, index)
+      let key = child.key
+      if (!key && (key === undefined || key === null)) {
+        key = pos
+      }
+      if (!keyEntities.get(key)) {
+        warnOnlyTreeNode()
+        return null
+      }
+
+      return cloneElement(child, {
+        props: {
+          eventKey: key,
+          expanded: expandedKeys.indexOf(key) !== -1,
+          selected: selectedKeys.indexOf(key) !== -1,
+          loaded: loadedKeys.indexOf(key) !== -1,
+          loading: loadingKeys.indexOf(key) !== -1,
+          checked: this.isKeyChecked(key),
+          halfChecked: halfCheckedKeys.indexOf(key) !== -1,
+          pos,
+
+          // [Legacy] Drag props
+          dragOver: dragOverNodeKey === key && dropPosition === 0,
+          dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
+          dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1
+        },
+        key
+      })
+    }
+  },
+
+  render () {
+    const { _treeNode: treeNode } = this.$data
+    const { prefixCls, focusable, showLine, tabIndex = 0 } = this.$props
+
+    return (
+      <ul
+        class={classNames(prefixCls, {
+          [`${prefixCls}-show-line`]: showLine
+        })}
+        role="tree"
+        unselectable="on"
+        tabIndex={focusable ? tabIndex : null}
+      >
+        {mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
+      </ul>
+    )
+  }
+}
+
+export { Tree }
+
+export default proxyComponent(Tree)

+ 571 - 0
src/components/vc-tree/src/TreeNode.jsx

@@ -0,0 +1,571 @@
+import PropTypes from 'ant-design-vue/es/_util/vue-types'
+import classNames from 'classnames'
+import { getNodeChildren, mapChildren, warnOnlyTreeNode } from './util'
+import { initDefaultProps, filterEmpty, getComponentFromProp } from 'ant-design-vue/es/_util/props-util'
+import BaseMixin from 'ant-design-vue/es/_util/BaseMixin'
+import getTransitionProps from 'ant-design-vue/es/_util/getTransitionProps'
+
+function noop () {}
+const ICON_OPEN = 'open'
+const ICON_CLOSE = 'close'
+
+const defaultTitle = '---'
+
+const TreeNode = {
+  name: 'TreeNode',
+  mixins: [BaseMixin],
+  __ANT_TREE_NODE: true,
+  props: initDefaultProps(
+    {
+      eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // Pass by parent `cloneElement`
+      prefixCls: PropTypes.string,
+      // className: PropTypes.string,
+      root: PropTypes.object,
+      // onSelect: PropTypes.func,
+
+      // By parent
+      expanded: PropTypes.bool,
+      selected: PropTypes.bool,
+      checked: PropTypes.bool,
+      loaded: PropTypes.bool,
+      loading: PropTypes.bool,
+      halfChecked: PropTypes.bool,
+      title: PropTypes.any,
+      pos: PropTypes.string,
+      dragOver: PropTypes.bool,
+      dragOverGapTop: PropTypes.bool,
+      dragOverGapBottom: PropTypes.bool,
+
+      // By user
+      isLeaf: PropTypes.bool,
+      checkable: PropTypes.bool,
+      selectable: PropTypes.bool,
+      disabled: PropTypes.bool,
+      disableCheckbox: PropTypes.bool,
+      icon: PropTypes.any,
+      dataRef: PropTypes.object,
+      switcherIcon: PropTypes.any,
+      label: PropTypes.any,
+      value: PropTypes.any
+    },
+    {}
+  ),
+
+  data () {
+    return {
+      dragNodeHighlight: false
+    }
+  },
+  inject: {
+    vcTree: { default: () => ({}) },
+    vcTreeNode: { default: () => ({}) }
+  },
+  provide () {
+    return {
+      vcTreeNode: this
+    }
+  },
+
+  // Isomorphic needn't load data in server side
+  mounted () {
+    const {
+      eventKey,
+      vcTree: { registerTreeNode }
+    } = this
+    this.syncLoadData(this.$props)
+    registerTreeNode && registerTreeNode(eventKey, this)
+  },
+  updated () {
+    this.syncLoadData(this.$props)
+  },
+  beforeDestroy () {
+    const {
+      eventKey,
+      vcTree: { registerTreeNode }
+    } = this
+    registerTreeNode && registerTreeNode(eventKey, null)
+  },
+
+  methods: {
+    onSelectorClick (e) {
+      // Click trigger before select/check operation
+      const {
+        vcTree: { onNodeClick }
+      } = this
+      onNodeClick(e, this)
+      if (this.isSelectable()) {
+        this.onSelect(e)
+      } else {
+        this.onCheck(e)
+      }
+    },
+
+    onSelectorDoubleClick (e) {
+      const {
+        vcTree: { onNodeDoubleClick }
+      } = this
+      onNodeDoubleClick(e, this)
+    },
+
+    onSelect (e) {
+      if (this.isDisabled()) return
+
+      const {
+        vcTree: { onNodeSelect }
+      } = this
+      e.preventDefault()
+      onNodeSelect(e, this)
+    },
+
+    onCheck (e) {
+      if (this.isDisabled()) return
+
+      const { disableCheckbox, checked } = this
+      const {
+        vcTree: { onNodeCheck }
+      } = this
+
+      if (!this.isCheckable() || disableCheckbox) return
+
+      e.preventDefault()
+      const targetChecked = !checked
+      onNodeCheck(e, this, targetChecked)
+    },
+
+    onMouseEnter (e) {
+      const {
+        vcTree: { onNodeMouseEnter }
+      } = this
+      onNodeMouseEnter(e, this)
+    },
+
+    onMouseLeave (e) {
+      const {
+        vcTree: { onNodeMouseLeave }
+      } = this
+      onNodeMouseLeave(e, this)
+    },
+
+    onContextMenu (e) {
+      const {
+        vcTree: { onNodeContextMenu }
+      } = this
+      onNodeContextMenu(e, this)
+    },
+
+    onDragStart (e) {
+      const {
+        vcTree: { onNodeDragStart }
+      } = this
+
+      e.stopPropagation()
+      this.setState({
+        dragNodeHighlight: true
+      })
+      onNodeDragStart(e, this)
+
+      try {
+        // ie throw error
+        // firefox-need-it
+        e.dataTransfer.setData('text/plain', '')
+      } catch (error) {
+        // empty
+      }
+    },
+
+    onDragEnter (e) {
+      const {
+        vcTree: { onNodeDragEnter }
+      } = this
+
+      e.preventDefault()
+      e.stopPropagation()
+      onNodeDragEnter(e, this)
+    },
+
+    onDragOver (e) {
+      const {
+        vcTree: { onNodeDragOver }
+      } = this
+
+      e.preventDefault()
+      e.stopPropagation()
+      onNodeDragOver(e, this)
+    },
+
+    onDragLeave (e) {
+      const {
+        vcTree: { onNodeDragLeave }
+      } = this
+
+      e.stopPropagation()
+      onNodeDragLeave(e, this)
+    },
+
+    onDragEnd (e) {
+      const {
+        vcTree: { onNodeDragEnd }
+      } = this
+
+      e.stopPropagation()
+      this.setState({
+        dragNodeHighlight: false
+      })
+      onNodeDragEnd(e, this)
+    },
+
+    onDrop (e) {
+      const {
+        vcTree: { onNodeDrop }
+      } = this
+
+      e.preventDefault()
+      e.stopPropagation()
+      this.setState({
+        dragNodeHighlight: false
+      })
+      onNodeDrop(e, this)
+    },
+
+    // Disabled item still can be switch
+    onExpand (e) {
+      const {
+        vcTree: { onNodeExpand }
+      } = this
+      onNodeExpand(e, this)
+    },
+
+    getNodeChildren () {
+      const {
+        $slots: { default: children }
+      } = this
+      const originList = filterEmpty(children)
+      const targetList = getNodeChildren(originList)
+
+      if (originList.length !== targetList.length) {
+        warnOnlyTreeNode()
+      }
+
+      return targetList
+    },
+
+    getNodeState () {
+      const { expanded } = this
+
+      if (this.isLeaf2()) {
+        return null
+      }
+
+      return expanded ? ICON_OPEN : ICON_CLOSE
+    },
+
+    isLeaf2 () {
+      const { isLeaf, loaded } = this
+      const {
+        vcTree: { loadData }
+      } = this
+
+      const hasChildren = this.getNodeChildren().length !== 0
+      if (isLeaf === false) {
+        return false
+      }
+      return isLeaf || (!loadData && !hasChildren) || (loadData && loaded && !hasChildren)
+    },
+
+    isDisabled () {
+      const { disabled } = this
+      const {
+        vcTree: { disabled: treeDisabled }
+      } = this
+
+      // Follow the logic of Selectable
+      if (disabled === false) {
+        return false
+      }
+
+      return !!(treeDisabled || disabled)
+    },
+
+    isCheckable () {
+      const { checkable } = this.$props
+      const {
+        vcTree: { checkable: treeCheckable }
+      } = this
+
+      // Return false if tree or treeNode is not checkable
+      if (!treeCheckable || checkable === false) return false
+      return treeCheckable
+    },
+
+    // Load data to avoid default expanded tree without data
+    syncLoadData (props) {
+      const { expanded, loading, loaded } = props
+      const {
+        vcTree: { loadData, onNodeLoad }
+      } = this
+      if (loading) return
+      // read from state to avoid loadData at same time
+      if (loadData && expanded && !this.isLeaf2()) {
+        // We needn't reload data when has children in sync logic
+        // It's only needed in node expanded
+        const hasChildren = this.getNodeChildren().length !== 0
+        if (!hasChildren && !loaded) {
+          onNodeLoad(this)
+        }
+      }
+    },
+
+    isSelectable () {
+      const { selectable } = this
+      const {
+        vcTree: { selectable: treeSelectable }
+      } = this
+
+      // Ignore when selectable is undefined or null
+      if (typeof selectable === 'boolean') {
+        return selectable
+      }
+
+      return treeSelectable
+    },
+
+    // Switcher
+    renderSwitcher () {
+      const { expanded } = this
+      const {
+        vcTree: { prefixCls }
+      } = this
+      const switcherIcon =
+        getComponentFromProp(this, 'switcherIcon', {}, false) ||
+        getComponentFromProp(this.vcTree, 'switcherIcon', {}, false)
+      if (this.isLeaf2()) {
+        return (
+          <span
+            key="switcher"
+            class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}
+          >
+            {typeof switcherIcon === 'function'
+              ? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: true })
+              : switcherIcon}
+          </span>
+        )
+      }
+
+      const switcherCls = classNames(
+        `${prefixCls}-switcher`,
+        `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`
+      )
+      return (
+        <span key="switcher" onClick={this.onExpand} class={switcherCls}>
+          {typeof switcherIcon === 'function'
+            ? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: false })
+            : switcherIcon}
+        </span>
+      )
+    },
+
+    // Checkbox
+    renderCheckbox () {
+      const { checked, halfChecked, disableCheckbox } = this
+      const {
+        vcTree: { prefixCls }
+      } = this
+      const disabled = this.isDisabled()
+      const checkable = this.isCheckable()
+
+      if (!checkable) return null
+
+      // [Legacy] Custom element should be separate with `checkable` in future
+      const $custom = typeof checkable !== 'boolean' ? checkable : null
+
+      return (
+        <span
+          key="checkbox"
+          class={classNames(
+            `${prefixCls}-checkbox`,
+            checked && `${prefixCls}-checkbox-checked`,
+            !checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
+            (disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`
+          )}
+          onClick={this.onCheck}
+        >
+          {$custom}
+        </span>
+      )
+    },
+
+    renderIcon () {
+      const { loading } = this
+      const {
+        vcTree: { prefixCls }
+      } = this
+
+      return (
+        <span
+          key="icon"
+          class={classNames(
+            `${prefixCls}-iconEle`,
+            `${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
+            loading && `${prefixCls}-icon_loading`
+          )}
+        />
+      )
+    },
+
+    // Icon + Title
+    renderSelector (h) {
+      const { selected, loading, dragNodeHighlight } = this
+      const icon = getComponentFromProp(this, 'icon', {}, false)
+      const {
+        vcTree: { prefixCls, showIcon, icon: treeIcon, draggable, loadData }
+      } = this
+      const disabled = this.isDisabled()
+      const title = getComponentFromProp(this, 'title', {}, false)
+      const wrapClass = `${prefixCls}-node-content-wrapper`
+
+      // Icon - Still show loading icon when loading without showIcon
+      let $icon
+
+      if (showIcon) {
+        const currentIcon = icon || treeIcon
+        $icon = currentIcon ? (
+          <span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
+            {typeof currentIcon === 'function'
+              ? currentIcon({ ...this.$props, ...this.$props.dataRef }, h)
+              : currentIcon}
+          </span>
+        ) : (
+          this.renderIcon()
+        )
+      } else if (loadData && loading) {
+        $icon = this.renderIcon()
+      }
+
+      const currentTitle = title
+      const $title = currentTitle ? (
+        <span class={`${prefixCls}-title`}>
+          {typeof currentTitle === 'function'
+            ? currentTitle({ ...this.$props, ...this.$props.dataRef }, h)
+            : currentTitle}
+        </span>
+      ) : (
+        <span class={`${prefixCls}-title`}>{defaultTitle}</span>
+      )
+
+      return (
+        <span
+          key="selector"
+          ref="selectHandle"
+          title={typeof title === 'string' ? title : ''}
+          class={classNames(
+            `${wrapClass}`,
+            `${wrapClass}-${this.getNodeState() || 'normal'}`,
+            !disabled && (selected || dragNodeHighlight) && `${prefixCls}-node-selected`,
+            !disabled && draggable && 'draggable'
+          )}
+          draggable={(!disabled && draggable) || undefined}
+          aria-grabbed={(!disabled && draggable) || undefined}
+          onMouseenter={this.onMouseEnter}
+          onMouseleave={this.onMouseLeave}
+          onContextmenu={this.onContextMenu}
+          onClick={this.onSelectorClick}
+          onDblclick={this.onSelectorDoubleClick}
+          onDragstart={draggable ? this.onDragStart : noop}
+        >
+          {$icon}
+          {$title}
+        </span>
+      )
+    },
+
+    // Children list wrapped with `Animation`
+    renderChildren () {
+      const { expanded, pos } = this
+      const {
+        vcTree: { prefixCls, openTransitionName, openAnimation, renderTreeNode }
+      } = this
+
+      let animProps = {}
+      if (openTransitionName) {
+        animProps = getTransitionProps(openTransitionName)
+      } else if (typeof openAnimation === 'object') {
+        animProps = { ...openAnimation }
+        animProps.props = { css: false, ...animProps.props }
+      }
+
+      // Children TreeNode
+      const nodeList = this.getNodeChildren()
+
+      if (nodeList.length === 0) {
+        return null
+      }
+
+      let $children
+      if (expanded) {
+        $children = (
+          <ul
+            class={classNames(
+              `${prefixCls}-child-tree`,
+              expanded && `${prefixCls}-child-tree-open`
+            )}
+            data-expanded={expanded}
+            role="group"
+          >
+            {mapChildren(nodeList, (node, index) => renderTreeNode(node, index, pos))}
+          </ul>
+        )
+      }
+
+      return <transition {...animProps}>{$children}</transition>
+    }
+  },
+
+  render (h) {
+    const {
+      dragOver,
+      dragOverGapTop,
+      dragOverGapBottom,
+      isLeaf,
+      expanded,
+      selected,
+      checked,
+      halfChecked,
+      loading
+    } = this.$props
+    const {
+      vcTree: { prefixCls, filterTreeNode, draggable }
+    } = this
+    const disabled = this.isDisabled()
+    return (
+      <li
+        class={{
+          [`${prefixCls}-treenode-disabled`]: disabled,
+          [`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
+          [`${prefixCls}-treenode-checkbox-checked`]: checked,
+          [`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
+          [`${prefixCls}-treenode-selected`]: selected,
+          [`${prefixCls}-treenode-loading`]: loading,
+          'drag-over': !disabled && dragOver,
+          'drag-over-gap-top': !disabled && dragOverGapTop,
+          'drag-over-gap-bottom': !disabled && dragOverGapBottom,
+          'filter-node': filterTreeNode && filterTreeNode(this)
+        }}
+        role="treeitem"
+        onDragenter={draggable ? this.onDragEnter : noop}
+        onDragover={draggable ? this.onDragOver : noop}
+        onDragleave={draggable ? this.onDragLeave : noop}
+        onDrop={draggable ? this.onDrop : noop}
+        onDragend={draggable ? this.onDragEnd : noop}
+      >
+        {this.renderSwitcher()}
+        {this.renderCheckbox()}
+        {this.renderSelector(h)}
+        {this.renderChildren()}
+      </li>
+    )
+  }
+}
+
+TreeNode.isTreeNode = 1
+
+export default TreeNode

+ 7 - 0
src/components/vc-tree/src/index.js

@@ -0,0 +1,7 @@
+import ProxyTree, { Tree } from './Tree'
+import TreeNode from './TreeNode'
+Tree.TreeNode = TreeNode
+ProxyTree.TreeNode = TreeNode
+
+export { Tree, TreeNode }
+export default ProxyTree

+ 428 - 0
src/components/vc-tree/src/util.js

@@ -0,0 +1,428 @@
+/* eslint no-loop-func: 0 */
+import warning from 'warning'
+import omit from 'omit.js'
+import TreeNode from './TreeNode'
+import { getSlotOptions, getOptionProps } from 'ant-design-vue/es/_util/props-util'
+const DRAG_SIDE_RANGE = 0.25
+const DRAG_MIN_GAP = 2
+
+let onlyTreeNodeWarned = false
+
+export function warnOnlyTreeNode () {
+  if (onlyTreeNodeWarned) return
+
+  onlyTreeNodeWarned = true
+  warning(false, 'Tree only accept TreeNode as children.')
+}
+
+export function arrDel (list, value) {
+  const clone = list.slice()
+  const index = clone.indexOf(value)
+  if (index >= 0) {
+    clone.splice(index, 1)
+  }
+  return clone
+}
+
+export function arrAdd (list, value) {
+  const clone = list.slice()
+  if (clone.indexOf(value) === -1) {
+    clone.push(value)
+  }
+  return clone
+}
+
+export function posToArr (pos) {
+  return pos.split('-')
+}
+
+export function getPosition (level, index) {
+  return `${level}-${index}`
+}
+
+export function isTreeNode (node) {
+  return getSlotOptions(node).isTreeNode
+}
+
+export function getNodeChildren (children = []) {
+  return children.filter(isTreeNode)
+}
+
+export function isCheckDisabled (node) {
+  const { disabled, disableCheckbox, checkable } = getOptionProps(node) || {}
+  return !!(disabled || disableCheckbox) || checkable === false
+}
+
+export function traverseTreeNodes (treeNodes, callback) {
+  function processNode (node, index, parent) {
+    const children = node ? node.componentOptions.children : treeNodes
+    const pos = node ? getPosition(parent.pos, index) : 0
+
+    // Filter children
+    const childList = getNodeChildren(children)
+
+    // Process node if is not root
+    if (node) {
+      let key = node.key
+      if (!key && (key === undefined || key === null)) {
+        key = pos
+      }
+      const data = {
+        node,
+        index,
+        pos,
+        key,
+        parentPos: parent.node ? parent.pos : null
+      }
+      callback(data)
+    }
+
+    // Process children node
+    childList.forEach((subNode, subIndex) => {
+      processNode(subNode, subIndex, { node, pos })
+    })
+  }
+
+  processNode(null)
+}
+
+/**
+ * Use `rc-util` `toArray` to get the children list which keeps the key.
+ * And return single node if children is only one(This can avoid `key` missing check).
+ */
+export function mapChildren (children = [], func) {
+  const list = children.map(func)
+  if (list.length === 1) {
+    return list[0]
+  }
+  return list
+}
+
+export function getDragNodesKeys (treeNodes, node) {
+  const { eventKey, pos } = getOptionProps(node)
+  const dragNodesKeys = []
+
+  traverseTreeNodes(treeNodes, ({ key }) => {
+    dragNodesKeys.push(key)
+  })
+  dragNodesKeys.push(eventKey || pos)
+  return dragNodesKeys
+}
+
+export function calcDropPosition (event, treeNode) {
+  const { clientY } = event
+  const { top, bottom, height } = treeNode.$refs.selectHandle.getBoundingClientRect()
+  const des = Math.max(height * DRAG_SIDE_RANGE, DRAG_MIN_GAP)
+
+  if (clientY <= top + des) {
+    return -1
+  }
+  if (clientY >= bottom - des) {
+    return 1
+  }
+  return 0
+}
+
+/**
+ * Return selectedKeys according with multiple prop
+ * @param selectedKeys
+ * @param props
+ * @returns [string]
+ */
+export function calcSelectedKeys (selectedKeys, props) {
+  if (!selectedKeys) {
+    return undefined
+  }
+
+  const { multiple } = props
+  if (multiple) {
+    return selectedKeys.slice()
+  }
+
+  if (selectedKeys.length) {
+    return [selectedKeys[0]]
+  }
+  return selectedKeys
+}
+
+/**
+ * Since React internal will convert key to string,
+ * we need do this to avoid `checkStrictly` use number match
+ */
+// function keyListToString (keyList) {
+//   if (!keyList) return keyList
+//   return keyList.map(key => String(key))
+// }
+
+const internalProcessProps = (props = {}) => {
+  return {
+    props: omit(props, ['on', 'key', 'class', 'className', 'style']),
+    on: props.on || {},
+    class: props.class || props.className,
+    style: props.style,
+    key: props.key
+  }
+}
+export function convertDataToTree (h, treeData, processor) {
+  if (!treeData) return []
+
+  const { processProps = internalProcessProps } = processor || {}
+  const list = Array.isArray(treeData) ? treeData : [treeData]
+  return list.map(({ children, ...props }) => {
+    const childrenNodes = convertDataToTree(h, children, processor)
+    return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>
+  })
+}
+
+// TODO: ========================= NEW LOGIC =========================
+/**
+ * Calculate treeNodes entities. `processTreeEntity` is used for `rc-tree-select`
+ * @param treeNodes
+ * @param processTreeEntity  User can customize the entity
+ */
+export function convertTreeToEntities (
+  treeNodes,
+  { initWrapper, processEntity, onProcessFinished } = {}
+) {
+  const posEntities = new Map()
+  const keyEntities = new Map()
+  let wrapper = {
+    posEntities,
+    keyEntities
+  }
+
+  if (initWrapper) {
+    wrapper = initWrapper(wrapper) || wrapper
+  }
+
+  traverseTreeNodes(treeNodes, item => {
+    const { node, index, pos, key, parentPos } = item
+    const entity = { node, index, key, pos }
+
+    posEntities.set(pos, entity)
+    keyEntities.set(key, entity)
+
+    // Fill children
+    entity.parent = posEntities.get(parentPos)
+    if (entity.parent) {
+      entity.parent.children = entity.parent.children || []
+      entity.parent.children.push(entity)
+    }
+
+    if (processEntity) {
+      processEntity(entity, wrapper)
+    }
+  })
+
+  if (onProcessFinished) {
+    onProcessFinished(wrapper)
+  }
+
+  return wrapper
+}
+
+/**
+ * Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
+ */
+export function parseCheckedKeys (keys) {
+  if (!keys) {
+    return null
+  }
+
+  // Convert keys to object format
+  let keyProps
+  if (Array.isArray(keys)) {
+    // [Legacy] Follow the api doc
+    keyProps = {
+      checkedKeys: keys,
+      halfCheckedKeys: undefined
+    }
+  } else if (typeof keys === 'object') {
+    keyProps = {
+      checkedKeys: keys.checked || undefined,
+      halfCheckedKeys: keys.halfChecked || undefined
+    }
+  } else {
+    warning(false, '`checkedKeys` is not an array or an object')
+    return null
+  }
+
+  // keyProps.checkedKeys = keyListToString(keyProps.checkedKeys)
+  // keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys)
+
+  return keyProps
+}
+
+/**
+ * Conduct check state by the keyList. It will conduct up & from the provided key.
+ * If the conduct path reach the disabled or already checked / unchecked node will stop conduct.
+ * @param keyList       list of keys
+ * @param isCheck       is check the node or not
+ * @param keyEntities   parsed by `convertTreeToEntities` function in Tree
+ * @param checkStatus   Can pass current checked status for process (usually for uncheck operation)
+ * @returns {{checkedKeys: [], halfCheckedKeys: []}}
+ */
+export function conductCheck (keyList, isCheck, keyEntities, checkStatus = {}) {
+  const checkedKeys = new Map()
+  const halfCheckedKeys = new Map(); // Record the key has some child checked (include child half checked)
+
+  (checkStatus.checkedKeys || []).forEach(key => {
+    checkedKeys.set(key, true)
+  });
+
+  (checkStatus.halfCheckedKeys || []).forEach(key => {
+    halfCheckedKeys.set(key, true)
+  })
+
+  // Conduct up
+  function conductUp (key) {
+    if (checkedKeys.get(key) === isCheck) return
+
+    const entity = keyEntities.get(key)
+    if (!entity) return
+
+    const { children, parent, node } = entity
+
+    if (isCheckDisabled(node)) return
+
+    // Check child node checked status
+    let everyChildChecked = true
+    let someChildChecked = false; // Child checked or half checked
+
+    (children || [])
+      .filter(child => !isCheckDisabled(child.node))
+      .forEach(({ key: childKey }) => {
+        const childChecked = checkedKeys.get(childKey)
+        const childHalfChecked = halfCheckedKeys.get(childKey)
+
+        if (childChecked || childHalfChecked) someChildChecked = true
+        if (!childChecked) everyChildChecked = false
+      })
+
+    // Update checked status
+    if (isCheck) {
+      checkedKeys.set(key, everyChildChecked)
+    } else {
+      checkedKeys.set(key, false)
+    }
+    halfCheckedKeys.set(key, someChildChecked)
+
+    if (parent) {
+      conductUp(parent.key)
+    }
+  }
+
+  // Conduct down
+  function conductDown (key) {
+    if (checkedKeys.get(key) === isCheck) return
+
+    const entity = keyEntities.get(key)
+    if (!entity) return
+
+    const { children, node } = entity
+
+    if (isCheckDisabled(node)) return
+
+    checkedKeys.set(key, isCheck);
+
+    (children || []).forEach(child => {
+      conductDown(child.key)
+    })
+  }
+
+  function conduct (key) {
+    const entity = keyEntities.get(key)
+
+    if (!entity) {
+      warning(false, `'${key}' does not exist in the tree.`)
+      return
+    }
+    const { children, parent, node } = entity
+    checkedKeys.set(key, isCheck)
+
+    if (isCheckDisabled(node)) return
+
+    // Conduct down
+    (children || [])
+      .filter(child => !isCheckDisabled(child.node))
+      .forEach(child => {
+        conductDown(child.key)
+      })
+
+    // Conduct up
+    if (parent) {
+      conductUp(parent.key)
+    }
+  }
+
+  (keyList || []).forEach(key => {
+    conduct(key)
+  })
+
+  const checkedKeyList = []
+  const halfCheckedKeyList = []
+
+  // Fill checked list
+  for (const [key, value] of checkedKeys) {
+    if (value) {
+      checkedKeyList.push(key)
+    }
+  }
+
+  // Fill half checked list
+  for (const [key, value] of halfCheckedKeys) {
+    if (!checkedKeys.get(key) && value) {
+      halfCheckedKeyList.push(key)
+    }
+  }
+
+  return {
+    checkedKeys: checkedKeyList,
+    halfCheckedKeys: halfCheckedKeyList
+  }
+}
+
+/**
+ * If user use `autoExpandParent` we should get the list of parent node
+ * @param keyList
+ * @param keyEntities
+ */
+export function conductExpandParent (keyList, keyEntities) {
+  const expandedKeys = new Map()
+
+  function conductUp (key) {
+    if (expandedKeys.get(key)) return
+
+    const entity = keyEntities.get(key)
+    if (!entity) return
+
+    expandedKeys.set(key, true)
+
+    const { parent, node } = entity
+    const props = getOptionProps(node)
+    if (props && props.disabled) return
+
+    if (parent) {
+      conductUp(parent.key)
+    }
+  }
+
+  (keyList || []).forEach(key => {
+    conductUp(key)
+  })
+
+  return [...expandedKeys.keys()]
+}
+
+/**
+ * Returns only the data- and aria- key/value pairs
+ * @param {object} props
+ */
+export function getDataAndAria (props) {
+  return Object.keys(props).reduce((prev, key) => {
+    if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-') {
+      prev[key] = props[key]
+    }
+    return prev
+  }, {})
+}

+ 33 - 0
src/config/defaultSettings.js

@@ -0,0 +1,33 @@
+/**
+ * 项目默认配置项
+ * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage
+ * navTheme - sidebar theme ['dark', 'light'] 两种主题
+ * colorWeak - 色盲模式
+ * layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局
+ * fixedHeader - 固定 Header : boolean
+ * fixSiderbar - 固定左侧菜单栏 : boolean
+ * autoHideHeader - 向下滚动时,隐藏 Header : boolean
+ * contentWidth - 内容区布局: 流式 |  固定
+ *
+ * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage)
+ *
+ */
+
+export default {
+  primaryColor: '#1890FF', // primary color of ant design
+  navTheme: 'light', // theme for nav menu
+  layout: 'sidemenu', // nav menu position: sidemenu or topmenu
+  contentWidth: 'Fixed', // layout of content: Fluid or Fixed, only works when layout is topmenu
+  fixedHeader: false, // sticky header
+  fixSiderbar: false, // sticky siderbar
+  autoHideHeader: false, //  auto hide header
+  colorWeak: false,
+  multiTab: false,
+  production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
+  // vue-ls options
+  storageOptions: {
+    namespace: 'pro__',
+    name: 'ls',
+    storage: 'local'
+  }
+}

+ 133 - 0
src/config/router.config.js

@@ -0,0 +1,133 @@
+// eslint-disable-next-line
+import { UserLayout, BasicLayout, PageView } from '@/layouts'
+import * as Icon from '@/core/icons'
+
+export const asyncRouterMap = [
+
+  {
+    path: '/',
+    name: 'index',
+    component: BasicLayout,
+    meta: { title: '超级管理后台' },
+    redirect: '/store/index',
+    children: [
+      {
+        name: 'Store',
+        path: 'store',
+        redirect: '/store/index',
+        component: PageView,
+        meta: { title: '商城管理', keepAlive: true, icon: Icon['shop'], permission: ['store'] },
+        children: [
+          {
+            path: 'index',
+            component: () => import(/* webpackChunkName: "store" */ '@/views/store/index'),
+            meta: { title: '商城列表', keepAlive: true, hiddenHeaderContent: false, permission: ['store'] }
+          },
+          {
+            path: '/store/recycle',
+            component: () => import(/* webpackChunkName: "store" */ '@/views/store/recycle'),
+            meta: { title: '回收站', keepAlive: true, permission: ['store'] }
+          }
+        ]
+      },
+
+      {
+        name: 'Menu',
+        path: 'menu',
+        redirect: '/menu/index',
+        component: PageView,
+        meta: { title: '菜单管理', keepAlive: true, icon: Icon['menu'], permission: ['access'] },
+        children: [
+          {
+            path: '/menu/index',
+            component: () => import(/* webpackChunkName: "menu" */ '@/views/menu/index'),
+            meta: { title: '菜单列表', keepAlive: true, permission: ['access'] }
+          },
+          {
+            path: '/menu/access/index',
+            component: () => import(/* webpackChunkName: "menu" */ '@/views/menu/access/index'),
+            meta: { title: 'API权限', keepAlive: true, permission: ['access'] }
+          }
+        ]
+      },
+
+      {
+        name: 'Setting',
+        path: 'setting',
+        redirect: '/setting/cache/clear',
+        component: PageView,
+        meta: { title: '系统管理', keepAlive: false, icon: Icon['setting'], permission: ['setting'] },
+        children: [
+          {
+            path: '/setting/cache',
+            component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/cache'),
+            meta: { title: '清理缓存', keepAlive: false, permission: ['setting'] }
+          },
+          {
+            path: '/setting/science',
+            component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/science'),
+            meta: { title: '环境检测', keepAlive: false, permission: ['setting'] }
+          },
+          {
+            path: '/setting/timer',
+            component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/timer'),
+            meta: { title: '定时任务', keepAlive: false, permission: ['setting'] }
+          },
+          // {
+          //   path: '/setting/queue',
+          //   component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/queue'),
+          //   meta: { title: '队列服务', keepAlive: false, permission: ['setting'] }
+          // },
+        ]
+      },
+
+      {
+        name: 'Manage',
+        path: '/manage',
+        redirect: '/manage/renew',
+        component: PageView,
+        hidden: true,
+        meta: { title: '管理员设置', keepAlive: false, permission: ['manage'] },
+        children: [
+          {
+            // name: 'Renew',
+            path: 'renew',
+            component: () => import(/* webpackChunkName: "user" */ '@/views/manage/renew')
+          }
+        ]
+      }
+
+    ]
+  },
+
+  {
+    path: '*', redirect: '/404', hidden: true
+  }
+]
+
+/**
+ * 基础路由
+ * @type { *[] }
+ */
+export const constantRouterMap = [
+  {
+    path: '/user',
+    component: UserLayout,
+    redirect: '/user/login',
+    hidden: true,
+    children: [
+      {
+        // name: 'Login',
+        path: 'login',
+        meta: { title: '超级管理后台' },
+        component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login')
+      }
+    ]
+  },
+
+  {
+    path: '/404',
+    component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/404')
+  }
+
+]

+ 32 - 0
src/core/bootstrap.js

@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import store from '@/store/'
+import {
+  ACCESS_TOKEN,
+  DEFAULT_COLOR,
+  DEFAULT_THEME,
+  DEFAULT_LAYOUT_MODE,
+  DEFAULT_COLOR_WEAK,
+  SIDEBAR_TYPE,
+  DEFAULT_FIXED_HEADER,
+  DEFAULT_FIXED_HEADER_HIDDEN,
+  DEFAULT_FIXED_SIDEMENU,
+  DEFAULT_CONTENT_WIDTH_TYPE,
+  DEFAULT_MULTI_TAB
+} from '@/store/mutation-types'
+import config from '@/config/defaultSettings'
+
+export default function Initializer () {
+  store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true))
+  store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
+  store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
+  store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
+  store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar))
+  store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
+  store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
+  store.commit('TOGGLE_WEAK', Vue.ls.get(DEFAULT_COLOR_WEAK, config.colorWeak))
+  store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
+  store.commit('TOGGLE_MULTI_TAB', Vue.ls.get(DEFAULT_MULTI_TAB, config.multiTab))
+  store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
+
+  // last step
+}

+ 34 - 0
src/core/directives/action.js

@@ -0,0 +1,34 @@
+import Vue from 'vue'
+import store from '@/store'
+
+/**
+ * Action 权限指令
+ * 指令用法:
+ *  - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下:
+ *    <i-button v-action:add >添加用户</a-button>
+ *    <a-button v-action:delete>删除用户</a-button>
+ *    <a v-action:edit @click="edit(record)">修改</a>
+ *
+ *  - 当前用户没有权限时,组件上使用了该指令则会被隐藏
+ *  - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可
+ *
+ *  @see https://github.com/sendya/ant-design-pro-vue/pull/53
+ */
+const action = Vue.directive('action', {
+  inserted: function (el, binding, vnode) {
+    const actionName = binding.arg
+    const roles = store.getters.roles
+    const elVal = vnode.context.$route.meta.permission
+    const permissionId = elVal instanceof String && [elVal] || elVal
+    roles.permissions.forEach(p => {
+      if (!permissionId.includes(p.permissionId)) {
+        return
+      }
+      if (p.actionList && !p.actionList.includes(actionName)) {
+        el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
+      }
+    })
+  }
+})
+
+export default action

+ 16 - 0
src/core/icons.js

@@ -0,0 +1,16 @@
+/**
+ * Custom icon list
+ * All icons are loaded here for easy management
+ * @see https://vue.ant.design/components/icon/#Custom-Font-Icon
+ *
+ * 自定义图标加载表
+ * 所有图标均从这里加载,方便管理
+ */
+import bxAnaalyse from '@/assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file.
+
+import shop from '@/assets/icons/shop.svg?inline' // icon-shangcheng
+import setting from '@/assets/icons/setting.svg?inline' // icon-shezhi
+import access from '@/assets/icons/access.svg?inline'
+import menu from '@/assets/icons/menu.svg?inline'
+
+export { bxAnaalyse, shop, setting, access, menu }

+ 103 - 0
src/core/lazy_lib/components_use.js

@@ -0,0 +1,103 @@
+
+/* eslint-disable */
+/**
+ * 该文件是为了按需加载,剔除掉了一些不需要的框架组件。
+ * 减少了编译支持库包大小
+ *
+ * 当需要更多组件依赖时,在该文件加入即可
+ */
+import Vue from 'vue'
+import {
+  ConfigProvider,
+  Layout,
+  Input,
+  InputNumber,
+  Button,
+  Switch,
+  Radio,
+  Checkbox,
+  Select,
+  TreeSelect,
+  Card,
+  Form,
+  Row,
+  Col,
+  Modal,
+  Table,
+  Tabs,
+  Icon,
+  Badge,
+  Popover,
+  Dropdown,
+  List,
+  Avatar,
+  Breadcrumb,
+  Steps,
+  Spin,
+  Menu,
+  Drawer,
+  Tooltip,
+  Alert,
+  Tag,
+  Divider,
+  DatePicker,
+  TimePicker,
+  Upload,
+  Progress,
+  Skeleton,
+  Popconfirm,
+  message,
+  notification
+} from 'ant-design-vue'
+
+import Tree from '@/components/tree'
+
+Vue.use(ConfigProvider)
+Vue.use(Layout)
+Vue.use(Input)
+Vue.use(InputNumber)
+Vue.use(Button)
+Vue.use(Switch)
+Vue.use(Radio)
+Vue.use(Checkbox)
+Vue.use(Select)
+Vue.use(TreeSelect)
+Vue.use(Card)
+Vue.use(Form)
+Vue.use(Row)
+Vue.use(Col)
+Vue.use(Modal)
+Vue.use(Table)
+Vue.use(Tabs)
+Vue.use(Icon)
+Vue.use(Badge)
+Vue.use(Popover)
+Vue.use(Dropdown)
+Vue.use(List)
+Vue.use(Avatar)
+Vue.use(Breadcrumb)
+Vue.use(Steps)
+Vue.use(Spin)
+Vue.use(Menu)
+Vue.use(Drawer)
+Vue.use(Tooltip)
+Vue.use(Alert)
+Vue.use(Tag)
+Vue.use(Divider)
+Vue.use(DatePicker)
+Vue.use(TimePicker)
+Vue.use(Upload)
+Vue.use(Progress)
+Vue.use(Skeleton)
+Vue.use(Popconfirm)
+Vue.use(notification)
+
+Vue.use(Tree)
+
+Vue.prototype.$confirm = Modal.confirm
+Vue.prototype.$message = message
+Vue.prototype.$notification = notification
+Vue.prototype.$info = Modal.info
+Vue.prototype.$success = Modal.success
+Vue.prototype.$error = Modal.error
+Vue.prototype.$warning = Modal.warning

+ 25 - 0
src/core/lazy_use.js

@@ -0,0 +1,25 @@
+import Vue from 'vue'
+import VueStorage from 'vue-ls'
+import config from '@/config/defaultSettings'
+
+// base library
+import '@/core/lazy_lib/components_use'
+import Viser from 'viser-vue'
+
+// ext library
+import VueClipboard from 'vue-clipboard2'
+// import MultiTab from '@/components/MultiTab'
+import PageLoading from '@/components/PageLoading'
+import PermissionHelper from '@/utils/helper/permission'
+import './directives/action'
+
+VueClipboard.config.autoSetContainer = true
+
+Vue.use(Viser)
+// Vue.use(MultiTab)
+Vue.use(PageLoading)
+Vue.use(VueStorage, config.storageOptions)
+Vue.use(VueClipboard)
+Vue.use(PermissionHelper)
+
+process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] NOTICE: Antd use lazy-load.')

+ 28 - 0
src/core/use.js

@@ -0,0 +1,28 @@
+import Vue from 'vue'
+import VueStorage from 'vue-ls'
+import config from '@/config/defaultSettings'
+
+// base library
+import Antd from 'ant-design-vue'
+import Viser from 'viser-vue'
+import 'ant-design-vue/dist/antd.less'
+
+// ext library
+import VueClipboard from 'vue-clipboard2'
+// import MultiTab from '@/components/MultiTab'
+import PageLoading from '@/components/PageLoading'
+import PermissionHelper from '@/utils/helper/permission'
+// import '@/components/use'
+import './directives/action'
+
+VueClipboard.config.autoSetContainer = true
+
+Vue.use(Antd)
+Vue.use(Viser)
+// Vue.use(MultiTab)
+Vue.use(PageLoading)
+Vue.use(VueStorage, config.storageOptions)
+Vue.use(VueClipboard)
+Vue.use(PermissionHelper)
+
+process.env.NODE_ENV !== 'production' && console.warn('[antd-pro] WARNING: Antd now use fulled imported.')

+ 618 - 0
src/global.less

@@ -0,0 +1,618 @@
+@import '~ant-design-vue/es/style/themes/default.less';
+@import './utils/utils.less';
+
+// body {}
+
+::-moz-selection {
+  background: #009688;
+  color: #FFF;
+}
+
+::selection {
+  background: #159b76;
+  color: #FFF;
+}
+
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background-color: #bdbdbd;
+  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
+}
+
+#app {
+  height: 100%;
+
+  &.colorWeak {
+    filter: invert(80%);
+  }
+
+  &.userLayout {
+    overflow: auto;
+  }
+}
+
+.layout.ant-layout {
+  height: auto;
+  overflow-x: hidden;
+
+  &.mobile,
+  &.tablet {
+    .ant-layout-content {
+      .content {
+        margin: 24px 0 0;
+      }
+    }
+
+    /**
+     * ant-table-wrapper
+     * 覆盖的表格手机模式样式,如果想修改在手机上表格最低宽度,可以在这里改动
+     */
+    .ant-table-wrapper {
+      .ant-table-content {
+        overflow-y: auto;
+      }
+
+      .ant-table-body {
+        min-width: 800px;
+      }
+    }
+
+    .topmenu {
+
+      /* 必须为 topmenu  才能启用流式布局 */
+      &.content-width-Fluid {
+        .header-index-wide {
+          margin-left: 0;
+        }
+      }
+    }
+  }
+
+  &.mobile {
+    .sidemenu {
+      .ant-header-fixedHeader {
+
+        &.ant-header-side-opened,
+        &.ant-header-side-closed {
+          width: 100%;
+        }
+      }
+    }
+  }
+
+  &.ant-layout-has-sider {
+    flex-direction: row;
+  }
+
+  .trigger {
+    font-size: 20px;
+    line-height: 64px;
+    padding: 0 24px;
+    cursor: pointer;
+    transition: color 0.3s;
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.025);
+    }
+  }
+
+  .topmenu {
+    .ant-header-fixedHeader {
+      position: fixed;
+      top: 0;
+      right: 0;
+      z-index: 9;
+      width: 100%;
+      transition: width 0.2s;
+
+      &.ant-header-side-opened {
+        width: 100%;
+      }
+
+      &.ant-header-side-closed {
+        width: 100%;
+      }
+    }
+
+    /* 必须为 topmenu  才能启用流式布局 */
+    &.content-width-Fluid {
+      .header-index-wide {
+        max-width: unset;
+
+        .header-index-left {
+          flex: 1 1 1000px;
+
+          .logo {
+            margin-left: 25px;
+          }
+
+          .ant-menu.ant-menu-horizontal {
+            max-width: calc(100vw - 190px - 238px - 25px);
+            flex: 1 1 calc(100vw - 190px - 238px - 25px);
+          }
+        }
+
+        .header-index-right {
+          margin-right: 25px;
+        }
+      }
+
+      .page-header-index-wide {
+        max-width: unset;
+      }
+    }
+  }
+
+  .sidemenu {
+    .ant-header-fixedHeader {
+      position: fixed;
+      top: 0;
+      right: 0;
+      z-index: 9;
+      width: 100%;
+      transition: width 0.2s;
+
+      &.ant-header-side-opened {
+        width: calc(100% - 256px);
+      }
+
+      &.ant-header-side-closed {
+        width: calc(100% - 80px);
+      }
+    }
+  }
+
+  .header {
+    height: 64px;
+    padding: 0 12px 0 0;
+    box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+    position: relative;
+    background: #fff;
+    background: #51a7f7;
+    color: #fff;
+
+    .content {
+      max-width: 1500px;
+      min-width: 1000px;
+      padding-left: 20px;
+      margin: 0px auto;
+
+      .title {
+        font-size: 18px;
+        user-select: none;
+      }
+    }
+  }
+
+  .header,
+  .top-nav-header-index {
+    .user-wrapper {
+      float: right;
+      height: 100%;
+
+      .action {
+        cursor: pointer;
+        padding: 0 20px;
+        display: inline-block;
+        transition: all 0.3s;
+        height: 100%;
+        // color     : rgba(0, 0, 0, 0.65);
+        color: #fff;
+
+        &:hover {
+          background: rgba(0, 0, 0, 0.025);
+        }
+
+        .avatar {
+          margin: 20px 8px 20px 0;
+          color: #1890ff;
+          background: hsla(0, 0%, 100%, 0.85);
+          vertical-align: middle;
+        }
+
+        .icon {
+          font-size: 16px;
+          padding: 4px;
+        }
+      }
+    }
+
+    &.dark {
+      .user-wrapper {
+        .action {
+          color: rgba(255, 255, 255, 0.85);
+
+          a {
+            color: rgba(255, 255, 255, 0.85);
+          }
+
+          &:hover {
+            background: rgba(255, 255, 255, 0.16);
+          }
+        }
+      }
+    }
+  }
+
+  &.mobile,
+  &.tablet {
+    .top-nav-header-index {
+      .header-index-wide {
+        .header-index-left {
+          .trigger {
+            color: rgba(255, 255, 255, 0.85);
+            padding: 0 12px;
+          }
+
+          .logo.top-nav-header {
+            flex: 0 0 56px;
+            text-align: center;
+            line-height: 58px;
+
+            h1 {
+              display: none;
+            }
+          }
+        }
+      }
+
+      &.light {
+        .header-index-wide {
+          .header-index-left {
+            .trigger {
+              color: rgba(0, 0, 0, 0.65);
+            }
+          }
+        }
+
+        //
+      }
+    }
+  }
+
+  &.tablet {
+
+    // overflow: hidden; text-overflow:ellipsis; white-space: nowrap;
+    .top-nav-header-index {
+      .header-index-wide {
+        .header-index-left {
+          .logo>a {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+        }
+
+        .ant-menu.ant-menu-horizontal {
+          flex: 1 1 auto;
+          white-space: normal;
+        }
+      }
+    }
+  }
+
+  .top-nav-header-index {
+    box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+    position: relative;
+    transition: background 0.3s, width 0.2s;
+
+    .header-index-wide {
+      max-width: 1200px;
+      margin: auto;
+      padding-left: 0;
+      display: flex;
+      height: 64px;
+
+      .ant-menu.ant-menu-horizontal {
+        max-width: 835px;
+        flex: 0 1 835px;
+        border: none;
+        height: 64px;
+        line-height: 64px;
+      }
+
+      .header-index-left {
+        flex: 0 1 1000px;
+        display: flex;
+
+        .logo.top-nav-header {
+          flex: 0 0 165px;
+          width: 165px;
+          height: 64px;
+          position: relative;
+          line-height: 64px;
+          transition: all 0.3s;
+          overflow: hidden;
+
+          img,
+          svg {
+            display: inline-block;
+            vertical-align: middle;
+            height: 32px;
+            width: 32px;
+          }
+
+          h1 {
+            color: #fff;
+            display: inline-block;
+            vertical-align: top;
+            font-size: 16px;
+            margin: 0 0 0 12px;
+            font-weight: 400;
+          }
+        }
+      }
+
+      .header-index-right {
+        flex: 0 0 238px;
+        align-self: flex-end;
+        height: 64px;
+        overflow: hidden;
+
+        .content-box {
+          float: right;
+
+          .action {
+            max-width: 140px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+        }
+      }
+    }
+
+    &.light {
+      background-color: #fff;
+
+      .header-index-wide {
+        .header-index-left {
+          .logo {
+            h1 {
+              color: #002140;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // 内容区
+  .layout-content {
+    margin: 24px 24px 0px;
+    height: 100%;
+    height: 64px;
+    padding: 0 12px 0 0;
+  }
+
+  // footer
+  .ant-layout-footer {
+    padding: 0;
+  }
+}
+
+.topmenu {
+  .page-header-index-wide {
+    max-width: 1200px;
+    margin: 0 auto;
+  }
+}
+
+// drawer-sider 自定义
+.ant-drawer.drawer-sider {
+  .sider {
+    box-shadow: none;
+  }
+
+  &.dark {
+    .ant-drawer-content {
+      background-color: rgb(0, 21, 41);
+    }
+  }
+
+  &.light {
+    box-shadow: none;
+
+    .ant-drawer-content {
+      background-color: #fff;
+    }
+  }
+
+  .ant-drawer-body {
+    padding: 0;
+  }
+}
+
+// 菜单样式
+.sider {
+  box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
+  position: relative;
+  // z-index: @ant-global-sider-zindex;
+  // min-height: 100vh;
+
+  // .ant-layout-sider-children {
+  //   overflow-y: hidden;
+
+  //   &:hover {
+  //     overflow-y: auto;
+  //   }
+  // }
+
+  &.ant-fixed-sidemenu {
+    position: fixed;
+    height: 100%;
+  }
+
+  .logo {
+    position: relative;
+    height: 64px;
+    padding-left: 24px;
+    overflow: hidden;
+    line-height: 64px;
+    background: #002140;
+    transition: all .3s;
+
+    img,
+    svg,
+    h1 {
+      display: inline-block;
+      vertical-align: middle;
+    }
+
+    img,
+    svg {
+      height: 32px;
+      width: 32px;
+    }
+
+    h1 {
+      color: #fff;
+      font-size: 20px;
+      margin: 0 0 0 12px;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+      font-weight: 600;
+      vertical-align: middle;
+    }
+  }
+
+  &.light {
+    background-color: #fff;
+    box-shadow: 2px 0px 8px 0px rgba(29, 35, 41, 0.05);
+
+    .logo {
+      background: #fff;
+      box-shadow: 1px 1px 0px 0px #e8e8e8;
+
+      h1 {
+        color: unset;
+      }
+    }
+
+    .ant-menu-light {
+      border-right-color: transparent;
+    }
+  }
+}
+
+.ant-menu-sub.ant-menu-inline>.ant-menu-item,
+.ant-menu-sub.ant-menu-inline>.ant-menu-submenu>.ant-menu-submenu-title {
+  height: 46px;
+  line-height: 46px;
+}
+
+.ant-menu-item .anticon,
+.ant-menu-submenu-title .anticon {
+  font-size: 16px;
+}
+
+// 外置的样式控制
+.user-dropdown-menu {
+  span {
+    user-select: none;
+  }
+}
+
+.user-dropdown-menu-wrapper.ant-dropdown-menu {
+  padding: 4px 0;
+
+  .ant-dropdown-menu-item {
+    width: 160px;
+    padding: 8px 12px;
+  }
+
+  .ant-dropdown-menu-item>.anticon:first-child,
+  .ant-dropdown-menu-item>a>.anticon:first-child,
+  .ant-dropdown-menu-submenu-title>.anticon:first-child .ant-dropdown-menu-submenu-title>a>.anticon:first-child {
+    min-width: 12px;
+    margin-right: 8px;
+  }
+}
+
+// 数据列表 样式
+.table-alert {
+  margin-bottom: 16px;
+}
+
+.table-page-search-wrapper {
+  .ant-form-inline {
+    .ant-form-item {
+      display: flex;
+      margin-bottom: 24px;
+      margin-right: 0;
+
+      .ant-form-item-control-wrapper {
+        flex: 1 1;
+        display: inline-block;
+        vertical-align: middle;
+      }
+
+      >.ant-form-item-label {
+        line-height: 32px;
+        padding-right: 8px;
+        width: auto;
+      }
+
+      .ant-form-item-control {
+        height: 32px;
+        line-height: 32px;
+      }
+    }
+  }
+
+  .table-page-search-submitButtons {
+    display: block;
+    margin-bottom: 24px;
+    white-space: nowrap;
+  }
+}
+
+.content {
+  .table-operator {
+    margin-bottom: 18px;
+
+    button {
+      margin-right: 8px;
+    }
+  }
+
+  .ant-card-body {
+    padding: 24px 45px;
+  }
+}
+
+.ant-alert {
+  font-size: 12.5px;
+}
+
+
+// 分割线
+.ant-divider {
+  margin: 30px 0 !important;
+
+  .ant-divider-inner-text {
+    font-size: 13px;
+    color: rgba(0, 0, 0, 0.59);
+  }
+}
+
+.ant-divider-horizontal.ant-divider-with-text-left::before {
+  width: 2%;
+}
+
+// ## 自定义模块 ##
+.ant-table {
+
+  // 操作栏 项目间距
+  .actions>a {
+    margin-right: 8px;
+
+    &:last-child {
+      margin-right: 0;
+    }
+  }
+}

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels