Browse Source

firsrt commit

许成详 6 years ago
parent
commit
ee9943ce32
79 changed files with 3271 additions and 0 deletions
  1. 12
    0
      .babelrc
  2. 14
    0
      .editorconfig
  3. 3
    0
      .eslintignore
  4. 198
    0
      .eslintrc.js
  5. 15
    0
      .gitignore
  6. 10
    0
      .postcssrc.js
  7. 5
    0
      .travis.yml
  8. 21
    0
      LICENSE
  9. 77
    0
      README-zh.md
  10. 41
    0
      build/build.js
  11. 54
    0
      build/check-versions.js
  12. BIN
      build/logo.png
  13. 101
    0
      build/utils.js
  14. 22
    0
      build/vue-loader.conf.js
  15. 100
    0
      build/webpack.base.conf.js
  16. 87
    0
      build/webpack.dev.conf.js
  17. 150
    0
      build/webpack.prod.conf.js
  18. 8
    0
      config/dev.env.js
  19. 89
    0
      config/index.js
  20. 5
    0
      config/prod.env.js
  21. BIN
      favicon.ico
  22. 12
    0
      index.html
  23. 80
    0
      package.json
  24. 11
    0
      src/App.vue
  25. 27
    0
      src/api/login.js
  26. 9
    0
      src/api/table.js
  27. BIN
      src/assets/404_images/404.png
  28. BIN
      src/assets/404_images/404_cloud.png
  29. 51
    0
      src/components/Breadcrumb/index.vue
  30. 44
    0
      src/components/Hamburger/index.vue
  31. 42
    0
      src/components/SvgIcon/index.vue
  32. 9
    0
      src/icons/index.js
  33. 1
    0
      src/icons/svg/example.svg
  34. 1
    0
      src/icons/svg/eye.svg
  35. 1
    0
      src/icons/svg/form.svg
  36. 1
    0
      src/icons/svg/nested.svg
  37. 1
    0
      src/icons/svg/password.svg
  38. 1
    0
      src/icons/svg/table.svg
  39. 1
    0
      src/icons/svg/tree.svg
  40. 1
    0
      src/icons/svg/user.svg
  41. 27
    0
      src/main.js
  42. 51
    0
      src/permission.js
  43. 142
    0
      src/router/index.js
  44. 11
    0
      src/store/getters.js
  45. 19
    0
      src/store/index.js
  46. 43
    0
      src/store/modules/app.js
  47. 63
    0
      src/store/modules/permission.js
  48. 87
    0
      src/store/modules/user.js
  49. 29
    0
      src/styles/element-ui.scss
  50. 78
    0
      src/styles/index.scss
  51. 27
    0
      src/styles/mixin.scss
  52. 119
    0
      src/styles/sidebar.scss
  53. 32
    0
      src/styles/transition.scss
  54. 4
    0
      src/styles/variables.scss
  55. 15
    0
      src/utils/auth.js
  56. 58
    0
      src/utils/index.js
  57. 66
    0
      src/utils/request.js
  58. 33
    0
      src/utils/validate.js
  59. 236
    0
      src/views/404.vue
  60. 32
    0
      src/views/dashboard/index.vue
  61. 85
    0
      src/views/form/index.vue
  62. 69
    0
      src/views/layout/Layout.vue
  63. 28
    0
      src/views/layout/components/AppMain.vue
  64. 102
    0
      src/views/layout/components/Navbar.vue
  65. 67
    0
      src/views/layout/components/Sidebar/SidebarItem.vue
  66. 33
    0
      src/views/layout/components/Sidebar/index.vue
  67. 3
    0
      src/views/layout/components/index.js
  68. 41
    0
      src/views/layout/mixin/ResizeHandler.js
  69. 182
    0
      src/views/login/index.vue
  70. 7
    0
      src/views/nested/menu1/index.vue
  71. 7
    0
      src/views/nested/menu1/menu1-1/index.vue
  72. 7
    0
      src/views/nested/menu1/menu1-2/index.vue
  73. 5
    0
      src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
  74. 5
    0
      src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
  75. 5
    0
      src/views/nested/menu1/menu1-3/index.vue
  76. 5
    0
      src/views/nested/menu2/index.vue
  77. 72
    0
      src/views/table/index.vue
  78. 71
    0
      src/views/tree/index.vue
  79. 0
    0
      static/.gitkeep

+ 12
- 0
.babelrc View File

@@ -0,0 +1,12 @@
1
+{
2
+  "presets": [
3
+    ["env", {
4
+      "modules": false,
5
+      "targets": {
6
+        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7
+      }
8
+    }],
9
+    "stage-2"
10
+  ],
11
+  "plugins":["transform-vue-jsx", "transform-runtime"]
12
+}

+ 14
- 0
.editorconfig View File

@@ -0,0 +1,14 @@
1
+# http://editorconfig.org
2
+root = true
3
+
4
+[*]
5
+charset = utf-8
6
+indent_style = space
7
+indent_size = 2
8
+end_of_line = lf
9
+insert_final_newline = true
10
+trim_trailing_whitespace = true
11
+
12
+[*.md]
13
+insert_final_newline = false
14
+trim_trailing_whitespace = false

+ 3
- 0
.eslintignore View File

