Przeglądaj źródła

Wesmiler 校企小程序后台web源代码部署

wesmiler 4 lat temu
rodzic
commit
048a5ecb79
100 zmienionych plików z 4429 dodań i 0 usunięć
  1. 3 0
      addons/admin/.browserslistrc
  2. 39 0
      addons/admin/.editorconfig
  3. 3 0
      addons/admin/.env.development
  4. 3 0
      addons/admin/.env.preview
  5. 75 0
      addons/admin/.eslintrc.js
  6. 1 0
      addons/admin/.gitattributes
  7. 21 0
      addons/admin/.gitignore
  8. 5 0
      addons/admin/.prettierrc
  9. 7 0
      addons/admin/.travis.yml
  10. 201 0
      addons/admin/LICENSE
  11. 57 0
      addons/admin/README.md
  12. 28 0
      addons/admin/babel.config.js
  13. 46 0
      addons/admin/config/plugin.config.js
  14. 31 0
      addons/admin/docs/add-page-loading-animate.md
  15. 95 0
      addons/admin/docs/load-on-demand.md
  16. 28 0
      addons/admin/docs/multi-tabs.md
  17. 40 0
      addons/admin/docs/webpack-bundle-analyzer.md
  18. 23 0
      addons/admin/jest.config.js
  19. 11 0
      addons/admin/jsconfig.json
  20. 66 0
      addons/admin/package.json
  21. 5 0
      addons/admin/postcss.config.js
  22. 10 0
      addons/admin/public/config.js
  23. 32 0
      addons/admin/public/index.html
  24. 1 0
      addons/admin/public/loading/loading.css
  25. 1 0
      addons/admin/public/loading/loading.html
  26. 5 0
      addons/admin/public/loading/option2/html_code_segment.html
  27. 1 0
      addons/admin/public/loading/option2/loading.css
  28. 1 0
      addons/admin/public/loading/option2/loading.svg
  29. 24 0
      addons/admin/src/App.vue
  30. 27 0
      addons/admin/src/api/admin/user.js
  31. 62 0
      addons/admin/src/api/api.config.js
  32. 27 0
      addons/admin/src/api/login.js
  33. 14 0
      addons/admin/src/api/setting/cache.js
  34. 13 0
      addons/admin/src/api/setting/science.js
  35. 62 0
      addons/admin/src/api/store.js
  36. 50 0
      addons/admin/src/api/store/api.js
  37. 74 0
      addons/admin/src/api/store/menu.js
  38. 69 0
      addons/admin/src/assets/background.svg
  39. 1 0
      addons/admin/src/assets/icons/access.svg
  40. 1 0
      addons/admin/src/assets/icons/bx-analyse.svg
  41. 1 0
      addons/admin/src/assets/icons/menu.svg
  42. 1 0
      addons/admin/src/assets/icons/setting.svg
  43. 1 0
      addons/admin/src/assets/icons/shop.svg
  44. BIN
      addons/admin/src/assets/logo.png
  45. 29 0
      addons/admin/src/assets/logo.svg
  46. 89 0
      addons/admin/src/components/ArticleListContent/ArticleListContent.vue
  47. 3 0
      addons/admin/src/components/ArticleListContent/index.js
  48. 46 0
      addons/admin/src/components/AvatarList/Item.vue
  49. 99 0
      addons/admin/src/components/AvatarList/List.vue
  50. 4 0
      addons/admin/src/components/AvatarList/index.js
  51. 60 0
      addons/admin/src/components/AvatarList/index.less
  52. 64 0
      addons/admin/src/components/AvatarList/index.md
  53. 62 0
      addons/admin/src/components/Charts/Bar.vue
  54. 120 0
      addons/admin/src/components/Charts/ChartCard.vue
  55. 67 0
      addons/admin/src/components/Charts/Liquid.vue
  56. 56 0
      addons/admin/src/components/Charts/MiniArea.vue
  57. 57 0
      addons/admin/src/components/Charts/MiniBar.vue
  58. 75 0
      addons/admin/src/components/Charts/MiniProgress.vue
  59. 40 0
      addons/admin/src/components/Charts/MiniSmoothArea.vue
  60. 68 0
      addons/admin/src/components/Charts/Radar.vue
  61. 77 0
      addons/admin/src/components/Charts/RankList.vue
  62. 113 0
      addons/admin/src/components/Charts/TagCloud.vue
  63. 64 0
      addons/admin/src/components/Charts/TransferBar.vue
  64. 82 0
      addons/admin/src/components/Charts/Trend.vue
  65. 13 0
      addons/admin/src/components/Charts/chart.less
  66. 14 0
      addons/admin/src/components/Charts/smooth.area.less
  67. 52 0
      addons/admin/src/components/ContentHeader/ContentHeader.vue
  68. 2 0
      addons/admin/src/components/ContentHeader/index.js
  69. 102 0
      addons/admin/src/components/CountDown/CountDown.vue
  70. 3 0
      addons/admin/src/components/CountDown/index.js
  71. 34 0
      addons/admin/src/components/CountDown/index.md
  72. 153 0
      addons/admin/src/components/DescriptionList/DescriptionList.vue
  73. 2 0
      addons/admin/src/components/DescriptionList/index.js
  74. 113 0
      addons/admin/src/components/Dialog.js
  75. 82 0
      addons/admin/src/components/Editor/QuillEditor.vue
  76. 57 0
      addons/admin/src/components/Editor/WangEditor.vue
  77. 64 0
      addons/admin/src/components/Ellipsis/Ellipsis.vue
  78. 3 0
      addons/admin/src/components/Ellipsis/index.js
  79. 38 0
      addons/admin/src/components/Ellipsis/index.md
  80. 130 0
      addons/admin/src/components/Exception/ExceptionPage.vue
  81. 2 0
      addons/admin/src/components/Exception/index.js
  82. 19 0
      addons/admin/src/components/Exception/type.js
  83. 30 0
      addons/admin/src/components/FooterToolbar/FooterToolBar.vue
  84. 4 0
      addons/admin/src/components/FooterToolbar/index.js
  85. 23 0
      addons/admin/src/components/FooterToolbar/index.less
  86. 48 0
      addons/admin/src/components/FooterToolbar/index.md
  87. 29 0
      addons/admin/src/components/GlobalFooter/GlobalFooter.vue
  88. 2 0
      addons/admin/src/components/GlobalFooter/index.js
  89. 133 0
      addons/admin/src/components/GlobalHeader/GlobalHeader.vue
  90. 2 0
      addons/admin/src/components/GlobalHeader/index.js
  91. 86 0
      addons/admin/src/components/IconSelector/IconSelector.vue
  92. 48 0
      addons/admin/src/components/IconSelector/README.md
  93. 36 0
      addons/admin/src/components/IconSelector/icons.js
  94. 2 0
      addons/admin/src/components/IconSelector/index.js
  95. 62 0
      addons/admin/src/components/Menu/SideMenu.vue
  96. 2 0
      addons/admin/src/components/Menu/index.js
  97. 180 0
      addons/admin/src/components/Menu/menu.js
  98. 156 0
      addons/admin/src/components/Menu/menu.render.js
  99. 162 0
      addons/admin/src/components/MultiTab/MultiTab.vue
  100. 0 0
      addons/admin/src/components/MultiTab/events.js

