Browse Source

第一次上传

贾艺驰 3 years ago
commit
d9bd9117af
100 changed files with 3093 additions and 0 deletions
  1. 10 0
      .babelrc
  2. 14 0
      .editorconfig
  3. 10 0
      .env.development
  4. 7 0
      .env.production
  5. 8 0
      .env.staging
  6. 4 0
      .eslintignore
  7. 198 0
      .eslintrc.js
  8. 16 0
      .gitignore
  9. 5 0
      .travis.yml
  10. 21 0
      LICENSE
  11. 2 0
      README-zh.md
  12. 1 0
      README.md
  13. 5 0
      babel.config.js
  14. 35 0
      build/index.js
  15. 24 0
      jest.config.js
  16. 98 0
      package.json
  17. 8 0
      postcss.config.js
  18. BIN
      public/favicon.ico
  19. 37 0
      public/index.html
  20. 16 0
      src/App.vue
  21. 78 0
      src/api/game.js
  22. 49 0
      src/api/push.js
  23. 57 0
      src/api/role.js
  24. 58 0
      src/api/scale.js
  25. 94 0
      src/api/user.js
  26. 46 0
      src/api/whitelist.js
  27. BIN
      src/assets/404_images/404.png
  28. BIN
      src/assets/404_images/404_cloud.png
  29. BIN
      src/assets/blog/1148331928.jpg
  30. BIN
      src/assets/blog/avatar1.png
  31. BIN
      src/assets/blog/avatar2.png
  32. BIN
      src/assets/blog/avatar3.png
  33. BIN
      src/assets/blog/avatar4.png
  34. BIN
      src/assets/blog/avatar5.png
  35. BIN
      src/assets/blog/avatar6.png
  36. BIN
      src/assets/blog/avatar7.png
  37. BIN
      src/assets/blog/avatar8.png
  38. BIN
      src/assets/blog/background-index.png
  39. BIN
      src/assets/blog/index1.png
  40. BIN
      src/assets/blog/qq.png
  41. BIN
      src/assets/blog/sina.png
  42. BIN
      src/assets/blog/wx.png
  43. 78 0
      src/components/Breadcrumb/index.vue
  44. 183 0
      src/components/Editor/index.vue
  45. 83 0
      src/components/Editor/plugin/imageUpload/cloudservicesuploadadapter.js
  46. 56 0
      src/components/Editor/plugin/imageUpload/easyimage.js
  47. 291 0
      src/components/Editor/plugin/imageUpload/fileuploader.js
  48. 77 0
      src/components/Editor/plugin/imageUpload/uploadgateway.js
  49. 44 0
      src/components/Hamburger/index.vue
  50. 101 0
      src/components/Pagination/index.vue
  51. 62 0
      src/components/SvgIcon/index.vue
  52. 61 0
      src/components/dblclick.vue
  53. 9 0
      src/icons/index.js
  54. 1 0
      src/icons/svg/add.svg
  55. 1 0
      src/icons/svg/chat.svg
  56. 1 0
      src/icons/svg/dashboard.svg
  57. 1 0
      src/icons/svg/donation.svg
  58. 1 0
      src/icons/svg/example.svg
  59. 1 0
      src/icons/svg/eye-open.svg
  60. 1 0
      src/icons/svg/eye.svg
  61. 1 0
      src/icons/svg/form.svg
  62. 1 0
      src/icons/svg/github.svg
  63. 1 0
      src/icons/svg/hot.svg
  64. 1 0
      src/icons/svg/like.svg
  65. 1 0
      src/icons/svg/link.svg
  66. 1 0
      src/icons/svg/login.svg
  67. 1 0
      src/icons/svg/nested.svg
  68. 1 0
      src/icons/svg/organization.svg
  69. 1 0
      src/icons/svg/password.svg
  70. 1 0
      src/icons/svg/post.svg
  71. 1 0
      src/icons/svg/system-setting.svg
  72. 1 0
      src/icons/svg/table.svg
  73. 1 0
      src/icons/svg/tree.svg
  74. 1 0
      src/icons/svg/user.svg
  75. 22 0
      src/icons/svgo.yml
  76. 40 0
      src/layout/components/AppMain.vue
  77. 128 0
      src/layout/components/Navbar.vue
  78. 26 0
      src/layout/components/Sidebar/FixiOSBug.js
  79. 29 0
      src/layout/components/Sidebar/Item.vue
  80. 36 0
      src/layout/components/Sidebar/Link.vue
  81. 82 0
      src/layout/components/Sidebar/Logo.vue
  82. 95 0
      src/layout/components/Sidebar/SidebarItem.vue
  83. 57 0
      src/layout/components/Sidebar/index.vue
  84. 3 0
      src/layout/components/index.js
  85. 93 0
      src/layout/index.vue
  86. 45 0
      src/layout/mixin/ResizeHandler.js
  87. 35 0
      src/main.js
  88. 80 0
      src/permission.js
  89. 61 0
      src/router/index.js
  90. 22 0
      src/router/modules/game.js
  91. 22 0
      src/router/modules/push.js
  92. 22 0
      src/router/modules/scale.js
  93. 35 0
      src/router/modules/system.js
  94. 29 0
      src/router/modules/user.js
  95. 17 0
      src/settings.js
  96. 10 0
      src/store/getters.js
  97. 21 0
      src/store/index.js
  98. 48 0
      src/store/modules/app.js
  99. 68 0
      src/store/modules/permission.js
  100. 0 0
      src/store/modules/settings.js

+ 10 - 0
.babelrc

@@ -0,0 +1,10 @@
+{
+  "plugins": [
+    ["prismjs", {
+      "languages": ["javascript", "css", "java", "html" ,"shell", "python", "sql" ,"plsql", "go", "clike", "powershell", "properties", "ini", "regex", "json", "git"],
+      "plugins": ["line-numbers"],
+      "theme": "tomorrow",
+      "css": true
+    }]
+  ]
+}

+ 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

+ 10 - 0
.env.development

@@ -0,0 +1,10 @@
+# just a flag
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = 'http://localhost:8080/'
+VUE_APP_IMAGE_HOST = 'http://localhost:8080/'
+VUE_APP_IMAGE_UPLOAD_URL = 'http://localhost:8080/upload'
+
+
+

+ 7 - 0
.env.production

@@ -0,0 +1,7 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = 'http://8.210.195.183:8080/'
+VUE_APP_IMAGE_HOST = 'http://8.210.195.183:8080/'
+VUE_APP_IMAGE_UPLOAD_URL = 'http://8.210.195.183:8080/upload'

+ 8 - 0
.env.staging

@@ -0,0 +1,8 @@
+NODE_ENV = production
+
+# just a flag
+ENV = 'staging'
+
+# base api
+VUE_APP_BASE_API = '/stage-api'
+

+ 4 - 0
.eslintignore

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

+ 198 - 0
.eslintrc.js

@@ -0,0 +1,198 @@
+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: {
+    "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

+ 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.

+ 2 - 0
README-zh.md

@@ -0,0 +1,2 @@
+change git to gitlab
+npm run svgo

+ 1 - 0
README.md

@@ -0,0 +1 @@
+change git to gitlab

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/app'
+  ]
+}

+ 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}`)
+}

+ 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/'
+}

+ 98 - 0
package.json

@@ -0,0 +1,98 @@
+{
+  "name": "vue-admin-template",
+  "version": "4.2.1",
+  "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
+  "author": "Pan <panfree23@gmail.com>",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vue-cli-service serve --open",
+    "build:prod": "vue-cli-service build",
+    "build:stage": "vue-cli-service build --mode staging",
+    "preview": "node build/index.js --preview",
+    "lint": "eslint --ext .js,.vue src",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "test:ci": "npm run lint && npm run test:unit",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
+  },
+  "dependencies": {
+    "@ckeditor/ckeditor5-alignment": "^16.0.0",
+    "@ckeditor/ckeditor5-autoformat": "^16.0.0",
+    "@ckeditor/ckeditor5-autosave": "^16.0.0",
+    "@ckeditor/ckeditor5-basic-styles": "^16.0.0",
+    "@ckeditor/ckeditor5-block-quote": "^16.0.0",
+    "@ckeditor/ckeditor5-code-block": "^16.0.0",
+    "@ckeditor/ckeditor5-dev-utils": "^12.0.5",
+    "@ckeditor/ckeditor5-dev-webpack-plugin": "^8.0.5",
+    "@ckeditor/ckeditor5-editor-classic": "^16.0.0",
+    "@ckeditor/ckeditor5-essentials": "^16.0.0",
+    "@ckeditor/ckeditor5-font": "^16.0.0",
+    "@ckeditor/ckeditor5-heading": "^16.0.0",
+    "@ckeditor/ckeditor5-highlight": "^16.0.0",
+    "@ckeditor/ckeditor5-horizontal-line": "^16.0.0",
+    "@ckeditor/ckeditor5-image": "^16.0.0",
+    "@ckeditor/ckeditor5-indent": "^16.0.0",
+    "@ckeditor/ckeditor5-link": "^16.0.0",
+    "@ckeditor/ckeditor5-list": "^16.0.0",
+    "@ckeditor/ckeditor5-media-embed": "^16.0.0",
+    "@ckeditor/ckeditor5-page-break": "^16.0.0",
+    "@ckeditor/ckeditor5-paragraph": "^16.0.0",
+    "@ckeditor/ckeditor5-paste-from-office": "^16.0.0",
+    "@ckeditor/ckeditor5-remove-format": "^16.0.0",
+    "@ckeditor/ckeditor5-table": "^16.0.0",
+    "@ckeditor/ckeditor5-theme-lark": "^16.0.0",
+    "@ckeditor/ckeditor5-upload": "^16.0.0",
+    "@ckeditor/ckeditor5-vue": "^1.0.1",
+    "@ckeditor/ckeditor5-word-count": "^16.0.0",
+    "animate.css": "^3.7.2",
+    "axios": "^0.19.0",
+    "babel-plugin-prismjs": "^1.1.1",
+    "element-ui": "2.13.0",
+    "js-cookie": "2.2.0",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
+    "postcss-loader": "^3.0.0",
+    "prismjs": "^1.17.1",
+    "raw-loader": "^0.5.1",
+    "scrollreveal": "^4.0.5",
+    "velocity-animate": "^2.0.5",
+    "vue": "2.6.10",
+    "vue-router": "3.0.6",
+    "vuex": "3.1.0"
+  },
+  "devDependencies": {
+    "@babel/core": "7.0.0",
+    "@babel/register": "7.0.0",
+    "@vue/cli-plugin-babel": "3.6.0",
+    "@vue/cli-plugin-eslint": "3.6.0",
+    "@vue/cli-plugin-unit-jest": "^3.9.0",
+    "@vue/cli-service": "3.6.0",
+    "@vue/test-utils": "1.0.0-beta.29",
+    "autoprefixer": "^9.5.1",
+    "babel-core": "7.0.0-bridge.0",
+    "babel-eslint": "10.0.1",
+    "babel-jest": "23.6.0",
+    "chalk": "2.4.2",
+    "connect": "3.6.6",
+    "eslint": "5.15.3",
+    "eslint-plugin-vue": "5.2.2",
+    "html-webpack-plugin": "3.2.0",
+    "node-sass": "^4.12.20",
+    "runjs": "^4.3.2",
+    "sass-loader": "^7.1.0",
+    "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"
+  },
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ]
+}

+ 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


+ 37 - 0
public/index.html

@@ -0,0 +1,37 @@
+<!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, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+    <style>
+      html, body {
+        overflow-x: hidden;
+      }
+      html {
+        overflow: -moz-hidden-unscrollable;
+        height: 100%;
+      }
+
+      body::-webkit-scrollbar {
+        display: none;
+      }
+
+      body {
+        -ms-overflow-style: none;
+        height: 100%;
+        width: calc(100vw + 18px);
+        overflow: auto;
+      }
+    </style>
+  </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>

+ 16 - 0
src/App.vue

@@ -0,0 +1,16 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+</script>
+<style>
+  #app{
+   width: 100vw;
+  }
+</style>

+ 78 - 0
src/api/game.js

@@ -0,0 +1,78 @@
+import request from '@/utils/request'
+import qs from 'qs'
+
+const GameApi = {
+  getList: function getList(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/game/log/list',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  del: function del(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/game/log/delete',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  detail: function detail(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/game/log/detail',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  playTimeDetail: function playTimeDetail(userId) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/game/log/game_time/list',
+      method: 'post',
+      data: qs.stringify({ userId: userId })
+    })
+  },
+  download: function download(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/game/log/download',
+      responseType: 'blob',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  deleteIds: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/game/log/delete/ids',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  gameConfig: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/game/config/list/uid',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  }
+}
+export default GameApi
+

+ 49 - 0
src/api/push.js

@@ -0,0 +1,49 @@
+import request from '@/utils/request'
+import qs from 'qs'
+
+const PushApi = {
+  getList: function getList(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/push/log/list',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  del: function del(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/push/log/delete',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  download: function download(params) {
+    console.log(params)
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/push/log/download',
+      responseType: 'blob',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  deleteIds: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/push/log/delete/ids',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  }
+}
+export default PushApi
+

+ 57 - 0
src/api/role.js

@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+import qs from 'qs'
+
+const RoleApi = {
+  getList: function getList(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/role/list',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  powerList: function powerList() {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/power/list',
+      method: 'post'
+    })
+  },
+  rolePowerList: function rolePowerList(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/role/get/power',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+
+  addPower: function addPower(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/role/add/power',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  add: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/role/add',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  }
+}
+export default RoleApi
+

+ 58 - 0
src/api/scale.js

@@ -0,0 +1,58 @@
+import request from '@/utils/request'
+import qs from 'qs'
+
+const ScaleApi = {
+  getList: function getList(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/scale/log/list',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  del: function del(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/scale/log/delete',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  detail: function detail(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/scale/log/detail',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  download: function download(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/scale/log/download',
+      responseType: 'blob',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  deleteIds: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/scale/log/delete/ids',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  }
+}
+export default ScaleApi
+

+ 94 - 0
src/api/user.js

@@ -0,0 +1,94 @@
+import request from '@/utils/request'
+import qs from 'qs'
+
+export function toLogin(data) {
+  return request({
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded'
+    },
+    url: 'login',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}
+
+export function getInfo(token) {
+  return request({
+    url: '/user/get',
+    method: 'post'
+  })
+}
+
+export function logout() {
+  return request({
+    url: '/logout',
+    method: 'post'
+  })
+}
+
+export function getList(params) {
+  return request({
+    url: '/user/list',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}
+
+export function updateRole(params) {
+  return request({
+    url: '/user/add/role',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}
+
+export function getRole(params) {
+  return request({
+    url: '/user/get/role',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}
+export function add(params) {
+  return request({
+    url: '/user/config/save',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}
+
+export function getuserList(params) {
+  return request({
+    url: '/user/config/list',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}
+
+export function deluser(params) {
+  return request({
+    url: '/user/config/delete',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}
+
+export function updateUser(params) {
+  return request({
+    url: '/user/config/update',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}
+
+export function download(params) {
+  return request({
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded'
+    },
+    url: '/user/config/download',
+    responseType: 'blob',
+    method: 'post',
+    data: qs.stringify(params)
+  })
+}

+ 46 - 0
src/api/whitelist.js

@@ -0,0 +1,46 @@
+import request from '@/utils/request'
+import qs from 'qs'
+
+const Whitelist = {
+  getList: function getList(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/sys/whitelist/list',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  add: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/sys/whitelist/add',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  update: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/sys/whitelist/update',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  },
+  del: function(params) {
+    return request({
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      url: '/sys/whitelist/delete',
+      method: 'post',
+      data: qs.stringify(params)
+    })
+  }
+}
+export default Whitelist

BIN
src/assets/404_images/404.png


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


BIN
src/assets/blog/1148331928.jpg


BIN
src/assets/blog/avatar1.png


BIN
src/assets/blog/avatar2.png


BIN
src/assets/blog/avatar3.png


BIN
src/assets/blog/avatar4.png


BIN
src/assets/blog/avatar5.png


BIN
src/assets/blog/avatar6.png


BIN
src/assets/blog/avatar7.png


BIN
src/assets/blog/avatar8.png


BIN
src/assets/blog/background-index.png


BIN
src/assets/blog/index1.png


BIN
src/assets/blog/qq.png


BIN
src/assets/blog/sina.png


BIN
src/assets/blog/wx.png


+ 78 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route() {
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: '首页' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 183 - 0
src/components/Editor/index.vue

@@ -0,0 +1,183 @@
+<template>
+  <ckeditor v-model="editorData" :editor="editor" :config="editorConfig" @ready="onEditorReady" />
+</template>
+
+<script>
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
+import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'
+import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold'
+import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic'
+import LinkPlugin from '@ckeditor/ckeditor5-link/src/link'
+import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph'
+import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'
+import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat'
+import Indent from '@ckeditor/ckeditor5-indent/src/indent'
+import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'
+import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'
+import Font from '@ckeditor/ckeditor5-font/src/font'
+import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'
+import Heading from '@ckeditor/ckeditor5-heading/src/heading'
+import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'
+
+import Image from '@ckeditor/ckeditor5-image/src/image'
+import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar'
+import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'
+import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle'
+import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'
+import EasyImage from './plugin/imageUpload/easyimage'
+
+import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed'
+
+import TodoList from '@ckeditor/ckeditor5-list/src/todolist'
+import List from '@ckeditor/ckeditor5-list/src/list'
+import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak'
+
+import Table from '@ckeditor/ckeditor5-table/src/table'
+import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar'
+
+import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat'
+import Autosave from '@ckeditor/ckeditor5-autosave/src/autosave'
+import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount'
+import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'
+export default {
+  name: 'Editor',
+  props: {
+    content: {
+      required: true,
+      type: String
+    }
+  },
+  data() {
+    return {
+      editor: ClassicEditor,
+      editorData: this.content,
+      editorConfig: {
+        plugins: [
+          EssentialsPlugin,
+          BoldPlugin,
+          ItalicPlugin,
+          LinkPlugin,
+          ParagraphPlugin,
+          Highlight,
+          RemoveFormat,
+          Indent,
+          IndentBlock,
+          BlockQuote,
+          HorizontalLine,
+          Font,
+          Alignment,
+          Heading,
+          EasyImage,
+          MediaEmbed,
+          Image, ImageToolbar, ImageCaption, ImageStyle, ImageResize,
+          // PasteFromOffice,
+          Autoformat, WordCount, Autosave,
+          TodoList, List, PageBreak,
+          Table, TableToolbar,
+          CodeBlock
+        ],
+        toolbar: {
+          items: [
+            'heading',
+            'outdent', 'indent',
+            'link',
+            'undo',
+            'redo',
+            'alignment',
+            'codeBlock',
+            'highlight',
+            'blockQuote',
+            'imageUpload',
+            'mediaEmbed',
+            'todoList',
+            'numberedList',
+            'bulletedList',
+            'bold',
+            'italic',
+            'insertTable',
+            'removeFormat',
+            'pageBreak',
+            'horizontalLine',
+            'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor'
+          ]
+        },
+        // viewportTopOffset: 50,
+        codeBlock: {
+          languages: [
+            { language: 'java', label: 'java' },
+            { language: 'shell', label: 'shell' },
+            { language: 'dart', label: 'dart' },
+            { language: 'python', label: 'python' },
+            { language: 'javascript', label: 'js' },
+            { language: 'sql', label: 'sql' },
+            { language: 'css', label: 'CSS' },
+            { language: 'xml', label: 'HTML/XML' },
+            { language: 'html', label: 'html' },
+            { language: 'git', label: 'git' },
+            { language: 'properties', label: 'properties' },
+            { language: 'json', label: 'json' }
+          ]
+        },
+        fontFamily: {
+          options: [
+            'default',
+            'Ubuntu, Arial, sans-serif',
+            'Ubuntu Mono, Courier New, Courier, monospace'
+          ]
+        },
+        image: {
+          toolbar: ['imageTextAlternative', '|', 'imageStyle:full', 'imageStyle:side', 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight'],
+          styles: ['full', 'side', 'alignLeft', 'alignCenter', 'alignRight']
+        },
+        mediaEmbed: {
+
+        },
+        table: {
+          contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
+        },
+        heading: {
+          options: [
+            { model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+            { model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
+            { model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
+            { model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
+            { model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' },
+            { model: 'heading5', view: 'h5', title: 'Heading 5', class: 'ck-heading_heading5' }
+          ]
+        },
+        wordCount: {
+          onUpdate: stats => {
+            // console.log(stats)
+          }
+        }
+        // autosave: {
+        //   save(editor) {
+        //     // todo
+        //     console.log(editor)
+        //   }
+        // },
+      }
+    }
+  },
+  watch: {
+    content: function(val) {
+      this.editorData = val
+    },
+    editorData: function() {
+      this.$emit('getContent', this.editorData)
+    }
+  },
+  methods: {
+    onEditorReady(editor) {
+    }
+  }
+}
+</script>
+<style>
+  .ck-content{
+    min-height: 600px !important;
+  }
+  .ck.ck-editor__top .ck-sticky-panel .ck-toolbar{
+    border-bottom-width: 1px;
+  }
+</style>

+ 83 - 0
src/components/Editor/plugin/imageUpload/cloudservicesuploadadapter.js

@@ -0,0 +1,83 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+* @module easy-image/cloudservicesuploadadapter
+*/
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin'
+import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository'
+import UploadGateway from './uploadgateway'
+
+/**
+ * A plugin that enables upload to [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/).
+ *
+ * It is mainly used by the {@link module:easy-image/easyimage~EasyImage} feature.
+ *
+ * After enabling this adapter you need to configure the CKEditor Cloud Services integration through
+ * {@link module:cloud-services/cloudservices~CloudServicesConfig `config.cloudServices`}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class CloudServicesUploadAdapter extends Plugin {
+  /**
+	 * @inheritDoc
+	 */
+  static get requires() {
+    return [FileRepository]
+  }
+
+  /**
+	 * @inheritDoc
+	 */
+  init() {
+    const editor = this.editor
+
+    const token = { value: 'Bearer ' + sessionStorage.getItem('vue_admin_template_token') }
+    const uploadUrl = process.env.VUE_APP_IMAGE_UPLOAD_URL
+
+    if (!token) {
+      return
+    }
+
+    this._uploadGateway = new CloudServicesUploadAdapter._UploadGateway(token, uploadUrl)
+
+    editor.plugins.get(FileRepository).createUploadAdapter = loader => {
+      return new Adapter(this._uploadGateway, loader)
+    }
+  }
+}
+
+/**
+ * @private
+ */
+class Adapter {
+  constructor(uploadGateway, loader) {
+    this.uploadGateway = uploadGateway
+
+    this.loader = loader
+  }
+
+  upload() {
+    return this.loader.file.then(file => {
+      this.fileUploader = this.uploadGateway.upload(file)
+
+      this.fileUploader.on('progress', (evt, data) => {
+        this.loader.uploadTotal = data.total
+        this.loader.uploaded = data.uploaded
+      })
+
+      return this.fileUploader.send()
+    })
+  }
+
+  abort() {
+    this.fileUploader.abort()
+  }
+}
+
+// Store the API in static property to easily overwrite it in tests.
+// Too bad dependency injection does not work in Webpack + ES 6 (const) + Babel.
+CloudServicesUploadAdapter._UploadGateway = UploadGateway

+ 56 - 0
src/components/Editor/plugin/imageUpload/easyimage.js

@@ -0,0 +1,56 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module easy-image/easyimage
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin'
+import CloudServicesUploadAdapter from './cloudservicesuploadadapter'
+import Image from '@ckeditor/ckeditor5-image/src/image'
+import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'
+
+/**
+ * The Easy Image feature, which makes the image upload in CKEditor 5 possible with virtually zero
+ * server setup. A part of the [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/)
+ * family.
+ *
+ * This is a "glue" plugin which enables:
+ *
+ * * {@link module:image/image~Image},
+ * * {@link module:image/imageupload~ImageUpload},
+ * * {@link module:easy-image/cloudservicesuploadadapter~CloudServicesUploadAdapter}.
+ *
+ * See the {@glink features/image-upload/easy-image "Easy Image integration" guide} to learn how to configure
+ * and use this feature.
+ *
+ * Check out the {@glink features/image-upload/image-upload comprehensive "Image upload" guide} to learn about
+ * other ways to upload images into CKEditor 5.
+ *
+ * **Note**: After enabling the Easy Image plugin you need to configure the
+ * [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/)
+ * integration through {@link module:cloud-services/cloudservices~CloudServicesConfig `config.cloudServices`}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class EasyImage extends Plugin {
+  /**
+	 * @inheritDoc
+	 */
+  static get requires() {
+    return [
+      CloudServicesUploadAdapter,
+      Image,
+      ImageUpload
+    ]
+  }
+
+  /**
+	 * @inheritDoc
+	 */
+  static get pluginName() {
+    return 'EasyImage'
+  }
+}

+ 291 - 0
src/components/Editor/plugin/imageUpload/fileuploader.js

@@ -0,0 +1,291 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module cloud-services-core/uploadgateway/fileuploader
+ */
+
+/* globals XMLHttpRequest, FormData, Blob, atob */
+
+import mix from '@ckeditor/ckeditor5-utils/src/mix'
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'
+
+const BASE64_HEADER_REG_EXP = /^data:(\S*?);base64,/
+
+/**
+ * FileUploader class used to upload single file.
+ */
+export default class FileUploader {
+  /**
+     * Creates `FileUploader` instance.
+     *
+     * @param {Blob|String} fileOrData A blob object or a data string encoded with Base64.
+     * @param {module:cloud-services-core/token~Token} token Token used for authentication.
+     * @param {String} apiAddress API address.
+     */
+  constructor(fileOrData, token, apiAddress) {
+    if (!fileOrData) {
+      /**
+             * File must be provided as the first argument.
+             *
+             * @error fileuploader-missing-file
+             */
+      throw new CKEditorError('fileuploader-missing-file: File must be provided as the first argument', null)
+    }
+
+    if (!token) {
+      /**
+             * Token must be provided as the second argument.
+             *
+             * @error fileuploader-missing-token
+             */
+      throw new CKEditorError('fileuploader-missing-token: Token must be provided as the second argument.', null)
+    }
+
+    if (!apiAddress) {
+      /**
+             * Api address must be provided as the third argument.
+             *
+             * @error fileuploader-missing-api-address
+             */
+      throw new CKEditorError('fileuploader-missing-api-address: Api address must be provided as the third argument.', null)
+    }
+
+    /**
+         * A file that is being uploaded.
+         *
+         * @type {Blob}
+         */
+    this.file = _isBase64(fileOrData) ? _base64ToBlob(fileOrData) : fileOrData
+
+    /**
+         * CKEditor Cloud Services access token.
+         *
+         * @type {module:cloud-services-core/token~Token}
+         * @private
+         */
+    this._token = token
+
+    /**
+         * CKEditor Cloud Services API address.
+         *
+         * @type {String}
+         * @private
+         */
+    this._apiAddress = apiAddress
+  }
+
+  /**
+     * Registers callback on `progress` event.
+     *
+     * @chainable
+     * @param {Function} callback
+     * @returns {module:cloud-services-core/uploadgateway/fileuploader~FileUploader}
+     */
+  onProgress(callback) {
+    this.on('progress', (event, data) => callback(data))
+
+    return this
+  }
+
+  /**
+     * Registers callback on `error` event. Event is called once when error occurs.
+     *
+     * @chainable
+     * @param {Function} callback
+     * @returns {module:cloud-services-core/uploadgateway/fileuploader~FileUploader}
+     */
+  onError(callback) {
+    this.once('error', (event, data) => callback(data))
+
+    return this
+  }
+
+  /**
+     * Aborts upload process.
+     */
+  abort() {
+    this.xhr.abort()
+  }
+
+  /**
+     * Sends XHR request to API.
+     *
+     * @chainable
+     * @returns {Promise.<Object>}
+     */
+  send() {
+    this._prepareRequest()
+    this._attachXHRListeners()
+
+    return this._sendRequest()
+  }
+
+  /**
+     * Prepares XHR request.
+     *
+     * @private
+     */
+  _prepareRequest() {
+    const xhr = new XMLHttpRequest()
+
+    xhr.open('POST', this._apiAddress)
+    xhr.setRequestHeader('Authorization', this._token.value)
+    xhr.responseType = 'json'
+
+    this.xhr = xhr
+  }
+
+  /**
+     * Attaches listeners to the XHR.
+     *
+     * @private
+     */
+  _attachXHRListeners() {
+    const that = this
+    const xhr = this.xhr
+
+    xhr.addEventListener('error', onError('Network Error'))
+    xhr.addEventListener('abort', onError('Abort'))
+
+    /* istanbul ignore else */
+    if (xhr.upload) {
+      xhr.upload.addEventListener('progress', event => {
+        if (event.lengthComputable) {
+          this.fire('progress', {
+            total: event.total,
+            uploaded: event.loaded
+          })
+        }
+      })
+    }
+
+    xhr.addEventListener('load', () => {
+      const statusCode = xhr.status
+      const xhrResponse = xhr.response
+
+      if (statusCode < 200 || statusCode > 299) {
+        return this.fire('error', xhrResponse.message || xhrResponse.error)
+      }
+    })
+
+    function onError(message) {
+      return () => that.fire('error', message)
+    }
+  }
+
+  /**
+     * Sends XHR request.
+     *
+     * @private
+     */
+  _sendRequest() {
+    const formData = new FormData()
+    const xhr = this.xhr
+
+    formData.append('file', this.file)
+
+    return new Promise((resolve, reject) => {
+      xhr.addEventListener('load', () => {
+        const statusCode = xhr.status
+        const xhrResponse = xhr.response
+
+        if (statusCode < 200 || statusCode > 299) {
+          if (xhrResponse.message) {
+            /**
+                         * Uploading file failed.
+                         *
+                         * @error fileuploader-uploading-data-failed
+                         */
+            return reject(new CKEditorError(
+              'fileuploader-uploading-data-failed: Uploading file failed.',
+              this,
+              { message: xhrResponse.message }
+            ))
+          }
+
+          return reject(xhrResponse.error)
+        }
+
+        return resolve(xhrResponse)
+      })
+
+      xhr.addEventListener('error', () => reject(new Error('Network Error')))
+      xhr.addEventListener('abort', () => reject(new Error('Abort')))
+
+      xhr.send(formData)
+    })
+  }
+
+  /**
+     * Fired when error occurs.
+     *
+     * @event error
+     * @param {String} error Error message
+     */
+
+  /**
+     * Fired on upload progress.
+     *
+     * @event progress
+     * @param {Object} status Total and uploaded status
+     */
+}
+
+mix(FileUploader, EmitterMixin)
+
+/**
+ * Transforms Base64 string data into file.
+ *
+ * @param {String} base64 String data.
+ * @param {Number} [sliceSize=512]
+ * @returns {Blob}
+ * @private
+ */
+function _base64ToBlob(base64, sliceSize = 512) {
+  try {
+    const contentType = base64.match(BASE64_HEADER_REG_EXP)[ 1 ]
+    const base64Data = atob(base64.replace(BASE64_HEADER_REG_EXP, ''))
+
+    const byteArrays = []
+
+    for (let offset = 0; offset < base64Data.length; offset += sliceSize) {
+      const slice = base64Data.slice(offset, offset + sliceSize)
+      const byteNumbers = new Array(slice.length)
+
+      for (let i = 0; i < slice.length; i++) {
+        byteNumbers[ i ] = slice.charCodeAt(i)
+      }
+
+      byteArrays.push(new Uint8Array(byteNumbers))
+    }
+
+    return new Blob(byteArrays, { type: contentType })
+  } catch (error) {
+    /**
+         * Problem with decoding Base64 image data.
+         *
+         * @error fileuploader-decoding-image-data-error
+         */
+    throw new CKEditorError('fileuploader-decoding-image-data-error: Problem with decoding Base64 image data.', null)
+  }
+}
+
+/**
+ * Checks that string is Base64.
+ *
+ * @param {String} string
+ * @returns {Boolean}
+ * @private
+ */
+function _isBase64(string) {
+  if (typeof string !== 'string') {
+    return false
+  }
+
+  const match = string.match(BASE64_HEADER_REG_EXP)
+  return !!(match && match.length)
+}

+ 77 - 0
src/components/Editor/plugin/imageUpload/uploadgateway.js

@@ -0,0 +1,77 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module cloud-services-core/uploadgateway/uploadgateway
+ */
+
+import FileUploader from './fileuploader'
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'
+
+/**
+ * UploadGateway abstracts file uploads to CKEditor Cloud Services.
+ */
+export default class UploadGateway {
+  /**
+     * Creates `UploadGateway` instance.
+     *
+     * @param {module:cloud-services-core/token~Token} token Token used for authentication.
+     * @param {String} apiAddress API address.
+     */
+  constructor(token, apiAddress) {
+    if (!token) {
+      /**
+             * Token must be provided.
+             *
+             * @error uploadgateway-missing-token
+             */
+      throw new CKEditorError('uploadgateway-missing-token: Token must be provided.', null)
+    }
+
+    if (!apiAddress) {
+      /**
+             * Api address must be provided.
+             *
+             * @error uploadgateway-missing-api-address
+             */
+      throw new CKEditorError('uploadgateway-missing-api-address: Api address must be provided.', null)
+    }
+
+    /**
+         * CKEditor Cloud Services access token.
+         *
+         * @type {module:cloud-services-core/token~Token}
+         * @private
+         */
+    this._token = token
+
+    /**
+         * CKEditor Cloud Services API address.
+         *
+         * @type {String}
+         * @private
+         */
+    this._apiAddress = apiAddress
+  }
+
+  /**
+     * Creates a {@link module:cloud-services-core/uploadgateway/fileuploader~FileUploader} instance that wraps
+     * file upload process. The file is being sent at a time when the
+     * {@link module:cloud-services-core/uploadgateway/fileuploader~FileUploader#send} method is called.
+     *
+     *     const token = await Token.create( 'https://token-endpoint' );
+     *     new UploadGateway( token, 'https://example.org' )
+     *        .upload( 'FILE' )
+     *        .onProgress( ( data ) => console.log( data ) )
+     *        .send()
+     *        .then( ( response ) => console.log( response ) );
+     *
+     * @param {Blob|String} fileOrData A blob object or a data string encoded with Base64.
+     * @returns {module:cloud-services-core/uploadgateway/fileuploader~FileUploader} Returns `FileUploader` instance.
+     */
+  upload(fileOrData) {
+    return new FileUploader(fileOrData, this._token, this._apiAddress)
+  }
+}

+ 44 - 0
src/components/Hamburger/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div style="padding: 0 15px;" @click="toggleClick">
+    <svg
+      :class="{'is-active':isActive}"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+    >
+      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
+    </svg>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Hamburger',
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    toggleClick() {
+      this.$emit('toggleClick')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 101 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,101 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scroll-to'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 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>

+ 61 - 0
src/components/dblclick.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="dblclick_box" @dblclick="showInput">
+    <div v-if="!isShowInput">
+      <span>{{ callback }}</span>
+    </div>
+    <div v-else>
+      <input v-model="callback" v-focus="isAutofocus" type="text" @blur="onblur" @keyup.enter="clickEnter">
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Dblclick',
+  directives: {
+    focus: {
+      inserted: function(el) {
+        el.focus()
+      }
+    }
+  },
+  props: {
+    content: String
+  },
+  data() {
+    return {
+      callback: this.content,
+      isShowInput: false,
+      isAutofocus: false
+    }
+  },
+  watch: {
+    content(val) {
+      this.callback = val
+    }
+  },
+
+  methods: {
+    showInput() {
+      this.isShowInput = true
+      this.isAutofocus = true
+    },
+    onblur() {
+      this.isShowInput = false
+      this.isAutofocus = false
+      this.callback = this.content
+    },
+    clickEnter() {
+      this.isShowInput = false
+      this.isAutofocus = false
+      this.$emit('getInputData', this.callback)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .dblclick_box {
+    min-height: 30px;
+  }
+</style>

+ 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)

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

@@ -0,0 +1 @@
+<svg height="448pt" viewBox="0 0 448 448" width="448pt" xmlns="http://www.w3.org/2000/svg"><path d="M408 184H272a8 8 0 0 1-8-8V40c0-22.09-17.91-40-40-40s-40 17.91-40 40v136a8 8 0 0 1-8 8H40c-22.09 0-40 17.91-40 40s17.91 40 40 40h136a8 8 0 0 1 8 8v136c0 22.09 17.91 40 40 40s40-17.91 40-40V272a8 8 0 0 1 8-8h136c22.09 0 40-17.91 40-40s-17.91-40-40-40zm0 0"/></svg>

File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/chat.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/dashboard.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/donation.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>

File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/eye-open.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/eye.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/form.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/github.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/hot.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/like.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/login.svg

@@ -0,0 +1 @@
+<svg height="512" viewBox="0 0 551.13 551.13" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M499.462 0H120.56c-9.52 0-17.223 7.703-17.223 17.223v51.668h34.446V34.446h344.456v482.239H137.783v-34.446h-34.446v51.668c0 9.52 7.703 17.223 17.223 17.223h378.902c9.52 0 17.223-7.703 17.223-17.223V17.223c0-9.52-7.704-17.223-17.223-17.223z"/><path d="M204.588 366.725l24.354 24.354 115.514-115.514-115.514-115.514-24.354 24.354 73.937 73.937H34.446v34.446h244.08z"/></svg>

File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/nested.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/organization.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/password.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/post.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/system-setting.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/table.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/tree.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'

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

@@ -0,0 +1,40 @@
+<template>
+  <section class="app-main">
+    <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>

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

@@ -0,0 +1,128 @@
+<template>
+  <div class="navbar">
+    <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
+
+    <breadcrumb class="breadcrumb-container" />
+
+    <div class="right-menu">
+      <el-dropdown class="avatar-container" trigger="click">
+        <div class="avatar-wrapper">
+          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
+          <i class="el-icon-caret-bottom" />
+        </div>
+        <el-dropdown-menu slot="dropdown" class="user-dropdown">
+          <el-dropdown-item divided>
+            <span style="display:block;" @click="logout">退出登錄</span>
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Breadcrumb from '@/components/Breadcrumb'
+import Hamburger from '@/components/Hamburger'
+
+export default {
+  components: {
+    Breadcrumb,
+    Hamburger
+  },
+  computed: {
+    ...mapGetters([
+      'sidebar',
+      'avatar'
+    ])
+  },
+  methods: {
+    toggleSideBar() {
+      this.$store.dispatch('app/toggleSideBar')
+    },
+    async logout() {
+      await this.$store.dispatch('user/logout')
+      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.navbar {
+  height: 50px;
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0,21,41,.08);
+
+  .hamburger-container {
+    line-height: 46px;
+    height: 100%;
+    float: left;
+    cursor: pointer;
+    transition: background .3s;
+    -webkit-tap-highlight-color:transparent;
+
+    &:hover {
+      background: rgba(0, 0, 0, .025)
+    }
+  }
+
+  .breadcrumb-container {
+    float: left;
+  }
+
+  .right-menu {
+    float: right;
+    height: 100%;
+    line-height: 50px;
+
+    &:focus {
+      outline: none;
+    }
+
+    .right-menu-item {
+      display: inline-block;
+      padding: 0 8px;
+      height: 100%;
+      font-size: 18px;
+      color: #5a5e66;
+      vertical-align: text-bottom;
+
+      &.hover-effect {
+        cursor: pointer;
+        transition: background .3s;
+
+        &:hover {
+          background: rgba(0, 0, 0, .025)
+        }
+      }
+    }
+
+    .avatar-container {
+      margin-right: 30px;
+
+      .avatar-wrapper {
+        margin-top: 5px;
+        position: relative;
+
+        .user-avatar {
+          cursor: pointer;
+          width: 40px;
+          height: 40px;
+          border-radius: 10px;
+        }
+
+        .el-icon-caret-bottom {
+          cursor: pointer;
+          position: absolute;
+          right: -20px;
+          top: 25px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+</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)
+        }
+      }
+    }
+  }
+}

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

@@ -0,0 +1,29 @@
+<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) {
+      vnodes.push(<svg-icon icon-class={icon}/>)
+    }
+
+    if (title) {
+      vnodes.push(<span slot='title'>{(title)}</span>)
+    }
+    return vnodes
+  }
+}
+</script>

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

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

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

@@ -0,0 +1,82 @@
+<template>
+  <div class="sidebar-logo-container" :class="{'collapse':collapse}">
+    <transition name="sidebarLogoFade">
+      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+        <img v-if="logo" :src="logo" class="sidebar-logo">
+        <h1 v-else class="sidebar-title">{{ title }} </h1>
+      </router-link>
+      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
+        <img v-if="logo" :src="logo" class="sidebar-logo">
+        <h1 class="sidebar-title">{{ title }} </h1>
+      </router-link>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SidebarLogo',
+  props: {
+    collapse: {
+      type: Boolean,
+      required: true
+    }
+  },
+  data() {
+    return {
+      title: 'Vue Admin Template',
+      logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sidebarLogoFade-enter-active {
+  transition: opacity 1.5s;
+}
+
+.sidebarLogoFade-enter,
+.sidebarLogoFade-leave-to {
+  opacity: 0;
+}
+
+.sidebar-logo-container {
+  position: relative;
+  width: 100%;
+  height: 50px;
+  line-height: 50px;
+  background: #2b2f3a;
+  text-align: center;
+  overflow: hidden;
+
+  & .sidebar-logo-link {
+    height: 100%;
+    width: 100%;
+
+    & .sidebar-logo {
+      width: 32px;
+      height: 32px;
+      vertical-align: middle;
+      margin-right: 12px;
+    }
+
+    & .sidebar-title {
+      display: inline-block;
+      margin: 0;
+      color: #fff;
+      font-weight: 600;
+      line-height: 50px;
+      font-size: 14px;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+      vertical-align: middle;
+    }
+  }
+
+  &.collapse {
+    .sidebar-logo {
+      margin-right: 0px;
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,95 @@
+<template>
+  <div v-if="!item.hidden" class="menu-wrapper">
+    <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 in item.children"
+        :key="child.path"
+        :is-nest="true"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+        class="nest-menu"
+      />
+    </el-submenu>
+  </div>
+</template>
+
+<script>
+import path from 'path'
+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
+      }
+      return path.resolve(this.basePath, routePath)
+    }
+  }
+}
+</script>

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

@@ -0,0 +1,57 @@
+<template>
+  <div :class="{'has-logo':showLogo}">
+    <logo v-if="showLogo" :collapse="isCollapse" />
+    <el-scrollbar wrap-class="scrollbar-wrapper">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="isCollapse"
+        :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 in permission_routes" :key="route.path" :item="route" :base-path="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 },
+  computed: {
+    ...mapGetters([
+      'permission_routes',
+      'sidebar'
+    ]),
+    routes() {
+      return this.$router.options.routes
+    },
+    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>

+ 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'

+ 93 - 0
src/layout/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <div :class="classObj" class="app-wrapper">
+    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+    <sidebar class="sidebar-container" />
+    <div class="main-container">
+      <div :class="{'fixed-header':fixedHeader}">
+        <navbar />
+      </div>
+      <app-main />
+    </div>
+  </div>
+</template>
+
+<script>
+import { Navbar, Sidebar, AppMain } from './components'
+import ResizeMixin from './mixin/ResizeHandler'
+
+export default {
+  name: 'Layout',
+  components: {
+    Navbar,
+    Sidebar,
+    AppMain
+  },
+  mixins: [ResizeMixin],
+  computed: {
+    sidebar() {
+      return this.$store.state.app.sidebar
+    },
+    device() {
+      return this.$store.state.app.device
+    },
+    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 })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import "~@/styles/mixin.scss";
+  @import "~@/styles/variables.scss";
+
+  .app-wrapper {
+    @include clearfix;
+    position: relative;
+    height: 100%;
+    width: 100%;
+    &.mobile.openSidebar{
+      position: fixed;
+      top: 0;
+    }
+  }
+  .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;
+  }
+
+  .hideSidebar .fixed-header {
+    width: calc(100% - 54px)
+  }
+
+  .mobile .fixed-header {
+    width: 100%;
+  }
+</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 })
+        }
+      }
+    }
+  }
+}

+ 35 - 0
src/main.js

@@ -0,0 +1,35 @@
+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/zh-CN' // 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 CKEditor from '@ckeditor/ckeditor5-vue'
+
+import animated from 'animate.css'
+
+Vue.use(animated)
+Vue.use(ElementUI, { locale })
+Vue.use(CKEditor)
+Vue.config.productionTip = false
+
+Vue.prototype.uploadUrl = process.env.VUE_APP_BASE_API + 'img/upload'
+Vue.prototype.apkUploadUrl = process.env.VUE_APP_BASE_API + 'apk/upload'
+
+new Vue({
+  el: '#app',
+  router,
+  store,
+  render: h => h(App)
+})

+ 80 - 0
src/permission.js

@@ -0,0 +1,80 @@
+import router from './router'
+import store from './store'
+import { Message } from 'element-ui'
+import NProgress from 'nprogress' // progress bar
+import 'nprogress/nprogress.css' // progress bar style