소스 검색

初始化

flowerpig 1 년 전
커밋
49d46eda45
100개의 변경된 파일6775개의 추가작업 그리고 0개의 파일을 삭제
  1. 14 0
      .editorconfig
  2. 5 0
      .env.development
  3. 6 0
      .env.production
  4. 8 0
      .env.staging
  5. 4 0
      .eslintignore
  6. 199 0
      .eslintrc.js
  7. 16 0
      .gitignore
  8. 34 0
      .gitlab-ci.yml
  9. 5 0
      .travis.yml
  10. 21 0
      LICENSE
  11. 107 0
      README-zh.md
  12. 96 0
      README.md
  13. 14 0
      babel.config.js
  14. 35 0
      build/index.js
  15. BIN
      dist.zip
  16. 24 0
      jest.config.js
  17. 9 0
      jsconfig.json
  18. 77 0
      package.json
  19. 8 0
      postcss.config.js
  20. BIN
      public/favicon.ico
  21. 17 0
      public/index.html
  22. 11 0
      src/App.vue
  23. 50 0
      src/api/aboutChannel.js
  24. 104 0
      src/api/aboutSanQi.js
  25. 24 0
      src/api/aboutServer.js
  26. 17 0
      src/api/apidoc.js
  27. 215 0
      src/api/gameStatistics.js
  28. 128 0
      src/api/mailSettings.js
  29. 85 0
      src/api/menuSetting.js
  30. 221 0
      src/api/playerManage.js
  31. 148 0
      src/api/playerResourceLog.js
  32. 87 0
      src/api/relatedRecharge.js
  33. 12 0
      src/api/routersMenu.js
  34. 421 0
      src/api/serverManage.js
  35. 252 0
      src/api/specialOperation.js
  36. 30 0
      src/api/user.js
  37. 100 0
      src/api/userSetting.js
  38. BIN
      src/assets/404_images/404.png
  39. BIN
      src/assets/404_images/404_cloud.png
  40. 0 0
      src/assets/500_images/500.svg
  41. 62 0
      src/components/SvgIcon/index.vue
  42. 87 0
      src/components/multipleDialog/index.vue
  43. 54 0
      src/components/radioDialog/index.vue
  44. 9 0
      src/icons/index.js
  45. 0 0
      src/icons/svg/dashboard.svg
  46. 1 0
      src/icons/svg/example.svg
  47. 1 0
      src/icons/svg/eye-open.svg
  48. 1 0
      src/icons/svg/eye.svg
  49. 0 0
      src/icons/svg/form.svg
  50. 1 0
      src/icons/svg/link.svg
  51. 1 0
      src/icons/svg/nested.svg
  52. 1 0
      src/icons/svg/password.svg
  53. 1 0
      src/icons/svg/table.svg
  54. 1 0
      src/icons/svg/tree.svg
  55. 1 0
      src/icons/svg/user.svg
  56. 22 0
      src/icons/svgo.yml
  57. 44 0
      src/layout/components/AppMain.vue
  58. 274 0
      src/layout/components/Navbar.vue
  59. 26 0
      src/layout/components/Sidebar/FixiOSBug.js
  60. 41 0
      src/layout/components/Sidebar/Item.vue
  61. 43 0
      src/layout/components/Sidebar/Link.vue
  62. 53 0
      src/layout/components/Sidebar/Logo.vue
  63. 105 0
      src/layout/components/Sidebar/SidebarItem.vue
  64. 71 0
      src/layout/components/Sidebar/index.vue
  65. 110 0
      src/layout/components/TagsView/ScrollPane.vue
  66. 307 0
      src/layout/components/TagsView/index.vue
  67. 3 0
      src/layout/components/index.js
  68. 134 0
      src/layout/index.vue
  69. 45 0
      src/layout/mixin/ResizeHandler.js
  70. 13 0
      src/layout/secondaryLayout.vue
  71. 71 0
      src/main.js
  72. 33 0
      src/permission.js
  73. 61 0
      src/router/index.js
  74. 16 0
      src/settings.js
  75. 9 0
      src/store/getters.js
  76. 32 0
      src/store/index.js
  77. 48 0
      src/store/modules/app.js
  78. 73 0
      src/store/modules/router.js
  79. 32 0
      src/store/modules/settings.js
  80. 163 0
      src/store/modules/tagsView.js
  81. 36 0
      src/store/modules/user.js
  82. 96 0
      src/styles/element-ui.scss
  83. 78 0
      src/styles/index.scss
  84. 28 0
      src/styles/mixin.scss
  85. 232 0
      src/styles/sidebar.scss
  86. 48 0
      src/styles/transition.scss
  87. 26 0
      src/styles/variables.scss
  88. 174 0
      src/utils/Blob.js
  89. 15 0
      src/utils/auth.js
  90. 10 0
      src/utils/get-page-title.js
  91. 117 0
      src/utils/index.js
  92. 61 0
      src/utils/request.js
  93. 21 0
      src/utils/validate.js
  94. 226 0
      src/views/404.vue
  95. 137 0
      src/views/500.vue
  96. 147 0
      src/views/gameSettings/censusSettings/bossDotData.vue
  97. 239 0
      src/views/gameSettings/censusSettings/consume.vue
  98. 143 0
      src/views/gameSettings/censusSettings/fogDotData.vue
  99. 144 0
      src/views/gameSettings/censusSettings/guideDotData.vue
  100. 143 0
      src/views/gameSettings/censusSettings/loginDotData.vue

+ 14 - 0
.editorconfig

@@ -0,0 +1,14 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 5 - 0
.env.development

@@ -0,0 +1,5 @@
+# 开发环境配置
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = '/dev-api'

+ 6 - 0
.env.production

@@ -0,0 +1,6 @@
+# 生产环境配置
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = '/prod-api'
+

+ 8 - 0
.env.staging

@@ -0,0 +1,8 @@
+NODE_ENV = production
+
+# 测试环境配置
+ENV = 'staging'
+
+# base api
+VUE_APP_BASE_API = '/stage-api'
+

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist

+ 199 - 0
.eslintrc.js

@@ -0,0 +1,199 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  // add your custom rules here
+  //it is base on https://github.com/vuejs/eslint-config-vue
+  rules: {
+    "complexity":["error",10],//圈复杂度
+    "vue/max-attributes-per-line": [2, {
+      "singleline": 10,
+      "multiline": {
+        "max": 1,
+        "allowFirstLine": false
+      }
+    }],
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/multiline-html-element-content-newline":"off",
+    "vue/name-property-casing": ["error", "PascalCase"],
+    "vue/no-v-html": "off",
+    'accessor-pairs': 2,
+    'arrow-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'block-spacing': [2, 'always'],
+    'brace-style': [2, '1tbs', {
+      'allowSingleLine': true
+    }],
+    'camelcase': [0, {
+      'properties': 'always'
+    }],
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'comma-style': [2, 'last'],
+    'constructor-super': 2,
+    'curly': [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    'eqeqeq': ["error", "always", {"null": "ignore"}],
+    'generator-star-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'handle-callback-err': [2, '^(err|error)$'],
+    'indent': [2, 2, {
+      'SwitchCase': 1
+    }],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [2, {
+      'beforeColon': false,
+      'afterColon': true
+    }],
+    'keyword-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'new-cap': [2, {
+      'newIsCap': true,
+      'capIsNew': false
+    }],
+    'new-parens': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 'off',
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-control-regex': 0,
+    'no-delete-var': 2,
+    'no-dupe-args': 2,
+    'no-dupe-class-members': 2,
+    'no-dupe-keys': 2,
+    'no-duplicate-case': 2,
+    'no-empty-character-class': 2,
+    'no-empty-pattern': 2,
+    'no-eval': 2,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-fallthrough': 2,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implied-eval': 2,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [2, {
+      'allowLoop': false,
+      'allowSwitch': false
+    }],
+    'no-lone-blocks': 2,
+    'no-mixed-spaces-and-tabs': 2,
+    'no-multi-spaces': 2,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [2, {
+      'max': 1
+    }],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-symbol': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-path-concat': 2,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-self-assign': 2,
+    'no-self-compare': 2,
+    'no-sequences': 2,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-this-before-super': 2,
+    'no-throw-literal': 2,
+    'no-trailing-spaces': 2,
+    'no-undef': 2,
+    'no-undef-init': 2,
+    'no-unexpected-multiline': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [2, {
+      'defaultAssignment': false
+    }],
+    'no-unreachable': 2,
+    'no-unsafe-finally': 2,
+    'no-unused-vars': [2, {
+      'vars': 'all',
+      'args': 'none'
+    }],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-useless-escape': 0,
+    'no-whitespace-before-property': 2,
+    'no-with': 2,
+    'one-var': [2, {
+      'initialized': 'never'
+    }],
+    'operator-linebreak': [2, 'after', {
+      'overrides': {
+        '?': 'before',
+        ':': 'before'
+      }
+    }],
+    'padded-blocks': [2, 'never'],
+    'quotes': [2, 'single', {
+      'avoidEscape': true,
+      'allowTemplateLiterals': true
+    }],
+    'semi': [2, 'never'],
+    'semi-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': [2, 'never'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [2, {
+      'words': true,
+      'nonwords': false
+    }],
+    'spaced-comment': [2, 'always', {
+      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+    }],
+    'template-curly-spacing': [2, 'never'],
+    'use-isnan': 2,
+    'valid-typeof': 2,
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    'yoda': [2, 'never'],
+    'prefer-const': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'object-curly-spacing': [2, 'always', {
+      objectsInObjects: false
+    }],
+    'array-bracket-spacing': [2, 'never']
+  }
+}

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+package-lock.json
+tests/**/coverage/
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 34 - 0
.gitlab-ci.yml

@@ -0,0 +1,34 @@
+stages:
+- install
+- build
+
+
+install-staging:
+  stage: install
+  tags:
+    - build
+  only:
+  - master
+  script:
+  - echo "=====start install====="
+  - npm install --ignore-scripts
+  - echo "=====end install====="
+  artifacts:
+    expire_in: 60 mins
+    paths:
+    - node_modules/
+  
+build-staging:
+  stage: build
+  tags:
+    - build
+  only:
+  - master
+  script:
+  - echo "=====start build====="
+  - npm run build:prod
+  - echo "=====end build====="
+  - echo "=====start deploy====="
+  - sudo /bin/cp -rf ./dist/ /game/x1/backstage-vue
+  - echo "=====end deploy====="
+

+ 5 - 0
.travis.yml

@@ -0,0 +1,5 @@
+language: node_js
+node_js: 10
+script: npm run test
+notifications:
+  email: false

+ 21 - 0
LICENSE

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

+ 107 - 0
README-zh.md