@@ -0,0 +1,3 @@
1
+build/*.js
2
+config/*.js
3
+src/assets

+ 198
- 0
.eslintrc.js View File

@@ -0,0 +1,198 @@
1
+module.exports = {
2
+  root: true,
3
+  parser: 'babel-eslint',
4
+  parserOptions: {
5
+    sourceType: 'module'
6
+  },
7
+  env: {
8
+    browser: true,
9
+    node: true,
10
+    es6: true,
11
+  },
12
+  extends: 'eslint:recommended',
13
+  // required to lint *.vue files
14
+  plugins: [
15
+    'html'
16
+  ],
17
+  // check if imports actually resolve
18
+  'settings': {
19
+    'import/resolver': {
20
+      'webpack': {
21
+        'config': 'build/webpack.base.conf.js'
22
+      }
23
+    }
24
+  },
25
+  // add your custom rules here
26
+  //it is base on https://github.com/vuejs/eslint-config-vue
27
+  rules: {
28
+    'accessor-pairs': 2,
29
+    'arrow-spacing': [2, {
30
+      'before': true,
31
+      'after': true
32
+    }],
33
+    'block-spacing': [2, 'always'],
34
+    'brace-style': [2, '1tbs', {
35
+      'allowSingleLine': true
36
+    }],
37
+    'camelcase': [0, {
38
+      'properties': 'always'
39
+    }],
40
+    'comma-dangle': [2, 'never'],
41
+    'comma-spacing': [2, {
42
+      'before': false,
43
+      'after': true
44
+    }],
45
+    'comma-style': [2, 'last'],
46
+    'constructor-super': 2,
47
+    'curly': [2, 'multi-line'],
48
+    'dot-location': [2, 'property'],
49
+    'eol-last': 2,
50
+    'eqeqeq': [2, 'allow-null'],
51
+    'generator-star-spacing': [2, {
52
+      'before': true,
53
+      'after': true
54
+    }],
55
+    'handle-callback-err': [2, '^(err|error)$'],
56
+    'indent': [2, 2, {
57
+      'SwitchCase': 1
58
+    }],
59
+    'jsx-quotes': [2, 'prefer-single'],
60
+    'key-spacing': [2, {
61
+      'beforeColon': false,
62
+      'afterColon': true
63
+    }],
64
+    'keyword-spacing': [2, {
65
+      'before': true,
66
+      'after': true
67
+    }],
68
+    'new-cap': [2, {
69
+      'newIsCap': true,
70
+      'capIsNew': false
71
+    }],
72
+    'new-parens': 2,
73
+    'no-array-constructor': 2,
74
+    'no-caller': 2,
75
+    'no-console': 'off',
76
+    'no-class-assign': 2,
77
+    'no-cond-assign': 2,
78
+    'no-const-assign': 2,
79
+    'no-control-regex': 2,
80
+    'no-delete-var': 2,
81
+    'no-dupe-args': 2,
82
+    'no-dupe-class-members': 2,
83
+    'no-dupe-keys': 2,
84
+    'no-duplicate-case': 2,
85
+    'no-empty-character-class': 2,
86
+    'no-empty-pattern': 2,
87
+    'no-eval': 2,
88
+    'no-ex-assign': 2,
89
+    'no-extend-native': 2,
90
+    'no-extra-bind': 2,
91
+    'no-extra-boolean-cast': 2,
92
+    'no-extra-parens': [2, 'functions'],
93
+    'no-fallthrough': 2,
94
+    'no-floating-decimal': 2,
95
+    'no-func-assign': 2,
96
+    'no-implied-eval': 2,
97
+    'no-inner-declarations': [2, 'functions'],
98
+    'no-invalid-regexp': 2,
99
+    'no-irregular-whitespace': 2,
100
+    'no-iterator': 2,
101
+    'no-label-var': 2,
102
+    'no-labels': [2, {
103
+      'allowLoop': false,
104
+      'allowSwitch': false
105
+    }],
106
+    'no-lone-blocks': 2,
107
+    'no-mixed-spaces-and-tabs': 2,
108
+    'no-multi-spaces': 2,
109
+    'no-multi-str': 2,
110
+    'no-multiple-empty-lines': [2, {
111
+      'max': 1
112
+    }],
113
+    'no-native-reassign': 2,
114
+    'no-negated-in-lhs': 2,
115
+    'no-new-object': 2,
116
+    'no-new-require': 2,
117
+    'no-new-symbol': 2,
118
+    'no-new-wrappers': 2,
119
+    'no-obj-calls': 2,
120
+    'no-octal': 2,
121
+    'no-octal-escape': 2,
122
+    'no-path-concat': 2,
123
+    'no-proto': 2,
124
+    'no-redeclare': 2,
125
+    'no-regex-spaces': 2,
126
+    'no-return-assign': [2, 'except-parens'],
127
+    'no-self-assign': 2,
128
+    'no-self-compare': 2,
129
+    'no-sequences': 2,
130
+    'no-shadow-restricted-names': 2,
131
+    'no-spaced-func': 2,
132
+    'no-sparse-arrays': 2,
133
+    'no-this-before-super': 2,
134
+    'no-throw-literal': 2,
135
+    'no-trailing-spaces': 2,
136
+    'no-undef': 2,
137
+    'no-undef-init': 2,
138
+    'no-unexpected-multiline': 2,
139
+    'no-unmodified-loop-condition': 2,
140
+    'no-unneeded-ternary': [2, {
141
+      'defaultAssignment': false
142
+    }],
143
+    'no-unreachable': 2,
144
+    'no-unsafe-finally': 2,
145
+    'no-unused-vars': [2, {
146
+      'vars': 'all',
147
+      'args': 'none'
148
+    }],
149
+    'no-useless-call': 2,
150
+    'no-useless-computed-key': 2,
151
+    'no-useless-constructor': 2,
152
+    'no-useless-escape': 0,
153
+    'no-whitespace-before-property': 2,
154
+    'no-with': 2,
155
+    'one-var': [2, {
156
+      'initialized': 'never'
157
+    }],
158
+    'operator-linebreak': [2, 'after', {
159
+      'overrides': {
160
+        '?': 'before',
161
+        ':': 'before'
162
+      }
163
+    }],
164
+    'padded-blocks': [2, 'never'],
165
+    'quotes': [2, 'single', {
166
+      'avoidEscape': true,
167
+      'allowTemplateLiterals': true
168
+    }],
169
+    'semi': [2, 'never'],
170
+    'semi-spacing': [2, {
171
+      'before': false,
172
+      'after': true
173
+    }],
174
+    'space-before-blocks': [2, 'always'],
175
+    'space-before-function-paren': [2, 'never'],
176
+    'space-in-parens': [2, 'never'],
177
+    'space-infix-ops': 2,
178
+    'space-unary-ops': [2, {
179
+      'words': true,
180
+      'nonwords': false
181
+    }],
182
+    'spaced-comment': [2, 'always', {
183
+      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
184
+    }],
185
+    'template-curly-spacing': [2, 'never'],
186
+    'use-isnan': 2,
187
+    'valid-typeof': 2,
188
+    'wrap-iife': [2, 'any'],
189
+    'yield-star-spacing': [2, 'both'],
190
+    'yoda': [2, 'never'],
191
+    'prefer-const': 2,
192
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
193
+    'object-curly-spacing': [2, 'always', {
194
+      objectsInObjects: false
195
+    }],
196
+    'array-bracket-spacing': [2, 'never']
197
+  }
198
+}

+ 15
- 0
.gitignore View File

@@ -0,0 +1,15 @@
1
+.DS_Store
2
+node_modules/
3
+dist/
4
+npm-debug.log*
5
+yarn-debug.log*
6
+yarn-error.log*
7
+package-lock.json
8
+
9
+# Editor directories and files
10
+.idea
11
+.vscode
12
+*.suo
13
+*.ntvs*
14
+*.njsproj
15
+*.sln

+ 10
- 0
.postcssrc.js View File

@@ -0,0 +1,10 @@
1
+// https://github.com/michael-ciniawsky/postcss-load-config
2
+
3
+module.exports = {
4
+  "plugins": {
5
+    "postcss-import": {},
6
+    "postcss-url": {},
7
+    // to edit target browsers: use "browserslist" field in package.json
8
+    "autoprefixer": {}
9
+  }
10
+}

+ 5
- 0
.travis.yml View File

@@ -0,0 +1,5 @@
1
+language: node_js
2
+node_js: stable
3
+script: npm run test
4
+notifications:
5
+  email: false

+ 21
- 0
LICENSE View File

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

+ 77
- 0
README-zh.md View File

@@ -0,0 +1,77 @@
1
+# vueAdmin-template
2
+
3
+> 这是一个 极简的vue admin 管理后台 它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
4
+
5
+[线上地址](http://panjiachen.github.io/vueAdmin-template)
6
+
7
+## Extra
8
+如果你想要根据用户角色来动态生成侧边栏和router,你可以使用改分支[permission-control](https://github.com/PanJiaChen/vueAdmin-template/tree/permission-control)
9
10
+ ## 相关项目
11
+ [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
12
+
13
+ [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
14
+
15
+写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
16
+ - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
17
+ - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
18
+ - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
19
+ - [手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
20
+ - [手摸手,带你封装一个vue component](https://segmentfault.com/a/1190000009090836)
21
+
22
+
23
+## Build Setup
24
+
25
+``` bash
26
+
27
+# Clone project
28
+git clone https://github.com/PanJiaChen/vueAdmin-template.git
29
+
30
+# Install dependencies
31
+npm install
32
+
33
+# 建议不要用cnpm  安装有各种诡异的bug 可以通过如下操作解决npm速度慢的问题
34
+npm install --registry=https://registry.npm.taobao.org
35
+
36
+# serve with hot reload at localhost:9528
37
+npm run dev
38
+
39
+# build for production with minification
40
+npm run build
41
+
42
+# build for production and view the bundle analyzer report
43
+npm run build --report
44
+```
45
+
46
+## Demo
47
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
48
+
49
+### Element-Ui 使用cdn教程
50
+首先找到 `index.html` ([根目录下](https://github.com/PanJiaChen/vueAdmin-template/blob/element-ui-cdn/index.html))
51
+
52
+引入 Element的css和js ,并且引入 vue 。因为 Element-Ui 是依赖 vue 的,所以必须在它之前引入 vue 。
53
+
54
+之后找到 [webpack.base.conf.js](https://github.com/PanJiaChen/vueAdmin-template/blob/element-ui-cdn/build/webpack.base.conf.js) 加入 `externals` 让webpack 不打包 vue 和 element
55
+```
56
+externals: {
57
+  vue: 'Vue',
58
+  'element-ui':'ELEMENT'
59
+}
60
+```
61
+
62
+之后还有一个小细节是如果你用了全局对象方式引入vue,就不需要 手动 `Vue.use(Vuex)` ,它会自动挂载,具体见 [issue](https://github.com/vuejs/vuex/issues/731)
63
+
64
+最终你可以使用 `npm run build --report` 查看效果
65
+如图:
66
+![demo](https://panjiachen.github.io/images/element-cdn.png)
67
+
68
+**[具体代码](https://github.com/PanJiaChen/vueAdmin-template/commit/746aff560932704ae821f82f10b8b2a9681d5177)**
69
+
70
+**[对应分支](https://github.com/PanJiaChen/vueAdmin-template/tree/element-ui-cdn)**
71
+
72
+## License
73
+[MIT](https://github.com/PanJiaChen/vueAdmin-template/blob/master/LICENSE) license.
74
+
75
+Copyright (c) 2017-present PanJiaChen
76
+
77
+

+ 41
- 0
build/build.js View File

@@ -0,0 +1,41 @@
1
+'use strict'
2
+require('./check-versions')()
3
+
4
+process.env.NODE_ENV = 'production'
5
+
6
+const ora = require('ora')
7
+const rm = require('rimraf')
8
+const path = require('path')
9
+const chalk = require('chalk')
10
+const webpack = require('webpack')
11
+const config = require('../config')
12
+const webpackConfig = require('./webpack.prod.conf')
13
+
14
+const spinner = ora('building for production...')
15
+spinner.start()
16
+
17
+rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18
+  if (err) throw err
19
+  webpack(webpackConfig, (err, stats) => {
20
+    spinner.stop()
21
+    if (err) throw err
22
+    process.stdout.write(stats.toString({
23
+      colors: true,
24
+      modules: false,
25
+      children: false,
26
+      chunks: false,
27
+      chunkModules: false
28
+    }) + '\n\n')
29
+
30
+    if (stats.hasErrors()) {
31
+      console.log(chalk.red('  Build failed with errors.\n'))
32
+      process.exit(1)
33
+    }
34
+
35
+    console.log(chalk.cyan('  Build complete.\n'))
36
+    console.log(chalk.yellow(
37
+      '  Tip: built files are meant to be served over an HTTP server.\n' +
38
+      '  Opening index.html over file:// won\'t work.\n'
39
+    ))
40
+  })
41
+})

+ 54
- 0
build/check-versions.js View File

@@ -0,0 +1,54 @@
1
+'use strict'
2
+const chalk = require('chalk')
3
+const semver = require('semver')
4
+const packageConfig = require('../package.json')
5
+const shell = require('shelljs')
6
+
7
+function exec (cmd) {
8
+  return require('child_process').execSync(cmd).toString().trim()
9
+}
10
+
11
+const versionRequirements = [
12
+  {
13
+    name: 'node',
14
+    currentVersion: semver.clean(process.version),
15
+    versionRequirement: packageConfig.engines.node
16
+  }
17
+]
18
+
19
+if (shell.which('npm')) {
20
+  versionRequirements.push({
21
+    name: 'npm',
22
+    currentVersion: exec('npm --version'),
23
+    versionRequirement: packageConfig.engines.npm
24
+  })
25
+}
26
+
27
+module.exports = function () {
28
+  const warnings = []
29
+
30
+  for (let i = 0; i < versionRequirements.length; i++) {
31
+    const mod = versionRequirements[i]
32
+
33
+    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
34
+      warnings.push(mod.name + ': ' +
35
+        chalk.red(mod.currentVersion) + ' should be ' +
36
+        chalk.green(mod.versionRequirement)
37
+      )
38
+    }
39
+  }
40
+
41
+  if (warnings.length) {
42
+    console.log('')
43
+    console.log(chalk.yellow('To use this template, you must update following to modules:'))
44
+    console.log()
45
+
46
+    for (let i = 0; i < warnings.length; i++) {
47
+      const warning = warnings[i]
48
+      console.log('  ' + warning)
49
+    }
50
+
51
+    console.log()
52
+    process.exit(1)
53
+  }
54
+}

BIN
build/logo.png View File


+ 101
- 0
build/utils.js View File

@@ -0,0 +1,101 @@
1
+'use strict'
2
+const path = require('path')
3
+const config = require('../config')
4
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
5
+const packageConfig = require('../package.json')
6
+
7
+exports.assetsPath = function (_path) {
8
+  const assetsSubDirectory = process.env.NODE_ENV === 'production'
9
+    ? config.build.assetsSubDirectory
10
+    : config.dev.assetsSubDirectory
11
+
12
+  return path.posix.join(assetsSubDirectory, _path)
13
+}
14
+
15
+exports.cssLoaders = function (options) {
16
+  options = options || {}
17
+
18
+  const cssLoader = {
19
+    loader: 'css-loader',
20
+    options: {
21
+      sourceMap: options.sourceMap
22
+    }
23
+  }
24
+
25
+  const postcssLoader = {
26
+    loader: 'postcss-loader',
27
+    options: {
28
+      sourceMap: options.sourceMap
29
+    }
30
+  }
31
+
32
+  // generate loader string to be used with extract text plugin
33
+  function generateLoaders (loader, loaderOptions) {
34
+    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
35
+
36
+    if (loader) {
37
+      loaders.push({
38
+        loader: loader + '-loader',
39
+        options: Object.assign({}, loaderOptions, {
40
+          sourceMap: options.sourceMap
41
+        })
42
+      })
43
+    }
44
+
45
+    // Extract CSS when that option is specified
46
+    // (which is the case during production build)
47
+    if (options.extract) {
48
+      return ExtractTextPlugin.extract({
49
+        use: loaders,
50
+        fallback: 'vue-style-loader'
51
+      })
52
+    } else {
53
+      return ['vue-style-loader'].concat(loaders)
54
+    }
55
+  }
56
+
57
+  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
58
+  return {
59
+    css: generateLoaders(),
60
+    postcss: generateLoaders(),
61
+    less: generateLoaders('less'),
62
+    sass: generateLoaders('sass', { indentedSyntax: true }),
63
+    scss: generateLoaders('sass'),
64
+    stylus: generateLoaders('stylus'),
65
+    styl: generateLoaders('stylus')
66
+  }
67
+}
68
+
69
+// Generate loaders for standalone style files (outside of .vue)
70
+exports.styleLoaders = function (options) {
71
+  const output = []
72
+  const loaders = exports.cssLoaders(options)
73
+
74
+  for (const extension in loaders) {
75
+    const loader = loaders[extension]
76
+    output.push({
77
+      test: new RegExp('\\.' + extension + '$'),
78
+      use: loader
79
+    })
80
+  }
81
+
82
+  return output
83
+}
84
+
85
+exports.createNotifierCallback = () => {
86
+  const notifier = require('node-notifier')
87
+
88
+  return (severity, errors) => {
89
+    if (severity !== 'error') return
90
+
91
+    const error = errors[0]
92
+    const filename = error.file && error.file.split('!').pop()
93
+
94
+    notifier.notify({
95
+      title: packageConfig.name,
96
+      message: severity + ': ' + error.name,
97
+      subtitle: filename || '',
98
+      icon: path.join(__dirname, 'logo.png')
99
+    })
100
+  }
101
+}

+ 22
- 0
build/vue-loader.conf.js View File

@@ -0,0 +1,22 @@
1
+'use strict'
2
+const utils = require('./utils')
3
+const config = require('../config')
4
+const isProduction = process.env.NODE_ENV === 'production'
5
+const sourceMapEnabled = isProduction
6
+  ? config.build.productionSourceMap
7
+  : config.dev.cssSourceMap
8
+
9
+module.exports = {
10
+  loaders: utils.cssLoaders({
11
+    sourceMap: sourceMapEnabled,
12
+    extract: isProduction
13
+  }),
14
+  cssSourceMap: sourceMapEnabled,
15
+  cacheBusting: config.dev.cacheBusting,
16
+  transformToRequire: {
17
+    video: ['src', 'poster'],
18
+    source: 'src',
19
+    img: 'src',
20
+    image: 'xlink:href'
21
+  }
22
+}

+ 100
- 0
build/webpack.base.conf.js View File

@@ -0,0 +1,100 @@
1
+'use strict'
2
+const path = require('path')
3
+const utils = require('./utils')
4
+const config = require('../config')
5
+const vueLoaderConfig = require('./vue-loader.conf')
6
+
7
+function resolve (dir) {
8
+  return path.join(__dirname, '..', dir)
9
+}
10
+
11
+const createLintingRule = () => ({
12
+  test: /\.(js|vue)$/,
13
+  loader: 'eslint-loader',
14
+  enforce: 'pre',
15
+  include: [resolve('src'), resolve('test')],
16
+  options: {
17
+    formatter: require('eslint-friendly-formatter'),
18
+    emitWarning: !config.dev.showEslintErrorsInOverlay
19
+  }
20
+})
21
+
22
+module.exports = {
23
+  context: path.resolve(__dirname, '../'),
24
+  entry: {
25
+    app: './src/main.js'
26
+  },
27
+  output: {
28
+    path: config.build.assetsRoot,
29
+    filename: '[name].js',
30
+    publicPath: process.env.NODE_ENV === 'production'
31
+      ? config.build.assetsPublicPath
32
+      : config.dev.assetsPublicPath
33
+  },
34
+  resolve: {
35
+    extensions: ['.js', '.vue', '.json'],
36
+    alias: {
37
+      '@': resolve('src')
38
+    }
39
+  },
40
+  module: {
41
+    rules: [
42
+      ...(config.dev.useEslint ? [createLintingRule()] : []),
43
+      {
44
+        test: /\.vue$/,
45
+        loader: 'vue-loader',
46
+        options: vueLoaderConfig
47
+      },
48
+      {
49
+        test: /\.js$/,
50
+        loader: 'babel-loader',
51
+        include: [resolve('src'), resolve('test') ,resolve('node_modules/webpack-dev-server/client')]
52
+      },
53
+      {
54
+        test: /\.svg$/,
55
+        loader: 'svg-sprite-loader',
56
+        include: [resolve('src/icons')],
57
+        options: {
58
+          symbolId: 'icon-[name]'
59
+        }
60
+      },
61
+      {
62
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
63
+        loader: 'url-loader',
64
+        exclude: [resolve('src/icons')],
65
+        options: {
66
+          limit: 10000,
67
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
68
+        }
69
+      },
70
+      {
71
+        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
72
+        loader: 'url-loader',
73
+        options: {
74
+          limit: 10000,
75
+          name: utils.assetsPath('media/[name].[hash:7].[ext]')
76
+        }
77
+      },
78
+      {
79
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
80
+        loader: 'url-loader',
81
+        options: {
82
+          limit: 10000,
83
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
84
+        }
85
+      }
86
+    ]
87
+  },
88
+  node: {
89
+    // prevent webpack from injecting useless setImmediate polyfill because Vue
90
+    // source contains it (although only uses it if it's native).
91
+    setImmediate: false,
92
+    // prevent webpack from injecting mocks to Node native modules
93
+    // that does not make sense for the client
94
+    dgram: 'empty',
95
+    fs: 'empty',
96
+    net: 'empty',
97
+    tls: 'empty',
98
+    child_process: 'empty'
99
+  }
100
+}

+ 87
- 0
build/webpack.dev.conf.js View File

@@ -0,0 +1,87 @@
1
+'use strict'
2
+const path = require('path')
3
+const utils = require('./utils')
4
+const webpack = require('webpack')
5
+const config = require('../config')
6
+const merge = require('webpack-merge')
7
+const baseWebpackConfig = require('./webpack.base.conf')
8
+const HtmlWebpackPlugin = require('html-webpack-plugin')
9
+const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
10
+const portfinder = require('portfinder')
11
+
12
+function resolve (dir) {
13
+  return path.join(__dirname, '..', dir)
14
+}
15
+
16
+const HOST = process.env.HOST
17
+const PORT = process.env.PORT && Number(process.env.PORT)
18
+
19
+const devWebpackConfig = merge(baseWebpackConfig, {
20
+  module: {
21
+    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
22
+  },
23
+  // cheap-module-eval-source-map is faster for development
24
+  devtool: config.dev.devtool,
25
+
26
+  // these devServer options should be customized in /config/index.js
27
+  devServer: {
28
+    clientLogLevel: 'warning',
29
+    historyApiFallback: true,
30
+    hot: true,
31
+    compress: true,
32
+    host: HOST || config.dev.host,
33
+    port: PORT || config.dev.port,
34
+    open: config.dev.autoOpenBrowser,
35
+    overlay: config.dev.errorOverlay
36
+      ? { warnings: false, errors: true }
37
+      : false,
38
+    publicPath: config.dev.assetsPublicPath,
39
+    proxy: config.dev.proxyTable,
40
+    quiet: true, // necessary for FriendlyErrorsPlugin
41
+    watchOptions: {
42
+      poll: config.dev.poll,
43
+    }
44
+  },
45
+  plugins: [
46
+    new webpack.DefinePlugin({
47
+      'process.env': require('../config/dev.env')
48
+    }),
49
+    new webpack.HotModuleReplacementPlugin(),
50
+    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
51
+    new webpack.NoEmitOnErrorsPlugin(),
52
+    // https://github.com/ampedandwired/html-webpack-plugin
53
+    new HtmlWebpackPlugin({
54
+      filename: 'index.html',
55
+      template: 'index.html',
56
+      inject: true,
57
+      favicon: resolve('favicon.ico'),
58
+      title: 'vue-element-admin'
59
+    }),
60
+  ]
61
+})
62
+
63
+module.exports = new Promise((resolve, reject) => {
64
+  portfinder.basePort = process.env.PORT || config.dev.port
65
+  portfinder.getPort((err, port) => {
66
+    if (err) {
67
+      reject(err)
68
+    } else {
69
+      // publish the new Port, necessary for e2e tests
70
+      process.env.PORT = port
71
+      // add port to devServer config
72
+      devWebpackConfig.devServer.port = port
73
+
74
+      // Add FriendlyErrorsPlugin
75
+      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
76
+        compilationSuccessInfo: {
77
+          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
78
+        },
79
+        onErrors: config.dev.notifyOnErrors
80
+        ? utils.createNotifierCallback()
81
+        : undefined
82
+      }))
83
+
84
+      resolve(devWebpackConfig)
85
+    }
86
+  })
87
+})

+ 150
- 0
build/webpack.prod.conf.js View File

@@ -0,0 +1,150 @@
1
+'use strict'
2
+const path = require('path')
3
+const utils = require('./utils')
4
+const webpack = require('webpack')
5
+const config = require('../config')
6
+const merge = require('webpack-merge')
7
+const baseWebpackConfig = require('./webpack.base.conf')
8
+const CopyWebpackPlugin = require('copy-webpack-plugin')
9
+const HtmlWebpackPlugin = require('html-webpack-plugin')
10
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
11
+const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
13
+
14
+function resolve (dir) {
15
+  return path.join(__dirname, '..', dir)
16
+}
17
+
18
+const env = require('../config/prod.env')
19
+
20
+const webpackConfig = merge(baseWebpackConfig, {
21
+  module: {
22
+    rules: utils.styleLoaders({
23
+      sourceMap: config.build.productionSourceMap,
24
+      extract: true,
25
+      usePostCSS: true
26
+    })
27
+  },
28
+  devtool: config.build.productionSourceMap ? config.build.devtool : false,
29
+  output: {
30
+    path: config.build.assetsRoot,
31
+    filename: utils.assetsPath('js/[name].[chunkhash].js'),
32
+    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
33
+  },
34
+  plugins: [
35
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
36
+    new webpack.DefinePlugin({
37
+      'process.env': env
38
+    }),
39
+    new UglifyJsPlugin({
40
+      uglifyOptions: {
41
+        compress: {
42
+          warnings: false
43
+        }
44
+      },
45
+      sourceMap: config.build.productionSourceMap,
46
+      parallel: true
47
+    }),
48
+    // extract css into its own file
49
+    new ExtractTextPlugin({
50
+      filename: utils.assetsPath('css/[name].[contenthash].css'),
51
+      // Setting the following option to `false` will not extract CSS from codesplit chunks.
52
+      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
53
+      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
54
+      allChunks: false,
55
+    }),
56
+    // Compress extracted CSS. We are using this plugin so that possible
57
+    // duplicated CSS from different components can be deduped.
58
+    new OptimizeCSSPlugin({
59
+      cssProcessorOptions: config.build.productionSourceMap
60
+        ? { safe: true, map: { inline: false } }
61
+        : { safe: true }
62
+    }),
63
+    // generate dist index.html with correct asset hash for caching.
64
+    // you can customize output by editing /index.html
65
+    // see https://github.com/ampedandwired/html-webpack-plugin
66
+    new HtmlWebpackPlugin({
67
+      filename: config.build.index,
68
+      template: 'index.html',
69
+      inject: true,
70
+      favicon: resolve('favicon.ico'),
71
+      title: 'vue-element-admin',
72
+      minify: {
73
+        removeComments: true,
74
+        collapseWhitespace: true,
75
+        removeAttributeQuotes: true
76
+        // more options:
77
+        // https://github.com/kangax/html-minifier#options-quick-reference
78
+      },
79
+      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
80
+      chunksSortMode: 'dependency'
81
+    }),
82
+    // keep module.id stable when vender modules does not change
83
+    new webpack.HashedModuleIdsPlugin(),
84
+    // enable scope hoisting
85
+    new webpack.optimize.ModuleConcatenationPlugin(),
86
+    // split vendor js into its own file
87
+    new webpack.optimize.CommonsChunkPlugin({
88
+      name: 'vendor',
89
+      minChunks (module) {
90
+        // any required modules inside node_modules are extracted to vendor
91
+        return (
92
+          module.resource &&
93
+          /\.js$/.test(module.resource) &&
94
+          module.resource.indexOf(
95
+            path.join(__dirname, '../node_modules')
96
+          ) === 0
97
+        )
98
+      }
99
+    }),
100
+    // extract webpack runtime and module manifest to its own file in order to
101
+    // prevent vendor hash from being updated whenever app bundle is updated
102
+    new webpack.optimize.CommonsChunkPlugin({
103
+      name: 'manifest',
104
+      minChunks: Infinity
105
+    }),
106
+    // This instance extracts shared chunks from code splitted chunks and bundles them
107
+    // in a separate chunk, similar to the vendor chunk
108
+    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
109
+    new webpack.optimize.CommonsChunkPlugin({
110
+      name: 'app',
111
+      async: 'vendor-async',
112
+      children: true,
113
+      minChunks: 3
114
+    }),
115
+
116
+    // copy custom static assets
117
+    new CopyWebpackPlugin([
118
+      {
119
+        from: path.resolve(__dirname, '../static'),
120
+        to: config.build.assetsSubDirectory,
121
+        ignore: ['.*']
122
+      }
123
+    ])
124
+  ]
125
+})
126
+
127
+if (config.build.productionGzip) {
128
+  const CompressionWebpackPlugin = require('compression-webpack-plugin')
129
+
130
+  webpackConfig.plugins.push(
131
+    new CompressionWebpackPlugin({
132
+      asset: '[path].gz[query]',
133
+      algorithm: 'gzip',
134
+      test: new RegExp(
135
+        '\\.(' +
136
+        config.build.productionGzipExtensions.join('|') +
137
+        ')$'
138
+      ),
139
+      threshold: 10240,
140
+      minRatio: 0.8
141
+    })
142
+  )
143
+}
144
+
145
+if (config.build.bundleAnalyzerReport) {
146
+  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
147
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
148
+}
149
+
150
+module.exports = webpackConfig

+ 8
- 0
config/dev.env.js View File

@@ -0,0 +1,8 @@
1
+'use strict'
2
+const merge = require('webpack-merge')
3
+const prodEnv = require('./prod.env')
4
+
5
+module.exports = merge(prodEnv, {
6
+  NODE_ENV: '"development"',
7
+  BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
8
+})

+ 89
- 0
config/index.js View File

@@ -0,0 +1,89 @@
1
+'use strict'
2
+// Template version: 1.2.6
3
+// see http://vuejs-templates.github.io/webpack for documentation.
4
+
5
+const path = require('path')
6
+
7
+module.exports = {
8
+  dev: {
9
+
10
+    // Paths
11
+    assetsSubDirectory: 'static',
12
+    assetsPublicPath: '/',
13
+    proxyTable: {},
14
+
15
+    // Various Dev Server settings
16
+    host: 'localhost', // can be overwritten by process.env.HOST
17
+    port: 9528, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
18
+    autoOpenBrowser: true,
19
+    errorOverlay: true,
20
+    notifyOnErrors: false,
21
+    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
22
+
23
+    // Use Eslint Loader?
24
+    // If true, your code will be linted during bundling and
25
+    // linting errors and warnings will be shown in the console.
26
+    useEslint: true,
27
+    // If true, eslint errors and warnings will also be shown in the error overlay
28
+    // in the browser.
29
+    showEslintErrorsInOverlay: false,
30
+
31
+    /**
32
+     * Source Maps
33
+     */
34
+
35
+    // https://webpack.js.org/configuration/devtool/#development
36
+    devtool: 'cheap-source-map',
37
+
38
+    // If you have problems debugging vue-files in devtools,
39
+    // set this to false - it *may* help
40
+    // https://vue-loader.vuejs.org/en/options.html#cachebusting
41
+    cacheBusting: true,
42
+
43
+    // CSS Sourcemaps off by default because relative paths are "buggy"
44
+    // with this option, according to the CSS-Loader README
45
+    // (https://github.com/webpack/css-loader#sourcemaps)
46
+    // In our experience, they generally work as expected,
47
+    // just be aware of this issue when enabling this option.
48
+    cssSourceMap: false,
49
+  },
50
+
51
+  build: {
52
+    // Template for index.html
53
+    index: path.resolve(__dirname, '../dist/index.html'),
54
+
55
+    // Paths
56
+    assetsRoot: path.resolve(__dirname, '../dist'),
57
+    assetsSubDirectory: 'static',
58
+
59
+    /**
60
+     * You can set by youself according to actual condition
61
+     * You will need to set this if you plan to deploy your site under a sub path,
62
+     * for example GitHub pages. If you plan to deploy your site to https://foo.github.io/bar/,
63
+     * then assetsPublicPath should be set to "/bar/".
64
+     * In most cases please use '/' !!!
65
+     */
66
+    assetsPublicPath: '/vueAdmin-template/', // If you are deployed on the root path, please use '/'
67
+
68
+    /**
69
+     * Source Maps
70
+     */
71
+
72
+    productionSourceMap: false,
73
+    // https://webpack.js.org/configuration/devtool/#production
74
+    devtool: '#source-map',
75
+
76
+    // Gzip off by default as many popular static hosts such as
77
+    // Surge or Netlify already gzip all static assets for you.
78
+    // Before setting to `true`, make sure to:
79
+    // npm install --save-dev compression-webpack-plugin
80
+    productionGzip: false,
81
+    productionGzipExtensions: ['js', 'css'],
82
+
83
+    // Run the build command with an extra argument to
84
+    // View the bundle analyzer report after build finishes:
85
+    // `npm run build --report`
86
+    // Set to `true` or `false` to always turn it on or off
87
+    bundleAnalyzerReport: process.env.npm_config_report
88
+  }
89
+}