+ 3 - 0
addons/admin/.browserslistrc

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

+ 39 - 0
addons/admin/.editorconfig

@@ -0,0 +1,39 @@
+[*]
+charset=utf-8
+end_of_line=lf
+insert_final_newline=false
+indent_style=space
+indent_size=2
+
+[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
+indent_style=space
+indent_size=2
+
+[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
+indent_style=space
+indent_size=2
+
+[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
+indent_style=space
+indent_size=2
+
+[*.svg]
+indent_style=space
+indent_size=2
+
+[*.js.map]
+indent_style=space
+indent_size=2
+
+[*.less]
+indent_style=space
+indent_size=2
+
+[*.vue]
+indent_style=space
+indent_size=2
+
+[{.analysis_options,*.yml,*.yaml}]
+indent_style=space
+indent_size=2
+

+ 3 - 0
addons/admin/.env.development

@@ -0,0 +1,3 @@
+NODE_ENV=development
+VUE_APP_PREVIEW=true
+VUE_APP_API_BASE_URL=/api

+ 3 - 0
addons/admin/.env.preview

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

+ 75 - 0
addons/admin/.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
addons/admin/.gitattributes

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

+ 21 - 0
addons/admin/.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
addons/admin/.prettierrc

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

+ 7 - 0
addons/admin/.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
addons/admin/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.

Plik diff jest za duży
+ 57 - 0
addons/admin/README.md


+ 28 - 0
addons/admin/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
addons/admin/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
addons/admin/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
addons/admin/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
addons/admin/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
addons/admin/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
addons/admin/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
addons/admin/jsconfig.json

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

+ 66 - 0
addons/admin/package.json

@@ -0,0 +1,66 @@
+{
+  "name": "vue-antd-pro",
+  "version": "2.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "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",
+    "postinstall": "opencollective-postinstall"
+  },
+  "dependencies": {
+    "@antv/data-set": "^0.10.2",
+    "ant-design-vue": "1.5.0-rc.6",
+    "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",
+    "mockjs2": "1.0.8",
+    "moment": "^2.24.0",
+    "nprogress": "^0.2.0",
+    "viser-vue": "^2.4.6",
+    "vue": "^2.6.10",
+    "vue-clipboard2": "^0.2.1",
+    "vue-cropper": "0.4.9",
+    "vue-ls": "^3.2.1",
+    "vue-quill-editor": "^3.0.6",
+    "vue-router": "^3.1.2",
+    "vue-svg-component-runtime": "^1.0.1",
+    "vuex": "^3.1.1",
+    "wangeditor": "^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-remove-console": "^6.9.4",
+    "eslint": "^5.16.0",
+    "eslint-plugin-html": "^5.0.0",
+    "eslint-plugin-vue": "^5.2.3",
+    "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.2.17"
+  },
+  "collective": {
+    "type": "opencollective",
+    "url": "https://opencollective.com/ant-design-pro-vue"
+  }
+}

+ 5 - 0
addons/admin/postcss.config.js

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

+ 10 - 0
addons/admin/public/config.js

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

Plik diff jest za duży
+ 32 - 0
addons/admin/public/index.html


Plik diff jest za duży
+ 1 - 0
addons/admin/public/loading/loading.css


+ 1 - 0
addons/admin/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>

Plik diff jest za duży
+ 5 - 0
addons/admin/public/loading/option2/html_code_segment.html


+ 1 - 0
addons/admin/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;}

Plik diff jest za duży
+ 1 - 0
addons/admin/public/loading/option2/loading.svg


+ 24 - 0
addons/admin/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
addons/admin/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
+  })
+}

+ 62 - 0
addons/admin/src/api/api.config.js

@@ -0,0 +1,62 @@
+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',
+
+    // 商家后台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'
+    }
+  }
+
+}

+ 27 - 0
addons/admin/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
addons/admin/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.store.setting.cache.clear,
+    method: 'post',
+    data: data
+  })
+}

+ 13 - 0
addons/admin/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.store.setting.science,
+    method: 'get'
+  })
+}

+ 62 - 0
addons/admin/src/api/store.js