@@ -0,0 +1,107 @@
+# vue-admin-template
+
+> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
+
+[线上地址](http://panjiachen.github.io/vue-admin-template)
+
+[国内访问](https://panjiachen.gitee.io/vue-admin-template)
+
+目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
+
+## Extra
+
+如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
+
+## GitAds
+
+[<img src="https://images.gitads.io/PanJiaChen/vue-admin-template" alt="GitAds" />](https://tracking.gitads.io/?repo=PanJiaChen/vue-admin-template)
+
+
+## 相关项目
+
+- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
+
+- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
+
+- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
+
+- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
+
+写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
+
+- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
+- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
+- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
+- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
+- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
+
+## Build Setup
+
+```bash
+# 克隆项目
+git clone https://github.com/PanJiaChen/vue-admin-template.git
+
+# 进入项目目录
+cd vue-admin-template
+
+# 安装依赖
+npm install
+
+# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
+npm install --registry=https://registry.npm.taobao.org
+
+# 启动服务
+npm run dev
+```
+
+浏览器访问 [http://localhost:9528](http://localhost:9528)
+
+## 发布
+
+```bash
+# 构建测试环境
+npm run build:stage
+
+# 构建生产环境
+npm run build:prod
+```
+
+## 其它
+
+```bash
+# 预览发布环境效果
+npm run preview
+
+# 预览发布环境效果 + 静态资源分析
+npm run preview -- --report
+
+# 代码格式检查
+npm run lint
+
+# 代码格式检查并自动修复
+npm run lint -- --fix
+```
+
+更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
+
+## 购买贴纸
+
+你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。
+
+## Demo
+
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
+
+## Browsers support
+
+Modern browsers and Internet Explorer 10+.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
+| --------- | --------- | --------- | --------- |
+| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
+
+## License
+
+[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
+
+Copyright (c) 2017-present PanJiaChen

+ 96 - 0
README.md

@@ -0,0 +1,96 @@
+# vue-admin-template
+
+English | [简体中文](./README-zh.md)
+
+> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
+
+**Live demo:** http://panjiachen.github.io/vue-admin-template
+
+
+**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
+
+
+## GitAds
+
+[<img src="https://images.gitads.io/PanJiaChen/vue-admin-template" alt="GitAds" />](https://tracking.gitads.io/?repo=PanJiaChen/vue-admin-template)
+
+## Build Setup
+
+
+```bash
+# clone the project
+git clone https://github.com/PanJiaChen/vue-admin-template.git
+
+# enter the project directory
+cd vue-admin-template
+
+# install dependency
+npm install
+
+# develop
+npm run dev
+```
+
+This will automatically open http://localhost:9528
+
+## Build
+
+```bash
+# build for test environment
+npm run build:stage
+
+# build for production environment
+npm run build:prod
+```
+
+## Advanced
+
+```bash
+# preview the release environment effect
+npm run preview
+
+# preview the release environment effect + static resource analysis
+npm run preview -- --report
+
+# code format check
+npm run lint
+
+# code format check and auto fix
+npm run lint -- --fix
+```
+
+Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
+
+## Demo
+
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
+
+## Extra
+
+If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
+
+For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
+
+## Related Project
+
+- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
+
+- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
+
+- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
+
+- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
+
+## Browsers support
+
+Modern browsers and Internet Explorer 10+.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
+| --------- | --------- | --------- | --------- |
+| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
+
+## License
+
+[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
+
+Copyright (c) 2017-present PanJiaChen

+ 14 - 0
babel.config.js

@@ -0,0 +1,14 @@
+module.exports = {
+  presets: [
+    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
+    '@vue/cli-plugin-babel/preset'
+  ],
+  'env': {
+    'development': {
+      // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
+      // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
+      // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
+      'plugins': ['dynamic-import-node']
+    }
+  }
+}

+ 35 - 0
build/index.js

@@ -0,0 +1,35 @@
+const { run } = require('runjs')
+const chalk = require('chalk')
+const config = require('../vue.config.js')
+const rawArgv = process.argv.slice(2)
+const args = rawArgv.join(' ')
+
+if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
+  const report = rawArgv.includes('--report')
+
+  run(`vue-cli-service build ${args}`)
+
+  const port = 9526
+  const publicPath = config.publicPath
+
+  var connect = require('connect')
+  var serveStatic = require('serve-static')
+  const app = connect()
+
+  app.use(
+    publicPath,
+    serveStatic('./dist', {
+      index: ['index.html', '/']
+    })
+  )
+
+  app.listen(port, function () {
+    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
+    if (report) {
+      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
+    }
+
+  })
+} else {
+  run(`vue-cli-service build ${args}`)
+}

BIN
dist.zip


+ 24 - 0
jest.config.js

@@ -0,0 +1,24 @@
+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)'
+  ],
+  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
+  // 'collectCoverage': true,
+  'coverageReporters': [
+    'lcov',
+    'text-summary'
+  ],
+  testURL: 'http://localhost/'
+}

+ 9 - 0
jsconfig.json

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

+ 77 - 0
package.json

@@ -0,0 +1,77 @@
+{
+  "name": "game-management",
+  "version": "1.0.0",
+  "description": "game management",
+  "author": "MADAO",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "dev2": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
+    "build:prod": "vue-cli-service build",
+    "build:stage": "vue-cli-service build --mode staging",
+    "preview": "node build/index.js --preview",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
+    "lint": "eslint --ext .js,.vue src",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "test:ci": "npm run lint && npm run test:unit"
+  },
+  "dependencies": {
+    "axios": "0.18.1",
+    "core-js": "3.6.5",
+    "default-passive-events": "^2.0.0",
+    "echarts": "^5.4.1",
+    "element-ui": "^2.15.14",
+    "file-saver": "^2.0.5",
+    "js-base64": "^3.7.5",
+    "js-cookie": "2.2.0",
+    "lib-flexible": "^0.3.2",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.4",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-browserify": "^1.0.1",
+    "path-to-regexp": "2.4.0",
+    "px2rem-loader": "^0.1.9",
+    "vue": "2.6.10",
+    "vue-router": "3.0.6",
+    "vue2-smooth-scroll": "^1.6.0",
+    "vuex": "3.1.0",
+    "vuex-persistedstate": "^4.1.0",
+    "xlsx": "^0.18.5"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "4.4.4",
+    "@vue/cli-plugin-eslint": "4.4.4",
+    "@vue/cli-plugin-unit-jest": "4.4.4",
+    "@vue/cli-service": "4.4.4",
+    "@vue/test-utils": "1.0.0-beta.29",
+    "autoprefixer": "9.5.1",
+    "babel-eslint": "10.1.0",
+    "babel-jest": "23.6.0",
+    "babel-plugin-dynamic-import-node": "2.3.3",
+    "chalk": "2.4.2",
+    "clean-webpack-plugin": "^4.0.0-alpha.0",
+    "connect": "3.6.6",
+    "eslint": "6.7.2",
+    "eslint-plugin-vue": "6.2.2",
+    "html-webpack-plugin": "3.2.0",
+    "mockjs": "1.0.1-beta3",
+    "runjs": "4.3.2",
+    "sass": "1.26.8",
+    "sass-loader": "8.0.2",
+    "script-ext-html-webpack-plugin": "2.1.3",
+    "script-loader": "^0.7.2",
+    "serve-static": "1.13.2",
+    "svg-sprite-loader": "4.1.3",
+    "svgo": "1.2.2",
+    "vue-template-compiler": "2.6.10"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ],
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "license": "MIT"
+}

+ 8 - 0
postcss.config.js

@@ -0,0 +1,8 @@
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+  'plugins': {
+    // to edit target browsers: use "browserslist" field in package.json
+    'autoprefixer': {}
+  }
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 11 - 0
src/App.vue

@@ -0,0 +1,11 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+</script>

+ 50 - 0
src/api/aboutChannel.js

@@ -0,0 +1,50 @@
+import request from '@/utils/request'
+
+/* ----------关于渠道---------*/
+
+// 切换渠道
+export function changeChannel(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannel/change.do',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道-下拉菜单选项
+export function channelOpt() {
+  return request({
+    url: '/gameChannel/options.do',
+    method: 'post'
+  })
+}
+
+// 渠道下当前的服务器
+export function channelServerOpt(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannelServer/currRelation.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道大区下拉菜单
+export function gameRegionOpt(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameRegion/options.do',
+    method: 'post',
+    data: formData
+  })
+}

+ 104 - 0
src/api/aboutSanQi.js

@@ -0,0 +1,104 @@
+import request from '@/utils/request'
+
+// 37互娱相关
+
+// ---------------获取数据-----------------//
+
+// 37互娱白名单
+export function sanQiHuYuWhiteIp() {
+  return request({
+    url: '/sanQiHuYuWhiteIp/data.auth',
+    method: 'post'
+  })
+}
+
+// 37互娱发放礼包配置
+export function sanQiHuYuGiftCfg(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/sanQiHuYuGiftCfg/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 37互娱渠道参数设置
+export function sanQiHuYuSetting(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/sanQiHuYuSetting/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// -------------可新增、可编辑-------------//
+
+// 37互娱白名单
+export function saveSanQiHuYuWhiteIp(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/sanQiHuYuWhiteIp/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 37互娱渠道参数设置
+export function savaSanQiHuYuSetting(id) {
+  const formData = new FormData()
+  formData.append('platformId', id)
+  return request({
+    url: '/sanQiHuYuSetting/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 37互娱发放礼包配置
+export function saveSanQiHuYuGiftCfg(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/sanQiHuYuGiftCfg/save.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------删除------------------//
+
+// 37互娱白名单
+export function deleteSanQiWhiteIp(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/sanQiHuYuWhiteIp/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 37互娱发放礼包配置
+export function deleteSanQiHuYuGiftCfg(id) {
+  const formData = new FormData()
+  formData.append('id', id)
+  return request({
+    url: '/sanQiHuYuGiftCfg/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}

+ 24 - 0
src/api/aboutServer.js

@@ -0,0 +1,24 @@
+import request from '@/utils/request'
+
+/* ----------关于服务器---------*/
+
+// 服务器-下拉菜单选项
+// export function serverOpt(data) {
+//   const formData = new FormData()
+//   for (const key in data) {
+//     formData.append(key, data[key])
+//   }
+//   return request({
+//     url: '/gameServer/options.do',
+//     method: 'post',
+//     data: formData
+//   })
+// }
+
+// 根据当前渠道获取服务器下拉菜单数据
+export function multiServer() {
+  return request({
+    url: '/gameServer/optionsInCurrChannel.do',
+    method: 'post'
+  })
+}

+ 17 - 0
src/api/apidoc.js

@@ -0,0 +1,17 @@
+
+import request from '@/utils/request'
+
+// -----------接口说明模块---------//
+
+// 接口说明
+export function apidoc(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/apidoc/data.auth',
+    method: 'post',
+    data: formData
+  })
+}

+ 215 - 0
src/api/gameStatistics.js

@@ -0,0 +1,215 @@
+import request from '@/utils/request'
+
+// 游戏统计数据 模块
+
+// ---------------获取数据-----------------//
+
+// 付费LTV
+export function playerLtv(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerLtv/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 玩家留存
+export function playerRemain(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerRemain/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 付费留存
+export function playerPayRemain(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerPayRemain/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 注册分时
+export function playerTimeLog(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerTimeLog/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 在线人数
+export function playerOnlineLog(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerOnlineLog/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 在线时长占比
+export function onlineTimeRatio(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerScatterLog/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 游戏汇总
+export function gameSum(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/serverResume/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 货币消费统计
+export function consumeSum(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/consume/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 登录数据打点
+export function loginDot(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/loginDotData/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 任务数据打点
+export function taskDot(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/taskDotData/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 引导数据打点
+export function guideDot(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/guideDotData/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 招募数据打点
+export function recruitDot(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/recruitDotData/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 招募刷新数据打点
+export function recruitRefreshDot(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/recruitRefreshDotData/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// BOSS击杀数据打点
+export function bossDot(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/bossDotData/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 场景区域解锁数据打点
+export function fogDot(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/fogDotData/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ---------------其他-----------------//
+
+// 手动触发昨天LTV数据统计
+export function manualTriggerLTV(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerLtv/manual.auth',
+    method: 'post',
+    data: formData
+  })
+}

+ 128 - 0
src/api/mailSettings.js

@@ -0,0 +1,128 @@
+import request from '@/utils/request'
+
+// 系统邮件相关 模块
+
+// ---------------获取数据-----------------//
+
+// 玩家邮件
+export function playerMails(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerMail/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 系统邮件
+export function mailDatas(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/mail/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 系统邮件发送结果
+export function mailResult(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/mail/mailResult.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------可新增、可编辑------------------//
+
+// 系统邮件
+export function saveMail(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/mail/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------删除------------------//
+
+// 系统邮件
+export function deleteMail(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/mail/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 玩家邮件
+export function deletePlayerMail(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerMail/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ----------------------------------------//
+
+// 系统邮件-审核通过
+export function mailPass(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/mail/checkPass.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 系统邮件-审核不通过
+export function mailNotPass(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/mail/checkNotPass.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 系统邮件-重新发送
+export function mailResend(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/mail/resend.auth',
+    method: 'post',
+    data: formData
+  })
+}

+ 85 - 0
src/api/menuSetting.js

@@ -0,0 +1,85 @@
+import request from '@/utils/request'
+
+// 菜单设置 模块
+
+// ---------------获取数据-----------------//
+
+// 功能菜单
+export function functionMenu(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/functionmenu/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 系统菜单
+export function systemMenu(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/systemmenu/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 模块菜单
+export function moduleMenu(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/modulemenu/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// -----------------编辑-------------------//
+
+// 功能菜单
+export function updateFunctionMenu(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/functionmenu/update.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 系统菜单
+export function updateSystemMenu(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/systemmenu/update.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 模块菜单
+export function updateModuleMenu(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/modulemenu/update.authw',
+    method: 'post',
+    data: formData
+  })
+}

+ 221 - 0
src/api/playerManage.js

@@ -0,0 +1,221 @@
+import request from '@/utils/request'
+
+// 玩家管理 模块
+
+// ---------------获取数据-----------------//
+
+// 玩家信息管理
+export function playerInfo(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/player/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 白名单管理
+export function whiteList(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/whiteList/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 金手指名单
+export function godFinger(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/godFinger/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 封禁账号
+export function forbidList(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/forbidList/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------新增------------------//
+
+// 白名单管理
+export function AddwhiteList(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/whiteList/add.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 封禁账号
+export function addForbidList(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/forbidList/add.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// -------------可新增、可编辑-------------//
+
+// 金手指名单
+export function saveGodFinger(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/godFinger/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------删除------------------//
+
+//  白名单管理
+export function deletewhiteList(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/whiteList/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+//  金手指名单
+export function deleteGodFinger(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/godFinger/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+//  封禁账号
+export function deleteForbidListr(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/forbidList/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ---------------------------------------//
+
+// 玩家数据详情(玩家账号信息)
+export function playerDetail(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/player/detail.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 玩家英雄数据(伙伴信息)
+export function playerHero(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/player/hero.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 在线情况
+export function onlineSituation(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/player/detail.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 玩家背包
+export function playerBag(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/player/bag.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 封禁玩家
+export function blockPlayer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerCurrOnline/forbid.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 踢玩家下线
+export function KickoutPlayer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/playerCurrOnline/forcedOffLine.auth',
+    method: 'post',
+    data: formData
+  })
+}

+ 148 - 0
src/api/playerResourceLog.js

@@ -0,0 +1,148 @@
+import request from '@/utils/request'
+
+// 玩家流水查询 模块
+
+// ---------------获取数据-----------------//
+
+// 金币日志
+export function goldLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/goldLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 已删邮件日志
+export function mailLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/mailLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 矿日志
+export function mineLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/mineLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 肉日志
+export function meatLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/meatLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 钻石日志
+export function diamondLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/diamondLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 道具日志
+export function itemLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/itemLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 角色经验日志
+export function playerExpLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/playerExpLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 英雄日志
+export function heroLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/heroLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 装备日志
+export function equipLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/equipLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 木日志
+export function woodLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/woodLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 英雄战力日志
+export function heroFightingLogs(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamelog/heroFightingLogs.auth',
+    method: 'post',
+    data: formData
+  })
+}

+ 87 - 0
src/api/relatedRecharge.js

@@ -0,0 +1,87 @@
+import request from '@/utils/request'
+
+// 充值相关 模块
+
+// ---------------获取数据-----------------//
+
+// 充值回调查询
+export function recharge(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/recharge/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 充值排名
+export function rechargeRank(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/rechargeRank/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 充值商品项目统计
+export function rechargeStats(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/rechargeProductCount/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 虚拟充值(内部充值)
+export function rechargeVirtual(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/rechargeVirtual/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------新增------------------//
+
+// 发放-虚拟充值(内部充值)
+export function AddRechargeVirtual(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/rechargeVirtual/add.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------其他------------------//
+
+// 人工补单
+export function manualRecharge(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/recharge/manual.authw',
+    method: 'post',
+    data: formData
+  })
+}

+ 12 - 0
src/api/routersMenu.js

@@ -0,0 +1,12 @@
+
+import request from '@/utils/request'
+
+// -----路由菜单----- //
+
+export function getRouters() {
+  return request({
+    url: '/menu.do',
+    method: 'post'
+  })
+}
+

+ 421 - 0
src/api/serverManage.js

@@ -0,0 +1,421 @@
+import request from '@/utils/request'
+
+// 服务器管理 模块
+
+// -----------------获取数据------------------//
+
+// 服务器管理
+export function gameServer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameServer/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道与服务器关系
+export function gameChannelServer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannelServer/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道管理
+export function gameChannel(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannel/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道大区管理
+export function gameRegion(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameRegion/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 开服管理
+export function serverOpen(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/serverOpen/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道权限管理
+export function userChannel(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/userChannel/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 游戏公告管理
+export function gameBulletin(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/bulletin/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 平台管理
+export function gamePlatform(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamePlatform/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 服务器维护管理
+export function serverMaintain(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameServerMaintain/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 客户端版本管理
+export function clientVersion(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/clientVersion/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ----------------可新增、可编辑-------------------//
+
+// 服务器管理
+export function saveGameServer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameServer/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道管理
+export function saveGameChannel(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannel/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道大区管理
+export function saveGameRegion(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameRegion/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 游戏公告管理
+export function saveGameBulletin(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/bulletin/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 平台管理
+export function savaGamePlatform(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gamePlatform/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 客户端版本管理
+export function savaClientVersion(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/clientVersion/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// -----------------新增----------------------//
+
+// 渠道与服务器关系
+export function addGameChannelServer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannelServer/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------编辑---------------------//
+
+// 渠道与服务器关系
+export function editGameChannelServer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannelServer/saveToRegion.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道权限管理
+export function editUserChannel(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/userChannel/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// -------------------删除--------------------//
+
+// 渠道大区管理
+export function delGameRegion(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameRegion/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 渠道管理
+export function delGameChannel(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameChannel/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 游戏公告管理
+export function delGameBulletin(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/bulletin/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 客户端版本管理
+export function delClientVersion(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/clientVersion/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// -------------------------------------------//
+
+// 设置服务器预期开服时间
+export function setOpenTime(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/serverOpen/editOpenTime.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 立刻开服
+export function nowOpenServer(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/serverOpen/openNow.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 可授权得渠道数据下拉菜单
+export function accreditOptions(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/userChannel/options.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 公告下拉菜单
+export function bulletinOptions() {
+  return request({
+    url: '/bulletin/options.do',
+    method: 'post'
+  })
+}
+
+// 平台管理下拉菜单
+export function platformOptions() {
+  return request({
+    url: '/gamePlatform/options.do',
+    method: 'post'
+  })
+}
+
+// 进行服务器维护
+export function conductMaintain(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameServerMaintain/maintain.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 重新发送进行服务器维护
+export function resendMaintain(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameServerMaintain/resendMaintain.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 结束服务器维护
+export function endMaintain(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameServerMaintain/endMaintain.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 重新发送结束服务器维护
+export function resendEndMaintain(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameServerMaintain/resendEndMaintain.authw',
+    method: 'post',
+    data: formData
+  })
+}

+ 252 - 0
src/api/specialOperation.js

@@ -0,0 +1,252 @@
+import request from '@/utils/request'
+
+// 特殊操作 模块
+
+// ---------------获取数据-----------------//
+
+// 热更代码
+export function fixCode(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/fixCode/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 热更代码结果
+export function fixCodeResult(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/fixCodeResult/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 热更配置
+export function fixDesign(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/fixDesign/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 热更配置结果
+export function fixDesignResult(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/fixDesignResult/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 流水事件
+export function waterEvent(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/gameCause/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 道具配置
+export function propsConfig(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/itemCfg/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 兑换码生成
+export function giftCode(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/giftCode/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 通用兑换码日志
+export function giftCodeShareLog(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/giftCodeShareLog/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 兑换码礼包配置
+export function giftCodeCfg(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/giftCodeCfg/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 客户端日志
+export function clientLog(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/clientLog/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 充值商品配置
+export function rechargeProductCfg(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/rechargeProductCfg/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------- 新增 -----------------//
+
+// 礼包码
+export function saveGiftCode(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/giftCode/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// --------------------------------------
+
+// 更新流水事件
+export function updateWaterEvent() {
+  return request({
+    url: '/gameCause/refresh.auth',
+    method: 'post'
+  })
+}
+
+// 流水事件下拉数据
+export function waterEventOpt() {
+  return request({
+    url: '/gameCause/options.do',
+    method: 'post'
+  })
+}
+
+// 更新游戏道具数据
+export function updateGameProps() {
+  return request({
+    url: '/itemCfg/refresh.auth',
+    method: 'post'
+  })
+}
+
+// 道具下拉菜单数据
+export function propsOpt() {
+  return request({
+    url: '/itemCfg/options.do',
+    method: 'post'
+  })
+}
+
+// 重发热更配置
+export function resendFixDesign(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/fixDesign/resend.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 重发热更代码
+export function resendFixCode(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/fixCode/resend.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 兑换码配置下拉菜单数据
+export function codeCfgOpt() {
+  return request({
+    url: '/giftCodeCfg/options.do',
+    method: 'post'
+  })
+}
+
+// 更新兑换码配置数据
+export function updateCodeCfg() {
+  return request({
+    url: '/giftCodeCfg/refresh.auth',
+    method: 'post'
+  })
+}
+
+// 更新充值商品配置数据
+export function updateRechargeProductCfg(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/rechargeProductCfg/refresh.auth',
+    method: 'post',
+    data: formData
+  })
+}

+ 30 - 0
src/api/user.js

@@ -0,0 +1,30 @@
+
+import request from '@/utils/request'
+
+// -----用户相关----- //
+
+// 登录
+export function login(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/login.gate',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 修改密码
+export function changePasssword(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/changePasssword.do',
+    method: 'post',
+    data: formData
+  })
+}

+ 100 - 0
src/api/userSetting.js

@@ -0,0 +1,100 @@
+import request from '@/utils/request'
+
+// -----------用户设置模块-----------//
+
+// 用户管理
+export function userManagement(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/user/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 用户权限管理
+export function userRight(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/userfunction/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 用户操作日志
+export function userLog(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/userlog/data.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------新增or编辑------------------//
+
+// 用户管理
+export function saveUser(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/user/save.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ------------------删除------------------//
+
+// 用户管理
+export function deleteUser(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/user/delete.authw',
+    method: 'post',
+    data: formData
+  })
+}
+
+// ---------------------------------------
+
+// 权限菜单下拉数据
+export function funcOptions(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/userfunction/options.auth',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 用户权限设置
+export function saveUserRight(data) {
+  const formData = new FormData()
+  for (const key in data) {
+    formData.append(key, data[key])
+  }
+  return request({
+    url: '/userfunction/save.authw',
+    method: 'post',
+    data: formData
+  })
+}

BIN
src/assets/404_images/404.png


BIN
src/assets/404_images/404_cloud.png


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
src/assets/500_images/500.svg


+ 62 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>

+ 87 - 0
src/components/multipleDialog/index.vue

@@ -0,0 +1,87 @@
+<!-- 多选对话框 -->
+<template>
+  <div>
+    <el-dialog
+      title="选择服务器"
+      :visible="visible"
+      width="48%"
+      :modal-append-to-body="false"
+      center
+      @close="handleCloseDialog"
+    >
+      <el-button-group>
+        <el-button
+          size="mini"
+          plain
+          type="primary"
+          @click="handleSelectAll"
+        >全选</el-button>
+        <el-button
+          size="mini"
+          plain
+          type="primary"
+          @click="handleDeselectAll"
+        >全不选</el-button>
+      </el-button-group>
+
+      <div style="margin: 30px 0" />
+
+      <el-checkbox-group v-model="checkedServer" @change="handleCheckedId">
+        <el-checkbox
+          v-for="item in serverOptions"
+          :key="item.value"
+          :label="item.value"
+          :vlaue="item.value"
+        >{{ item.text }}</el-checkbox>
+      </el-checkbox-group>
+      <template #footer>
+        <el-button type="primary" size="small" @click="handleConfirm">确 认</el-button>
+        <el-button size="small" @click="handleCloseDialog">取 消</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    visible: Boolean,
+    serverOptions: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      checkedServer: [] // 选中的服务器
+    }
+  },
+  methods: {
+    // 全选
+    handleSelectAll() {
+      this.checkedServer = this.serverOptions.map((item) => item.value)
+    },
+
+    // 全不选
+    handleDeselectAll() {
+      this.checkedServer = []
+    },
+
+    // 选中的服务器ID
+    handleCheckedId(val) {
+      this.checkedServer = val
+    },
+
+    // 确认按钮
+    handleConfirm() {
+      this.$emit('checkedId', this.checkedServer)
+    },
+
+    // 关闭弹窗
+    handleCloseDialog() {
+      this.$emit('update:visible', false)
+    }
+  }
+
+}
+</script>

+ 54 - 0
src/components/radioDialog/index.vue

@@ -0,0 +1,54 @@
+<!-- 单选对话框 -->
+<template>
+  <div>
+    <el-dialog
+      :visible="visible"
+      title="选择服务器"
+      width="48%"
+      :modal-append-to-body="false"
+      center
+      @close="closeDialog"
+    >
+      <el-radio-group v-model="checkedServer">
+        <el-radio
+          v-for="item in serverOptions"
+          :key="item.value"
+          :label="item.value"
+          :vlaue="item.value"
+        >{{ item.text }}</el-radio>
+      </el-radio-group>
+      <span slot="footer">
+        <el-button type="primary" size="small" @click="serverConfirm">确 认</el-button>
+        <el-button size="small" @click="closeDialog">取 消</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    visible: Boolean,
+    serverOptions: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      checkedServer: '' // 选中的服务器
+    }
+  },
+  methods: {
+    // 确认按钮
+    serverConfirm() {
+      this.$emit('checkedId', this.checkedServer)
+      this.closeDialog()
+    },
+    // 关闭弹窗
+    closeDialog() {
+      this.$emit('update:visible', false)
+    }
+  }
+}
+</script>

+ 9 - 0
src/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from '@/components/SvgIcon'// svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon)
+
+const req = require.context('./svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+requireAll(req)

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
src/icons/svg/dashboard.svg


+ 1 - 0
src/icons/svg/example.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

+ 1 - 0
src/icons/svg/eye-open.svg

@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

+ 1 - 0
src/icons/svg/eye.svg

@@ -0,0 +1 @@
+<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
src/icons/svg/form.svg


+ 1 - 0
src/icons/svg/link.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>

+ 1 - 0
src/icons/svg/nested.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>

+ 1 - 0
src/icons/svg/password.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

+ 1 - 0
src/icons/svg/table.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>

+ 1 - 0
src/icons/svg/tree.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>

+ 1 - 0
src/icons/svg/user.svg

@@ -0,0 +1 @@
+<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

+ 22 - 0
src/icons/svgo.yml

@@ -0,0 +1,22 @@
+# replace default config
+
+# multipass: true
+# full: true
+
+plugins:
+
+  # - name
+  #
+  # or:
+  # - name: false
+  # - name: true
+  #
+  # or:
+  # - name:
+  #     param1: 1
+  #     param2: 2
+
+- removeAttrs:
+    attrs:
+      - 'fill'
+      - 'fill-rule'

+ 44 - 0
src/layout/components/AppMain.vue

@@ -0,0 +1,44 @@
+<template>
+  <section class="app-main">
+    <keep-alive>
+      <router-view :key="key" />
+    </keep-alive>
+
+    <!-- <transition name="fade-transform" mode="out-in">
+      <router-view :key="key" />
+    </transition> -->
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'AppMain',
+  computed: {
+    key() {
+      return this.$route.path
+    }
+  }
+}
+</script>
+
+<style scoped>
+.app-main {
+  /*50 = navbar  */
+  min-height: calc(100vh - 50px);
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+}
+.fixed-header+.app-main {
+  padding-top: 50px;
+}
+</style>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 15px;
+  }
+}
+</style>

+ 274 - 0
src/layout/components/Navbar.vue

@@ -0,0 +1,274 @@
+<template>
+  <div>
+    <div class="navbar">
+      <!-- 切换渠道 -->
+      <el-dropdown trigger="click">
+        <el-button type="text" class="el-dropdown-link">
+          切换渠道<i class="el-icon-arrow-down el-icon--right" />
+        </el-button>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item v-for="(item,index) in channelOption" :key="index" @click.native="channelSelect(item)">{{ item.text }}</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+
+      <div class="selected-channel">{{ channelName }}</div>
+
+      <!-- 用户名及退出 -->
+      <el-dropdown trigger="click">
+        <el-button type="text" class="user-action">
+          <i class="el-icon-user-solid" />
+          <span>{{ getUsername() }}</span>
+          <i class="el-icon-arrow-down el-icon--right" />
+        </el-button>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item @click.native="dialogVisible = true">修改密码</el-dropdown-item>
+          <el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+
+    <!-- 弹窗 -->
+    <div class="dialog">
+      <el-dialog title="修改密码" :visible.sync="dialogVisible" width="30%" center @close="closeDialog">
+        <el-form ref="ruleForm" :model="ruleForm" :rules="rules" status-icon label-width="120px" class="demo-ruleForm">
+          <el-form-item label="旧密码" prop="oldPassword">
+            <el-input v-model="ruleForm.oldPassword" type="password" />
+          </el-form-item>
+          <el-form-item label="新密码" prop="newPassword">
+            <el-input v-model="ruleForm.newPassword" type="password" />
+          </el-form-item>
+          <el-form-item label="确认密码" prop="confirmPassword">
+            <el-input v-model="ruleForm.confirmPassword" type="password" placeholder="确认密码与新密码保持一致" />
+          </el-form-item>
+        </el-form>
+        <span slot="footer" class="dialog-footer">
+          <el-button size="medium" type="primary" @click="confirmBtn">确 定</el-button>
+          <el-button size="medium" @click="closeDialog">取 消</el-button>
+        </span>
+      </el-dialog>
+    </div>
+
+  </div>
+
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { channelOpt, changeChannel } from '@/api/aboutChannel'
+import { Base64 } from 'js-base64'
+import { changePasssword } from '@/api/user'
+
+export default {
+  data() {
+    return {
+      channel: '', // 渠道
+      channelName: '',
+      channelOption: [], // 渠道选项
+      dialogVisible: false,
+      ruleForm: {
+        oldPassword: '',
+        newPassword: '',
+        confirmPassword: ''
+      },
+      // 校验条件
+      rules: {
+        oldPassword: [{ validator: this.validateOldPass, trigger: ['blur', 'change'] }],
+        newPassword: [{ validator: this.validateNewPass, trigger: ['blur', 'change'] }],
+        confirmPassword: [{ validator: this.validatePass, trigger: ['blur', 'change'] }]
+      }
+    }
+  },
+
+  computed: {
+    ...mapGetters([
+      'sidebar',
+      'avatar'
+    ])
+  },
+
+  created() {
+    this.getChannel().then(() => {
+      if (this.channelOption.length > 0) {
+        this.channel = localStorage.getItem('selectedChannel') || this.channelOption[0].value
+        this.channelName = this.channelOption.find(item => item.value == this.channel)?.text
+        changeChannel({ channelId: this.channel })
+      }
+    })
+  },
+
+  methods: {
+    // 获取用户名
+    getUsername() {
+      return localStorage.getItem('username')
+    },
+
+    // 退出-按钮
+    async logout() {
+      await this.$store.dispatch('user/logout')
+      this.$router.push('/login')
+
+      // 清除用户名及密码
+      localStorage.removeItem('username')
+      localStorage.removeItem('password')
+    },
+
+    // 校验旧密码
+    validateOldPass(rule, value, callback) {
+      if (value === '') {
+        callback(new Error('请输入旧密码'))
+      } else {
+        const oldPsw = Base64.decode(localStorage.getItem('password'))
+        if (this.ruleForm.oldPassword !== oldPsw) {
+          callback(new Error('当前输入的密码与旧密码不一致'))
+        }
+        callback()
+      }
+    },
+
+    // 校验新密码
+    validateNewPass(rule, value, callback) {
+      if (value === '') {
+        callback(new Error('请输入新密码'))
+      } else {
+        if (this.ruleForm.confirmPassword !== '') {
+          this.$refs.ruleForm.validateField('confirmPassword')
+        }
+        callback()
+      }
+    },
+
+    // 校验确认密码
+    validatePass(rule, value, callback) {
+      if (value === '') {
+        callback(new Error('请再次输入密码'))
+      } else if (value !== this.ruleForm.newPassword) {
+        callback(new Error('两次输入的密码不一致!'))
+      } else {
+        callback()
+      }
+    },
+
+    // 确认按钮
+    async confirmBtn() {
+      await this.$refs.ruleForm.validate(async(valid) => {
+        if (valid) {
+          const res = await changePasssword(this.ruleForm)
+          if (res.data.code === 0) {
+            this.logout()
+            this.$message({
+              type: 'success',
+              message: '密码修改成功,请使用新密码重新登录。',
+              duration: 4000
+            })
+          }
+          this.closeDialog()
+        }
+      })
+    },
+
+    // 关闭主弹窗
+    closeDialog() {
+      this.ruleForm = {
+        oldPassword: '',
+        newPassword: '',
+        confirmPassword: ''
+      }
+      this.$refs.ruleForm.clearValidate()
+      this.dialogVisible = false
+    },
+
+    // 渠道选择项
+    async getChannel() {
+      const res = await channelOpt()
+      this.channelOption = res.data.data.options
+    },
+
+    // 渠道选中
+    async channelSelect(item) {
+      this.channel = item.value
+      this.channelName = item.text
+      await changeChannel({ channelId: this.channel })
+
+      // 更新渠道名称并保存到 localStorage
+      this.channelName = this.channelOption.find(channel => channel.value == this.channel).text
+      localStorage.setItem('selectedChannel', this.channel)
+
+      location.reload() // 刷新整个页面
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.navbar {
+  display: flex;
+  height: 65px;
+  line-height:65px;
+}
+
+.dialog{
+  line-height: 0;
+}
+
+.user-action{
+ font-size: 15px;
+ color: #fff;
+ padding: 0 10px;
+ .el-icon--right{
+  font-size: 12px;
+  color: #d3dce6;
+ }
+ .el-icon-user-solid{
+  padding:5px;
+  color: #fff;
+  background-color:#d3dce67a;
+  border-radius: 50%;
+ }
+}
+
+.el-dropdown-link {
+   color: #409EFF;
+   font-size: 15px;
+   padding: 0 5px;
+   .el-icon--right{
+  font-size: 12px;
+}
+}
+
+.selected-channel{
+  color: #fff;
+  font-size: 15px;
+  padding: 0 15px ;
+}
+
+.el-dropdown-menu{
+  z-index: 3000 !important;
+  background:#23262f;
+  top: 54px !important;
+  border-radius: 3px;
+
+}
+
+.el-dropdown-menu__item{
+  color: #fff;
+  min-width: 140px;
+  &:hover{
+    background:#545c64;
+    border-radius: 0;
+  }
+}
+
+::v-deep .el-input__inner{
+  border-radius: 0 4px 4px 0;
+}
+</style>
+
+<style>
+/* 消除小三角 */
+.el-popper[x-placement^=bottom] .popper__arrow{
+  border: none;
+}
+.el-popper[x-placement^=bottom] .popper__arrow::after{
+  border: none;
+}
+</style>

+ 26 - 0
src/layout/components/Sidebar/FixiOSBug.js

@@ -0,0 +1,26 @@
+export default {
+  computed: {
+    device() {
+      return this.$store.state.app.device
+    }
+  },
+  mounted() {
+    // In order to fix the click on menu on the ios device will trigger the mouseleave bug
+    // https://github.com/PanJiaChen/vue-element-admin/issues/1135
+    this.fixBugIniOS()
+  },
+  methods: {
+    fixBugIniOS() {
+      const $subMenu = this.$refs.subMenu
+      if ($subMenu) {
+        const handleMouseleave = $subMenu.handleMouseleave
+        $subMenu.handleMouseleave = (e) => {
+          if (this.device === 'mobile') {
+            return
+          }
+          handleMouseleave(e)
+        }
+      }
+    }
+  }
+}

+ 41 - 0
src/layout/components/Sidebar/Item.vue

@@ -0,0 +1,41 @@
+<script>
+export default {
+  name: 'MenuItem',
+  functional: true,
+  props: {
+    icon: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+  render(h, context) {
+    const { icon, title } = context.props
+    const vnodes = []
+
+    if (icon) {
+      if (icon.includes('el-icon')) {
+        vnodes.push(<i class={[icon, 'sub-el-icon']} />)
+      } else {
+        vnodes.push(<svg-icon icon-class={icon}/>)
+      }
+    }
+
+    if (title) {
+      vnodes.push(<span slot='title'>{(title)}</span>)
+    }
+    return vnodes
+  }
+}
+</script>
+
+<style scoped>
+.sub-el-icon {
+  color: currentColor;
+  width: 1em;
+  height: 1em;
+}
+</style>

+ 43 - 0
src/layout/components/Sidebar/Link.vue

@@ -0,0 +1,43 @@
+<template>
+  <component :is="type" v-bind="linkProps(to)">
+    <slot />
+  </component>
+</template>
+
+<script>
+import { isExternal } from '@/utils/validate'
+
+export default {
+  props: {
+    to: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.to)
+    },
+    type() {
+      if (this.isExternal) {
+        return 'a'
+      }
+      return 'router-link'
+    }
+  },
+  methods: {
+    linkProps(to) {
+      if (this.isExternal) {
+        return {
+          href: to,
+          target: '_blank',
+          rel: 'noopener'
+        }
+      }
+      return {
+        to: to
+      }
+    }
+  }
+}
+</script>

+ 53 - 0
src/layout/components/Sidebar/Logo.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="sidebar-logo-container">
+    <router-link class="sidebar-logo-link" to="/">
+      <h1 class="sidebar-title">{{ title }} </h1>
+    </router-link>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SidebarLogo',
+  props: {
+    collapse: {
+      type: Boolean,
+      required: true
+    }
+  },
+  data() {
+    return {
+      title: 'X1管理后台'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+.sidebar-logo-container {
+  position: relative;
+  width: 100%;
+  height: 65px;
+  line-height: 65px;
+  background: #23262f;
+  text-align: center;
+  overflow: hidden;
+
+  & .sidebar-logo-link {
+    height: 100%;
+    width: 100%;
+
+    & .sidebar-title {
+      display: inline-block;
+      margin: 0;
+      color: #409EFF;
+      line-height: 50px;
+      font-size: 24px;
+      // font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+      vertical-align: middle;
+    }
+  }
+
+}
+</style>

+ 105 - 0
src/layout/components/Sidebar/SidebarItem.vue

@@ -0,0 +1,105 @@
+<template>
+  <div v-if="!item.hidden">
+    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
+      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
+        <el-menu-item
+          :index="resolvePath(onlyOneChild.path)"
+          :class="{'submenu-title-noDropdown':!isNest}"
+        >
+          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
+        </el-menu-item>
+      </app-link>
+    </template>
+
+    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
+      <template slot="title">
+        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
+      </template>
+      <sidebar-item
+        v-for="(child,index) in item.children"
+        :key="index"
+        :is-nest="true"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+        class="nest-menu"
+      />
+    </el-submenu>
+  </div>
+</template>
+
+<script>
+import path from 'path-browserify'
+import { isExternal } from '@/utils/validate'
+import Item from './Item'
+import AppLink from './Link'
+import FixiOSBug from './FixiOSBug'
+
+export default {
+  name: 'SidebarItem',
+  components: { Item, AppLink },
+  mixins: [FixiOSBug],
+  props: {
+    // route object
+    item: {
+      type: Object,
+      required: true
+    },
+    isNest: {
+      type: Boolean,
+      default: false
+    },
+    basePath: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
+    // TODO: refactor with render function
+    this.onlyOneChild = null
+    return {}
+  },
+  methods: {
+
+    hasOneShowingChild(children = [], parent) {
+      const showingChildren = children.filter(item => {
+        if (item.hidden) {
+          return false
+        } else {
+          // Temp set(will be used if only has one showing child)
+          // 临时设置(将在只有一个显示子节点时使用)
+          this.onlyOneChild = item
+          return true
+        }
+      })
+
+      // When there is only one child router, the child router is displayed by default
+      // 当只有一个子路由器时,默认显示该子路由器
+      if (showingChildren.length === 1) {
+        return true
+      }
+
+      // Show parent if there are no child router to display
+      // 如果没有要显示的子路由器,则显示父路由器
+      if (showingChildren.length === 0) {
+        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
+        return true
+      }
+
+      return false
+    },
+    resolvePath(routePath) {
+      if (isExternal(routePath)) {
+        return routePath
+      }
+      if (isExternal(this.basePath)) {
+        return this.basePath
+      }
+
+      // 完整路径 = 父级路径(/level/level_3) + 路由路径
+      const fullPath = path.resolve(this.basePath, routePath)
+      return fullPath
+    }
+  }
+}
+</script>

+ 71 - 0
src/layout/components/Sidebar/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <div :class="{'has-logo':showLogo}">
+    <logo v-if="showLogo" :collapse="isCollapse" />
+    <el-scrollbar wrap-class="scrollbar-wrapper">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="false"
+        :background-color="variables.menuBg"
+        :text-color="variables.menuText"
+        :unique-opened="false"
+        :active-text-color="variables.menuActiveText"
+        :collapse-transition="false"
+        mode="vertical"
+      >
+        <sidebar-item
+          v-for="(route,index) in routes"
+          :key="index"
+          :item="route"
+          :base-path="basePath + '/' + route.path"
+        />
+      </el-menu>
+    </el-scrollbar>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Logo from './Logo'
+import SidebarItem from './SidebarItem'
+import variables from '@/styles/variables.scss'
+
+export default {
+  components: { SidebarItem, Logo },
+  props: {
+    children: {
+      type: Array,
+      default: () => ([])
+    },
+    basePath: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'sidebar'
+    ]),
+    routes() {
+      return this.children
+    },
+    activeMenu() {
+      const route = this.$route
+      const { meta, path } = route
+      // if set path, the sidebar will highlight the path you set
+      if (meta.activeMenu) {
+        return meta.activeMenu
+      }
+      return path
+    },
+    showLogo() {
+      return this.$store.state.settings.sidebarLogo
+    },
+    variables() {
+      return variables
+    },
+    isCollapse() {
+      return !this.sidebar.opened
+    }
+  }
+}
+</script>

+ 110 - 0
src/layout/components/TagsView/ScrollPane.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="scroll-container">
+    <div ref="wrap" class="scroll-wrap" @wheel.native.prevent="handleScroll">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'ScrollPane',
+  data() {
+    return {
+      left: 0
+    }
+  },
+  computed: {
+    scrollWrapper() {
+      return this.$refs.wrap
+    }
+  },
+  mounted() {
+    this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
+  },
+  beforeDestroy() {
+    this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
+  },
+  methods: {
+    handleScroll(e) {
+      const eventDelta = e.wheelDelta || -e.deltaY * 40
+      const $scrollWrapper = this.scrollWrapper
+      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+    },
+    emitScroll() {
+      this.$emit('scroll')
+    },
+    moveToTarget(currentTag) {
+      const $scrollWrapper = this.scrollWrapper
+      const containerWidth = $scrollWrapper.offsetWidth // 获取滚动容器的宽度
+      const tagList = Array.from(this.$parent.$refs.tag)
+
+      const tagAndTagSpacing = 4 // 标签之间的间距
+      let firstTag = null
+      let lastTag = null
+
+      if (tagList.length > 0) {
+        firstTag = tagList[0]// 获取第一个标签
+        lastTag = tagList[tagList.length - 1] // 获取最后一个标签
+      }
+
+      if (firstTag === currentTag) {
+        $scrollWrapper.scrollLeft = 0
+      } else if (lastTag === currentTag) {
+        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - containerWidth
+      } else {
+        // 获取当前标签的上一个跟下一个标签
+        const currentIndex = tagList.findIndex(item => item === currentTag)
+        const prevTag = tagList[currentIndex - 1]
+        const nextTag = tagList[currentIndex + 1]
+
+        // 标签的左偏移在nextTag之后
+        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
+        // 标签的左偏移在prevTag之前
+        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
+
+        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + containerWidth) {
+          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - containerWidth
+        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+
+  .scroll-wrap{
+    width: 100%;
+    height: 40px;
+    overflow-x: auto;
+
+    &::-webkit-scrollbar{
+      display: none;
+    }
+
+    &:hover::-webkit-scrollbar{
+      display: block;
+      height: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb{
+     border-radius: 5px;
+     background-color: #c7c9cca9;
+    }
+
+    &::-webkit-scrollbar-track{
+     border-radius: 0px;
+    }
+  }
+}
+</style>
+

+ 307 - 0
src/layout/components/TagsView/index.vue

@@ -0,0 +1,307 @@
+<template>
+  <div id="tags-view-container" class="tags-view-container">
+    <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
+      <router-link
+        v-for="tag in visitedViews"
+        ref="tag"
+        :key="tag.path"
+        :class="isActive(tag)?'active':''"
+        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
+        tag="span"
+        class="tags-view-item"
+        @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
+        @contextmenu.prevent.native="openMenu(tag,$event)"
+      >
+        {{ tag.title }}
+        <div v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
+      </router-link>
+    </scroll-pane>
+    <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
+      <!-- <li @click="refreshSelectedTag(selectedTag)">刷新</li> -->
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭当前</li>
+      <li @click="closeOthersTags">关闭其他</li>
+      <li @click="closeAllTags(selectedTag)">关闭全部</li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import ScrollPane from './ScrollPane'
+import path from 'path'
+
+export default {
+  components: { ScrollPane },
+  data() {
+    return {
+      visible: false,
+      top: 0,
+      left: 0,
+      selectedTag: {},
+      affixTags: []
+    }
+  },
+  computed: {
+    visitedViews() {
+      return this.$store.state.tagsView.visitedViews
+    },
+    routes() {
+      return this.$store.state.permission.routes
+    }
+  },
+  watch: {
+    $route() {
+      this.addTags()
+      this.moveToCurrentTag()
+    },
+    visible(value) {
+      if (value) {
+        document.body.addEventListener('click', this.closeMenu)
+      } else {
+        document.body.removeEventListener('click', this.closeMenu)
+      }
+    }
+  },
+  mounted() {
+    // this.initTags()
+    this.addTags()
+
+    // 新添加
+    // 先引入 this.$nextTick 方法
+    this.$nextTick(() => {
+      const scrollPane = this.$refs.scrollPane
+      scrollPane.scrollLeft = scrollPane.scrollWidth - scrollPane.offsetWidth
+    })
+  },
+  methods: {
+    isActive(route) {
+      return route.path === this.$route.path
+    },
+    isAffix(tag) {
+      return tag.meta && tag.meta.affix
+    },
+    filterAffixTags(routes, basePath = '/') {
+      let tags = []
+      routes.forEach(route => {
+        if (route.meta && route.meta.affix) {
+          const tagPath = path.resolve(basePath, route.path)
+          tags.push({
+            fullPath: tagPath,
+            path: tagPath,
+            name: route.name,
+            meta: { ...route.meta }
+          })
+        }
+        if (route.children) {
+          const tempTags = this.filterAffixTags(route.children, route.path)
+          if (tempTags.length >= 1) {
+            tags = [...tags, ...tempTags]
+          }
+        }
+      })
+      return tags
+    },
+    initTags() {
+      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
+      for (const tag of affixTags) {
+        // Must have tag name
+        if (tag.name) {
+          this.$store.dispatch('tagsView/addVisitedView', tag)
+        }
+      }
+    },
+    addTags() {
+      const { name } = this.$route
+      if (name) {
+        this.$store.dispatch('tagsView/addView', this.$route)
+      }
+      return false
+    },
+    moveToCurrentTag() {
+      const tags = this.$refs.tag
+      this.$nextTick(() => {
+        for (const tag of tags) {
+          if (tag.to.path === this.$route.path) {
+            this.$refs.scrollPane.moveToTarget(tag)
+            // when query is different then update
+            if (tag.to.fullPath !== this.$route.fullPath) {
+              this.$store.dispatch('tagsView/updateVisitedView', this.$route)
+            }
+            break
+          }
+        }
+      })
+    },
+    // refreshSelectedTag(view) {
+    //   this.$store.dispatch('tagsView/delCachedView', view).then(() => {
+    //     const { fullPath } = view
+    //     this.$nextTick(() => {
+    //       this.$router.replace({
+    //         path: '/redirect' + fullPath
+    //       })
+    //     })
+    //   })
+    // },
+    closeSelectedTag(view) {
+      this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
+        if (this.isActive(view)) {
+          this.toLastView(visitedViews, view)
+        }
+      })
+    },
+    closeOthersTags() {
+      this.$router.push(this.selectedTag)
+      this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
+        this.moveToCurrentTag()
+      })
+    },
+    closeAllTags(view) {
+      this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
+        if (this.affixTags.some(tag => tag.path === view.path)) {
+          return
+        }
+        this.toLastView(visitedViews, view)
+      })
+    },
+    toLastView(visitedViews, view) {
+      const latestView = visitedViews.slice(-1)[0]
+      if (latestView) {
+        this.$router.push(latestView.fullPath)
+      } else {
+        // now the default is to redirect to the home page if there is no tags-view,
+        // you can adjust it according to your needs.
+        if (view.name === 'Home') {
+          // to reload home page
+          this.$router.replace({ path: '/redirect' + view.fullPath })
+        } else {
+          this.$router.push('/')
+        }
+      }
+    },
+    openMenu(tag, e) {
+      const menuMinWidth = 105
+      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
+      const offsetWidth = this.$el.offsetWidth // container width
+      const maxLeft = offsetWidth - menuMinWidth // left boundary
+      const left = e.clientX - offsetLeft + 15 // 15: margin right
+
+      if (left > maxLeft) {
+        this.left = maxLeft
+      } else {
+        this.left = left
+      }
+
+      this.top = e.clientY - 50
+      this.visible = true
+      this.selectedTag = tag
+    },
+    closeMenu() {
+      this.visible = false
+    },
+    handleScroll() {
+      this.closeMenu()
+    }
+  }
+}
+</script>
+
+  <style lang="scss" scoped>
+  .tags-view-container {
+    z-index: 3000;
+    position: sticky;
+    height: 34px;
+    width: 100%;
+    background: #fff;
+    border-bottom: 1px solid #d8dce5;
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+    .tags-view-wrapper {
+      .tags-view-item {
+        display: inline-block;
+        position: relative;
+        cursor: pointer;
+        height: 26px;
+        line-height: 26px;
+        border: 1px solid #d8dce5;
+        color: #495060;
+        background: #fff;
+        padding: 0 8px;
+        font-size: 12px;
+        margin-left: 5px;
+        margin-top: 4px;
+        &:first-of-type {
+          margin-left: 15px;
+        }
+        &:last-of-type {
+          margin-right: 15px;
+        }
+        &.active {
+          background-color: #409eff;
+          color: #fff;
+          border-color: #409eff;
+          &::before {
+            content: '';
+            background: #fff;
+            display: inline-block;
+            width: 8px;
+            height: 8px;
+            border-radius: 50%;
+            position: relative;
+            margin-right: 2px;
+          }
+        }
+      }
+      //新添加
+      &.overflow-x-auto {
+        overflow-x: auto;
+      }
+    }
+    .contextmenu {
+      margin: 0;
+      background: #fff;
+      z-index: 3000;
+      position: absolute;
+      list-style-type: none;
+      padding: 5px 0;
+      border-radius: 4px;
+      font-size: 12px;
+      font-weight: 400;
+      color: #333;
+      box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
+      li {
+        margin: 0;
+        padding: 7px 16px;
+        cursor: pointer;
+        &:hover {
+          background: #eee;
+          color: #409eff;
+        }
+      }
+    }
+  }
+  </style>
+
+  <style lang="scss">
+  //reset element css of el-icon-close
+  .tags-view-wrapper {
+    .tags-view-item {
+      .el-icon-close {
+        width: 16px;
+        height: 16px;
+        vertical-align: 2px;
+        border-radius: 50%;
+        text-align: center;
+        transition: all .3s cubic-bezier(.645, .045, .355, 1);
+        transform-origin: 100% 50%;
+        &:before {
+          transform: scale(.6);
+          display: inline-block;
+          vertical-align: -3px;
+        }
+        &:hover {
+          background-color: #b4bccc;
+          color: #fff;
+        }
+      }
+    }
+  }
+  </style>
+

+ 3 - 0
src/layout/components/index.js

@@ -0,0 +1,3 @@
+export { default as Navbar } from './Navbar'
+export { default as Sidebar } from './Sidebar'
+export { default as AppMain } from './AppMain'

+ 134 - 0
src/layout/index.vue

@@ -0,0 +1,134 @@
+<template>
+  <div :class="classObj">
+    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+
+    <div class="myMenu">
+      <!-- 一级路由 -->
+      <div v-for="(item,j) in routeMeta" :key="j" class="menuIten" :class="{ active: index === j }" @click="activeMata(j)">{{ item.meta.title }}</div>
+
+      <!-- 用户名和退出按钮 -->
+      <navbar class="navbar" />
+    </div>
+
+    <!-- 二级路由 -->
+    <sidebar :children="routeMeta[index].children" :base-path="routeMeta[index].path" class="sidebar-container" />
+
+    <div class="main-container">
+      <div :class="{'fixed-header':fixedHeader}" />
+      <!-- 小标签 -->
+      <TagsView />
+      <app-main />
+    </div>
+  </div>
+</template>
+
+<script>
+import { Navbar, Sidebar, AppMain } from './components'
+import TagsView from './components/TagsView/index.vue'
+import ResizeMixin from './mixin/ResizeHandler'
+
+export default {
+  name: 'Layout',
+  components: {
+    Navbar,
+    Sidebar,
+    AppMain,
+    TagsView
+  },
+  mixins: [ResizeMixin],
+  data() {
+    return {
+      index: 0
+    }
+  },
+  computed: {
+    sidebar() {
+      return this.$store.state.app.sidebar
+    },
+    device() {
+      return this.$store.state.app.device
+    },
+
+    // 路由
+    routeMeta() {
+      const routerArr = this.$store.state.router.routes.filter((item) => {
+        return item.meta
+      })
+      return routerArr
+    },
+
+    fixedHeader() {
+      return this.$store.state.settings.fixedHeader
+    },
+    classObj() {
+      return {
+        // hideSidebar: !this.sidebar.opened,
+        // openSidebar: this.sidebar.opened,
+        withoutAnimation: this.sidebar.withoutAnimation,
+        mobile: this.device === 'mobile'
+      }
+    }
+  },
+  methods: {
+    handleClickOutside() {
+      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
+    },
+    activeMata(j) {
+      this.index = j
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import "~@/styles/mixin.scss";
+  @import "~@/styles/variables.scss";
+
+  // 一级路由
+  .myMenu{
+    z-index:3000;
+    position: relative;
+    background-color: #23262f;
+    display: flex;
+    height: 65px;
+    padding-left: 210px;
+
+    .menuIten{
+      color: #fff;
+      font-size: 15px;
+      line-height: 65px;
+      padding-left: 30px;
+      padding-right: 30px;
+   }
+
+     // 用户名和退出按钮
+    .navbar{
+     position: absolute;
+     right: 5px;
+    }
+  }
+
+  .active{
+      color: #409eff !important;
+      border-bottom: 3px solid #409eff;
+  }
+
+  .drawer-bg {
+    background: #000;
+    opacity: 0.3;
+    width: 100%;
+    top: 0;
+    height: 100%;
+    position: absolute;
+    z-index: 999;
+  }
+
+  .fixed-header {
+    position: fixed;
+    top: 0;
+    right: 0;
+    z-index: 9;
+    width: calc(100% - #{$sideBarWidth});
+    transition: width 0.28s;
+  }
+</style>

+ 45 - 0
src/layout/mixin/ResizeHandler.js

@@ -0,0 +1,45 @@
+import store from '@/store'
+
+const { body } = document
+const WIDTH = 992 // refer to Bootstrap's responsive design
+
+export default {
+  watch: {
+    $route(route) {
+      if (this.device === 'mobile' && this.sidebar.opened) {
+        store.dispatch('app/closeSideBar', { withoutAnimation: false })
+      }
+    }
+  },
+  beforeMount() {
+    window.addEventListener('resize', this.$_resizeHandler)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.$_resizeHandler)
+  },
+  mounted() {
+    const isMobile = this.$_isMobile()
+    if (isMobile) {
+      store.dispatch('app/toggleDevice', 'mobile')
+      store.dispatch('app/closeSideBar', { withoutAnimation: true })
+    }
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_isMobile() {
+      const rect = body.getBoundingClientRect()
+      return rect.width - 1 < WIDTH
+    },
+    $_resizeHandler() {
+      if (!document.hidden) {
+        const isMobile = this.$_isMobile()
+        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
+
+        if (isMobile) {
+          store.dispatch('app/closeSideBar', { withoutAnimation: true })
+        }
+      }
+    }
+  }
+}

+ 13 - 0
src/layout/secondaryLayout.vue

@@ -0,0 +1,13 @@
+<template>
+  <router-view />
+</template>
+
+<script>
+export default {
+  name: 'SecondaryLayout'
+}
+</script>
+
+<style>
+</style>
+

+ 71 - 0
src/main.js

@@ -0,0 +1,71 @@
+import Vue from 'vue'
+
+import 'normalize.css/normalize.css' // A modern alternative to CSS resets
+
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+// import locale from 'element-ui/lib/locale/lang/en' // lang i18n
+
+import '@/styles/index.scss' // global css
+
+import App from './App'
+import store from './store'
+import router from './router'
+
+import '@/icons' // icon
+import '@/permission' // permission control
+
+import 'lib-flexible/flexible'
+import * as echarts from 'echarts'
+
+import 'default-passive-events'
+
+// import VueSmoothScroll from 'vue2-smooth-scroll'// 隐藏所有滚动条
+
+// 防抖
+import { debounce } from 'lodash'
+Vue.prototype.$debounce = debounce
+
+import moment from 'moment'
+Vue.prototype.$moment = moment
+
+// 时间过滤器(获取到的该时间戳为毫秒)
+Vue.filter('formatTime', function(timestamp) {
+  return moment(timestamp).format('YYYY-MM-DD HH:mm:ss')
+})
+// 今日在线时长过滤器(获取到的该时间戳为秒)
+Vue.filter('formatDuration', function(timestamp) {
+  return moment.utc(timestamp * 1000).format('HH时mm分ss秒')
+})
+
+// 过滤器(获取到的该时间戳为秒)-天时分秒
+// Vue.filter('formatNumDays', function(timestamp) {
+//   const duration = moment.duration(timestamp, 'seconds')
+//   const day = duration.asDays()
+//   const hours = duration.hours()
+//   const minutes = duration.minutes()
+//   const seconds = duration.seconds()
+//   return Math.floor(day) + '天' + hours + '时' + minutes + '分' + seconds + '秒'
+// })
+
+Vue.prototype.$echarts = echarts
+
+// 全局修改默认配置,点击空白处不能关闭弹窗
+ElementUI.Dialog.props.closeOnClickModal.default = false
+// 全局修改默认配置,按下ESC不能关闭弹窗
+ElementUI.Dialog.props.closeOnPressEscape.default = false
+
+// Vue.use(VueSmoothScroll) // 隐藏所有滚动条
+
+ElementUI.Dialog.props.lockScroll.default = false// 弹出框的时候滚动条隐藏和出现导致页面抖动问题
+
+Vue.use(ElementUI)
+
+Vue.config.productionTip = false
+
+new Vue({
+  el: '#app',
+  router,
+  store,
+  render: h => h(App)
+})

+ 33 - 0
src/permission.js

@@ -0,0 +1,33 @@
+
+// 导航守卫
+
+import router from './router'
+import store from './store'
+
+router.beforeEach(async(to, from, next) => {
+  if (store.state.user.token) {
+    if (!store.state.router.routes.length && to.query.type !== 'error') {
+      await store.dispatch('router/GenerateRoutes')
+      next(to.path)
+    } else {
+      // 已登录状态
+      if (to.path === '/login') {
+        next('/')
+      } else {
+        next()
+      }
+    }
+  } else {
+    // 未登录状态
+    if (to.path === '/login' || to.path === '/404') {
+      next()
+    } else {
+      next('/login')
+    }
+  }
+})
+
+router.afterEach(() => {
+  // finish progress bar
+  // NProgress.done()
+})

+ 61 - 0
src/router/index.js

@@ -0,0 +1,61 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+
+Vue.use(Router)
+
+/* Layout */
+import Layout from '@/layout'
+
+// 公共路由
+export const constantRoutes = [
+
+  {
+    path: '/login',
+    component: () => import('@/views/login/index'),
+    hidden: true
+  },
+
+  {
+    path: '/404',
+    component: () => import('@/views/404'),
+    hidden: true
+  },
+
+  {
+    path: '/500',
+    component: () => import('@/views/500'),
+    hidden: true
+  },
+
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/home',
+    children: [
+      {
+        path: 'home',
+        component: () => import('@/views/home/index'),
+        name: 'Home',
+        meta: { title: '首页', affix: true }
+      }
+    ]
+  }
+
+  // 404 page must be placed at the end !!!
+  // { path: '*', redirect: '/404', hidden: true }
+]
+
+const createRouter = () => new Router({
+  // mode: 'history',
+  scrollBehavior: () => ({ y: 0 }),
+  routes: constantRoutes
+})
+
+const router = createRouter()
+
+export function resetRouter() {
+  const newRouter = createRouter()
+  router.matcher = newRouter.matcher // reset router
+}
+
+export default router

+ 16 - 0
src/settings.js

@@ -0,0 +1,16 @@
+module.exports = {
+
+  title: 'X1',
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether fix the header
+   */
+  fixedHeader: false,
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether show the logo in sidebar
+   */
+  sidebarLogo: true
+}

+ 9 - 0
src/store/getters.js

@@ -0,0 +1,9 @@
+const getters = {
+  sidebar: state => state.app.sidebar,
+  device: state => state.app.device,
+  token: state => state.user.token,
+  avatar: state => state.user.avatar,
+  name: state => state.user.name,
+  routes: state => state.router.routes
+}
+export default getters

+ 32 - 0
src/store/index.js

@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import getters from './getters'
+// import app from './modules/app'
+// import settings from './modules/settings'
+// import user from './modules/user'
+// import router from './modules/router'
+// import tags from './modules/tags'
+const modulesFiles = require.context('./modules', true, /\.js$/)
+
+const modules = modulesFiles.keys().reduce((modules, modulePath) => {
+  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
+  const value = modulesFiles(modulePath)
+  modules[moduleName] = value.default
+  return modules
+}, {})
+
+Vue.use(Vuex)
+
+const store = new Vuex.Store({
+  modules,
+  // : {
+  //   app,
+  //   settings,
+  //   user,
+  //   router,
+  //   tags
+  // },
+  getters
+})
+
+export default store

+ 48 - 0
src/store/modules/app.js

@@ -0,0 +1,48 @@
+import Cookies from 'js-cookie'
+
+const state = {
+  sidebar: {
+    opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+    withoutAnimation: false
+  },
+  device: 'desktop'
+}
+
+const mutations = {
+  TOGGLE_SIDEBAR: state => {
+    state.sidebar.opened = !state.sidebar.opened
+    state.sidebar.withoutAnimation = false
+    if (state.sidebar.opened) {
+      Cookies.set('sidebarStatus', 1)
+    } else {
+      Cookies.set('sidebarStatus', 0)
+    }
+  },
+  CLOSE_SIDEBAR: (state, withoutAnimation) => {
+    Cookies.set('sidebarStatus', 0)
+    state.sidebar.opened = false
+    state.sidebar.withoutAnimation = withoutAnimation
+  },
+  TOGGLE_DEVICE: (state, device) => {
+    state.device = device
+  }
+}
+
+const actions = {
+  toggleSideBar({ commit }) {
+    commit('TOGGLE_SIDEBAR')
+  },
+  closeSideBar({ commit }, { withoutAnimation }) {
+    commit('CLOSE_SIDEBAR', withoutAnimation)
+  },
+  toggleDevice({ commit }, device) {
+    commit('TOGGLE_DEVICE', device)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 73 - 0
src/store/modules/router.js

@@ -0,0 +1,73 @@
+import router, { constantRoutes } from '@/router'
+import { getRouters } from '@/api/routersMenu'
+import Layout from '@/layout'
+
+const state = {
+  routes: [],
+  addRoutes: [],
+  navbarIndex: localStorage.getItem('navbarIndex') || 0
+}
+const mutations = {
+  SET_ROUTES: (state, routes) => {
+    state.routes = state.routes.filter(item => item.path !== '*')
+    state.addRoutes = routes
+    // 拼接公共路由和动态路由
+    state.routes = constantRoutes.concat(routes)
+    router.addRoutes(routes)
+  },
+  SET_NAVBAR_INDEX(state, index) {
+    state.navbarIndex = index
+    localStorage.setItem('navbarIndex', index)
+  }
+}
+
+const actions = {
+
+  // 生成路由
+  GenerateRoutes({ commit }) {
+    return new Promise((resolve) => {
+      // 请求路由数据
+      getRouters().then((res) => {
+        const accessedRoutes = res.data.data.systemMenus
+        // console.log(24, accessedRoutes)
+        accessedRoutes.push({ path: '*', redirect: '/404', hidden: true })
+        commit('SET_ROUTES', handleRoutes(accessedRoutes))
+        resolve(accessedRoutes)
+      })
+    })
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+
+// 处理路由
+function handleRoutes(routeArr) {
+  return routeArr.map(route => {
+    if (route.children) {
+      if (route.component === 'Layout') {
+        route.component = Layout
+      } else if (route.component) {
+        route.component = loadLayout(route.component) // 整体使用才是懒加载
+      }
+      route.children = handleRoutes(route.children)
+    } else {
+      route.component = loadView(route.component) // (resolve) => require([`@/views${name}/${route.name}`], resolve)
+    }
+    return route
+  })
+}
+
+export const loadLayout = (view) => {
+  // 路由懒加载  === import 同  require 同义  都是路由懒加载
+  return (resolve) => require([`@/${view}`], resolve) // 整体使用才是懒加载
+}
+export const loadView = (view) => {
+  // 路由懒加载  === import 同  require 同义  都是路由懒加载
+  return (resolve) => require([`@/views/${view}`], resolve) // 整体使用才是懒加载
+}
+

+ 32 - 0
src/store/modules/settings.js

@@ -0,0 +1,32 @@
+import defaultSettings from '@/settings'
+
+const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
+
+const state = {
+  showSettings: showSettings,
+  fixedHeader: fixedHeader,
+  sidebarLogo: sidebarLogo
+}
+
+const mutations = {
+  CHANGE_SETTING: (state, { key, value }) => {
+    // eslint-disable-next-line no-prototype-builtins
+    if (state.hasOwnProperty(key)) {
+      state[key] = value
+    }
+  }
+}
+
+const actions = {
+  changeSetting({ commit }, data) {
+    commit('CHANGE_SETTING', data)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+

+ 163 - 0
src/store/modules/tagsView.js

@@ -0,0 +1,163 @@
+const state = {
+  visitedViews: [],
+  cachedViews: []
+}
+
+const mutations = {
+  ADD_VISITED_VIEW: (state, view) => {
+    if (state.visitedViews.some(v => v.path === view.path)) return
+    const Item = {
+      path: view.path,
+      fullPath: view.fullPath,
+      meta: view.meta,
+      name: view.name,
+      title: view.meta.title || '无标题'
+    }
+    state.visitedViews.push(Item)
+  },
+  ADD_CACHED_VIEW: (state, view) => {
+    if (state.cachedViews.includes(view.name)) return
+    if (!view.meta.noCache) {
+      state.cachedViews.push(view.name)
+    }
+  },
+
+  DEL_VISITED_VIEW: (state, view) => {
+    for (const [i, v] of state.visitedViews.entries()) {
+      if (v.path === view.path) {
+        state.visitedViews.splice(i, 1)
+        break
+      }
+    }
+  },
+  DEL_CACHED_VIEW: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    index > -1 && state.cachedViews.splice(index, 1)
+  },
+
+  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
+    state.visitedViews = state.visitedViews.filter(v => {
+      return v.meta.affix || v.path === view.path
+    })
+  },
+  DEL_OTHERS_CACHED_VIEWS: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    if (index > -1) {
+      state.cachedViews = state.cachedViews.slice(index, index + 1)
+    } else {
+      // if index = -1, there is no cached tags
+      state.cachedViews = []
+    }
+  },
+
+  DEL_ALL_VISITED_VIEWS: state => {
+    // keep affix tags
+    const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
+    state.visitedViews = affixTags
+  },
+  DEL_ALL_CACHED_VIEWS: state => {
+    state.cachedViews = []
+  },
+
+  UPDATE_VISITED_VIEW: (state, view) => {
+    for (let v of state.visitedViews) {
+      if (v.path === view.path) {
+        v = Object.assign(v, view)
+        break
+      }
+    }
+  }
+}
+
+const actions = {
+  addView({ dispatch }, view) {
+    dispatch('addVisitedView', view)
+    dispatch('addCachedView', view)
+  },
+  addVisitedView({ commit }, view) {
+    commit('ADD_VISITED_VIEW', view)
+  },
+  addCachedView({ commit }, view) {
+    commit('ADD_CACHED_VIEW', view)
+  },
+
+  delView({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delVisitedView', view)
+      dispatch('delCachedView', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delVisitedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_VISITED_VIEW', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delCachedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_CACHED_VIEW', view)
+      resolve([...state.cachedViews])
+    })
+  },
+
+  delOthersViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delOthersVisitedViews', view)
+      dispatch('delOthersCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delOthersVisitedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_VISITED_VIEWS', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delOthersCachedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_CACHED_VIEWS', view)
+      resolve([...state.cachedViews])
+    })
+  },
+
+  delAllViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delAllVisitedViews', view)
+      dispatch('delAllCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
+      })
+    })
+  },
+  delAllVisitedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_VISITED_VIEWS')
+      resolve([...state.visitedViews])
+    })
+  },
+  delAllCachedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_CACHED_VIEWS')
+      resolve([...state.cachedViews])
+    })
+  },
+
+  updateVisitedView({ commit }, view) {
+    commit('UPDATE_VISITED_VIEW', view)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

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

@@ -0,0 +1,36 @@
+import { login } from '@/api/user'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+
+export default {
+  namespaced: true,
+  state: {
+    token: getToken()
+  },
+
+  mutations: {
+    set_token(state, token) {
+      state.token = token
+      setToken(token)
+    },
+    remove_token(state) {
+      state.token = null
+      removeToken()
+    }
+  },
+
+  actions: {
+
+    // 登录
+    async Newlogin(store, data) {
+      const res = await login(data)
+      store.commit('set_token', res.data.data.token)
+    },
+
+    // 退出登录
+    logout(store) {
+      store.commit('remove_token')
+    }
+
+  }
+}
+

+ 96 - 0
src/styles/element-ui.scss

@@ -0,0 +1,96 @@
+// cover some element-ui styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+  font-weight: 400 !important;
+}
+
+.el-upload {
+  input[type="file"] {
+    display: none !important;
+  }
+}
+
+.el-upload__input {
+  display: none;
+}
+
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+  transform: none;
+  left: 0;
+  position: relative;
+  margin: 0 auto;
+}
+
+// refine element ui upload
+.upload-container {
+  .el-upload {
+    width: 100%;
+
+    .el-upload-dragger {
+      width: 100%;
+      height: 200px;
+    }
+  }
+}
+
+// dropdown
+.el-dropdown-menu {
+  a {
+    display: block
+  }
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+  box-sizing: content-box;
+}
+
+
+//---------自定义公共样式---- ----
+
+.el-table{
+  margin: 15px 0px;
+}
+
+//弹窗公共样式
+.el-form-item__label{
+  background-color: #f5f7fa;
+  border: 1px solid #dcdfe6;
+  border-right: 0px;
+  height: 40px;
+  text-align: center;
+  padding: 0px;
+  border-radius: 4px 0px 0px 4px;
+  color: #909399;
+}
+.el-dialog__header{
+  background-color: #409eff;
+}
+.el-dialog__title{
+  color: #fff;
+}
+.el-dialog__headerbtn .el-dialog__close {
+  color: #fff;
+  font-size: 20px;
+  &:hover{
+    color: #FFF;
+  }
+}
+.el-dialog--center .el-dialog__body{
+  padding:30px 30px 5px 30px;
+}
+
+.el-message{
+  z-index: 5000 !important;
+}
+
+.el-checkbox{
+  margin-bottom: 10px;
+}
+
+.el-radio{
+  margin-bottom: 10px;
+}

+ 78 - 0
src/styles/index.scss

@@ -0,0 +1,78 @@
+@import './variables.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+
+body {
+  height: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+// label {
+//   font-weight: 700;
+// }
+
+html {
+  height: 100%;
+  box-sizing: border-box;
+}
+
+#app {
+  height: 100%;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: inherit;
+}
+
+a:focus,
+a:active {
+  outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+  cursor: pointer;
+  color: inherit;
+  text-decoration: none;
+}
+
+div:focus {
+  outline: none;
+}
+
+.clearfix {
+  &:after {
+    visibility: hidden;
+    display: block;
+    font-size: 0;
+    content: " ";
+    clear: both;
+    height: 0;
+  }
+}
+
+// main-container global css
+.app-container {
+  padding: 20px;
+}
+
+// 隐藏所有滚动条
+// *{
+//   ::-webkit-scrollbar {
+//      display: none;
+//    }
+//  }
+
+
+// 隐藏浏览器右侧滚动条
+html,body {
+  overflow: hidden;
+}

+ 28 - 0
src/styles/mixin.scss

@@ -0,0 +1,28 @@
+@mixin clearfix {
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+}
+
+@mixin scrollBar {
+  &::-webkit-scrollbar-track-piece {
+    background: #d3dce6;
+  }
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #99a9bf;
+    border-radius: 20px;
+  }
+}
+
+@mixin relative {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}

+ 232 - 0
src/styles/sidebar.scss

@@ -0,0 +1,232 @@
+@import './variables.scss';
+#app {
+
+  .main-container {
+    min-height: 100%;
+    transition: margin-left .28s;
+    margin-left: $sideBarWidth;
+    position: relative;
+  }
+
+  .sidebar-container {
+    transition: width 0.28s;
+    width: $sideBarWidth !important;
+    background-color: $menuBg;
+    height: 100%;
+    position: fixed;
+    font-size: 0px;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1001;
+    overflow: hidden;
+
+    // reset element-ui css
+    .horizontal-collapse-transition {
+      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+    }
+
+    .scrollbar-wrapper {
+      overflow-x: hidden !important;
+    }
+
+    .el-scrollbar__bar.is-vertical {
+      right: 0px;
+    }
+
+    .el-scrollbar {
+      height: 100%;
+    }
+
+    &.has-logo {
+      z-index: 3001;
+      .el-scrollbar {
+        height: 100%;
+        padding-bottom: 90px;
+      }
+    
+      ;
+    }
+
+    .is-horizontal {
+      display: none;
+    }
+
+    a {
+      display: inline-block;
+      width: 100%;
+      overflow: hidden;
+    }
+
+    .svg-icon {
+      margin-right: 16px;
+    }
+
+    .sub-el-icon {
+      margin-right: 12px;
+      margin-left: -2px;
+    }
+
+    .el-menu {
+      border: none;
+      height: 100%;
+      width: 100% !important;
+
+    }
+
+    // menu hover
+    .submenu-title-noDropdown,
+    .el-submenu__title {
+      &:hover {
+        background-color: $menuHover !important;
+      }
+    }
+
+    .is-active>.el-submenu__title {
+      color: $subMenuActiveText !important;
+    }
+
+    & .nest-menu .el-submenu>.el-submenu__title,
+    & .el-submenu .el-menu-item {
+      min-width: $sideBarWidth !important;
+      background-color: $subMenuBg !important;
+
+      &:hover {
+        background-color: $subMenuHover !important;
+      }
+    }
+  }
+
+  .hideSidebar {
+    .sidebar-container {
+      width: 54px !important;
+    }
+
+    .main-container {
+      margin-left: 54px;
+    }
+
+    .submenu-title-noDropdown {
+      padding: 0 !important;
+      position: relative;
+
+      .el-tooltip {
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
+
+        .sub-el-icon {
+          margin-left: 19px;
+        }
+      }
+    }
+
+    .el-submenu {
+      overflow: hidden;
+
+      &>.el-submenu__title {
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
+
+        .sub-el-icon {
+          margin-left: 19px;
+        }
+
+        .el-submenu__icon-arrow {
+          display: none;
+        }
+      }
+    }
+
+    .el-menu--collapse {
+      .el-submenu {
+        &>.el-submenu__title {
+          &>span {
+            height: 0;
+            width: 0;
+            overflow: hidden;
+            visibility: hidden;
+            display: inline-block;
+          }
+        }
+      }
+    }
+  }
+
+  .el-menu--collapse .el-menu .el-submenu {
+    min-width: $sideBarWidth !important;
+  }
+
+  // mobile responsive
+  .mobile {
+    .main-container {
+      margin-left: 0px;
+    }
+
+    .sidebar-container {
+      transition: transform .28s;
+      width: $sideBarWidth !important;
+    }
+
+    &.hideSidebar {
+      .sidebar-container {
+        pointer-events: none;
+        transition-duration: 0.3s;
+        transform: translate3d(-$sideBarWidth, 0, 0);
+      }
+    }
+  }
+
+  .withoutAnimation {
+
+    .main-container,
+    .sidebar-container {
+      transition: none;
+    }
+  }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+  &>.el-menu {
+    .svg-icon {
+      margin-right: 16px;
+    }
+    .sub-el-icon {
+      margin-right: 12px;
+      margin-left: -2px;
+    }
+  }
+
+  .nest-menu .el-submenu>.el-submenu__title,
+  .el-menu-item {
+    &:hover {
+      // you can use $subMenuHover
+      background-color: $menuHover !important;
+    }
+  }
+
+  // the scroll bar appears when the subMenu is too long
+  >.el-menu--popup {
+    max-height: 100vh;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar-track-piece {
+      background: #d3dce6;
+    }
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #99a9bf;
+      border-radius: 20px;
+    }
+  }
+}

+ 48 - 0
src/styles/transition.scss

@@ -0,0 +1,48 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all .5s;
+}
+
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-move {
+  transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}

+ 26 - 0
src/styles/variables.scss

@@ -0,0 +1,26 @@
+// sidebar
+
+$menuText:#fff;
+$menuActiveText:#409EFF;
+$subMenuActiveText:#409EFF; //https://github.com/ElemeFE/element/issues/12951
+
+$menuBg:#304156;
+$menuHover:#263445;
+
+$subMenuBg:#1f2d3d;
+$subMenuHover:#5b697acb;
+
+$sideBarWidth: 210px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  subMenuActiveText: $subMenuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  subMenuBg: $subMenuBg;
+  subMenuHover: $subMenuHover;
+  sideBarWidth: $sideBarWidth;
+}

+ 174 - 0
src/utils/Blob.js

@@ -0,0 +1,174 @@
+/* eslint-disable */
+/* Blob.js
+ * A Blob implementation.
+ * 2014-05-27
+ *
+ * By Eli Grey, http://eligrey.com
+ * By Devin Samarin, https://github.com/eboyjr
+ * License: X11/MIT
+ *   See LICENSE.md
+ */
+
+/*global self, unescape */
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+ plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
+
+(function (view) {
+  view.URL = view.URL || view.webkitURL;
+
+  if (view.Blob && view.URL) {
+    try {
+      new Blob;
+      return;
+    } catch (e) {}
+  }
+
+  // Internally we use a BlobBuilder implementation to base Blob off of
+  // in order to support older browsers that only have BlobBuilder
+  var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function (view) {
+    var
+      get_class = function (object) {
+        return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
+      },
+      FakeBlobBuilder = function BlobBuilder() {
+        this.data = [];
+      },
+      FakeBlob = function Blob(data, type, encoding) {
+        this.data = data;
+        this.size = data.length;
+        this.type = type;
+        this.encoding = encoding;
+      },
+      FBB_proto = FakeBlobBuilder.prototype,
+      FB_proto = FakeBlob.prototype,
+      FileReaderSync = view.FileReaderSync,
+      FileException = function (type) {
+        this.code = this[this.name = type];
+      },
+      file_ex_codes = (
+        "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " +
+        "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
+      ).split(" "),
+      file_ex_code = file_ex_codes.length,
+      real_URL = view.URL || view.webkitURL || view,
+      real_create_object_URL = real_URL.createObjectURL,
+      real_revoke_object_URL = real_URL.revokeObjectURL,
+      URL = real_URL,
+      btoa = view.btoa,
+      atob = view.atob
+
+      ,
+      ArrayBuffer = view.ArrayBuffer,
+      Uint8Array = view.Uint8Array;
+    FakeBlob.fake = FB_proto.fake = true;
+    while (file_ex_code--) {
+      FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
+    }
+    if (!real_URL.createObjectURL) {
+      URL = view.URL = {};
+    }
+    URL.createObjectURL = function (blob) {
+      var
+        type = blob.type,
+        data_URI_header;
+      if (type === null) {
+        type = "application/octet-stream";
+      }
+      if (blob instanceof FakeBlob) {
+        data_URI_header = "data:" + type;
+        if (blob.encoding === "base64") {
+          return data_URI_header + ";base64," + blob.data;
+        } else if (blob.encoding === "URI") {
+          return data_URI_header + "," + decodeURIComponent(blob.data);
+        }
+        if (btoa) {
+          return data_URI_header + ";base64," + btoa(blob.data);
+        } else {
+          return data_URI_header + "," + encodeURIComponent(blob.data);
+        }
+      } else if (real_create_object_URL) {
+        return real_create_object_URL.call(real_URL, blob);
+      }
+    };
+    URL.revokeObjectURL = function (object_URL) {
+      if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
+        real_revoke_object_URL.call(real_URL, object_URL);
+      }
+    };
+    FBB_proto.append = function (data /*, endings*/ ) {
+      var bb = this.data;
+      // decode data to a binary string
+      if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
+        var
+          str = "",
+          buf = new Uint8Array(data),
+          i = 0,
+          buf_len = buf.length;
+        for (; i < buf_len; i++) {
+          str += String.fromCharCode(buf[i]);
+        }
+        bb.push(str);
+      } else if (get_class(data) === "Blob" || get_class(data) === "File") {
+        if (FileReaderSync) {
+          var fr = new FileReaderSync;
+          bb.push(fr.readAsBinaryString(data));
+        } else {
+          // async FileReader won't work as BlobBuilder is sync
+          throw new FileException("NOT_READABLE_ERR");
+        }
+      } else if (data instanceof FakeBlob) {
+        if (data.encoding === "base64" && atob) {
+          bb.push(atob(data.data));
+        } else if (data.encoding === "URI") {
+          bb.push(decodeURIComponent(data.data));
+        } else if (data.encoding === "raw") {
+          bb.push(data.data);
+        }
+      } else {
+        if (typeof data !== "string") {
+          data += ""; // convert unsupported types to strings
+        }
+        // decode UTF-16 to binary string
+        bb.push(unescape(encodeURIComponent(data)));
+      }
+    };
+    FBB_proto.getBlob = function (type) {
+      if (!arguments.length) {
+        type = null;
+      }
+      return new FakeBlob(this.data.join(""), type, "raw");
+    };
+    FBB_proto.toString = function () {
+      return "[object BlobBuilder]";
+    };
+    FB_proto.slice = function (start, end, type) {
+      var args = arguments.length;
+      if (args < 3) {
+        type = null;
+      }
+      return new FakeBlob(
+        this.data.slice(start, args > 1 ? end : this.data.length), type, this.encoding
+      );
+    };
+    FB_proto.toString = function () {
+      return "[object Blob]";
+    };
+    FB_proto.close = function () {
+      this.size = this.data.length = 0;
+    };
+    return FakeBlobBuilder;
+  }(view));
+
+  view.Blob = function Blob(blobParts, options) {
+    var type = options ? (options.type || "") : "";
+    var builder = new BlobBuilder();
+    if (blobParts) {
+      for (var i = 0, len = blobParts.length; i < len; i++) {
+        builder.append(blobParts[i]);
+      }
+    }
+    return builder.getBlob(type);
+  };
+}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

+ 15 - 0
src/utils/auth.js

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

+ 10 - 0
src/utils/get-page-title.js

@@ -0,0 +1,10 @@
+import defaultSettings from '@/settings'
+
+const title = defaultSettings.title || 'X1'
+
+export default function getPageTitle(pageTitle) {
+  if (pageTitle) {
+    return `${pageTitle} - ${title}`
+  }
+  return `${title}`
+}

+ 117 - 0
src/utils/index.js

@@ -0,0 +1,117 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * Parse the time to string
+ * @param {(Object|string|number)} time
+ * @param {string} cFormat
+ * @returns {string | null}
+ */
+export function parseTime(time, cFormat) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string')) {
+      if ((/^[0-9]+$/.test(time))) {
+        // support "1548221490638"
+        time = parseInt(time)
+      } else {
+        // support safari
+        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
+        time = time.replace(new RegExp(/-/gm), '/')
+      }
+    }
+
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+    const value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
+    return value.toString().padStart(2, '0')
+  })
+  return time_str
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+  if (('' + time).length === 10) {
+    time = parseInt(time) * 1000
+  } else {
+    time = +time
+  }
+  const d = new Date(time)
+  const now = Date.now()
+
+  const diff = (now - d) / 1000
+
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) {
+    // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return (
+      d.getMonth() +
+      1 +
+      '月' +
+      d.getDate() +
+      '日' +
+      d.getHours() +
+      '时' +
+      d.getMinutes() +
+      '分'
+    )
+  }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}

+ 61 - 0
src/utils/request.js

@@ -0,0 +1,61 @@
+import router from '@/router'
+import store from '@/store'
+import axios from 'axios'
+import { Message } from 'element-ui'
+export const baseApiUrl = process.env.VUE_APP_BASE_API
+
+const service = axios.create({
+  baseURL: baseApiUrl, // url = base url + request url
+  timeout: 5000 // request timeout
+})
+
+// 请求拦截器
+service.interceptors.request.use(
+  config => {
+    // 设置token
+    if (store.state.user.token) {
+      config.headers['token'] = store.state.user.token
+    }
+
+    // 设置请求头
+    if (config.method.toUpperCase() === 'post') {
+      config.headers['Content-Type'] = 'multipart/form-data'
+    }
+    return config
+  },
+  error => {
+    return Promise.reject(error)
+  }
+)
+
+// 响应拦截器
+service.interceptors.response.use(
+  response => {
+    if (typeof response.data !== 'object') {
+      store.dispatch('user/logout')
+      router.replace('/login?type=error') // 正常应该跳转错误页面 500
+      // router.replace('/500?type=error')
+    }
+
+    if (response.data.code !== 0) {
+      Message.error(response.data.message)
+    }
+
+    return response
+  },
+  err => {
+    // 给出提示
+    let message = err.message
+    if (message === 'Network Error') {
+      message = '后端接口连接异常'
+    } else if (message.includes('timeout')) {
+      message = '系统接口请求超时'
+    } else if (message.includes('Request failed with status code')) {
+      message = '系统接口' + message.substr(message.length - 3) + '异常'
+    }
+    Message.error(message)
+    return Promise.reject(err)
+  }
+)
+
+export default service

+ 21 - 0
src/utils/validate.js

@@ -0,0 +1,21 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+  const valid_map = ['admin', 'editor']
+  return valid_map.indexOf(str.trim()) >= 0
+}
+

+ 226 - 0
src/views/404.vue

@@ -0,0 +1,226 @@
+<template>
+  <div class="wscn-http404-container">
+    <div class="wscn-http404">
+      <div class="pic-404">
+        <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
+        <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
+        <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
+        <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
+      </div>
+      <div class="bullshit">
+        <div class="bullshit__oops">404错误!</div>
+        <div class="bullshit__headline">{{ message }}</div>
+        <div class="bullshit__info">
+          对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。</div>
+        <router-link to="/" class="bullshit__return-home">返回首页</router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'Page404',
+  computed: {
+    message() {
+      return '找不到网页...'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.wscn-http404-container{
+  transform: translate(-50%,-50%);
+  position: absolute;
+  top: 40%;
+  left: 50%;
+}
+.wscn-http404 {
+  position: relative;
+  width: 1200px;
+  padding: 0 50px;
+  overflow: hidden;
+  .pic-404 {
+    position: relative;
+    float: left;
+    width: 600px;
+    overflow: hidden;
+    &__parent {
+      width: 100%;
+    }
+    &__child {
+      position: absolute;
+      &.left {
+        width: 80px;
+        top: 17px;
+        left: 220px;
+        opacity: 0;
+        animation-name: cloudLeft;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1s;
+      }
+      &.mid {
+        width: 46px;
+        top: 10px;
+        left: 420px;
+        opacity: 0;
+        animation-name: cloudMid;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1.2s;
+      }
+      &.right {
+        width: 62px;
+        top: 100px;
+        left: 500px;
+        opacity: 0;
+        animation-name: cloudRight;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1s;
+      }
+      @keyframes cloudLeft {
+        0% {
+          top: 17px;
+          left: 220px;
+          opacity: 0;
+        }
+        20% {
+          top: 33px;
+          left: 188px;
+          opacity: 1;
+        }
+        80% {
+          top: 81px;
+          left: 92px;
+          opacity: 1;
+        }
+        100% {
+          top: 97px;
+          left: 60px;
+          opacity: 0;
+        }
+      }
+      @keyframes cloudMid {
+        0% {
+          top: 10px;
+          left: 420px;
+          opacity: 0;
+        }
+        20% {
+          top: 40px;
+          left: 360px;
+          opacity: 1;
+        }
+        70% {
+          top: 130px;
+          left: 180px;
+          opacity: 1;
+        }
+        100% {
+          top: 160px;
+          left: 120px;
+          opacity: 0;
+        }
+      }
+      @keyframes cloudRight {
+        0% {
+          top: 100px;
+          left: 500px;
+          opacity: 0;
+        }
+        20% {
+          top: 120px;
+          left: 460px;
+          opacity: 1;
+        }
+        80% {
+          top: 180px;
+          left: 340px;
+          opacity: 1;
+        }
+        100% {
+          top: 200px;
+          left: 300px;
+          opacity: 0;
+        }
+      }
+    }
+  }
+  .bullshit {
+    position: relative;
+    float: left;
+    width: 300px;
+    padding: 30px 0;
+    overflow: hidden;
+    &__oops {
+      font-size: 32px;
+      font-weight: bold;
+      line-height: 40px;
+      color: #1482f0;
+      opacity: 0;
+      margin-bottom: 20px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-fill-mode: forwards;
+    }
+    &__headline {
+      font-size: 20px;
+      line-height: 24px;
+      color: #222;
+      font-weight: bold;
+      opacity: 0;
+      margin-bottom: 10px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.1s;
+      animation-fill-mode: forwards;
+    }
+    &__info {
+      font-size: 13px;
+      line-height: 21px;
+      color: grey;
+      opacity: 0;
+      margin-bottom: 30px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.2s;
+      animation-fill-mode: forwards;
+    }
+    &__return-home {
+      display: block;
+      float: left;
+      width: 110px;
+      height: 36px;
+      background: #1482f0;
+      border-radius: 100px;
+      text-align: center;
+      color: #ffffff;
+      opacity: 0;
+      font-size: 14px;
+      line-height: 36px;
+      cursor: pointer;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.3s;
+      animation-fill-mode: forwards;
+    }
+    @keyframes slideUp {
+      0% {
+        transform: translateY(60px);
+        opacity: 0;
+      }
+      100% {
+        transform: translateY(0);
+        opacity: 1;
+      }
+    }
+  }
+}
+</style>

+ 137 - 0
src/views/500.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="wscn-http500-container">
+    <div class="wscn-http500">
+      <div class="pic-500">
+        <img class="pic-500__parent" src="@/assets/500_images/500.svg">
+      </div>
+      <div class="bullshit">
+        <div class="bullshit__oops">500错误!</div>
+        <div class="bullshit__headline">{{ message }}</div>
+        <div class="bullshit__info">
+          此错误意味着您正在访问的网站出现了服务器问题,此问题阻止了该网页的显示。您可以尝试返回登录页重新登录或者询问后端。
+        </div>
+        <a to="" class="bullshit__return-login" @click="returnToLogin">重新登录</a>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Page500',
+  computed: {
+    message() {
+      return '服务器出错了...'
+    }
+  },
+  methods: {
+    async returnToLogin() {
+      await this.$store.dispatch('user/logout')
+      this.$router.push('/login')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.wscn-http500-container {
+  position: absolute;
+  top: 40%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.wscn-http500 {
+  position: relative;
+  width: 1200px;
+  padding: 0 50px;
+  overflow: hidden;
+
+  .pic-500 {
+    position: relative;
+    float: left;
+    width: 600px;
+    overflow: hidden;
+
+    &__parent {
+      width: 80%;
+    }
+  }
+
+  .bullshit {
+    position: relative;
+    float: left;
+    width: 300px;
+    padding: 30px 0;
+    overflow: hidden;
+
+    &__oops {
+      margin-bottom: 20px;
+      font-size: 32px;
+      font-weight: bold;
+      line-height: 40px;
+      color: #1482f0;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-fill-mode: forwards;
+    }
+
+    &__headline {
+      margin-bottom: 10px;
+      font-size: 20px;
+      font-weight: bold;
+      line-height: 24px;
+      color: #222;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.1s;
+      animation-fill-mode: forwards;
+    }
+
+    &__info {
+      margin-bottom: 30px;
+      font-size: 13px;
+      line-height: 21px;
+      color: grey;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.2s;
+      animation-fill-mode: forwards;
+    }
+
+    &__return-login {
+      display: block;
+      float: left;
+      width: 110px;
+      height: 36px;
+      font-size: 14px;
+      line-height: 36px;
+      color: #fff;
+      text-align: center;
+      cursor: pointer;
+      background: #409eff;
+      border-radius: 100px;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.3s;
+      animation-fill-mode: forwards;
+    }
+
+    @keyframes slideUp {
+      0% {
+        opacity: 0;
+        transform: translateY(60px);
+      }
+
+      100% {
+        opacity: 1;
+        transform: translateY(0);
+      }
+    }
+  }
+}
+</style>

+ 147 - 0
src/views/gameSettings/censusSettings/bossDotData.vue

@@ -0,0 +1,147 @@
+<!-- BOSS击杀数据打点 -->
+<template>
+  <div>
+    <el-card>
+      <div class="topInquire">
+        <div>
+          <span>服务器:</span>
+          <el-input v-model="serverText" style="width:200px" size="small" disabled placeholder="请选择" />
+          <el-button
+            size="small"
+            type="primary"
+            plain
+            icon="el-icon-circle-plus-outline"
+            @click="radioDialog=true"
+          >选择服务器</el-button>
+        </div>
+
+        <el-button size="small" icon="el-icon-search" type="primary" @click="handleSearch">搜索</el-button>
+      </div>
+
+      <el-table
+        ref="table"
+        border
+        :data="bossDotData"
+        :header-cell-style="{background:'#f5f7fa'}"
+        :height="tableHeight"
+      >
+        <el-table-column prop="bossId" label="bossId" min-width="120" />
+        <el-table-column prop="bossName" label="boss名称" min-width="120" />
+        <el-table-column prop="level" label="等级" min-width="120" />
+        <el-table-column prop="canChallengeNum" label="可挑战人数" min-width="120" />
+        <el-table-column prop="challengeNum" label="挑战人数" min-width="120" />
+        <el-table-column prop="playerNum" label="玩家人数" min-width="120" />
+        <el-table-column prop="killNum" label="击杀人数" min-width="120" />
+        <el-table-column prop="killRate" label="击杀率" min-width="120">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.killNum/scope.row.playerNum*100) }}%
+          </template>
+        </el-table-column>
+        <el-table-column prop="minFighting" label="最小击杀战力" min-width="120" />
+      </el-table>
+
+      <!-- 服务器-单选弹窗 -->
+      <ServerDialog
+        :visible.sync="radioDialog"
+        :server-options="serverOptions"
+        @checkedId="handleCheckedId"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import ServerDialog from '@/components/radioDialog/index.vue'
+import { multiServer } from '@/api/aboutServer'
+import { bossDot } from '@/api/gameStatistics'
+
+export default {
+  components: {
+    ServerDialog
+  },
+
+  data() {
+    return {
+      bossDotData: [],
+      tableHeight: 0,
+      serverText: '',
+      radioDialog: false,
+      serverOptions: [],
+      serverUid: ''
+    }
+  },
+
+  created() {
+    this.getServer()
+  },
+
+  mounted() {
+    this.$nextTick(() => {
+      this.getTableHeight()
+    })
+    window.addEventListener('resize', this.$debounce(this.getTableHeight, 100))
+  },
+
+  beforeDestroy() {
+    window.removeEventListener('resize', this.getTableHeight)
+  },
+
+  methods: {
+    // 获取服务器
+    async getServer() {
+      const res = await multiServer()
+      this.serverOptions = res.data.data.options
+        .sort((a, b) => a.value - b.value)
+    },
+
+    // 选中的服务器ID
+    handleCheckedId(val) {
+      this.serverUid = val
+
+      const matchedOption = this.serverOptions.find(obj => obj.value === this.serverUid)
+      if (matchedOption) {
+        this.serverText = matchedOption.text
+      } else {
+        this.serverText = ''
+      }
+      this.radioDialog = false
+    },
+
+    // 搜索
+    async handleSearch() {
+      if (!this.serverText) {
+        this.$message.error('请选择服务器,服务器为必选项')
+      } else {
+        const result = await bossDot({ serverUid: this.serverUid })
+        this.bossDotData = result.data.data.data
+      }
+    },
+
+    // 表格自适应高度
+    getTableHeight() {
+      if (this.$refs.table && this.$refs.table.$el) {
+        this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 140
+      } else {
+        this.tableHeight = 300
+      }
+    },
+
+    // 保留两位小数
+    formatNumber(value) {
+      return value.toFixed(2)
+    }
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+.topInquire{
+  font-weight: bold;
+  font-size: 14px;
+  color: #6a7488;
+  margin-bottom: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 30px;
+}
+</style>

+ 239 - 0
src/views/gameSettings/censusSettings/consume.vue

@@ -0,0 +1,239 @@
+<!-- 货币消费统计 -->
+<template>
+  <div>
+    <el-card>
+      <div class="topInquire">
+        <div>
+          <span>日期:</span>
+          <el-date-picker
+            v-model="date"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="选择开始日期"
+            end-placeholder="选择结束日期"
+            value-format="timestamp"
+            size="small"
+          />
+        </div>
+        <div>
+          <span>资源类型:</span>
+          <el-select v-model.number="resourceType" size="small" clearable>
+            <el-option :value="2" label="钻石" />
+            <el-option :value="1" label="金币" />
+            <el-option :value="5" label="肉" />
+            <el-option :value="4" label="木材" />
+            <el-option :value="6" label="矿石" />
+          </el-select>
+        </div>
+        <div>
+          <span>消费类型:</span>
+          <el-select v-model.number="consumeType" size="small" clearable>
+            <el-option :value="1" label="消耗" />
+            <el-option :value="2" label="产出" />
+          </el-select>
+        </div>
+        <div>
+          <span>服务器:</span>
+          <el-input v-model="serverText" style="width:200px" size="small" disabled placeholder="请选择" />
+          <el-button
+            size="small"
+            type="primary"
+            plain
+            icon="el-icon-circle-plus-outline"
+            @click="radioDialog = true"
+          >选择服务器</el-button>
+        </div>
+
+        <el-button size="small" type="primary" icon="el-icon-search" @click="handleInquire">查询</el-button>
+      </div>
+
+      <div>
+        <el-table
+          ref="table"
+          border
+          :data="consumeSumData"
+          :header-cell-style="{background:'#f5f7fa'}"
+          :height="tableHeight"
+        >
+          <el-table-column prop="index" label="序号" min-width="100" />
+          <el-table-column prop="gameCause" label="流水类型编号" min-width="120" />
+          <el-table-column prop="gameCauseTxt" label="流水类型名称" min-width="160" />
+          <el-table-column prop="resourceType" label="资源类型" min-width="120">
+            <template slot-scope="scope">
+              {{ getType(scope.row.resourceType) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="consumeType" label="消费类型" min-width="120">
+            <template slot-scope="scope">
+              {{ scope.row.consumeType==1?'消耗':'产出' }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="playerNum" label="消费人数" min-width="120" />
+          <el-table-column prop="countNum" label="消费次数" min-width="120" />
+          <el-table-column prop="total" label="消费总数" min-width="120" />
+          <el-table-column prop="avg" label="平均消费数" min-width="120">
+            <template slot-scope="scope">
+              {{ formatNumber(scope.row.avg) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="weight" label="占比" min-width="120">
+            <template slot-scope="scope">
+              {{ formatNumber(scope.row.weight*100) }}%
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <!-- 分页 -->
+      <el-pagination
+        background
+        layout="total, prev, pager, next, jumper"
+        :total="total"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        @current-change="changePage"
+      />
+    </el-card>
+
+    <!-- 服务器-单选弹窗 -->
+    <ServerDialog
+      :visible.sync="radioDialog"
+      :server-options="serverOptions"
+      @checkedId="handleCheckedId"
+    />
+  </div>
+</template>
+
+<script>
+import ServerDialog from '@/components/radioDialog/index.vue'
+import { consumeSum } from '@/api/gameStatistics'
+import { multiServer } from '@/api/aboutServer'
+const typeEnum = { 1: '金币', 2: '钻石', 4: '木材', 5: '肉', 6: '矿石' }
+
+export default {
+  components: {
+    ServerDialog
+  },
+  data() {
+    return {
+      consumeSumData: [],
+      total: 0,
+      currentPage: 1, // 当前页
+      pageSize: 200, // 每页显示条数
+      tableHeight: 0, // 表格高度
+      date: [new Date().setDate(1), new Date()], // 日期
+      serverUid: '', // 选中的服务器id
+      serverOptions: [], // 服务器选项
+      radioDialog: false, // 服务器弹窗
+      serverText: '', // 服务器名称
+      resourceType: 2, // 资源类型
+      consumeType: 1// 消费类型
+    }
+  },
+
+  created() {
+    this.getServer()
+  },
+
+  mounted() {
+    this.$nextTick(() => {
+      this.getTableHeight()
+    })
+    window.addEventListener('resize', this.$debounce(this.getTableHeight, 100))
+  },
+
+  beforeDestroy() {
+    window.removeEventListener('resize', this.getTableHeight)
+  },
+
+  methods: {
+    // 获取服务器
+    async getServer() {
+      const res = await multiServer()
+      this.serverOptions = res.data.data.options.sort((a, b) => a.value - b.value)
+    },
+
+    // 选中的服务器ID
+    handleCheckedId(val) {
+      this.serverUid = val
+
+      const matchedOption = this.serverOptions.find(obj => obj.value === this.serverUid)
+      if (matchedOption) {
+        this.serverText = matchedOption.text
+      } else {
+        this.serverText = ''
+      }
+      this.radioDialog = false
+    },
+
+    // 搜索按钮
+    async handleInquire() {
+      try {
+        const query = { page: this.currentPage, limit: this.pageSize }
+        if (this.date && this.date.length === 2) {
+          // 时间戳毫秒转化为秒
+          query.startTime = this.$moment(this.date[0]).unix()
+          query.endTime = this.$moment(this.date[1]).unix()
+        }
+        if (this.resourceType) {
+          query.resourceType = this.resourceType
+        }
+        if (this.consumeType) {
+          query.consumeType = this.consumeType
+        }
+        if (!this.serverText) {
+          this.$message.error('请选择服务器,服务器为必选项')
+        } else {
+          query.serverUid = this.serverUid
+
+          const result = await consumeSum(query)
+          this.consumeSumData = result.data.data.data
+          this.total = result.data.data.count
+        }
+      } catch (error) {
+        console.error(error)
+      }
+    },
+
+    // 切换页面
+    changePage(page) {
+      this.currentPage = page
+      this.handleInquire()
+    },
+
+    // 获取资源类型
+    getType(value) {
+      return typeEnum[value]
+    },
+
+    // 保留两位小数(不四舍五入)
+    formatNumber(value) {
+      return (parseInt(value * 100) / 100).toFixed(2)
+    },
+
+    // 表格自适应高度
+    getTableHeight() {
+      if (this.$refs.table && this.$refs.table.$el) {
+        this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 170
+      } else {
+        this.tableHeight = 300
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+  .topInquire{
+    font-weight: bold;
+    font-size: 14px;
+    color: #6a7488;
+    margin-bottom: 20px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 30px;
+  }
+
+</style>
+

+ 143 - 0
src/views/gameSettings/censusSettings/fogDotData.vue

@@ -0,0 +1,143 @@
+<!-- 场景区域解锁数据打点 -->
+<template>
+  <div>
+    <el-card>
+      <div class="topInquire">
+        <div>
+          <span>服务器:</span>
+          <el-input v-model="serverText" style="width:200px" size="small" disabled placeholder="请选择" />
+          <el-button
+            size="small"
+            type="primary"
+            plain
+            icon="el-icon-circle-plus-outline"
+            @click="radioDialog=true"
+          >选择服务器</el-button>
+        </div>
+
+        <el-button size="small" icon="el-icon-search" type="primary" @click="handleSearch">搜索</el-button>
+      </div>
+
+      <el-table
+        ref="table"
+        border
+        :data="fogDotData"
+        :header-cell-style="{background:'#f5f7fa'}"
+        :height="tableHeight"
+      >
+        <el-table-column prop="fogId" label="区域ID" />
+        <el-table-column prop="minFighting" label="最小解锁战力" />
+        <el-table-column prop="canNum" label="可解锁人数" />
+        <el-table-column prop="num" label="解锁人数" />
+        <el-table-column prop="rate" label="解锁人数占比">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.num/scope.row.canNum*100) }}%
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 服务器-单选弹窗 -->
+      <ServerDialog
+        :visible.sync="radioDialog"
+        :server-options="serverOptions"
+        @checkedId="handleCheckedId"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import ServerDialog from '@/components/radioDialog/index.vue'
+import { multiServer } from '@/api/aboutServer'
+import { fogDot } from '@/api/gameStatistics'
+
+export default {
+  components: {
+    ServerDialog
+  },
+
+  data() {
+    return {
+      fogDotData: [],
+      tableHeight: 0,
+      serverText: '',
+      radioDialog: false,
+      serverOptions: [],
+      serverUid: ''
+    }
+  },
+
+  created() {
+    this.getServer()
+  },
+
+  mounted() {
+    this.$nextTick(() => {
+      this.getTableHeight()
+    })
+    window.addEventListener('resize', this.$debounce(this.getTableHeight, 100))
+  },
+
+  beforeDestroy() {
+    window.removeEventListener('resize', this.getTableHeight)
+  },
+
+  methods: {
+    // 获取服务器
+    async getServer() {
+      const res = await multiServer()
+      this.serverOptions = res.data.data.options
+        .sort((a, b) => a.value - b.value)
+    },
+
+    // 选中的服务器ID
+    handleCheckedId(val) {
+      this.serverUid = val
+
+      const matchedOption = this.serverOptions.find(obj => obj.value === this.serverUid)
+      if (matchedOption) {
+        this.serverText = matchedOption.text
+      } else {
+        this.serverText = ''
+      }
+      this.radioDialog = false
+    },
+
+    // 搜索
+    async handleSearch() {
+      if (!this.serverText) {
+        this.$message.error('请选择服务器,服务器为必选项')
+      } else {
+        const result = await fogDot({ serverUid: this.serverUid })
+        this.fogDotData = result.data.data.data
+      }
+    },
+
+    // 表格自适应高度
+    getTableHeight() {
+      if (this.$refs.table && this.$refs.table.$el) {
+        this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 140
+      } else {
+        this.tableHeight = 300
+      }
+    },
+
+    // 保留两位小数
+    formatNumber(value) {
+      return value.toFixed(2)
+    }
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+.topInquire{
+  font-weight: bold;
+  font-size: 14px;
+  color: #6a7488;
+  margin-bottom: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 30px;
+}
+</style>

+ 144 - 0
src/views/gameSettings/censusSettings/guideDotData.vue

@@ -0,0 +1,144 @@
+<!-- 引导数据打点 -->
+<template>
+  <div>
+    <el-card>
+      <div class="topInquire">
+        <div>
+          <span>服务器:</span>
+          <el-input v-model="serverText" style="width:200px" size="small" disabled placeholder="请选择" />
+          <el-button
+            size="small"
+            type="primary"
+            plain
+            icon="el-icon-circle-plus-outline"
+            @click="radioDialog=true"
+          >选择服务器</el-button>
+        </div>
+
+        <el-button size="small" icon="el-icon-search" type="primary" @click="handleSearch">搜索</el-button>
+      </div>
+
+      <el-table
+        ref="table"
+        border
+        :data="guideDotData"
+        :header-cell-style="{background:'#f5f7fa'}"
+        :height="tableHeight"
+      >
+        <el-table-column prop="guideId" label="引导ID" min-width="120" />
+        <el-table-column prop="guideName" label="引导名称" min-width="120" />
+        <el-table-column prop="step" label="步骤" min-width="120" />
+        <el-table-column prop="num" label="数量" min-width="120" />
+        <el-table-column prop="loseNum" label="流失数" min-width="120" />
+        <el-table-column prop="loseRate" label="流失率" min-width="120">
+          <template slot-scope="scope">
+            {{ formatNumber(scope.row.loseNum/scope.row.num*100) }}%
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 服务器-单选弹窗 -->
+      <ServerDialog
+        :visible.sync="radioDialog"
+        :server-options="serverOptions"
+        @checkedId="handleCheckedId"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import ServerDialog from '@/components/radioDialog/index.vue'
+import { multiServer } from '@/api/aboutServer'
+import { guideDot } from '@/api/gameStatistics'
+
+export default {
+  components: {
+    ServerDialog
+  },
+
+  data() {
+    return {
+      guideDotData: [],
+      tableHeight: 0,
+      serverText: '',
+      radioDialog: false,
+      serverOptions: [],
+      serverUid: ''
+    }
+  },
+
+  created() {
+    this.getServer()
+  },
+
+  mounted() {
+    this.$nextTick(() => {
+      this.getTableHeight()
+    })
+    window.addEventListener('resize', this.$debounce(this.getTableHeight, 100))
+  },
+
+  beforeDestroy() {
+    window.removeEventListener('resize', this.getTableHeight)
+  },
+
+  methods: {
+    // 获取服务器
+    async getServer() {
+      const res = await multiServer()
+      this.serverOptions = res.data.data.options
+        .sort((a, b) => a.value - b.value)
+    },
+
+    // 选中的服务器ID
+    handleCheckedId(val) {
+      this.serverUid = val
+
+      const matchedOption = this.serverOptions.find(obj => obj.value === this.serverUid)
+      if (matchedOption) {
+        this.serverText = matchedOption.text
+      } else {
+        this.serverText = ''
+      }
+      this.radioDialog = false
+    },
+
+    // 搜索
+    async handleSearch() {
+      if (!this.serverText) {
+        this.$message.error('请选择服务器,服务器为必选项')
+      } else {
+        const result = await guideDot({ serverUid: this.serverUid })
+        this.guideDotData = result.data.data.data
+      }
+    },
+
+    // 表格自适应高度
+    getTableHeight() {
+      if (this.$refs.table && this.$refs.table.$el) {
+        this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 140
+      } else {
+        this.tableHeight = 300
+      }
+    },
+
+    // 保留两位小数
+    formatNumber(value) {
+      return value.toFixed(2)
+    }
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+.topInquire{
+  font-weight: bold;
+  font-size: 14px;
+  color: #6a7488;
+  margin-bottom: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 30px;
+}
+</style>

+ 143 - 0
src/views/gameSettings/censusSettings/loginDotData.vue

@@ -0,0 +1,143 @@
+<!-- 登录数据打点 -->
+<template>
+  <div>
+    <el-card>
+      <div class="topInquire">
+        <div>
+          <span>日期:</span>
+          <el-date-picker
+            v-model="date"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="选择开始日期"
+            end-placeholder="选择结束日期"
+            value-format="timestamp"
+            size="small"
+          />
+        </div>
+
+        <el-button size="small" icon="el-icon-search" type="primary" @click="handleSearch">搜索</el-button>
+      </div>
+
+      <el-table
+        ref="table"
+        border
+        :data="loginDotData"
+        :header-cell-style="{background:'#f5f7fa'}"
+        :height="tableHeight"
+      >
+        <el-table-column prop="id" label="数据ID" min-width="120" />
+        <el-table-column prop="channelId" label="渠道ID" min-width="120" />
+        <el-table-column prop="dateStr" label="日期" min-width="120" />
+        <el-table-column prop="reqResUrlBegin" label="根据版本号请求资源路径" min-width="180" />
+        <el-table-column prop="reqResUrlSucess" label="根据版本号请求资源路径成功" min-width="210" />
+        <el-table-column prop="reqResUrlFail" label="根据版本号请求资源路径成功" min-width="210" />
+        <el-table-column prop="sdkLoginBegin" label="SDK开始登陆" min-width="180" />
+        <el-table-column prop="sdkLoginSucess" label="SDK的登录成功" min-width="180" />
+        <el-table-column prop="sdkLoginFail" label="SDK的登录失败" min-width="180" />
+        <el-table-column prop="reqServerVerifyBegin" label="请求服务器账号验证" min-width="180" />
+        <el-table-column prop="reqServerVerifySucess" label="请求服务器账号验证成功" min-width="180" />
+        <el-table-column prop="reqServerVerifyFail" label="请求服务器账号验证失败" min-width="180" />
+        <el-table-column prop="reqServerListBegin" label="请求服务器列表" min-width="180" />
+        <el-table-column prop="reqServerListSucess" label="请求服务器列表成功" min-width="180" />
+        <el-table-column prop="reqServerListFail" label="请求服务器列表失败" min-width="180" />
+        <el-table-column prop="startGame" label="点击开始游戏" min-width="180" />
+        <el-table-column prop="reqSocketConnectSucess" label="向服务器socket连接成功" min-width="180" />
+        <el-table-column prop="reqSocketLoginBegin" label="向服务器发送登陆请求协议" min-width="200" />
+        <el-table-column prop="reqSocketLoginSucess" label="向服务器发送登陆请求协议成功" min-width="230" />
+        <el-table-column prop="reqSocketLoginFail" label="向服务器发送登陆请求协议失败" min-width="230" />
+        <el-table-column prop="createSucc" label="创号成功" min-width="180" />
+        <el-table-column prop="reqServerPushDataEnd" label="服务器推送数据结束" min-width="180" />
+        <el-table-column prop="inGame" label="进入游戏" min-width="180" />
+      </el-table>
+
+      <el-pagination
+        background
+        layout="total, prev, pager, next, jumper"
+        :total="total"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        @current-change="changePage"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { loginDot } from '@/api/gameStatistics'
+
+export default {
+  data() {
+    return {
+      loginDotData: [],
+      total: 0,
+      currentPage: 1,
+      pageSize: 200,
+      tableHeight: 0,
+      date: [new Date().setDate(1), new Date()]
+    }
+  },
+
+  created() {
+    this.handleSearch()
+  },
+
+  mounted() {
+    this.$nextTick(() => {
+      this.getTableHeight()
+    })
+    window.addEventListener('resize', this.$debounce(this.getTableHeight, 100))
+  },
+
+  beforeDestroy() {
+    window.removeEventListener('resize', this.getTableHeight)
+  },
+
+  methods: {
+    // 搜索
+    async handleSearch() {
+      try {
+        const query = { page: this.currentPage, limit: this.pageSize }
+
+        if (this.date && this.date.length === 2) {
+          query.startTime = this.$moment(this.date[0]).unix()
+          query.endTime = this.$moment(this.date[1]).unix()
+        }
+
+        const result = await loginDot(query)
+        this.loginDotData = result.data.data.data
+        this.total = result.data.data.count
+      } catch (error) {
+        console.error(error)
+      }
+    },
+
+    // 切换页面
+    changePage(page) {
+      this.currentPage = page
+      this.handleSearch()
+    },
+
+    // 表格自适应高度
+    getTableHeight() {
+      if (this.$refs.table && this.$refs.table.$el) {
+        this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 170
+      } else {
+        this.tableHeight = 300
+      }
+    }
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+  .topInquire{
+    font-weight: bold;
+    font-size: 14px;
+    color: #6a7488;
+    margin-bottom: 20px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 30px;
+  }
+</style>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.