+ 5
- 0
config/prod.env.js View File

@@ -0,0 +1,5 @@
1
+'use strict'
2
+module.exports = {
3
+  NODE_ENV: '"production"',
4
+  BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
5
+}

BIN
favicon.ico View File


+ 12
- 0
index.html View File

@@ -0,0 +1,12 @@
1
+<!DOCTYPE html>
2
+<html>
3
+  <head>
4
+    <meta charset="utf-8">
5
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+    <title>vue-admin-template</title>
7
+  </head>
8
+  <body>
9
+    <div id="app"></div>
10
+    <!-- built files will be auto injected -->
11
+  </body>
12
+</html>

+ 80
- 0
package.json View File

@@ -0,0 +1,80 @@
1
+{
2
+  "name": "vue-admin-template",
3
+  "version": "3.6.0",
4
+  "license": "MIT",
5
+  "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
6
+  "author": "Pan <panfree23@gmail.com>",
7
+  "scripts": {
8
+    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
9
+    "start": "npm run dev",
10
+    "build": "node build/build.js",
11
+    "build:report": "npm_config_report=true node build/build.js",
12
+    "lint": "eslint --ext .js,.vue src",
13
+    "test": "npm run lint"
14
+  },
15
+  "dependencies": {
16
+    "axios": "0.17.1",
17
+    "element-ui": "2.3.4",
18
+    "js-cookie": "2.2.0",
19
+    "normalize.css": "7.0.0",
20
+    "nprogress": "0.2.0",
21
+    "vue": "2.5.10",
22
+    "vue-router": "3.0.1",
23
+    "vuex": "3.0.1"
24
+  },
25
+  "devDependencies": {
26
+    "autoprefixer": "7.2.3",
27
+    "babel-core": "6.26.0",
28
+    "babel-eslint": "8.0.3",
29
+    "babel-helper-vue-jsx-merge-props": "2.0.3",
30
+    "babel-loader": "7.1.2",
31
+    "babel-plugin-syntax-jsx": "6.18.0",
32
+    "babel-plugin-transform-runtime": "6.23.0",
33
+    "babel-plugin-transform-vue-jsx": "3.5.0",
34
+    "babel-preset-env": "1.6.1",
35
+    "babel-preset-stage-2": "6.24.1",
36
+    "chalk": "2.3.0",
37
+    "copy-webpack-plugin": "4.2.3",
38
+    "css-loader": "0.28.7",
39
+    "eslint": "4.13.1",
40
+    "eslint-friendly-formatter": "3.0.0",
41
+    "eslint-loader": "1.9.0",
42
+    "eslint-plugin-html": "4.0.1",
43
+    "eventsource-polyfill": "0.9.6",
44
+    "extract-text-webpack-plugin": "3.0.2",
45
+    "file-loader": "1.1.5",
46
+    "friendly-errors-webpack-plugin": "1.6.1",
47
+    "html-webpack-plugin": "2.30.1",
48
+    "node-notifier": "5.1.2",
49
+    "node-sass": "^4.7.2",
50
+    "optimize-css-assets-webpack-plugin": "3.2.0",
51
+    "ora": "1.3.0",
52
+    "portfinder": "1.0.13",
53
+    "postcss-import": "11.0.0",
54
+    "postcss-loader": "2.0.9",
55
+    "postcss-url": "7.3.0",
56
+    "rimraf": "2.6.2",
57
+    "sass-loader": "6.0.6",
58
+    "semver": "5.4.1",
59
+    "shelljs": "0.7.8",
60
+    "svg-sprite-loader": "3.5.2",
61
+    "uglifyjs-webpack-plugin": "1.1.3",
62
+    "url-loader": "0.6.2",
63
+    "vue-loader": "13.7.2",
64
+    "vue-style-loader": "3.0.3",
65
+    "vue-template-compiler": "2.5.10",
66
+    "webpack": "3.10.0",
67
+    "webpack-bundle-analyzer": "2.9.1",
68
+    "webpack-dev-server": "2.9.7",
69
+    "webpack-merge": "4.1.1"
70
+  },
71
+  "engines": {
72
+    "node": ">= 4.0.0",
73
+    "npm": ">= 3.0.0"
74
+  },
75
+  "browserslist": [
76
+    "> 1%",
77
+    "last 2 versions",
78
+    "not ie <= 8"
79
+  ]
80
+}

+ 11
- 0
src/App.vue View File

@@ -0,0 +1,11 @@
1
+<template>
2
+  <div id="app">
3
+    <router-view></router-view>
4
+  </div>
5
+</template>
6
+
7
+<script>
8
+export default {
9
+  name: 'App'
10
+}
11
+</script>

+ 27
- 0
src/api/login.js View File

@@ -0,0 +1,27 @@
1
+import request from '@/utils/request'
2
+
3
+export function login(username, password) {
4
+  return request({
5
+    url: '/user/login',
6
+    method: 'post',
7
+    data: {
8
+      username,
9
+      password
10
+    }
11
+  })
12
+}
13
+
14
+export function getInfo(token) {
15
+  return request({
16
+    url: '/user/info',
17
+    method: 'get',
18
+    params: { token }
19
+  })
20
+}
21
+
22
+export function logout() {
23
+  return request({
24
+    url: '/user/logout',
25
+    method: 'post'
26
+  })
27
+}

+ 9
- 0
src/api/table.js View File

@@ -0,0 +1,9 @@
1
+import request from '@/utils/request'
2
+
3
+export function getList(params) {
4
+  return request({
5
+    url: '/table/list',
6
+    method: 'get',
7
+    params
8
+  })
9
+}

BIN
src/assets/404_images/404.png View File


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


+ 51
- 0
src/components/Breadcrumb/index.vue View File

@@ -0,0 +1,51 @@
1
+<template>
2
+  <el-breadcrumb class="app-breadcrumb" separator="/">
3
+    <transition-group name="breadcrumb">
4
+      <el-breadcrumb-item v-for="(item,index)  in levelList" :key="item.path" v-if="item.meta.title">
5
+        <span v-if="item.redirect==='noredirect'||index==levelList.length-1" class="no-redirect">{{item.meta.title}}</span>
6
+        <router-link v-else :to="item.redirect||item.path">{{item.meta.title}}</router-link>
7
+      </el-breadcrumb-item>
8
+    </transition-group>
9
+  </el-breadcrumb>
10
+</template>
11
+
12
+<script>
13
+export default {
14
+  created() {
15
+    this.getBreadcrumb()
16
+  },
17
+  data() {
18
+    return {
19
+      levelList: null
20
+    }
21
+  },
22
+  watch: {
23
+    $route() {
24
+      this.getBreadcrumb()
25
+    }
26
+  },
27
+  methods: {
28
+    getBreadcrumb() {
29
+      let matched = this.$route.matched.filter(item => item.name)
30
+      const first = matched[0]
31
+      if (first && first.name !== 'dashboard') {
32
+        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
33
+      }
34
+      this.levelList = matched
35
+    }
36
+  }
37
+}
38
+</script>
39
+
40
+<style rel="stylesheet/scss" lang="scss" scoped>
41
+  .app-breadcrumb.el-breadcrumb {
42
+    display: inline-block;
43
+    font-size: 14px;
44
+    line-height: 50px;
45
+    margin-left: 10px;
46
+    .no-redirect {
47
+      color: #97a8be;
48
+      cursor: text;
49
+    }
50
+  }
51
+</style>

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