@@ -0,0 +1,62 @@
+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
+  })
+}
+
+/**
+ * 回收站列表
+ * @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 move (data) {
+  return axios({
+    url: api.store.move,
+    method: 'post',
+    data: data
+  })
+}

+ 50 - 0
addons/admin/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
addons/admin/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
addons/admin/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>

Plik diff jest za duży
+ 1 - 0
addons/admin/src/assets/icons/access.svg


Plik diff jest za duży
+ 1 - 0
addons/admin/src/assets/icons/bx-analyse.svg


Plik diff jest za duży
+ 1 - 0
addons/admin/src/assets/icons/menu.svg


Plik diff jest za duży
+ 1 - 0
addons/admin/src/assets/icons/setting.svg


Plik diff jest za duży
+ 1 - 0
addons/admin/src/assets/icons/shop.svg


BIN
addons/admin/src/assets/logo.png


Plik diff jest za duży
+ 29 - 0
addons/admin/src/assets/logo.svg


+ 89 - 0
addons/admin/src/components/ArticleListContent/ArticleListContent.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="antd-pro-components-article-list-content-index-listContent">
+    <div class="description">
+      <slot>
+        {{ description }}
+      </slot>
+    </div>
+    <div class="extra">
+      <a-avatar :src="avatar" size="small" />
+      <a :href="href">{{ owner }}</a> 发布在 <a :href="href">{{ href }}</a>
+      <em>{{ updateAt | moment }}</em>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ArticleListContent',
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'antd-pro-components-article-list-content-index-listContent'
+    },
+    description: {
+      type: String,
+      default: ''
+    },
+    owner: {
+      type: String,
+      required: true
+    },
+    avatar: {
+      type: String,
+      required: true
+    },
+    href: {
+      type: String,
+      required: true
+    },
+    updateAt: {
+      type: String,
+      required: true
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+@import '../index.less';
+
+.antd-pro-components-article-list-content-index-listContent {
+  .description {
+    max-width: 720px;
+    line-height: 22px;
+  }
+  .extra {
+    margin-top: 16px;
+    color: @text-color-secondary;
+    line-height: 22px;
+
+    & /deep/ .ant-avatar {
+      position: relative;
+      top: 1px;
+      width: 20px;
+      height: 20px;
+      margin-right: 8px;
+      vertical-align: top;
+    }
+
+    & > em {
+      margin-left: 16px;
+      color: @disabled-color;
+      font-style: normal;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .antd-pro-components-article-list-content-index-listContent {
+    .extra {
+      & > em {
+        display: block;
+        margin-top: 8px;
+        margin-left: 0;
+      }
+    }
+  }
+}
+</style>

+ 3 - 0
addons/admin/src/components/ArticleListContent/index.js

@@ -0,0 +1,3 @@
+import ArticleListContent from './ArticleListContent'
+
+export default ArticleListContent

+ 46 - 0
addons/admin/src/components/AvatarList/Item.vue

@@ -0,0 +1,46 @@
+<template>
+  <tooltip v-if="tips !== ''">
+    <template slot="title">{{ tips }}</template>
+    <avatar :size="avatarSize" :src="src" />
+  </tooltip>
+  <avatar v-else :size="avatarSize" :src="src" />
+</template>
+
+<script>
+import Avatar from 'ant-design-vue/es/avatar'
+import Tooltip from 'ant-design-vue/es/tooltip'
+
+export default {
+  name: 'AvatarItem',
+  components: {
+    Avatar,
+    Tooltip
+  },
+  props: {
+    tips: {
+      type: String,
+      default: '',
+      required: false
+    },
+    src: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {
+      size: this.$parent.size
+    }
+  },
+  computed: {
+    avatarSize () {
+      return this.size !== 'mini' && this.size || 20
+    }
+  },
+  watch: {
+    '$parent.size' (val) {
+      this.size = val
+    }
+  }
+}
+</script>

+ 99 - 0
addons/admin/src/components/AvatarList/List.vue

@@ -0,0 +1,99 @@
+<!--
+<template>
+  <div :class="[prefixCls]">
+    <ul>
+      <slot></slot>
+      <template v-for="item in filterEmpty($slots.default).slice(0, 3)"></template>
+
+      <template v-if="maxLength > 0 && filterEmpty($slots.default).length > maxLength">
+        <avatar-item :size="size">
+          <avatar :size="size !== 'mini' && size || 20" :style="excessItemsStyle">{{ `+${maxLength}` }}</avatar>
+        </avatar-item>
+      </template>
+    </ul>
+  </div>
+</template>
+-->
+
+<script>
+import Avatar from 'ant-design-vue/es/avatar'
+import AvatarItem from './Item'
+import { filterEmpty } from '@/components/_util/util'
+
+export default {
+  AvatarItem,
+  name: 'AvatarList',
+  components: {
+    Avatar,
+    AvatarItem
+  },
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-pro-avatar-list'
+    },
+    /**
+       * 头像大小 类型: large、small 、mini, default
+       * 默认值: default
+       */
+    size: {
+      type: [String, Number],
+      default: 'default'
+    },
+    /**
+       * 要显示的最大项目
+       */
+    maxLength: {
+      type: Number,
+      default: 0
+    },
+    /**
+       * 多余的项目风格
+       */
+    excessItemsStyle: {
+      type: Object,
+      default: () => {
+        return {
+          color: '#f56a00',
+          backgroundColor: '#fde3cf'
+        }
+      }
+    }
+  },
+  data () {
+    return {}
+  },
+  methods: {
+    getItems (items) {
+      const classString = {
+        [`${this.prefixCls}-item`]: true,
+        [`${this.size}`]: true
+      }
+
+      if (this.maxLength > 0) {
+        items = items.slice(0, this.maxLength)
+        items.push((<Avatar size={ this.size } style={ this.excessItemsStyle }>{`+${this.maxLength}`}</Avatar>))
+      }
+      const itemList = items.map((item) => (
+        <li class={ classString }>{ item }</li>
+      ))
+      return itemList
+    }
+  },
+  render () {
+    const { prefixCls, size } = this.$props
+    const classString = {
+      [`${prefixCls}`]: true,
+      [`${size}`]: true
+    }
+    const items = filterEmpty(this.$slots.default)
+    const itemsDom = items && items.length ? <ul class={`${prefixCls}-items`}>{ this.getItems(items) }</ul> : null
+
+    return (
+      <div class={ classString }>
+        { itemsDom }
+      </div>
+    )
+  }
+}
+</script>

+ 4 - 0
addons/admin/src/components/AvatarList/index.js

@@ -0,0 +1,4 @@
+import AvatarList from './List'
+import './index.less'
+
+export default AvatarList

+ 60 - 0
addons/admin/src/components/AvatarList/index.less

@@ -0,0 +1,60 @@
+@import "../index";
+
+@avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list";
+@avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item";
+
+.@{avatar-list-prefix-cls} {
+  display: inline-block;
+
+  ul {
+    list-style: none;
+    display: inline-block;
+    padding: 0;
+    margin: 0 0 0 8px;
+    font-size: 0;
+  }
+}
+
+.@{avatar-list-item-prefix-cls} {
+  display: inline-block;
+  font-size: @font-size-base;
+  margin-left: -8px;
+  width: @avatar-size-base;
+  height: @avatar-size-base;
+
+  :global {
+    .ant-avatar {
+      border: 1px solid #fff;
+      cursor: pointer;
+    }
+  }
+
+  &.large {
+    width: @avatar-size-lg;
+    height: @avatar-size-lg;
+  }
+
+  &.small {
+    width: @avatar-size-sm;
+    height: @avatar-size-sm;
+  }
+
+  &.mini {
+    width: 20px;
+    height: 20px;
+
+    :global {
+      .ant-avatar {
+        width: 20px;
+        height: 20px;
+        line-height: 20px;
+
+        .ant-avatar-string {
+          font-size: 12px;
+          line-height: 18px;
+        }
+      }
+    }
+  }
+}
+

+ 64 - 0
addons/admin/src/components/AvatarList/index.md

@@ -0,0 +1,64 @@
+# AvatarList 用户头像列表
+
+
+一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
+
+
+
+引用方式:
+
+```javascript
+import AvatarList from '@/components/AvatarList'
+const AvatarListItem = AvatarList.AvatarItem
+
+export default {
+    components: {
+        AvatarList,
+        AvatarListItem
+    }
+}
+```
+
+
+
+## 代码演示  [demo](https://pro.loacg.com/test/home)
+
+```html
+<avatar-list size="mini">
+    <avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
+    <avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
+    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
+</avatar-list>
+```
+或
+```html
+<avatar-list :max-length="3">
+    <avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
+    <avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
+    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
+    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
+    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
+    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
+    <avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
+</avatar-list>
+```
+
+
+
+## API
+
+### AvatarList
+
+| 参数               | 说明       | 类型                                 | 默认值       |
+| ---------------- | -------- | ---------------------------------- | --------- |
+| size             | 头像大小     | `large`、`small` 、`mini`, `default` | `default` |
+| maxLength        | 要显示的最大项目 | number                             | -         |
+| excessItemsStyle | 多余的项目风格  | CSSProperties                      | -         |
+
+### AvatarList.Item
+
+| 参数   | 说明     | 类型        | 默认值 |
+| ---- | ------ | --------- | --- |
+| tips | 头像展示文案 | string | -   |
+| src  | 头像图片连接 | string    | -   |
+

+ 62 - 0
addons/admin/src/components/Charts/Bar.vue