@@ -0,0 +1,44 @@
1
+<template>
2
+  <div>
3
+    <svg t="1492500959545" @click="toggleClick" class="hamburger" :class="{'is-active':isActive}" style="" viewBox="0 0 1024 1024"
4
+      version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1691" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64">
5
+      <path d="M966.8023 568.849776 57.196677 568.849776c-31.397081 0-56.850799-25.452695-56.850799-56.850799l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 543.397081 998.200404 568.849776 966.8023 568.849776z"
6
+        p-id="1692"></path>
7
+      <path d="M966.8023 881.527125 57.196677 881.527125c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 856.07443 998.200404 881.527125 966.8023 881.527125z"
8
+        p-id="1693"></path>
9
+      <path d="M966.8023 256.17345 57.196677 256.17345c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.850799 56.850799-56.850799l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.850799l0 0C1023.653099 230.720755 998.200404 256.17345 966.8023 256.17345z"
10
+        p-id="1694"></path>
11
+    </svg>
12
+  </div>
13
+</template>
14
+
15
+<script>
16
+export default {
17
+  name: 'hamburger',
18
+  props: {
19
+    isActive: {
20
+      type: Boolean,
21
+      default: false
22
+    },
23
+    toggleClick: {
24
+      type: Function,
25
+      default: null
26
+    }
27
+  }
28
+}
29
+</script>
30
+
31
+<style scoped>
32
+.hamburger {
33
+	display: inline-block;
34
+	cursor: pointer;
35
+	width: 20px;
36
+	height: 20px;
37
+	transform: rotate(90deg);
38
+	transition: .38s;
39
+	transform-origin: 50% 50%;
40
+}
41
+.hamburger.is-active {
42
+	transform: rotate(0deg);
43
+}
44
+</style>

+ 42
- 0
src/components/SvgIcon/index.vue View File

@@ -0,0 +1,42 @@
1
+<template>
2
+  <svg :class="svgClass" aria-hidden="true">
3
+    <use :xlink:href="iconName"></use>
4
+  </svg>
5
+</template>
6
+
7
+<script>
8
+export default {
9
+  name: 'svg-icon',
10
+  props: {
11
+    iconClass: {
12
+      type: String,
13
+      required: true
14
+    },
15
+    className: {
16
+      type: String
17
+    }
18
+  },
19
+  computed: {
20
+    iconName() {
21
+      return `#icon-${this.iconClass}`
22
+    },
23
+    svgClass() {
24
+      if (this.className) {
25
+        return 'svg-icon ' + this.className
26
+      } else {
27
+        return 'svg-icon'
28
+      }
29
+    }
30
+  }
31
+}
32
+</script>
33
+
34
+<style scoped>
35
+.svg-icon {
36
+  width: 1em;
37
+  height: 1em;
38
+  vertical-align: -0.15em;
39
+  fill: currentColor;
40
+  overflow: hidden;
41
+}
42
+</style>

+ 9
- 0
src/icons/index.js View File

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

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

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1511504199105" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1815" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M770.56 460.8h250.88C998.4 220.16 803.84 25.6 563.2 2.56v250.88c104.96 20.48 186.88 102.4 207.36 207.36z m0 0M460.8 253.44V2.56C220.16 25.6 25.6 220.16 2.56 460.8h250.88c20.48-104.96 102.4-186.88 207.36-207.36z m0 0M563.2 770.56v250.88c243.2-23.04 435.2-217.6 460.8-460.8H773.12C750.08 668.16 668.16 750.08 563.2 770.56z m0 0M253.44 563.2H2.56c23.04 243.2 217.6 435.2 460.8 460.8V773.12C355.84 750.08 273.92 668.16 253.44 563.2z m0 0" fill="" p-id="1816"></path></svg>

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

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1503993826520" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7878" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M941.677063 391.710356c9.337669-14.005992 6.224772-32.68133-6.224772-43.575447-14.005992-10.894118-32.68133-7.78122-43.575447 6.224771-1.556449 1.556449-174.300768 205.426673-379.727441 205.426673-199.200878 0-379.727441-205.426673-381.28389-206.982098-10.894118-12.450567-31.124881-14.005992-43.575448-3.112898-12.450567 10.894118-14.005992 31.124881-3.112897 43.575448 3.112897 4.668323 40.46255 46.687322 99.600439 93.375667l-79.369676 82.48155c-12.450567 12.450567-10.894118 32.68133 1.556449 43.575448 3.112897 6.224772 10.894118 9.337669 18.675338 9.337669 7.78122 0 15.562441-3.112897 21.787213-9.337669l85.594447-88.706321c40.46255 28.013007 88.706321 54.469566 141.619438 73.14388L340.959485 707.631586c-4.668323 17.118889 4.669346 34.237779 21.787213 38.906101h9.337669c14.005992 0 26.456558-9.337669 29.568432-23.343661l32.68133-110.494556c24.90011 4.668323 51.356668 7.78122 77.813227 7.78122s52.913117-3.112897 77.813227-7.78122l32.68133 108.938108c3.112897 14.005992 17.118889 23.343661 29.569456 23.343661 3.112897 0 6.224772 0 7.78122-1.556449 17.118889-4.669346 26.456558-21.787212 21.788236-38.906102l-32.68133-108.938108c52.913117-18.675338 101.156888-45.131897 141.619438-73.14388l84.037998 87.150896c6.224772 6.224772 14.005992 9.337669 21.787212 9.337669 7.78122 0 15.562441-3.112897 21.787212-9.337669 12.450567-12.450567 12.450567-31.124881 1.556449-43.575448l-79.369675-82.48155c63.808258-46.688345 101.158934-91.820242 101.158934-91.820242z" p-id="7879"></path></svg>

+ 1
- 0
src/icons/svg/form.svg View File

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1511504319223" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3230" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M942.827259 80.3367c-11.42419-11.406794-26.41051-17.117866-41.377386-17.117866-14.985296 0-29.952172 5.711072-41.358967 17.117866L719.392444 221.014696l-19.441794 19.441794L681.577187 258.832 569.516971 370.909611 375.99749 564.411697l0 0.019443 0 84.372619 81.145112 0 0.010233 0 95.418186-95.435583 213.398228-213.400275 3.14155-3.14155-0.019443 0 9.979282-9.977235 0 0L942.827259 163.073052C965.697129 140.259464 965.697129 103.186104 942.827259 80.3367z" p-id="3231"></path><path d="M793.542234 367.521444 580.14196 580.939115 484.72582 676.376745 473.299583 687.800935 457.152834 687.800935 375.99749 687.800935 337.000314 687.800935 337.000314 648.803759 337.000314 564.411697 337.000314 548.264948 348.424504 536.838711 541.943986 343.338672 654.004201 231.259014 665.428392 219.834824 64.020082 219.834824 64.020082 960.781166 804.966425 960.781166 804.966425 356.116697 796.607036 364.475062Z" p-id="3232"></path></svg>

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

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1529559567446" class="icon" style="" viewBox="0 0 1167 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1767" xmlns:xlink="http://www.w3.org/1999/xlink" width="227.9296875" height="200"><defs><style type="text/css"></style></defs><path d="M0.015952 74.459413A2.286 2.286 1440 1 0 145.85218 74.459413 2.286 2.286 1440 1 0 0.015952 74.459413zM291.720312 1.525347 1166.801488 1.525347 1166.801488 147.361574 291.720312 147.361574zM291.720312 366.163773A2.286 2.286 1440 1 0 437.55654 366.163773 2.286 2.286 1440 1 0 291.720312 366.163773zM583.424672 293.229707 1166.801488 293.229707 1166.801488 439.065934 583.424672 439.065934zM291.720312 949.540588A2.286 2.286 1440 1 0 437.55654 949.540588 2.286 2.286 1440 1 0 291.720312 949.540588zM583.424672 876.638427 1166.801488 876.638427 1166.801488 1022.474654 583.424672 1022.474654zM583.424672 657.836228A2.286 2.286 1440 1 0 729.2609 657.836228 2.286 2.286 1440 1 0 583.424672 657.836228zM875.129032 584.934067 1166.801488 584.934067 1166.801488 730.770294 875.129032 730.770294z" p-id="1768"></path></svg>

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

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1503994678729" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9229" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M780.8 354.579692 665.6 354.579692 665.6 311.689846c0-72.310154-19.849846-193.299692-153.6-193.299692-138.870154 0-153.6 135.049846-153.6 193.299692l0 42.889846L243.2 354.579692 243.2 311.689846C243.2 122.249846 348.790154 0 512 0s268.8 122.249846 268.8 311.689846L780.8 354.579692zM588.8 669.420308C588.8 625.900308 554.220308 590.769231 512 590.769231s-76.8 35.131077-76.8 78.651077c0 29.459692 15.399385 54.468923 38.439385 67.820308l0 89.639385c0 21.740308 17.250462 39.699692 38.4 39.699692s38.4-17.959385 38.4-39.699692l0-89.639385C573.44 723.889231 588.8 698.88 588.8 669.420308zM896 512l0 393.609846c0 65.260308-51.869538 118.390154-115.2 118.390154L243.2 1024c-63.291077 0-115.2-53.129846-115.2-118.390154L128 512c0-65.220923 51.869538-118.390154 115.2-118.390154l537.6 0C844.130462 393.609846 896 446.779077 896 512z" p-id="9230"></path></svg>

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

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1511504440567" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5070" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M568.6 0h454.9v454.9H568.6V0z m0 568.6h454.9v454.9H568.6V568.6zM0 568.6h454.9v454.9H0V568.6zM0 0h454.9v454.9H0V0z" fill="" p-id="5071"></path></svg>

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

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1511512690058" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3507" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M1013.703 693.345c6.865 6.865 10.297 14.874 10.297 24.027l0 205.944c0 9.916-3.432 18.115-10.297 24.599-6.865 6.483-15.255 9.725-25.171 9.725L782.588 957.64c-9.153 0-17.162-3.242-24.027-9.725-6.865-6.483-10.297-14.683-10.297-24.599L748.264 717.372c0-6.102 1.526-11.823 4.577-17.162s7.246-9.534 12.586-12.586 11.06-4.577 17.162-4.577l77.801 0L860.39 546.896c0-4.577-1.144-8.772-3.432-12.586s-5.339-6.865-9.153-9.153-8.009-3.432-12.585-3.432L543.464 521.725l0 161.323 77.801 0c9.153 0 17.162 3.432 24.027 10.297s10.297 14.874 10.297 24.027l0 205.944c0 6.102-1.526 11.823-4.577 17.162s-7.246 9.534-12.585 12.585-11.06 4.577-17.162 4.577L415.321 957.64c-6.102 0-11.823-1.526-17.162-4.577s-9.725-7.246-13.158-12.585-5.149-11.06-5.149-17.162L379.852 717.372c0-9.153 3.432-17.162 10.297-24.027s15.255-10.297 25.171-10.297l76.657 0L491.977 521.725 188.782 521.725c-7.628 0-13.92 2.479-18.878 7.437-4.958 4.958-7.437 10.869-7.437 17.734l0 136.152 77.801 0c9.916 0 18.115 3.432 24.599 10.297s9.725 14.874 9.725 24.027l0 205.944c0 9.916-3.242 18.115-9.725 24.599-6.483 6.483-14.683 9.725-24.599 9.725L34.324 957.64c-3.814 0-7.437-0.572-10.869-1.716-3.432-1.144-6.483-2.67-9.153-4.577-2.67-1.907-5.149-4.386-7.437-7.437-2.288-3.051-4.004-6.293-5.149-9.725C0.572 930.753 0 927.13 0 923.316L0 717.372c0-3.051 0.381-6.102 1.144-9.153s1.907-5.721 3.432-8.009 3.432-4.577 5.721-6.865 4.577-4.195 6.865-5.721 4.958-2.67 8.009-3.432 6.102-1.144 9.153-1.144l77.801 0L112.125 495.41c0-6.865 2.479-12.776 7.437-17.734s10.869-7.437 17.734-7.437l354.682 0L491.978 342.096l-76.657 0c-9.916 0-18.306-3.432-25.171-10.297s-10.297-14.874-10.297-24.027L379.853 101.828c0-9.916 3.432-18.306 10.297-25.171s15.255-10.297 25.171-10.297l205.944 0c6.102 0 11.823 1.716 17.162 5.149 5.339 3.432 9.534 7.818 12.585 13.158 3.051 5.339 4.577 11.06 4.577 17.162l0 205.944c0 9.153-3.432 17.162-10.297 24.027s-14.874 10.297-24.027 10.297l-77.801 0 0 128.143L885.56 470.24c7.628 0 13.92 2.479 18.878 7.437s7.437 10.869 7.437 17.734l0 187.638 76.657 0C998.448 683.048 1006.838 686.48 1013.703 693.345z" p-id="3508"></path></svg>

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

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1503993891882" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7986" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M504.951 511.98c93.49 0 169.28-74.002 169.28-165.26 0-91.276-75.79-165.248-169.28-165.248-93.486 0-169.287 73.972-169.279 165.248-0.001 91.258 75.793 165.26 169.28 165.26z m77.6 55.098H441.466c-120.767 0-218.678 95.564-218.678 213.45V794.3c0 48.183 97.911 48.229 218.678 48.229H582.55c120.754 0 218.66-1.78 218.66-48.229v-13.77c0-117.887-97.898-213.45-218.66-213.45z" p-id="7987"></path></svg>

+ 27
- 0
src/main.js View File

@@ -0,0 +1,27 @@
1
+import Vue from 'vue'
2
+
3
+import 'normalize.css/normalize.css'// A modern alternative to CSS resets
4
+
5
+import ElementUI from 'element-ui'
6
+import 'element-ui/lib/theme-chalk/index.css'
7
+import locale from 'element-ui/lib/locale/lang/en' // lang i18n
8
+
9
+import '@/styles/index.scss' // global css
10
+
11
+import App from './App'
12
+import router from './router'
13
+import store from './store'
14
+
15
+import '@/icons' // icon
16
+import '@/permission' // permission control
17
+
18
+Vue.use(ElementUI, { locale })
19
+
20
+Vue.config.productionTip = false
21
+
22
+new Vue({
23
+  el: '#app',
24
+  router,
25
+  store,
26
+  render: h => h(App)
27
+})

+ 51
- 0
src/permission.js View File

@@ -0,0 +1,51 @@
1
+import router from './router'
2
+import store from './store'
3
+import { Message } from 'element-ui'
4
+import NProgress from 'nprogress' // progress bar
5
+import 'nprogress/nprogress.css'// progress bar style
6
+import { getToken } from '@/utils/auth' // getToken from cookie
7
+
8
+NProgress.configure({ showSpinner: false })// NProgress Configuration
9
+
10
+const whiteList = ['/login']// no redirect whitelist
11
+
12
+router.beforeEach((to, from, next) => {
13
+  NProgress.start() // start progress bar
14
+  if (getToken()) { // determine if there has token
15
+    /* has token*/
16
+    if (to.path === '/login') {
17
+      next({ path: '/' })
18
+      NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
19
+    } else {
20
+      if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
21
+        store.dispatch('GetInfo').then(res => { // 拉取user_info
22
+          console.log(res.data.roles)
23
+          const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
24
+          store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
25
+            router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
26
+            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
27
+          })
28
+        }).catch((err) => {
29
+          store.dispatch('FedLogOut').then(() => {
30
+            Message.error(err || 'Verification failed, please login again')
31
+            next({ path: '/' })
32
+          })
33
+        })
34
+      } else {
35
+        next()
36
+      }
37
+    }
38
+  } else {
39
+    /* has no token*/
40
+    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
41
+      next()
42
+    } else {
43
+      next('/login') // 否则全部重定向到登录页
44
+      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
45
+    }
46
+  }
47
+})
48
+
49
+router.afterEach(() => {
50
+  NProgress.done() // finish progress bar
51
+})

+ 142
- 0
src/router/index.js View File

@@ -0,0 +1,142 @@
1
+import Vue from 'vue'
2
+import Router from 'vue-router'
3
+
4
+// in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading;
5
+// detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading
6
+
7
+Vue.use(Router)
8
+
9
+/* Layout */
10
+import Layout from '../views/layout/Layout'
11
+
12
+/**
13
+* hidden: true                   if `hidden:true` will not show in the sidebar(default is false)
14
+* alwaysShow: true               if set true, will always show the root menu, whatever its child routes length
15
+*                                if not set alwaysShow, only more than one route under the children
16
+*                                it will becomes nested mode, otherwise not show the root menu
17
+* redirect: noredirect           if `redirect:noredirect` will no redirct in the breadcrumb
18
+* name:'router-name'             the name is used by <keep-alive> (must set!!!)
19
+* meta : {
20
+    title: 'title'               the name show in submenu and breadcrumb (recommend set)
21
+    icon: 'svg-name'             the icon show in the sidebar,
22
+  }
23
+**/
24
+export const constantRouterMap = [
25
+  { path: '/login', component: () => import('@/views/login/index'), hidden: true },
26
+  { path: '/404', component: () => import('@/views/404'), hidden: true },
27
+
28
+  {
29
+    path: '/',
30
+    component: Layout,
31
+    redirect: '/dashboard',
32
+    name: 'Dashboard',
33
+    hidden: true,
34
+    children: [{
35
+      path: 'dashboard',
36
+      component: () => import('@/views/dashboard/index')
37
+    }]
38
+  }
39
+]
40
+
41
+export default new Router({
42
+  // mode: 'history', //后端支持可开
43
+  scrollBehavior: () => ({ y: 0 }),
44
+  routes: constantRouterMap
45
+})
46
+
47
+export const asyncRouterMap = [
48
+  {
49
+    path: '/example',
50
+    component: Layout,
51
+    redirect: '/example/table',
52
+    name: 'Example',
53
+    meta: { title: 'Example', icon: 'example' },
54
+    children: [
55
+      {
56
+        path: 'table',
57
+        name: 'Table',
58
+        component: () => import('@/views/table/index'),
59
+        meta: { title: 'Table', icon: 'table' }
60
+      },
61
+      {
62
+        path: 'tree',
63
+        name: 'Tree',
64
+        component: () => import('@/views/tree/index'),
65
+        meta: { title: 'Tree', icon: 'tree', roles: ['admin'] }
66
+      }
67
+    ]
68
+  },
69
+
70
+  {
71
+    path: '/form',
72
+    component: Layout,
73
+    children: [
74
+      {
75
+        path: 'index',
76
+        name: 'Form',
77
+        component: () => import('@/views/form/index'),
78
+        meta: { title: 'Form', icon: 'form' }
79
+      }
80
+    ]
81
+  },
82
+
83
+  {
84
+    path: '/nested',
85
+    component: Layout,
86
+    redirect: '/nested/menu1',
87
+    name: 'nested',
88
+    meta: {
89
+      title: 'nested',
90
+      icon: 'nested'
91
+    },
92
+    children: [
93
+      {
94
+        path: 'menu1',
95
+        component: () => import('@/views/nested/menu1/index'), // Parent router-view
96
+        name: 'menu1',
97
+        meta: { title: 'menu1' },
98
+        children: [
99
+          {
100
+            path: 'menu1-1',
101
+            component: () => import('@/views/nested/menu1/menu1-1'),
102
+            name: 'menu1-1',
103
+            meta: { title: 'menu1-1' }
104
+          },
105
+          {
106
+            path: 'menu1-2',
107
+            component: () => import('@/views/nested/menu1/menu1-2'),
108
+            name: 'menu1-2',
109
+            meta: { title: 'menu1-2' },
110
+            children: [
111
+              {
112
+                path: 'menu1-2-1',
113
+                component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
114
+                name: 'menu1-2-1',
115
+                meta: { title: 'menu1-2-1' }
116
+              },
117
+              {
118
+                path: 'menu1-2-2',
119
+                component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
120
+                name: 'menu1-2-2',
121
+                meta: { title: 'menu1-2-2' }
122
+              }
123
+            ]
124
+          },
125
+          {
126
+            path: 'menu1-3',
127
+            component: () => import('@/views/nested/menu1/menu1-3'),
128
+            name: 'menu1-3',
129
+            meta: { title: 'menu1-3' }
130
+          }
131
+        ]
132
+      },
133
+      {
134
+        path: 'menu2',
135
+        component: () => import('@/views/nested/menu2/index'),
136
+        meta: { title: 'menu2' }
137
+      }
138
+    ]
139
+  },
140
+
141
+  { path: '*', redirect: '/404', hidden: true }
142
+]

+ 11
- 0
src/store/getters.js View File

@@ -0,0 +1,11 @@
1
+const getters = {
2
+  sidebar: state => state.app.sidebar,
3
+  device: state => state.app.device,
4
+  token: state => state.user.token,
5
+  avatar: state => state.user.avatar,
6
+  name: state => state.user.name,
7
+  roles: state => state.user.roles,
8
+  permission_routers: state => state.permission.routers,
9
+  addRouters: state => state.permission.addRouters
10
+}
11
+export default getters

+ 19
- 0
src/store/index.js View File

@@ -0,0 +1,19 @@
1
+import Vue from 'vue'
2
+import Vuex from 'vuex'
3
+import app from './modules/app'
4
+import user from './modules/user'
5
+import permission from './modules/permission'
6
+import getters from './getters'
7
+
8
+Vue.use(Vuex)
9
+
10
+const store = new Vuex.Store({
11
+  modules: {
12
+    app,
13
+    user,
14
+    permission
15
+  },
16
+  getters
17
+})
18
+
19
+export default store

+ 43
- 0
src/store/modules/app.js View File

@@ -0,0 +1,43 @@
1
+import Cookies from 'js-cookie'
2
+
3
+const app = {
4
+  state: {
5
+    sidebar: {
6
+      opened: !+Cookies.get('sidebarStatus'),
7
+      withoutAnimation: false
8
+    },
9
+    device: 'desktop'
10
+  },
11
+  mutations: {
12
+    TOGGLE_SIDEBAR: state => {
13
+      if (state.sidebar.opened) {
14
+        Cookies.set('sidebarStatus', 1)
15
+      } else {
16
+        Cookies.set('sidebarStatus', 0)
17
+      }
18
+      state.sidebar.opened = !state.sidebar.opened
19
+      state.sidebar.withoutAnimation = false
20
+    },
21
+    CLOSE_SIDEBAR: (state, withoutAnimation) => {
22
+      Cookies.set('sidebarStatus', 1)
23
+      state.sidebar.opened = false
24
+      state.sidebar.withoutAnimation = withoutAnimation
25
+    },
26
+    TOGGLE_DEVICE: (state, device) => {
27
+      state.device = device
28
+    }
29
+  },
30
+  actions: {
31
+    ToggleSideBar: ({ commit }) => {
32
+      commit('TOGGLE_SIDEBAR')
33
+    },
34
+    CloseSideBar({ commit }, { withoutAnimation }) {
35
+      commit('CLOSE_SIDEBAR', withoutAnimation)
36
+    },
37
+    ToggleDevice({ commit }, device) {
38
+      commit('TOGGLE_DEVICE', device)
39
+    }
40
+  }
41
+}
42
+
43
+export default app

+ 63
- 0
src/store/modules/permission.js View File

@@ -0,0 +1,63 @@
1
+import { asyncRouterMap, constantRouterMap } from '@/router'
2
+
3
+/**
4
+ * 通过meta.role判断是否与当前用户权限匹配
5
+ * @param roles
6
+ * @param route
7
+ */
8
+function hasPermission(roles, route) {
9
+  if (route.meta && route.meta.roles) {
10
+    return roles.some(role => route.meta.roles.indexOf(role) >= 0)
11
+  } else {
12
+    return true
13
+  }
14
+}
15
+
16
+/**
17
+ * 递归过滤异步路由表,返回符合用户角色权限的路由表
18
+ * @param asyncRouterMap
19
+ * @param roles
20
+ */
21
+function filterAsyncRouter(asyncRouterMap, roles) {
22
+  const accessedRouters = asyncRouterMap.filter(route => {
23
+    if (hasPermission(roles, route)) {
24
+      if (route.children && route.children.length) {
25
+        route.children = filterAsyncRouter(route.children, roles)
26
+      }
27
+      return true
28
+    }
29
+    return false
30
+  })
31
+  return accessedRouters
32
+}
33
+
34
+const permission = {
35
+  state: {
36
+    routers: constantRouterMap,
37
+    addRouters: []
38
+  },
39
+  mutations: {
40
+    SET_ROUTERS: (state, routers) => {
41
+      state.addRouters = routers
42
+      state.routers = constantRouterMap.concat(routers)
43
+    }
44
+  },
45
+  actions: {
46
+    GenerateRoutes({ commit }, data) {
47
+      return new Promise(resolve => {
48
+        const { roles } = data
49
+        let accessedRouters
50
+        if (roles.indexOf('admin') >= 0) {
51
+          accessedRouters = asyncRouterMap
52
+        } else {
53
+          accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
54
+        }
55
+        console.log(accessedRouters)
56
+        commit('SET_ROUTERS', accessedRouters)
57
+        resolve()
58
+      })
59
+    }
60
+  }
61
+}
62
+
63
+export default permission

+ 87
- 0
src/store/modules/user.js View File

@@ -0,0 +1,87 @@
1
+import { login, logout, getInfo } from '@/api/login'
2
+import { getToken, setToken, removeToken } from '@/utils/auth'
3
+
4
+const user = {
5
+  state: {
6
+    token: getToken(),
7
+    name: '',
8
+    avatar: '',
9
+    roles: []
10
+  },
11
+
12
+  mutations: {
13
+    SET_TOKEN: (state, token) => {
14
+      state.token = token
15
+    },
16
+    SET_NAME: (state, name) => {
17
+      state.name = name
18
+    },
19
+    SET_AVATAR: (state, avatar) => {
20
+      state.avatar = avatar
21
+    },
22
+    SET_ROLES: (state, roles) => {
23
+      state.roles = roles
24
+    }
25
+  },
26
+
27
+  actions: {
28
+    // 登录
29
+    Login({ commit }, userInfo) {
30
+      const username = userInfo.username.trim()
31
+      return new Promise((resolve, reject) => {
32
+        login(username, userInfo.password).then(response => {
33
+          const data = response.data
34
+          setToken(data.token)
35
+          commit('SET_TOKEN', data.token)
36
+          resolve()
37
+        }).catch(error => {
38
+          reject(error)
39
+        })
40
+      })
41
+    },
42
+
43
+    // 获取用户信息
44
+    GetInfo({ commit, state }) {
45
+      return new Promise((resolve, reject) => {
46
+        getInfo(state.token).then(response => {
47
+          const data = response.data
48
+          if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
49
+            commit('SET_ROLES', data.roles)
50
+          } else {
51
+            reject('getInfo: roles must be a non-null array !')
52
+          }
53
+          commit('SET_NAME', data.name)
54
+          commit('SET_AVATAR', data.avatar)
55
+          resolve(response)
56
+        }).catch(error => {
57
+          reject(error)
58
+        })
59
+      })
60
+    },
61
+
62
+    // 登出
63
+    LogOut({ commit, state }) {
64
+      return new Promise((resolve, reject) => {
65
+        logout(state.token).then(() => {
66
+          commit('SET_TOKEN', '')
67
+          commit('SET_ROLES', [])
68
+          removeToken()
69
+          resolve()
70
+        }).catch(error => {
71
+          reject(error)
72
+        })
73
+      })
74
+    },
75
+
76
+    // 前端 登出
77
+    FedLogOut({ commit }) {
78
+      return new Promise(resolve => {
79
+        commit('SET_TOKEN', '')
80
+        removeToken()
81
+        resolve()
82
+      })
83
+    }
84
+  }
85
+}
86
+
87
+export default user

+ 29
- 0
src/styles/element-ui.scss View File

@@ -0,0 +1,29 @@
1
+ //to reset element-ui default css
2
+.el-upload {
3
+  input[type="file"] {
4
+    display: none !important;
5
+  }
6
+}
7
+
8
+.el-upload__input {
9
+  display: none;
10
+}
11
+
12
+//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
13
+.el-dialog {
14
+  transform: none;
15
+  left: 0;
16
+  position: relative;
17
+  margin: 0 auto;
18
+}
19
+
20
+//element ui upload
21
+.upload-container {
22
+  .el-upload {
23
+    width: 100%;
24
+    .el-upload-dragger {
25
+      width: 100%;
26
+      height: 200px;
27
+    }
28
+  }
29
+}

+ 78
- 0
src/styles/index.scss View File

@@ -0,0 +1,78 @@
1
+@import './variables.scss';
2
+@import './mixin.scss';
3
+@import './transition.scss';
4
+@import './element-ui.scss';
5
+@import './sidebar.scss';
6
+
7
+body {
8
+  height: 100%;
9
+  -moz-osx-font-smoothing: grayscale;
10
+  -webkit-font-smoothing: antialiased;
11
+  text-rendering: optimizeLegibility;
12
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
13
+}
14
+
15
+label {
16
+  font-weight: 700;
17
+}
18
+
19
+html {
20
+  height: 100%;
21
+  box-sizing: border-box;
22
+}
23
+
24
+#app{
25
+  height: 100%;
26
+}
27
+
28
+*,
29
+*:before,
30
+*:after {
31
+  box-sizing: inherit;
32
+}
33
+
34
+a,
35
+a:focus,
36
+a:hover {
37
+  cursor: pointer;
38
+  color: inherit;
39
+  outline: none;
40
+  text-decoration: none;
41
+}
42
+
43
+div:focus{
44
+  outline: none;
45
+ }
46
+
47
+a:focus,
48
+a:active {
49
+  outline: none;
50
+}
51
+
52
+a,
53
+a:focus,
54
+a:hover {
55
+  cursor: pointer;
56
+  color: inherit;
57
+  text-decoration: none;
58
+}
59
+
60
+.clearfix {
61
+  &:after {
62
+    visibility: hidden;
63
+    display: block;
64
+    font-size: 0;
65
+    content: " ";
66
+    clear: both;
67
+    height: 0;
68
+  }
69
+}
70
+
71
+//main-container全局样式
72
+.app-main{
73
+  min-height: 100%
74
+}
75
+
76
+.app-container {
77
+  padding: 20px;
78
+}

+ 27
- 0
src/styles/mixin.scss View File

@@ -0,0 +1,27 @@
1
+@mixin clearfix {
2
+  &:after {
3
+    content: "";
4
+    display: table;
5
+    clear: both;
6
+  }
7
+}
8
+
9
+@mixin scrollBar {
10
+  &::-webkit-scrollbar-track-piece {
11
+    background: #d3dce6;
12
+  }
13
+  &::-webkit-scrollbar {
14
+    width: 6px;
15
+  }
16
+  &::-webkit-scrollbar-thumb {
17
+    background: #99a9bf;
18
+    border-radius: 20px;
19
+  }
20
+}
21
+
22
+@mixin relative {
23
+  position: relative;
24
+  width: 100%;
25
+  height: 100%;
26
+}
27
+

+ 119
- 0
src/styles/sidebar.scss View File

@@ -0,0 +1,119 @@
1
+#app {
2
+  // 主体区域
3
+  .main-container {
4
+    min-height: 100%;
5
+    transition: margin-left .28s;
6
+    margin-left: 180px;
7
+    position: relative;
8
+  }
9
+  // 侧边栏
10
+  .sidebar-container {
11
+    transition: width 0.28s;
12
+    width: 180px !important;
13
+    height: 100%;
14
+    position: fixed;
15
+    font-size: 0px;
16
+    top: 0;
17
+    bottom: 0;
18
+    left: 0;
19
+    z-index: 1001;
20
+    overflow: hidden;
21
+    //reset element-ui css
22
+    .horizontal-collapse-transition {
23
+      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
24
+    }
25
+    .scrollbar-wrapper {
26
+      height: calc(100% + 15px);
27
+      .el-scrollbar__view {
28
+        height: 100%;
29
+      }
30
+    }
31
+    .is-horizontal {
32
+      display: none;
33
+    }
34
+    a {
35
+      display: inline-block;
36
+      width: 100%;
37
+      overflow: hidden;
38
+    }
39
+    .svg-icon {
40
+      margin-right: 16px;
41
+    }
42
+    .el-menu {
43
+      border: none;
44
+      height: 100%;
45
+      width: 100% !important;
46
+    }
47
+  }
48
+  .hideSidebar {
49
+    .sidebar-container {
50
+      width: 36px !important;
51
+    }
52
+    .main-container {
53
+      margin-left: 36px;
54
+    }
55
+    .submenu-title-noDropdown {
56
+      padding-left: 10px !important;
57
+      position: relative;
58
+      .el-tooltip {
59
+        padding: 0 10px !important;
60
+      }
61
+    }
62
+    .el-submenu {
63
+      overflow: hidden;
64
+      &>.el-submenu__title {
65
+        padding-left: 10px !important;
66
+        .el-submenu__icon-arrow {
67
+          display: none;
68
+        }
69
+      }
70
+    }
71
+    .el-menu--collapse {
72
+      .el-submenu {
73
+        &>.el-submenu__title {
74
+          &>span {
75
+            height: 0;
76
+            width: 0;
77
+            overflow: hidden;
78
+            visibility: hidden;
79
+            display: inline-block;
80
+          }
81
+        }
82
+      }
83
+    }
84
+  }
85
+  .sidebar-container .nest-menu .el-submenu>.el-submenu__title,
86
+  .sidebar-container .el-submenu .el-menu-item {
87
+    min-width: 180px !important;
88
+    background-color: $subMenuBg !important;
89
+    &:hover {
90
+      background-color: $menuHover !important;
91
+    }
92
+  }
93
+  .el-menu--collapse .el-menu .el-submenu {
94
+    min-width: 180px !important;
95
+  }
96
+
97
+  //适配移动端
98
+  .mobile {
99
+    .main-container {
100
+      margin-left: 0px;
101
+    }
102
+    .sidebar-container {
103
+      transition: transform .28s;
104
+      width: 180px !important;
105
+    }
106
+    &.hideSidebar {
107
+      .sidebar-container {
108
+        transition-duration: 0.3s;
109
+        transform: translate3d(-180px, 0, 0);
110
+      }
111
+    }
112
+  }
113
+  .withoutAnimation {
114
+    .main-container,
115
+    .sidebar-container {
116
+      transition: none;
117
+    }
118
+  }
119
+}

+ 32
- 0
src/styles/transition.scss View File

@@ -0,0 +1,32 @@
1
+//globl transition css
2
+
3
+/*fade*/
4
+.fade-enter-active,
5
+.fade-leave-active {
6
+  transition: opacity 0.28s;
7
+}
8
+
9
+.fade-enter,
10
+.fade-leave-active {
11
+  opacity: 0;
12
+}
13
+
14
+/*fade*/
15
+.breadcrumb-enter-active,
16
+.breadcrumb-leave-active {
17
+  transition: all .5s;
18
+}
19
+
20
+.breadcrumb-enter,
21
+.breadcrumb-leave-active {
22
+  opacity: 0;
23
+  transform: translateX(20px);
24
+}
25
+
26
+.breadcrumb-move {
27
+  transition: all .5s;
28
+}
29
+
30
+.breadcrumb-leave-active {
31
+  position: absolute;
32
+}

+ 4
- 0
src/styles/variables.scss View File

@@ -0,0 +1,4 @@
1
+//sidebar
2
+$menuBg:#304156;
3
+$subMenuBg:#1f2d3d;
4
+$menuHover:#001528;

+ 15
- 0
src/utils/auth.js View File

@@ -0,0 +1,15 @@
1
+import Cookies from 'js-cookie'
2
+
3
+const TokenKey = 'Admin-Token'
4
+
5
+export function getToken() {
6
+  return Cookies.get(TokenKey)
7
+}
8
+
9
+export function setToken(token) {
10
+  return Cookies.set(TokenKey, token)
11
+}
12
+
13
+export function removeToken() {
14
+  return Cookies.remove(TokenKey)
15
+}

+ 58
- 0
src/utils/index.js View File

@@ -0,0 +1,58 @@
1
+/**
2
+ * Created by jiachenpan on 16/11/18.
3
+ */
4
+
5
+export function parseTime(time, cFormat) {
6
+  if (arguments.length === 0) {
7
+    return null
8
+  }
9
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
10
+  let date
11
+  if (typeof time === 'object') {
12
+    date = time
13
+  } else {
14
+    if (('' + time).length === 10) time = parseInt(time) * 1000
15
+    date = new Date(time)
16
+  }
17
+  const formatObj = {
18
+    y: date.getFullYear(),
19
+    m: date.getMonth() + 1,
20
+    d: date.getDate(),
21
+    h: date.getHours(),
22
+    i: date.getMinutes(),
23
+    s: date.getSeconds(),
24
+    a: date.getDay()
25
+  }
26
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
27
+    let value = formatObj[key]
28
+    if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
29
+    if (result.length > 0 && value < 10) {
30
+      value = '0' + value
31
+    }
32
+    return value || 0
33
+  })
34
+  return time_str
35
+}
36
+
37
+export function formatTime(time, option) {
38
+  time = +time * 1000
39
+  const d = new Date(time)
40
+  const now = Date.now()
41
+
42
+  const diff = (now - d) / 1000
43
+
44
+  if (diff < 30) {
45
+    return '刚刚'
46
+  } else if (diff < 3600) { // less 1 hour
47
+    return Math.ceil(diff / 60) + '分钟前'
48
+  } else if (diff < 3600 * 24) {
49
+    return Math.ceil(diff / 3600) + '小时前'
50
+  } else if (diff < 3600 * 24 * 2) {
51
+    return '1天前'
52
+  }
53
+  if (option) {
54
+    return parseTime(time, option)
55
+  } else {
56
+    return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
57
+  }
58
+}

+ 66
- 0
src/utils/request.js View File

@@ -0,0 +1,66 @@
1
+import axios from 'axios'
2
+import { Message, MessageBox } from 'element-ui'
3
+import store from '../store'
4
+import { getToken } from '@/utils/auth'
5
+
6
+// 创建axios实例
7
+const service = axios.create({
8
+  baseURL: process.env.BASE_API, // api的base_url
9
+  timeout: 5000 // 请求超时时间
10
+})
11
+
12
+// request拦截器
13
+service.interceptors.request.use(config => {
14
+  if (store.getters.token) {
15
+    config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
16
+  }
17
+  return config
18
+}, error => {
19
+  // Do something with request error
20
+  console.log(error) // for debug
21
+  Promise.reject(error)
22
+})
23
+
24
+// respone拦截器
25
+service.interceptors.response.use(
26
+  response => {
27
+  /**
28
+  * code为非20000是抛错 可结合自己业务进行修改
29
+  */
30
+    const res = response.data
31
+    if (res.code !== 20000) {
32
+      Message({
33
+        message: res.message,
34
+        type: 'error',
35
+        duration: 5 * 1000
36
+      })
37
+
38
+      // 50008:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
39
+      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
40
+        MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
41
+          confirmButtonText: '重新登录',
42
+          cancelButtonText: '取消',
43
+          type: 'warning'
44
+        }).then(() => {
45
+          store.dispatch('FedLogOut').then(() => {
46
+            location.reload()// 为了重新实例化vue-router对象 避免bug
47
+          })
48
+        })
49
+      }
50
+      return Promise.reject('error')
51
+    } else {
52
+      return response.data
53
+    }
54
+  },
55
+  error => {
56
+    console.log('err' + error)// for debug
57
+    Message({
58
+      message: error.message,
59
+      type: 'error',
60
+      duration: 5 * 1000
61
+    })
62
+    return Promise.reject(error)
63
+  }
64
+)
65
+
66
+export default service

+ 33
- 0
src/utils/validate.js View File

@@ -0,0 +1,33 @@
1
+/**
2
+ * Created by jiachenpan on 16/11/18.
3
+ */
4
+
5
+export function isvalidUsername(str) {
6
+  const valid_map = ['admin', 'editor']
7
+  return valid_map.indexOf(str.trim()) >= 0
8
+}
9
+
10
+/* 合法uri*/
11
+export function validateURL(textval) {
12
+  const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
13
+  return urlregex.test(textval)
14
+}
15
+
16
+/* 小写字母*/
17
+export function validateLowerCase(str) {
18
+  const reg = /^[a-z]+$/
19
+  return reg.test(str)
20
+}
21
+
22
+/* 大写字母*/
23
+export function validateUpperCase(str) {
24
+  const reg = /^[A-Z]+$/
25
+  return reg.test(str)
26
+}
27
+
28
+/* 大小写字母*/
29
+export function validatAlphabets(str) {
30
+  const reg = /^[A-Za-z]+$/
31
+  return reg.test(str)
32
+}
33
+

+ 236
- 0
src/views/404.vue View File

@@ -0,0 +1,236 @@
1
+<template>
2
+  <div class="wscn-http404-container">
3
+    <div class="wscn-http404">
4
+      <div class="pic-404">
5
+        <img class="pic-404__parent" :src="img_404" alt="404">
6
+        <img class="pic-404__child left" :src="img_404_cloud" alt="404">
7
+        <img class="pic-404__child mid" :src="img_404_cloud" alt="404">
8
+        <img class="pic-404__child right" :src="img_404_cloud" alt="404">
9
+      </div>
10
+      <div class="bullshit">
11
+        <div class="bullshit__oops">OOPS!</div>
12
+        <div class="bullshit__info">版权所有
13
+          <a class='link-type' href='https://wallstreetcn.com' target='_blank'>华尔街见闻</a>
14
+        </div>
15
+        <div class="bullshit__headline">{{ message }}</div>
16
+        <div class="bullshit__info">请检查您输入的网址是否正确,请点击以下按钮返回主页或者发送错误报告</div>
17
+        <a href="" class="bullshit__return-home">返回首页</a>
18
+      </div>
19
+    </div>
20
+  </div>
21
+</template>
22
+
23
+<script>
24
+import img_404 from '@/assets/404_images/404.png'
25
+import img_404_cloud from '@/assets/404_images/404_cloud.png'
26
+
27
+export default {
28
+  name: 'page404',
29
+  data() {
30
+    return {
31
+      img_404,
32
+      img_404_cloud
33
+    }
34
+  },
35
+  computed: {
36
+    message() {
37
+      return '网管说这个页面你不能进......'
38
+    }
39
+  }
40
+}
41
+</script>
42
+
43
+<style rel="stylesheet/scss" lang="scss" scoped>
44
+.wscn-http404-container{
45
+  transform: translate(-50%,-50%);
46
+  position: absolute;
47
+  top: 40%;
48
+  left: 50%;
49
+}
50
+.wscn-http404 {
51
+  position: relative;
52
+  width: 1200px;
53
+  padding: 0 50px;
54
+  overflow: hidden;
55
+  .pic-404 {
56
+    position: relative;
57
+    float: left;
58
+    width: 600px;
59
+    overflow: hidden;
60
+    &__parent {
61
+      width: 100%;
62
+    }
63
+    &__child {
64
+      position: absolute;
65
+      &.left {
66
+        width: 80px;
67
+        top: 17px;
68
+        left: 220px;
69
+        opacity: 0;
70
+        animation-name: cloudLeft;
71
+        animation-duration: 2s;
72
+        animation-timing-function: linear;
73
+        animation-fill-mode: forwards;
74
+        animation-delay: 1s;
75
+      }
76
+      &.mid {
77
+        width: 46px;
78
+        top: 10px;
79
+        left: 420px;
80
+        opacity: 0;
81
+        animation-name: cloudMid;
82
+        animation-duration: 2s;
83
+        animation-timing-function: linear;
84
+        animation-fill-mode: forwards;
85
+        animation-delay: 1.2s;
86
+      }
87
+      &.right {
88
+        width: 62px;
89
+        top: 100px;
90
+        left: 500px;
91
+        opacity: 0;
92
+        animation-name: cloudRight;
93
+        animation-duration: 2s;
94
+        animation-timing-function: linear;
95
+        animation-fill-mode: forwards;
96
+        animation-delay: 1s;
97
+      }
98
+      @keyframes cloudLeft {
99
+        0% {
100
+          top: 17px;
101
+          left: 220px;
102
+          opacity: 0;
103
+        }
104
+        20% {
105
+          top: 33px;
106
+          left: 188px;
107
+          opacity: 1;
108
+        }
109
+        80% {
110
+          top: 81px;
111
+          left: 92px;
112
+          opacity: 1;
113
+        }
114
+        100% {
115
+          top: 97px;
116
+          left: 60px;
117
+          opacity: 0;
118
+        }
119
+      }
120
+      @keyframes cloudMid {
121
+        0% {
122
+          top: 10px;
123
+          left: 420px;
124
+          opacity: 0;
125
+        }
126
+        20% {
127
+          top: 40px;
128
+          left: 360px;
129
+          opacity: 1;
130
+        }
131
+        70% {
132
+          top: 130px;
133
+          left: 180px;
134
+          opacity: 1;
135
+        }
136
+        100% {
137
+          top: 160px;
138
+          left: 120px;
139
+          opacity: 0;
140
+        }
141
+      }
142
+      @keyframes cloudRight {
143
+        0% {
144
+          top: 100px;
145
+          left: 500px;
146
+          opacity: 0;
147
+        }
148
+        20% {
149
+          top: 120px;
150
+          left: 460px;
151
+          opacity: 1;
152
+        }
153
+        80% {
154
+          top: 180px;
155
+          left: 340px;
156
+          opacity: 1;
157
+        }
158
+        100% {
159
+          top: 200px;
160
+          left: 300px;
161
+          opacity: 0;
162
+        }
163
+      }
164
+    }
165
+  }
166
+  .bullshit {
167
+    position: relative;
168
+    float: left;
169
+    width: 300px;
170
+    padding: 30px 0;
171
+    overflow: hidden;
172
+    &__oops {
173
+      font-size: 32px;
174
+      font-weight: bold;
175
+      line-height: 40px;
176
+      color: #1482f0;
177
+      opacity: 0;
178
+      margin-bottom: 20px;
179
+      animation-name: slideUp;
180
+      animation-duration: 0.5s;
181
+      animation-fill-mode: forwards;
182
+    }
183
+    &__headline {
184
+      font-size: 20px;
185
+      line-height: 24px;
186
+      color: #222;
187
+      font-weight: bold;
188
+      opacity: 0;
189
+      margin-bottom: 10px;
190
+      animation-name: slideUp;
191
+      animation-duration: 0.5s;
192
+      animation-delay: 0.1s;
193
+      animation-fill-mode: forwards;
194
+    }
195
+    &__info {
196
+      font-size: 13px;
197
+      line-height: 21px;
198
+      color: grey;
199
+      opacity: 0;
200
+      margin-bottom: 30px;
201
+      animation-name: slideUp;
202
+      animation-duration: 0.5s;
203
+      animation-delay: 0.2s;
204
+      animation-fill-mode: forwards;
205
+    }
206
+    &__return-home {
207
+      display: block;
208
+      float: left;
209
+      width: 110px;
210
+      height: 36px;
211
+      background: #1482f0;
212
+      border-radius: 100px;
213
+      text-align: center;
214
+      color: #ffffff;
215
+      opacity: 0;
216
+      font-size: 14px;
217
+      line-height: 36px;
218
+      cursor: pointer;
219
+      animation-name: slideUp;
220
+      animation-duration: 0.5s;
221
+      animation-delay: 0.3s;
222
+      animation-fill-mode: forwards;
223
+    }
224
+    @keyframes slideUp {
225
+      0% {
226
+        transform: translateY(60px);
227
+        opacity: 0;
228
+      }
229
+      100% {
230
+        transform: translateY(0);
231
+        opacity: 1;
232
+      }
233
+    }
234
+  }
235
+}
236
+</style>

+ 32
- 0
src/views/dashboard/index.vue View File

@@ -0,0 +1,32 @@
1
+<template>
2
+  <div class="dashboard-container">
3
+    <div class="dashboard-text">name:{{name}}</div>
4
+    <div class="dashboard-text">roles:<span v-for='role in roles' :key='role'>{{role}}</span></div>
5
+  </div>
6
+</template>
7
+
8
+<script>
9
+import { mapGetters } from 'vuex'
10
+
11
+export default {
12
+  name: 'dashboard',
13
+  computed: {
14
+    ...mapGetters([
15
+      'name',
16
+      'roles'
17
+    ])
18
+  }
19
+}
20
+</script>
21
+
22
+<style rel="stylesheet/scss" lang="scss" scoped>
23
+.dashboard {
24
+  &-container {
25
+    margin: 30px;
26
+  }
27
+  &-text {
28
+    font-size: 30px;
29
+    line-height: 46px;
30
+  }
31
+}
32
+</style>

+ 85
- 0
src/views/form/index.vue View File

@@ -0,0 +1,85 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form ref="form" :model="form" label-width="120px">
4
+      <el-form-item label="Activity name">
5
+        <el-input v-model="form.name"></el-input>
6
+      </el-form-item>
7
+      <el-form-item label="Activity zone">
8
+        <el-select v-model="form.region" placeholder="please select your zone">
9
+          <el-option label="Zone one" value="shanghai"></el-option>
10
+          <el-option label="Zone two" value="beijing"></el-option>
11
+        </el-select>
12
+      </el-form-item>
13
+      <el-form-item label="Activity time">
14
+        <el-col :span="11">
15
+          <el-date-picker type="date" placeholder="Pick a date" v-model="form.date1" style="width: 100%;"></el-date-picker>
16
+        </el-col>
17
+        <el-col class="line" :span="2">-</el-col>
18
+        <el-col :span="11">
19
+          <el-time-picker type="fixed-time" placeholder="Pick a time" v-model="form.date2" style="width: 100%;"></el-time-picker>
20
+        </el-col>
21
+      </el-form-item>
22
+      <el-form-item label="Instant delivery">
23
+        <el-switch v-model="form.delivery"></el-switch>
24
+      </el-form-item>
25
+      <el-form-item label="Activity type">
26
+        <el-checkbox-group v-model="form.type">
27
+          <el-checkbox label="Online activities" name="type"></el-checkbox>
28
+          <el-checkbox label="Promotion activities" name="type"></el-checkbox>
29
+          <el-checkbox label="Offline activities" name="type"></el-checkbox>
30
+          <el-checkbox label="Simple brand exposure" name="type"></el-checkbox>
31
+        </el-checkbox-group>
32
+      </el-form-item>
33
+      <el-form-item label="Resources">
34
+        <el-radio-group v-model="form.resource">
35
+          <el-radio label="Sponsor"></el-radio>
36
+          <el-radio label="Venue"></el-radio>
37
+        </el-radio-group>
38
+      </el-form-item>
39
+      <el-form-item label="Activity form">
40
+        <el-input type="textarea" v-model="form.desc"></el-input>
41
+      </el-form-item>
42
+      <el-form-item>
43
+        <el-button type="primary" @click="onSubmit">Create</el-button>
44
+        <el-button @click="onCancel">Cancel</el-button>
45
+      </el-form-item>
46
+    </el-form>
47
+  </div>
48
+</template>
49
+
50
+<script>
51
+export default {
52
+  data() {
53
+    return {
54
+      form: {
55
+        name: '',
56
+        region: '',
57
+        date1: '',
58
+        date2: '',
59
+        delivery: false,
60
+        type: [],
61
+        resource: '',
62
+        desc: ''
63
+      }
64
+    }
65
+  },
66
+  methods: {
67
+    onSubmit() {
68
+      this.$message('submit!')
69
+    },
70
+    onCancel() {
71
+      this.$message({
72
+        message: 'cancel!',
73
+        type: 'warning'
74
+      })
75
+    }
76
+  }
77
+}
78
+</script>
79
+
80
+<style scoped>
81
+.line{
82
+  text-align: center;
83
+}
84
+</style>
85
+

+ 69
- 0
src/views/layout/Layout.vue View File

@@ -0,0 +1,69 @@
1
+<template>
2
+  <div class="app-wrapper" :class="classObj">
3
+    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"></div>
4
+    <sidebar class="sidebar-container"></sidebar>
5
+    <div class="main-container">
6
+      <navbar></navbar>
7
+      <app-main></app-main>
8
+    </div>
9
+  </div>
10
+</template>
11
+
12
+<script>
13
+import { Navbar, Sidebar, AppMain } from './components'
14
+import ResizeMixin from './mixin/ResizeHandler'
15
+
16
+export default {
17
+  name: 'layout',
18
+  components: {
19
+    Navbar,
20
+    Sidebar,
21
+    AppMain
22
+  },
23
+  mixins: [ResizeMixin],
24
+  computed: {
25
+    sidebar() {
26
+      return this.$store.state.app.sidebar
27
+    },
28
+    device() {
29
+      return this.$store.state.app.device
30
+    },
31
+    classObj() {
32
+      return {
33
+        hideSidebar: !this.sidebar.opened,
34
+        openSidebar: this.sidebar.opened,
35
+        withoutAnimation: this.sidebar.withoutAnimation,
36
+        mobile: this.device === 'mobile'
37
+      }
38
+    }
39
+  },
40
+  methods: {
41
+    handleClickOutside() {
42
+      this.$store.dispatch('CloseSideBar', { withoutAnimation: false })
43
+    }
44
+  }
45
+}
46
+</script>
47
+
48
+<style rel="stylesheet/scss" lang="scss" scoped>
49
+  @import "src/styles/mixin.scss";
50
+  .app-wrapper {
51
+    @include clearfix;
52
+    position: relative;
53
+    height: 100%;
54
+    width: 100%;
55
+    &.mobile.openSidebar{
56
+      position: fixed;
57
+      top: 0;
58
+    }
59
+  }
60
+  .drawer-bg {
61
+    background: #000;
62
+    opacity: 0.3;
63
+    width: 100%;
64
+    top: 0;
65
+    height: 100%;
66
+    position: absolute;
67
+    z-index: 999;
68
+  }
69
+</style>

+ 28
- 0
src/views/layout/components/AppMain.vue View File

@@ -0,0 +1,28 @@
1
+<template>
2
+  <section class="app-main">
3
+    <transition name="fade" mode="out-in">
4
+      <!-- <router-view :key="key"></router-view> -->
5
+      <router-view></router-view>
6
+    </transition>
7
+  </section>
8
+</template>
9
+
10
+<script>
11
+export default {
12
+  name: 'AppMain',
13
+  computed: {
14
+    // key() {
15
+    //   return this.$route.name !== undefined ? this.$route.name + +new Date() : this.$route + +new Date()
16
+    // }
17
+  }
18
+}
19
+</script>
20
+
21
+<style scoped>
22
+.app-main {
23
+  /*50 = navbar  */
24
+  min-height: calc(100vh - 50px);
25
+  position: relative;
26
+  overflow: hidden;
27
+}
28
+</style>

+ 102
- 0
src/views/layout/components/Navbar.vue View File

@@ -0,0 +1,102 @@
1
+<template>
2
+  <el-menu class="navbar" mode="horizontal">
3
+    <hamburger class="hamburger-container" :toggleClick="toggleSideBar" :isActive="sidebar.opened"></hamburger>
4
+    <breadcrumb></breadcrumb>
5
+    <span class="remark">我的角色是xxx</span>
6
+    <el-dropdown class="avatar-container" trigger="click">
7
+      <div class="avatar-wrapper">
8
+        <img class="user-avatar" :src="avatar+'?imageView2/1/w/80/h/80'">
9
+        <i class="el-icon-caret-bottom"></i>
10
+      </div>
11
+      <el-dropdown-menu class="user-dropdown" slot="dropdown">
12
+        <router-link class="inlineBlock" to="/">
13
+          <el-dropdown-item>
14
+            Home
15
+          </el-dropdown-item>
16
+        </router-link>
17
+        <el-dropdown-item divided>
18
+          <span @click="logout" style="display:block;">LogOut</span>
19
+        </el-dropdown-item>
20
+      </el-dropdown-menu>
21
+    </el-dropdown>
22
+  </el-menu>
23
+</template>
24
+
25
+<script>
26
+import { mapGetters } from 'vuex'
27
+import Breadcrumb from '@/components/Breadcrumb'
28
+import Hamburger from '@/components/Hamburger'
29
+
30
+export default {
31
+  components: {
32
+    Breadcrumb,
33
+    Hamburger
34
+  },
35
+  computed: {
36
+    ...mapGetters([
37
+      'sidebar',
38
+      'avatar'
39
+    ])
40
+  },
41
+  methods: {
42
+    toggleSideBar() {
43
+      this.$store.dispatch('ToggleSideBar')
44
+    },
45
+    logout() {
46
+      this.$store.dispatch('LogOut').then(() => {
47
+        location.reload() // 为了重新实例化vue-router对象 避免bug
48
+      })
49
+    }
50
+  }
51
+}
52
+</script>
53
+
54
+<style rel="stylesheet/scss" lang="scss" scoped>
55
+.navbar {
56
+  height: 50px;
57
+  line-height: 50px;
58
+  border-radius: 0px !important;
59
+  .hamburger-container {
60
+    line-height: 58px;
61
+    height: 50px;
62
+    float: left;
63
+    padding: 0 10px;
64
+  }
65
+  .screenfull {
66
+    position: absolute;
67
+    right: 90px;
68
+    top: 16px;
69
+    color: red;
70
+  }
71
+  .avatar-container {
72
+    height: 50px;
73
+    display: inline-block;
74
+    position: absolute;
75
+    right: 35px;
76
+    .avatar-wrapper {
77
+      cursor: pointer;
78
+      margin-top: 5px;
79
+      position: relative;
80
+      .user-avatar {
81
+        width: 40px;
82
+        height: 40px;
83
+        border-radius: 10px;
84
+      }
85
+      .el-icon-caret-bottom {
86
+        position: absolute;
87
+        right: -20px;
88
+        top: 25px;
89
+        font-size: 12px;
90
+      }
91
+    }
92
+  }
93
+  .remark{
94
+    display: inline-block;
95
+    float: right;
96
+    margin-right: 100px;
97
+    font-size: 13px;
98
+    color: #666;
99
+  }
100
+}
101
+</style>
102
+

+ 67
- 0
src/views/layout/components/Sidebar/SidebarItem.vue View File

@@ -0,0 +1,67 @@
1
+<template>
2
+  <div v-if="!item.hidden&&item.children" class="menu-wrapper">
3
+
4
+      <router-link v-if="hasOneShowingChildren(item.children) && !item.children[0].children&&!item.alwaysShow" :to="resolvePath(item.children[0].path)">
5
+        <el-menu-item :index="resolvePath(item.children[0].path)" :class="{'submenu-title-noDropdown':!isNest}">
6
+          <svg-icon v-if="item.children[0].meta&&item.children[0].meta.icon" :icon-class="item.children[0].meta.icon"></svg-icon>
7
+          <span v-if="item.children[0].meta&&item.children[0].meta.title" slot="title">{{item.children[0].meta.title}}</span>
8
+        </el-menu-item>
9
+      </router-link>
10
+
11
+      <el-submenu v-else :index="item.name||item.path">
12
+        <template slot="title">
13
+          <svg-icon v-if="item.meta&&item.meta.icon" :icon-class="item.meta.icon"></svg-icon>
14
+          <span v-if="item.meta&&item.meta.title" slot="title">{{item.meta.title}}</span>
15
+        </template>
16
+
17
+        <template v-for="child in item.children" v-if="!child.hidden">
18
+          <sidebar-item :is-nest="true" class="nest-menu" v-if="child.children&&child.children.length>0" :item="child" :key="child.path" :base-path="resolvePath(child.path)"></sidebar-item>
19
+
20
+          <router-link v-else :to="resolvePath(child.path)" :key="child.name">
21
+            <el-menu-item :index="resolvePath(child.path)">
22
+              <svg-icon v-if="child.meta&&child.meta.icon" :icon-class="child.meta.icon"></svg-icon>
23
+              <span v-if="child.meta&&child.meta.title" slot="title">{{child.meta.title}}</span>
24
+            </el-menu-item>
25
+          </router-link>
26
+        </template>
27
+      </el-submenu>
28
+
29
+  </div>
30
+</template>
31
+
32
+<script>
33
+import path from 'path'
34
+
35
+export default {
36
+  name: 'SidebarItem',
37
+  props: {
38
+    // route配置json
39
+    item: {
40
+      type: Object,
41
+      required: true
42
+    },
43
+    isNest: {
44
+      type: Boolean,
45
+      default: false
46
+    },
47
+    basePath: {
48
+      type: String,
49
+      default: ''
50
+    }
51
+  },
52
+  methods: {
53
+    hasOneShowingChildren(children) {
54
+      const showingChildren = children.filter(item => {
55
+        return !item.hidden
56
+      })
57
+      if (showingChildren.length === 1) {
58
+        return true
59
+      }
60
+      return false
61
+    },
62
+    resolvePath(...paths) {
63
+      return path.resolve(this.basePath, ...paths)
64
+    }
65
+  }
66
+}
67
+</script>

+ 33
- 0
src/views/layout/components/Sidebar/index.vue View File

@@ -0,0 +1,33 @@
1
+<template>
2
+  <el-scrollbar wrapClass="scrollbar-wrapper">
3
+    <el-menu
4
+      mode="vertical"
5
+      :show-timeout="200"
6
+      :default-active="$route.path"
7
+      :collapse="isCollapse"
8
+      background-color="#304156"
9
+      text-color="#bfcbd9"
10
+      active-text-color="#409EFF"
11
+    >
12
+      <sidebar-item v-for="route in permission_routers" :key="route.name" :item="route" :base-path="route.path"></sidebar-item>
13
+    </el-menu>
14
+  </el-scrollbar>
15
+</template>
16
+
17
+<script>
18
+import { mapGetters } from 'vuex'
19
+import SidebarItem from './SidebarItem'
20
+
21
+export default {
22
+  components: { SidebarItem },
23
+  computed: {
24
+    ...mapGetters([
25
+      'permission_routers',
26
+      'sidebar'
27
+    ]),
28
+    isCollapse() {
29
+      return !this.sidebar.opened
30
+    }
31
+  }
32
+}
33
+</script>

+ 3
- 0
src/views/layout/components/index.js View File

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

+ 41
- 0
src/views/layout/mixin/ResizeHandler.js View File

@@ -0,0 +1,41 @@
1
+import store from '@/store'
2
+
3
+const { body } = document
4
+const WIDTH = 1024
5
+const RATIO = 3
6
+
7
+export default {
8
+  watch: {
9
+    $route(route) {
10
+      if (this.device === 'mobile' && this.sidebar.opened) {
11
+        store.dispatch('CloseSideBar', { withoutAnimation: false })
12
+      }
13
+    }
14
+  },
15
+  beforeMount() {
16
+    window.addEventListener('resize', this.resizeHandler)
17
+  },
18
+  mounted() {
19
+    const isMobile = this.isMobile()
20
+    if (isMobile) {
21
+      store.dispatch('ToggleDevice', 'mobile')
22
+      store.dispatch('CloseSideBar', { withoutAnimation: true })
23
+    }
24
+  },
25
+  methods: {
26
+    isMobile() {
27
+      const rect = body.getBoundingClientRect()
28
+      return rect.width - RATIO < WIDTH
29
+    },
30
+    resizeHandler() {
31
+      if (!document.hidden) {
32
+        const isMobile = this.isMobile()
33
+        store.dispatch('ToggleDevice', isMobile ? 'mobile' : 'desktop')
34
+
35
+        if (isMobile) {
36
+          store.dispatch('CloseSideBar', { withoutAnimation: true })
37
+        }
38
+      }
39
+    }
40
+  }
41
+}

+ 182
- 0
src/views/login/index.vue View File

@@ -0,0 +1,182 @@
1
+<template>
2
+  <div class="login-container">
3
+    <el-form class="login-form" autoComplete="on" :model="loginForm" :rules="loginRules" ref="loginForm" label-position="left">
4
+      <h3 class="title">城的空间</h3>
5
+      <el-form-item prop="username">
6
+        <span class="svg-container svg-container_login">
7
+          <svg-icon icon-class="user" />
8
+        </span>
9
+        <el-input name="username" type="text" v-model="loginForm.username" autoComplete="on" placeholder="username" />
10
+      </el-form-item>
11
+      <el-form-item prop="password">
12
+        <span class="svg-container">
13
+          <svg-icon icon-class="password"></svg-icon>
14
+        </span>
15
+        <el-input name="password" :type="pwdType" @keyup.enter.native="handleLogin" v-model="loginForm.password" autoComplete="on"
16
+          placeholder="password"></el-input>
17
+          <span class="show-pwd" @click="showPwd"><svg-icon icon-class="eye" /></span>
18
+      </el-form-item>
19
+      <el-form-item>
20
+        <el-button type="primary" style="width:100%;" :loading="loading" @click.native.prevent="handleLogin">
21
+          Sign in
22
+        </el-button>
23
+      </el-form-item>
24
+      <!-- <div class="tips">
25
+        <span style="margin-right:20px;">username: admin</span>
26
+        <span> password: admin</span>
27
+      </div> -->
28
+    </el-form>
29
+  </div>
30
+</template>
31
+
32
+<script>
33
+import { isvalidUsername } from '@/utils/validate'
34
+
35
+export default {
36
+  name: 'login',
37
+  data() {
38
+    const validateUsername = (rule, value, callback) => {
39
+      if (!isvalidUsername(value)) {
40
+        callback(new Error('请输入正确的用户名'))
41
+      } else {
42
+        callback()
43
+      }
44
+    }
45
+    const validatePass = (rule, value, callback) => {
46
+      if (value.length < 5) {
47
+        callback(new Error('密码不能小于5位'))
48
+      } else {
49
+        callback()
50
+      }
51
+    }
52
+    return {
53
+      loginForm: {
54
+        username: 'admin',
55
+        password: 'admin'
56
+      },
57
+      loginRules: {
58
+        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
59
+        password: [{ required: true, trigger: 'blur', validator: validatePass }]
60
+      },
61
+      loading: false,
62
+      pwdType: 'password'
63
+    }
64
+  },
65
+  methods: {
66
+    showPwd() {
67
+      if (this.pwdType === 'password') {
68
+        this.pwdType = ''
69
+      } else {
70
+        this.pwdType = 'password'
71
+      }
72
+    },
73
+    handleLogin() {
74
+      this.$refs.loginForm.validate(valid => {
75
+        if (valid) {
76
+          this.loading = true
77
+          this.$store.dispatch('Login', this.loginForm).then(() => {
78
+            this.loading = false
79
+            this.$router.push({ path: '/' })
80
+          }).catch(() => {
81
+            this.loading = false
82
+          })
83
+        } else {
84
+          console.log('error submit!!')
85
+          return false
86
+        }
87
+      })
88
+    }
89
+  }
90
+}
91
+</script>
92
+
93
+<style rel="stylesheet/scss" lang="scss">
94
+$bg:#2d3a4b;
95
+$light_gray:#eee;
96
+
97
+/* reset element-ui css */
98
+.login-container {
99
+  .el-input {
100
+    display: inline-block;
101
+    height: 47px;
102
+    width: 85%;
103
+    input {
104
+      background: transparent;
105
+      border: 0px;
106
+      -webkit-appearance: none;
107
+      border-radius: 0px;
108
+      padding: 12px 5px 12px 15px;
109
+      color: $light_gray;
110
+      height: 47px;
111
+      &:-webkit-autofill {
112
+        -webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
113
+        -webkit-text-fill-color: #fff !important;
114
+      }
115
+    }
116
+  }
117
+  .el-form-item {
118
+    border: 1px solid rgba(255, 255, 255, 0.1);
119
+    background: rgba(0, 0, 0, 0.1);
120
+    border-radius: 5px;
121
+    color: #454545;
122
+  }
123
+}
124
+
125
+</style>
126
+
127
+<style rel="stylesheet/scss" lang="scss" scoped>
128
+$bg:#2d3a4b;
129
+$dark_gray:#889aa4;
130
+$light_gray:#eee;
131
+.login-container {
132
+  position: fixed;
133
+  height: 100%;
134
+  width: 100%;
135
+  background-color: $bg;
136
+  .login-form {
137
+    position: absolute;
138
+    left: 0;
139
+    right: 0;
140
+    width: 520px;
141
+    padding: 35px 35px 15px 35px;
142
+    margin: 120px auto;
143
+  }
144
+  .tips {
145
+    font-size: 14px;
146
+    color: #fff;
147
+    margin-bottom: 10px;
148
+    span {
149
+      &:first-of-type {
150
+        margin-right: 16px;
151
+      }
152
+    }
153
+  }
154
+  .svg-container {
155
+    padding: 6px 5px 6px 15px;
156
+    color: $dark_gray;
157
+    vertical-align: middle;
158
+    width: 30px;
159
+    display: inline-block;
160
+    &_login {
161
+      font-size: 20px;
162
+    }
163
+  }
164
+  .title {
165
+    font-size: 26px;
166
+    font-weight: 400;
167
+    color: $light_gray;
168
+    margin: 0px auto 40px auto;
169
+    text-align: center;
170
+    font-weight: bold;
171
+  }
172
+  .show-pwd {
173
+    position: absolute;
174
+    right: 10px;
175
+    top: 7px;
176
+    font-size: 16px;
177
+    color: $dark_gray;
178
+    cursor: pointer;
179
+    user-select: none;
180
+  }
181
+}
182
+</style>

+ 7
- 0
src/views/nested/menu1/index.vue View File

@@ -0,0 +1,7 @@
1
+<template >
2
+  <div style="padding:30px;">
3
+    <el-alert title="menu 1" :closable="false">
4
+      <router-view />
5
+    </el-alert>
6
+  </div>
7
+</template>

+ 7
- 0
src/views/nested/menu1/menu1-1/index.vue View File

@@ -0,0 +1,7 @@
1
+<template >
2
+  <div style="padding:30px;">
3
+    <el-alert title="menu 1-1" type="success" :closable="false">
4
+      <router-view />
5
+    </el-alert>
6
+  </div>
7
+</template>

+ 7
- 0
src/views/nested/menu1/menu1-2/index.vue View File

@@ -0,0 +1,7 @@
1
+<template>
2
+  <div style="padding:30px;">
3
+    <el-alert title="menu 1-2" type="success" :closable="false">
4
+      <router-view />
5
+    </el-alert>
6
+  </div>
7
+</template>

+ 5
- 0
src/views/nested/menu1/menu1-2/menu1-2-1/index.vue View File

@@ -0,0 +1,5 @@
1
+<template functional>
2
+  <div style="padding:30px;">
3
+    <el-alert title="menu 1-2-1" type="warning" :closable="false" />
4
+  </div>
5
+</template>

+ 5
- 0
src/views/nested/menu1/menu1-2/menu1-2-2/index.vue View File

@@ -0,0 +1,5 @@
1
+<template functional>
2
+  <div style="padding:30px;">
3
+    <el-alert title="menu 1-2-2" type="warning" :closable="false" />
4
+  </div>
5
+</template>

+ 5
- 0
src/views/nested/menu1/menu1-3/index.vue View File

@@ -0,0 +1,5 @@
1
+<template functional>
2
+  <div style="padding:30px;">
3
+    <el-alert title="menu 1-3" type="success" :closable="false" />
4
+  </div>
5
+</template>

+ 5
- 0
src/views/nested/menu2/index.vue View File

@@ -0,0 +1,5 @@
1
+<template>
2
+  <div style="padding:30px;">
3
+    <el-alert title="menu 2" :closable="false" />
4
+  </div>
5
+</template>

+ 72
- 0
src/views/table/index.vue View File

@@ -0,0 +1,72 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-table :data="list" v-loading="listLoading" element-loading-text="Loading" border fit highlight-current-row>
4
+      <el-table-column align="center" label='ID' width="95">
5
+        <template slot-scope="scope">
6
+          {{scope.$index}}
7
+        </template>
8
+      </el-table-column>
9
+      <el-table-column label="Title">
10
+        <template slot-scope="scope">
11
+          {{scope.row.title}}
12
+        </template>
13
+      </el-table-column>
14
+      <el-table-column label="Author" width="110" align="center">
15
+        <template slot-scope="scope">
16
+          <span>{{scope.row.author}}</span>
17
+        </template>
18
+      </el-table-column>
19
+      <el-table-column label="Pageviews" width="110" align="center">
20
+        <template slot-scope="scope">
21
+          {{scope.row.pageviews}}
22
+        </template>
23
+      </el-table-column>
24
+      <el-table-column class-name="status-col" label="Status" width="110" align="center">
25
+        <template slot-scope="scope">
26
+          <el-tag :type="scope.row.status | statusFilter">{{scope.row.status}}</el-tag>
27
+        </template>
28
+      </el-table-column>
29
+      <el-table-column align="center" prop="created_at" label="Display_time" width="200">
30
+        <template slot-scope="scope">
31
+          <i class="el-icon-time"></i>
32
+          <span>{{scope.row.display_time}}</span>
33
+        </template>
34
+      </el-table-column>
35
+    </el-table>
36
+  </div>
37
+</template>
38
+
39
+<script>
40
+import { getList } from '@/api/table'
41
+
42
+export default {
43
+  data() {
44
+    return {
45
+      list: null,
46
+      listLoading: true
47
+    }
48
+  },
49
+  filters: {
50
+    statusFilter(status) {
51
+      const statusMap = {
52
+        published: 'success',
53
+        draft: 'gray',
54
+        deleted: 'danger'
55
+      }
56
+      return statusMap[status]
57
+    }
58
+  },
59
+  created() {
60
+    this.fetchData()
61
+  },
62
+  methods: {
63
+    fetchData() {
64
+      this.listLoading = true
65
+      getList(this.listQuery).then(response => {
66
+        this.list = response.data.items
67
+        this.listLoading = false
68
+      })
69
+    }
70
+  }
71
+}
72
+</script>

+ 71
- 0
src/views/tree/index.vue View File

@@ -0,0 +1,71 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-input placeholder="Filter keyword" v-model="filterText" style="margin-bottom:30px;"></el-input>
4
+
5
+    <el-tree class="filter-tree" :data="data2" :props="defaultProps" default-expand-all :filter-node-method="filterNode" ref="tree2"></el-tree>
6
+
7
+  </div>
8
+</template>
9
+
10
+<script>
11
+export default {
12
+  watch: {
13
+    filterText(val) {
14
+      this.$refs.tree2.filter(val)
15
+    }
16
+  },
17
+
18
+  methods: {
19
+    filterNode(value, data) {
20
+      if (!value) return true
21
+      return data.label.indexOf(value) !== -1
22
+    }
23
+  },
24
+
25
+  data() {
26
+    return {
27
+      filterText: '',
28
+      data2: [{
29
+        id: 1,
30
+        label: 'Level one 1',
31
+        children: [{
32
+          id: 4,
33
+          label: 'Level two 1-1',
34
+          children: [{
35
+            id: 9,
36
+            label: 'Level three 1-1-1'
37
+          }, {
38
+            id: 10,
39
+            label: 'Level three 1-1-2'
40
+          }]
41
+        }]
42
+      }, {
43
+        id: 2,
44
+        label: 'Level one 2',
45
+        children: [{
46
+          id: 5,
47
+          label: 'Level two 2-1'
48
+        }, {
49
+          id: 6,
50
+          label: 'Level two 2-2'
51
+        }]
52
+      }, {
53
+        id: 3,
54
+        label: 'Level one 3',
55
+        children: [{
56
+          id: 7,
57
+          label: 'Level two 3-1'
58
+        }, {
59
+          id: 8,
60
+          label: 'Level two 3-2'
61
+        }]
62
+      }],
63
+      defaultProps: {
64
+        children: 'children',
65
+        label: 'label'
66
+      }
67
+    }
68
+  }
69
+}
70
+</script>
71
+

+ 0
- 0
static/.gitkeep View File