@@ -0,0 +1,62 @@
+<template>
+  <div :style="{ padding: '0 0 32px 32px' }">
+    <h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
+    <v-chart
+      height="254"
+      :data="data"
+      :forceFit="true"
+      :padding="['auto', 'auto', '40', '50']">
+      <v-tooltip />
+      <v-axis />
+      <v-bar position="x*y"/>
+    </v-chart>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Bar',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    data: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    scale: {
+      type: Array,
+      default: () => {
+        return [{
+          dataKey: 'x',
+          min: 2
+        }, {
+          dataKey: 'y',
+          title: '时间',
+          min: 1,
+          max: 22
+        }]
+      }
+    },
+    tooltip: {
+      type: Array,
+      default: () => {
+        return [
+          'x*y',
+          (x, y) => ({
+            name: x,
+            value: y
+          })
+        ]
+      }
+    }
+  },
+  data () {
+    return {
+    }
+  }
+}
+</script>

+ 120 - 0
addons/admin/src/components/Charts/ChartCard.vue

@@ -0,0 +1,120 @@
+<template>
+  <a-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
+    <div class="chart-card-header">
+      <div class="meta">
+        <span class="chart-card-title">
+          <slot name="title">
+            {{ title }}
+          </slot>
+        </span>
+        <span class="chart-card-action">
+          <slot name="action"></slot>
+        </span>
+      </div>
+      <div class="total">
+        <slot name="total">
+          <span>{{ typeof total === 'function' && total() || total }}</span>
+        </slot>
+      </div>
+    </div>
+    <div class="chart-card-content">
+      <div class="content-fix">
+        <slot></slot>
+      </div>
+    </div>
+    <div class="chart-card-footer">
+      <div class="field">
+        <slot name="footer"></slot>
+      </div>
+    </div>
+  </a-card>
+</template>
+
+<script>
+export default {
+  name: 'ChartCard',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    total: {
+      type: [Function, Number, String],
+      required: false,
+      default: null
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .chart-card-header {
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+
+    .meta {
+      position: relative;
+      overflow: hidden;
+      width: 100%;
+      color: rgba(0, 0, 0, .45);
+      font-size: 14px;
+      line-height: 22px;
+    }
+  }
+
+  .chart-card-action {
+    cursor: pointer;
+    position: absolute;
+    top: 0;
+    right: 0;
+  }
+
+  .chart-card-footer {
+    border-top: 1px solid #e8e8e8;
+    padding-top: 9px;
+    margin-top: 8px;
+
+    > * {
+      position: relative;
+    }
+
+    .field {
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      margin: 0;
+    }
+  }
+
+  .chart-card-content {
+    margin-bottom: 12px;
+    position: relative;
+    height: 46px;
+    width: 100%;
+
+    .content-fix {
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      width: 100%;
+    }
+  }
+
+  .total {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    white-space: nowrap;
+    color: #000;
+    margin-top: 4px;
+    margin-bottom: 0;
+    font-size: 30px;
+    line-height: 38px;
+    height: 38px;
+  }
+</style>

+ 67 - 0
addons/admin/src/components/Charts/Liquid.vue

@@ -0,0 +1,67 @@
+<template>
+  <div>
+    <v-chart
+      :forceFit="true"
+      :height="height"
+      :width="width"
+      :data="data"
+      :scale="scale"
+      :padding="0">
+      <v-tooltip />
+      <v-interval
+        :shape="['liquid-fill-gauge']"
+        position="transfer*value"
+        color=""
+        :v-style="{
+          lineWidth: 10,
+          opacity: 0.75
+        }"
+        :tooltip="[
+          'transfer*value',
+          (transfer, value) => {
+            return {
+              name: transfer,
+              value,
+            };
+          },
+        ]"
+      ></v-interval>
+      <v-guide
+        v-for="(row, index) in data"
+        :key="index"
+        type="text"
+        :top="true"
+        :position="{
+          gender: row.transfer,
+          value: 45
+        }"
+        :content="row.value + '%'"
+        :v-style="{
+          fontSize: 100,
+          textAlign: 'center',
+          opacity: 0.75,
+        }"
+      />
+    </v-chart>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Liquid',
+  props: {
+    height: {
+      type: Number,
+      default: 0
+    },
+    width: {
+      type: Number,
+      default: 0
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 56 - 0
addons/admin/src/components/Charts/MiniArea.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="antv-chart-mini">
+    <div class="chart-wrapper" :style="{ height: 46 }">
+      <v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 0, 18, 0]">
+        <v-tooltip />
+        <v-smooth-area position="x*y" />
+      </v-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+import moment from 'moment'
+const data = []
+const beginDay = new Date().getTime()
+
+for (let i = 0; i < 10; i++) {
+  data.push({
+    x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+    y: Math.round(Math.random() * 10)
+  })
+}
+
+const tooltip = [
+  'x*y',
+  (x, y) => ({
+    name: x,
+    value: y
+  })
+]
+const scale = [{
+  dataKey: 'x',
+  min: 2
+}, {
+  dataKey: 'y',
+  title: '时间',
+  min: 1,
+  max: 22
+}]
+
+export default {
+  name: 'MiniArea',
+  data () {
+    return {
+      data,
+      tooltip,
+      scale,
+      height: 100
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  @import "chart";
+</style>

+ 57 - 0
addons/admin/src/components/Charts/MiniBar.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="antv-chart-mini">
+    <div class="chart-wrapper" :style="{ height: 46 }">
+      <v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
+        <v-tooltip />
+        <v-bar position="x*y" />
+      </v-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+import moment from 'moment'
+const data = []
+const beginDay = new Date().getTime()
+
+for (let i = 0; i < 10; i++) {
+  data.push({
+    x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+    y: Math.round(Math.random() * 10)
+  })
+}
+
+const tooltip = [
+  'x*y',
+  (x, y) => ({
+    name: x,
+    value: y
+  })
+]
+
+const scale = [{
+  dataKey: 'x',
+  min: 2
+}, {
+  dataKey: 'y',
+  title: '时间',
+  min: 1,
+  max: 30
+}]
+
+export default {
+  name: 'MiniBar',
+  data () {
+    return {
+      data,
+      tooltip,
+      scale,
+      height: 100
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  @import "chart";
+</style>

+ 75 - 0
addons/admin/src/components/Charts/MiniProgress.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="chart-mini-progress">
+    <div class="target" :style="{ left: target + '%'}">
+      <span :style="{ backgroundColor: color }" />
+      <span :style="{ backgroundColor: color }"/>
+    </div>
+    <div class="progress-wrapper">
+      <div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height }"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MiniProgress',
+  props: {
+    target: {
+      type: Number,
+      default: 0
+    },
+    height: {
+      type: String,
+      default: '10px'
+    },
+    color: {
+      type: String,
+      default: '#13C2C2'
+    },
+    percentage: {
+      type: Number,
+      default: 0
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .chart-mini-progress {
+    padding: 5px 0;
+    position: relative;
+    width: 100%;
+
+    .target {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+
+      span {
+        border-radius: 100px;
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 4px;
+        width: 2px;
+
+        &:last-child {
+          top: auto;
+          bottom: 0;
+        }
+      }
+    }
+    .progress-wrapper {
+      background-color: #f5f5f5;
+      position: relative;
+
+      .progress {
+        transition: all .4s cubic-bezier(.08,.82,.17,1) 0s;
+        border-radius: 1px 0 0 1px;
+        background-color: #1890ff;
+        width: 0;
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 40 - 0
addons/admin/src/components/Charts/MiniSmoothArea.vue

@@ -0,0 +1,40 @@
+<template>
+  <div :class="prefixCls">
+    <div class="chart-wrapper" :style="{ height: 46 }">
+      <v-chart :force-fit="true" :height="100" :data="dataSource" :scale="scale" :padding="[36, 0, 18, 0]">
+        <v-tooltip />
+        <v-smooth-line position="x*y" :size="2" />
+        <v-smooth-area position="x*y" />
+      </v-chart>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MiniSmoothArea',
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-pro-smooth-area'
+    },
+    scale: {
+      type: [Object, Array],
+      required: true
+    },
+    dataSource: {
+      type: Array,
+      required: true
+    }
+  },
+  data () {
+    return {
+      height: 100
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  @import "smooth.area.less";
+</style>

+ 68 - 0
addons/admin/src/components/Charts/Radar.vue

@@ -0,0 +1,68 @@
+<template>
+  <v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
+    <v-tooltip></v-tooltip>
+    <v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
+    <v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
+    <v-legend dataKey="user" marker="circle" :offset="30" />
+    <v-coord type="polar" radius="0.8" />
+    <v-line position="item*score" color="user" :size="2" />
+    <v-point position="item*score" color="user" :size="4" shape="circle" />
+  </v-chart>
+</template>
+
+<script>
+const axis1Opts = {
+  dataKey: 'item',
+  line: null,
+  tickLine: null,
+  grid: {
+    lineStyle: {
+      lineDash: null
+    },
+    hideFirstLine: false
+  }
+}
+const axis2Opts = {
+  dataKey: 'score',
+  line: null,
+  tickLine: null,
+  grid: {
+    type: 'polygon',
+    lineStyle: {
+      lineDash: null
+    }
+  }
+}
+
+const scale = [
+  {
+    dataKey: 'score',
+    min: 0,
+    max: 80
+  }, {
+    dataKey: 'user',
+    alias: '类型'
+  }
+]
+
+export default {
+  name: 'Radar',
+  props: {
+    data: {
+      type: Array,
+      default: null
+    }
+  },
+  data () {
+    return {
+      axis1Opts,
+      axis2Opts,
+      scale
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 77 - 0
addons/admin/src/components/Charts/RankList.vue

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

+ 113 - 0
addons/admin/src/components/Charts/TagCloud.vue

@@ -0,0 +1,113 @@
+<template>
+  <v-chart :width="width" :height="height" :padding="[0]" :data="data" :scale="scale">
+    <v-tooltip :show-title="false" />
+    <v-coord type="rect" direction="TL" />
+    <v-point position="x*y" color="category" shape="cloud" tooltip="value*category" />
+  </v-chart>
+</template>
+
+<script>
+import { registerShape } from 'viser-vue'
+const DataSet = require('@antv/data-set')
+
+const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png'
+
+const scale = [
+  { dataKey: 'x', nice: false },
+  { dataKey: 'y', nice: false }
+]
+
+registerShape('point', 'cloud', {
+  draw (cfg, container) {
+    return container.addShape('text', {
+      attrs: {
+        fillOpacity: cfg.opacity,
+        fontSize: cfg.origin._origin.size,
+        rotate: cfg.origin._origin.rotate,
+        text: cfg.origin._origin.text,
+        textAlign: 'center',
+        fontFamily: cfg.origin._origin.font,
+        fill: cfg.color,
+        textBaseline: 'Alphabetic',
+        ...cfg.style,
+        x: cfg.x,
+        y: cfg.y
+      }
+    })
+  }
+})
+
+export default {
+  name: 'TagCloud',
+  props: {
+    tagList: {
+      type: Array,
+      required: true
+    },
+    height: {
+      type: Number,
+      default: 400
+    },
+    width: {
+      type: Number,
+      default: 640
+    }
+  },
+  data () {
+    return {
+      data: [],
+      scale
+    }
+  },
+  watch: {
+    tagList: function (val) {
+      if (val.length > 0) {
+        this.initTagCloud(val)
+      }
+    }
+  },
+  mounted () {
+    if (this.tagList.length > 0) {
+      this.initTagCloud(this.tagList)
+    }
+  },
+  methods: {
+    initTagCloud (dataSource) {
+      const { height, width } = this
+
+      const dv = new DataSet.View().source(dataSource)
+      const range = dv.range('value')
+      const min = range[0]
+      const max = range[1]
+      const imageMask = new Image()
+      imageMask.crossOrigin = ''
+      imageMask.src = imgUrl
+      imageMask.onload = () => {
+        dv.transform({
+          type: 'tag-cloud',
+          fields: ['name', 'value'],
+          size: [width, height],
+          imageMask,
+          font: 'Verdana',
+          padding: 0,
+          timeInterval: 5000, // max execute time
+          rotate () {
+            let random = ~~(Math.random() * 4) % 4
+            if (random === 2) {
+              random = 0
+            }
+            return random * 90 // 0, 90, 270
+          },
+          fontSize (d) {
+            if (d.value) {
+              return ((d.value - min) / (max - min)) * (32 - 8) + 8
+            }
+            return 0
+          }
+        })
+        this.data = dv.rows
+      }
+    }
+  }
+}
+</script>

+ 64 - 0
addons/admin/src/components/Charts/TransferBar.vue

@@ -0,0 +1,64 @@
+<template>
+  <div :style="{ padding: '0 0 32px 32px' }">
+    <h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
+    <v-chart
+      height="254"
+      :data="data"
+      :scale="scale"
+      :forceFit="true"
+      :padding="['auto', 'auto', '40', '50']">
+      <v-tooltip />
+      <v-axis />
+      <v-bar position="x*y"/>
+    </v-chart>
+  </div>
+</template>
+
+<script>
+const tooltip = [
+  'x*y',
+  (x, y) => ({
+    name: x,
+    value: y
+  })
+]
+const scale = [{
+  dataKey: 'x',
+  title: '日期(天)',
+  alias: '日期(天)',
+  min: 2
+}, {
+  dataKey: 'y',
+  title: '流量(Gb)',
+  alias: '流量(Gb)',
+  min: 1
+}]
+
+export default {
+  name: 'Bar',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {
+      data: [],
+      scale,
+      tooltip
+    }
+  },
+  created () {
+    this.getMonthBar()
+  },
+  methods: {
+    getMonthBar () {
+      this.$http.get('/analysis/month-bar')
+        .then(res => {
+          this.data = res.result
+        })
+    }
+  }
+}
+</script>

+ 82 - 0
addons/admin/src/components/Charts/Trend.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="chart-trend">
+    {{ term }}
+    <span>{{ rate }}%</span>
+    <span :class="['trend-icon', trend]"><a-icon :type="'caret-' + trend"/></span>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Trend',
+  props: {
+    term: {
+      type: String,
+      default: '',
+      required: true
+    },
+    percentage: {
+      type: Number,
+      default: null
+    },
+    type: {
+      type: Boolean,
+      default: null
+    },
+    target: {
+      type: Number,
+      default: 0
+    },
+    value: {
+      type: Number,
+      default: 0
+    },
+    fixed: {
+      type: Number,
+      default: 2
+    }
+  },
+  data () {
+    return {
+      trend: this.type && 'up' || 'down',
+      rate: this.percentage
+    }
+  },
+  created () {
+    const type = this.type === null ? this.value >= this.target : this.type
+    this.trend = type ? 'up' : 'down'
+    this.rate = (this.percentage === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percentage).toFixed(this.fixed)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .chart-trend {
+    display: inline-block;
+    font-size: 14px;
+    line-height: 22px;
+
+    .trend-icon {
+      font-size: 12px;
+
+      &.up, &.down {
+        margin-left: 4px;
+        position: relative;
+        top: 1px;
+
+        i {
+          font-size: 12px;
+          transform: scale(.83);
+        }
+      }
+
+      &.up {
+        color: #f5222d;
+      }
+      &.down {
+        color: #52c41a;
+        top: -1px;
+      }
+    }
+  }
+</style>

+ 13 - 0
addons/admin/src/components/Charts/chart.less

@@ -0,0 +1,13 @@
+.antv-chart-mini {
+  position: relative;
+  width: 100%;
+
+  .chart-wrapper {
+    position: absolute;
+    bottom: -28px;
+    width: 100%;
+
+/*    margin: 0 -5px;
+    overflow: hidden;*/
+  }
+}

+ 14 - 0
addons/admin/src/components/Charts/smooth.area.less

@@ -0,0 +1,14 @@
+@import "../index";
+
+@smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area";
+
+.@{smoothArea-prefix-cls} {
+    position: relative;
+    width: 100%;
+
+    .chart-wrapper {
+        position: absolute;
+        bottom: -28px;
+        width: 100%;
+    }
+}

+ 52 - 0
addons/admin/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
addons/admin/src/components/ContentHeader/index.js

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

+ 102 - 0
addons/admin/src/components/CountDown/CountDown.vue

@@ -0,0 +1,102 @@
+<template>
+  <span>
+    {{ lastTime | format }}
+  </span>
+</template>
+
+<script>
+
+function fixedZero (val) {
+  return val * 1 < 10 ? `0${val}` : val
+}
+
+export default {
+  name: 'CountDown',
+  props: {
+    format: {
+      type: Function,
+      default: undefined
+    },
+    target: {
+      type: [Date, Number],
+      required: true
+    },
+    onEnd: {
+      type: Function,
+      default: () => ({})
+    }
+  },
+  data () {
+    return {
+      dateTime: '0',
+      originTargetTime: 0,
+      lastTime: 0,
+      timer: 0,
+      interval: 1000
+    }
+  },
+  filters: {
+    format (time) {
+      const hours = 60 * 60 * 1000
+      const minutes = 60 * 1000
+
+      const h = Math.floor(time / hours)
+      const m = Math.floor((time - h * hours) / minutes)
+      const s = Math.floor((time - h * hours - m * minutes) / 1000)
+      return `${fixedZero(h)}:${fixedZero(m)}:${fixedZero(s)}`
+    }
+  },
+  created () {
+    this.initTime()
+    this.tick()
+  },
+  methods: {
+    initTime () {
+      let lastTime = 0
+      let targetTime = 0
+      this.originTargetTime = this.target
+      try {
+        if (Object.prototype.toString.call(this.target) === '[object Date]') {
+          targetTime = this.target
+        } else {
+          targetTime = new Date(this.target).getTime()
+        }
+      } catch (e) {
+        throw new Error('invalid target prop')
+      }
+
+      lastTime = targetTime - new Date().getTime()
+
+      this.lastTime = lastTime < 0 ? 0 : lastTime
+    },
+    tick () {
+      const { onEnd } = this
+
+      this.timer = setTimeout(() => {
+        if (this.lastTime < this.interval) {
+          clearTimeout(this.timer)
+          this.lastTime = 0
+          if (typeof onEnd === 'function') {
+            onEnd()
+          }
+        } else {
+          this.lastTime -= this.interval
+          this.tick()
+        }
+      }, this.interval)
+    }
+  },
+  beforeUpdate () {
+    if (this.originTargetTime !== this.target) {
+      this.initTime()
+    }
+  },
+  beforeDestroy () {
+    clearTimeout(this.timer)
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 3 - 0
addons/admin/src/components/CountDown/index.js

@@ -0,0 +1,3 @@
+import CountDown from './CountDown'
+
+export default CountDown

+ 34 - 0
addons/admin/src/components/CountDown/index.md

@@ -0,0 +1,34 @@
+# CountDown 倒计时
+
+倒计时组件。
+
+
+
+引用方式:
+
+```javascript
+import CountDown from '@/components/CountDown/CountDown'
+
+export default {
+    components: {
+        CountDown
+    }
+}
+```
+
+
+
+## 代码演示  [demo](https://pro.loacg.com/test/home)
+
+```html
+<count-down :target="new Date().getTime() + 3000000" :on-end="onEndHandle" />
+```
+
+
+
+## API
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| target | 目标时间 | Date | - |
+| onEnd |  倒计时结束回调 | funtion | -|

+ 153 - 0
addons/admin/src/components/DescriptionList/DescriptionList.vue

@@ -0,0 +1,153 @@
+<template>
+  <div :class="['description-list', size, layout === 'vertical' ? 'vertical': 'horizontal']">
+    <div v-if="title" class="title">{{ title }}</div>
+    <a-row>
+      <slot></slot>
+    </a-row>
+  </div>
+</template>
+
+<script>
+import { Col } from 'ant-design-vue/es/grid/'
+
+const Item = {
+  name: 'DetailListItem',
+  props: {
+    term: {
+      type: String,
+      default: '',
+      required: false
+    }
+  },
+  inject: {
+    col: {
+      type: Number
+    }
+  },
+  render () {
+    return (
+      <Col {...{ props: responsive[this.col] }}>
+        <div class="term">{this.$props.term}</div>
+        <div class="content">{this.$slots.default}</div>
+      </Col>
+    )
+  }
+}
+
+const responsive = {
+  1: { xs: 24 },
+  2: { xs: 24, sm: 12 },
+  3: { xs: 24, sm: 12, md: 8 },
+  4: { xs: 24, sm: 12, md: 6 }
+}
+
+export default {
+  name: 'DetailList',
+  Item: Item,
+  components: {
+    Col
+  },
+  props: {
+    title: {
+      type: String,
+      default: '',
+      required: false
+    },
+    col: {
+      type: Number,
+      required: false,
+      default: 3
+    },
+    size: {
+      type: String,
+      required: false,
+      default: 'large'
+    },
+    layout: {
+      type: String,
+      required: false,
+      default: 'horizontal'
+    }
+  },
+  provide () {
+    return {
+      col: this.col > 4 ? 4 : this.col
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+  .description-list {
+
+    .title {
+      color: rgba(0,0,0,.85);
+      font-size: 14px;
+      font-weight: 500;
+      margin-bottom: 16px;
+    }
+
+    /deep/ .term {
+      color: rgba(0,0,0,.85);
+      display: table-cell;
+      line-height: 20px;
+      margin-right: 8px;
+      padding-bottom: 16px;
+      white-space: nowrap;
+
+      &:not(:empty):after {
+        content: ":";
+        margin: 0 8px 0 2px;
+        position: relative;
+        top: -.5px;
+      }
+    }
+
+    /deep/ .content {
+      color: rgba(0,0,0,.65);
+      display: table-cell;
+      min-height: 22px;
+      line-height: 22px;
+      padding-bottom: 16px;
+      width: 100%;
+      &:empty {
+        content: ' ';
+        height: 38px;
+        padding-bottom: 16px;
+      }
+    }
+
+    &.small {
+
+      .title {
+        font-size: 14px;
+        color: rgba(0, 0, 0, .65);
+        font-weight: normal;
+        margin-bottom: 12px;
+      }
+      /deep/ .term, .content {
+        padding-bottom: 8px;
+      }
+    }
+
+    &.large {
+      /deep/ .term, .content {
+        padding-bottom: 16px;
+      }
+
+      .title {
+        font-size: 16px;
+      }
+    }
+
+    &.vertical {
+      .term {
+        padding-bottom: 8px;
+      }
+      /deep/ .term, .content {
+        display: block;
+      }
+    }
+  }
+</style>

+ 2 - 0
addons/admin/src/components/DescriptionList/index.js

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

+ 113 - 0
addons/admin/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)
+      }
+    }
+  })
+}

+ 82 - 0
addons/admin/src/components/Editor/QuillEditor.vue

@@ -0,0 +1,82 @@
+<template>
+  <div :class="prefixCls">
+    <quill-editor
+      v-model="content"
+      ref="myQuillEditor"
+      :options="editorOption"
+      @blur="onEditorBlur($event)"
+      @focus="onEditorFocus($event)"
+      @ready="onEditorReady($event)"
+      @change="onEditorChange($event)">
+    </quill-editor>
+
+  </div>
+</template>
+
+<script>
+import 'quill/dist/quill.core.css'
+import 'quill/dist/quill.snow.css'
+import 'quill/dist/quill.bubble.css'
+
+import { quillEditor } from 'vue-quill-editor'
+
+export default {
+  name: 'QuillEditor',
+  components: {
+    quillEditor
+  },
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-editor-quill'
+    },
+    // 表单校验用字段
+    // eslint-disable-next-line
+    value: {
+      type: String
+    }
+  },
+  data () {
+    return {
+      content: null,
+      editorOption: {
+        // some quill options
+      }
+    }
+  },
+  methods: {
+    onEditorBlur (quill) {
+      console.log('editor blur!', quill)
+    },
+    onEditorFocus (quill) {
+      console.log('editor focus!', quill)
+    },
+    onEditorReady (quill) {
+      console.log('editor ready!', quill)
+    },
+    onEditorChange ({ quill, html, text }) {
+      console.log('editor change!', quill, html, text)
+      this.$emit('change', html)
+    }
+  },
+  watch: {
+    value (val) {
+      this.content = val
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+@import url('../index.less');
+
+/* 覆盖 quill 默认边框圆角为 ant 默认圆角,用于统一 ant 组件风格 */
+.ant-editor-quill {
+  /deep/ .ql-toolbar.ql-snow {
+    border-radius: @border-radius-base @border-radius-base 0 0;
+  }
+  /deep/ .ql-container.ql-snow {
+    border-radius: 0 0 @border-radius-base @border-radius-base;
+  }
+}
+</style>

+ 57 - 0
addons/admin/src/components/Editor/WangEditor.vue

@@ -0,0 +1,57 @@
+<template>
+  <div :class="prefixCls">
+    <div ref="editor" class="editor-wrapper"></div>
+  </div>
+</template>
+
+<script>
+import WEditor from 'wangeditor'
+
+export default {
+  name: 'WangEditor',
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-editor-wang'
+    },
+    // eslint-disable-next-line
+    value: {
+      type: String
+    }
+  },
+  data () {
+    return {
+      editor: null,
+      editorContent: null
+    }
+  },
+  watch: {
+    value (val) {
+      this.editorContent = val
+      this.editor.txt.html(val)
+    }
+  },
+  mounted () {
+    this.initEditor()
+  },
+  methods: {
+    initEditor () {
+      this.editor = new WEditor(this.$refs.editor)
+      // this.editor.onchangeTimeout = 200
+      this.editor.customConfig.onchange = (html) => {
+        this.editorContent = html
+        this.$emit('change', this.editorContent)
+      }
+      this.editor.create()
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.ant-editor-wang {
+  .editor-wrapper {
+    text-align: left;
+  }
+}
+</style>

+ 64 - 0
addons/admin/src/components/Ellipsis/Ellipsis.vue

@@ -0,0 +1,64 @@
+<script>
+import Tooltip from 'ant-design-vue/es/tooltip'
+import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
+/*
+    const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
+
+    const TooltipOverlayStyle = {
+      overflowWrap: 'break-word',
+      wordWrap: 'break-word',
+    };
+  */
+
+export default {
+  name: 'Ellipsis',
+  components: {
+    Tooltip
+  },
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-pro-ellipsis'
+    },
+    tooltip: {
+      type: Boolean
+    },
+    length: {
+      type: Number,
+      required: true
+    },
+    lines: {
+      type: Number,
+      default: 1
+    },
+    fullWidthRecognition: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    getStrDom (str, fullLength) {
+      return (
+        <span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
+      )
+    },
+    getTooltip (fullStr, fullLength) {
+      return (
+        <Tooltip>
+          <template slot="title">{ fullStr }</template>
+          { this.getStrDom(fullStr, fullLength) }
+        </Tooltip>
+      )
+    }
+  },
+  render () {
+    const { tooltip, length } = this.$props
+    const str = this.$slots.default.map(vNode => vNode.text).join('')
+    const fullLength = getStrFullLength(str)
+    const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
+    return (
+      strDom
+    )
+  }
+}
+</script>

+ 3 - 0
addons/admin/src/components/Ellipsis/index.js

@@ -0,0 +1,3 @@
+import Ellipsis from './Ellipsis'
+
+export default Ellipsis

+ 38 - 0
addons/admin/src/components/Ellipsis/index.md

@@ -0,0 +1,38 @@
+# Ellipsis 文本自动省略号
+
+文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
+
+
+
+引用方式:
+
+```javascript
+import Ellipsis from '@/components/Ellipsis'
+
+export default {
+    components: {
+        Ellipsis
+    }
+}
+```
+
+
+
+## 代码演示  [demo](https://pro.loacg.com/test/home)
+
+```html
+<ellipsis :length="100" tooltip>
+        There were injuries alleged in three cases in 2015, and a
+        fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
+</ellipsis>
+```
+
+
+
+## API
+
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+tooltip | 移动到文本展示完整内容的提示 | boolean | -
+length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -

+ 130 - 0
addons/admin/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
addons/admin/src/components/Exception/index.js

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

+ 19 - 0
addons/admin/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

+ 30 - 0
addons/admin/src/components/FooterToolbar/FooterToolBar.vue

@@ -0,0 +1,30 @@
+<template>
+  <div :class="prefixCls">
+    <div style="float: left">
+      <slot name="extra">{{ extra }}</slot>
+    </div>
+    <div style="float: right">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FooterToolBar',
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-pro-footer-toolbar'
+    },
+    extra: {
+      type: [String, Object],
+      default: ''
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+</style>

+ 4 - 0
addons/admin/src/components/FooterToolbar/index.js

@@ -0,0 +1,4 @@
+import FooterToolBar from './FooterToolBar'
+import './index.less'
+
+export default FooterToolBar

+ 23 - 0
addons/admin/src/components/FooterToolbar/index.less

@@ -0,0 +1,23 @@
+@import "../index";
+
+@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
+
+.@{footer-toolbar-prefix-cls} {
+  position: fixed;
+  width: 100%;
+  bottom: 0;
+  right: 0;
+  height: 56px;
+  line-height: 56px;
+  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
+  background: #fff;
+  border-top: 1px solid #e8e8e8;
+  padding: 0 24px;
+  z-index: 9;
+
+  &:after {
+    content: "";
+    display: block;
+    clear: both;
+  }
+}

+ 48 - 0
addons/admin/src/components/FooterToolbar/index.md

@@ -0,0 +1,48 @@
+# FooterToolbar 底部工具栏
+
+固定在底部的工具栏。
+
+
+
+## 何时使用
+
+固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
+
+
+
+引用方式:
+
+```javascript
+import FooterToolBar from '@/components/FooterToolbar'
+
+export default {
+    components: {
+        FooterToolBar
+    }
+}
+```
+
+
+
+## 代码演示
+
+```html
+<footer-tool-bar>
+    <a-button type="primary" @click="validate" :loading="loading">提交</a-button>
+</footer-tool-bar>
+```
+或
+```html
+<footer-tool-bar extra="扩展信息提示">
+    <a-button type="primary" @click="validate" :loading="loading">提交</a-button>
+</footer-tool-bar>
+```
+
+
+## API
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+children (slot) | 工具栏内容,向右对齐 | - | -
+extra | 额外信息,向左对齐 | String, Object | -
+

+ 29 - 0
addons/admin/src/components/GlobalFooter/GlobalFooter.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="footer">
+    <div class="copyright">
+      <!-- <span>Copyright ©2020 萤火科技 (yiovo.com) 鲁ICP备18036811号-1</span> -->
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'GlobalFooter',
+  data () {
+    return {}
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.footer {
+  padding: 0 16px;
+  margin: 48px 0 24px;
+  text-align: center;
+
+  .copyright {
+    color: rgba(0, 0, 0, 0.45);
+    font-size: 14px;
+  }
+}
+</style>

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

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

+ 133 - 0
addons/admin/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
addons/admin/src/components/GlobalHeader/index.js

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

+ 86 - 0
addons/admin/src/components/IconSelector/IconSelector.vue

@@ -0,0 +1,86 @@
+<template>
+  <div :class="prefixCls">
+    <a-tabs v-model="currentTab" @change="handleTabChange">
+      <a-tab-pane v-for="v in icons" :tab="v.title" :key="v.key">
+        <ul>
+          <li v-for="(icon, key) in v.icons" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
+            <a-icon :type="icon" :style="{ fontSize: '36px' }" />
+          </li>
+        </ul>
+      </a-tab-pane>
+    </a-tabs>
+  </div>
+</template>
+
+<script>
+import icons from './icons'
+
+export default {
+  name: 'IconSelect',
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-pro-icon-selector'
+    },
+    // eslint-disable-next-line
+    value: {
+      type: String
+    }
+  },
+  data () {
+    return {
+      selectedIcon: this.value || '',
+      currentTab: 'directional',
+      icons
+    }
+  },
+  watch: {
+    value (val) {
+      this.selectedIcon = val
+      this.autoSwitchTab()
+    }
+  },
+  created () {
+    if (this.value) {
+      this.autoSwitchTab()
+    }
+  },
+  methods: {
+    handleSelectedIcon (icon) {
+      this.selectedIcon = icon
+      this.$emit('change', icon)
+    },
+    handleTabChange (activeKey) {
+      this.currentTab = activeKey
+    },
+    autoSwitchTab () {
+      icons.some(item => item.icons.some(icon => icon === this.value) && (this.currentTab = item.key))
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  @import "../index.less";
+
+  ul{
+    list-style: none;
+    padding: 0;
+    overflow-y: scroll;
+    height: 250px;
+
+    li{
+      display: inline-block;
+      padding: @padding-sm;
+      margin: 3px 0;
+      border-radius: @border-radius-base;
+
+      &:hover, &.active{
+        // box-shadow: 0px 0px 5px 2px @primary-color;
+        cursor: pointer;
+        color: @white;
+        background-color: @primary-color;
+      }
+    }
+  }
+</style>

+ 48 - 0
addons/admin/src/components/IconSelector/README.md

@@ -0,0 +1,48 @@
+IconSelector
+====
+
+> 图标选择组件,常用于为某一个数据设定一个图标时使用
+> eg: 设定菜单列表时,为每个菜单设定一个图标
+
+该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装
+
+
+
+### 使用方式
+
+```vue
+<template>
+	<div>
+       <icon-selector @change="handleIconChange"/>
+    </div>
+</template>
+
+<script>
+import IconSelector from '@/components/IconSelector'
+
+export default {
+  name: 'YourView',
+  components: {
+    IconSelector
+  },
+  data () {
+    return {
+    }
+  },
+  methods: {
+    handleIconChange (icon) {
+      console.log('change Icon', icon)
+    }
+  }
+}
+</script>
+```
+
+
+
+### 事件
+
+
+| 名称   | 说明                       | 类型   | 默认值 |
+| ------ | -------------------------- | ------ | ------ |
+| change | 当改变了 `icon` 选中项触发 | String | -      |

Plik diff jest za duży
+ 36 - 0
addons/admin/src/components/IconSelector/icons.js


+ 2 - 0
addons/admin/src/components/IconSelector/index.js

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

+ 62 - 0
addons/admin/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
addons/admin/src/components/Menu/index.js

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

+ 180 - 0
addons/admin/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
addons/admin/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
addons/admin/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>

+ 0 - 0
addons/admin/src/components/MultiTab/events.js


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików