Bläddra i källkod

[release]4.1.0 (#211)

花裤衩 5 år sedan
förälder
incheckning
a5d4bbda75
92 ändrade filer med 1895 tillägg och 1475 borttagningar
  1. 0 12
      .babelrc
  2. 14 0
      .env.development
  3. 6 0
      .env.production
  4. 8 0
      .env.staging
  5. 2 1
      .eslintignore
  6. 5 3
      .eslintrc.js
  7. 1 0
      .gitignore
  8. 2 4
      .postcssrc.js
  9. 32 32
      README-zh.md
  10. 40 39
      README.md
  11. 5 0
      babel.config.js
  12. 0 45
      build/build.js
  13. 0 64
      build/check-versions.js
  14. 35 0
      build/index.js
  15. BIN
      build/logo.png
  16. 0 108
      build/utils.js
  17. 0 5
      build/vue-loader.conf.js
  18. 0 108
      build/webpack.base.conf.js
  19. 0 95
      build/webpack.dev.conf.js
  20. 0 177
      build/webpack.prod.conf.js
  21. 0 8
      config/dev.env.js
  22. 0 86
      config/index.js
  23. 0 5
      config/prod.env.js
  24. 0 12
      index.html
  25. 24 0
      jest.config.js
  26. 61 21
      mock/index.js
  27. 64 0
      mock/mock-server.js
  28. 25 16
      mock/table.js
  29. 44 24
      mock/user.js
  30. 0 14
      mock/utils.js
  31. 39 62
      package.json
  32. 0 0
      public/favicon.ico
  33. 17 0
      public/index.html
  34. 1 1
      src/App.vue
  35. 2 5
      src/api/login.js
  36. 23 14
      src/components/Breadcrumb/index.vue
  37. 9 7
      src/components/Hamburger/index.vue
  38. 1 1
      src/components/SvgIcon/index.vue
  39. 2 2
      src/icons/index.js
  40. 1 0
      src/icons/svg/dashboard.svg
  41. 1 1
      src/icons/svg/eye-open.svg
  42. 1 1
      src/icons/svg/link.svg
  43. 1 1
      src/icons/svg/table.svg
  44. 8 6
      src/views/layout/components/AppMain.vue
  45. 139 0
      src/layout/components/Navbar.vue
  46. 26 0
      src/layout/components/Sidebar/FixiOSBug.js
  47. 8 9
      src/views/layout/components/Sidebar/Item.vue
  48. 1 1
      src/views/layout/components/Sidebar/Link.vue
  49. 82 0
      src/layout/components/Sidebar/Logo.vue
  50. 11 7
      src/views/layout/components/Sidebar/SidebarItem.vue
  51. 54 0
      src/layout/components/Sidebar/index.vue
  52. 0 0
      src/layout/components/index.js
  53. 30 6
      src/views/layout/Layout.vue
  54. 45 0
      src/layout/mixin/ResizeHandler.js
  55. 7 7
      src/main.js
  56. 50 19
      src/permission.js
  57. 58 24
      src/router/index.js
  58. 16 0
      src/settings.js
  59. 2 1
      src/store/getters.js
  60. 5 1
      src/store/index.js
  61. 39 34
      src/store/modules/app.js
  62. 69 0
      src/store/modules/permission.js
  63. 31 0
      src/store/modules/settings.js
  64. 85 70
      src/store/modules/user.js
  65. 17 3
      src/styles/element-ui.scss
  66. 5 18
      src/styles/index.scss
  67. 15 8
      src/styles/sidebar.scss
  68. 4 4
      src/styles/transition.scss
  69. 1 1
      src/styles/variables.scss
  70. 10 0
      src/utils/get-page-title.js
  71. 90 0
      src/utils/index.js
  72. 40 28
      src/utils/request.js
  73. 14 6
      src/utils/validate.js
  74. 6 6
      src/views/404.vue
  75. 3 3
      src/views/dashboard/index.vue
  76. 13 13
      src/views/form/index.vue
  77. 0 95
      src/views/layout/components/Navbar.vue
  78. 0 39
      src/views/layout/components/Sidebar/index.vue
  79. 0 40
      src/views/layout/mixin/ResizeHandler.js
  80. 88 47
      src/views/login/index.vue
  81. 1 1
      src/views/nested/menu1/index.vue
  82. 1 1
      src/views/nested/menu1/menu1-1/index.vue
  83. 4 3
      src/views/table/index.vue
  84. 0 0
      static/.gitkeep
  85. 5 0
      tests/unit/.eslintrc.js
  86. 98 0
      tests/unit/components/Breadcrumb.spec.js
  87. 18 0
      tests/unit/components/Hamburger.spec.js
  88. 22 0
      tests/unit/components/SvgIcon.spec.js
  89. 30 0
      tests/unit/utils/formatTime.spec.js
  90. 28 0
      tests/unit/utils/parseTime.spec.js
  91. 17 0
      tests/unit/utils/validate.spec.js
  92. 133 0
      vue.config.js

+ 0 - 12
.babelrc

@@ -1,12 +0,0 @@
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
.env.development

@@ -0,0 +1,14 @@
1
+# just a flag
2
+ENV = 'development'
3
+
4
+# base api
5
+VUE_APP_BASE_API = '/dev-api'
6
+
7
+# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
8
+# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
9
+# It only does one thing by converting all import() to require().
10
+# This configuration can significantly increase the speed of hot updates,
11
+# when you have a large number of pages.
12
+# Detail:  https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
13
+
14
+VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 6 - 0
.env.production

@@ -0,0 +1,6 @@
1
+# just a flag
2
+ENV = 'production'
3
+
4
+# base api
5
+VUE_APP_BASE_API = '/prod-api'
6
+

+ 8 - 0
.env.staging

@@ -0,0 +1,8 @@
1
+NODE_ENV = production
2
+
3
+# just a flag
4
+ENV = 'staging'
5
+
6
+# base api
7
+VUE_APP_BASE_API = '/stage-api'
8
+

+ 2 - 1
.eslintignore

@@ -1,3 +1,4 @@
1 1
 build/*.js
2
-config/*.js
3 2
 src/assets
3
+public
4
+dist

+ 5 - 3
.eslintrc.js

@@ -21,7 +21,10 @@ module.exports = {
21 21
         "allowFirstLine": false
22 22
       }
23 23
     }],
24
+    "vue/singleline-html-element-content-newline": "off",
25
+    "vue/multiline-html-element-content-newline":"off",
24 26
     "vue/name-property-casing": ["error", "PascalCase"],
27
+    "vue/no-v-html": "off",
25 28
     'accessor-pairs': 2,
26 29
     'arrow-spacing': [2, {
27 30
       'before': true,
@@ -44,7 +47,7 @@ module.exports = {
44 47
     'curly': [2, 'multi-line'],
45 48
     'dot-location': [2, 'property'],
46 49
     'eol-last': 2,
47
-    'eqeqeq': [2, 'allow-null'],
50
+    'eqeqeq': ["error", "always", {"null": "ignore"}],
48 51
     'generator-star-spacing': [2, {
49 52
       'before': true,
50 53
       'after': true
@@ -73,7 +76,7 @@ module.exports = {
73 76
     'no-class-assign': 2,
74 77
     'no-cond-assign': 2,
75 78
     'no-const-assign': 2,
76
-    'no-control-regex': 2,
79
+    'no-control-regex': 0,
77 80
     'no-delete-var': 2,
78 81
     'no-dupe-args': 2,
79 82
     'no-dupe-class-members': 2,
@@ -193,4 +196,3 @@ module.exports = {
193 196
     'array-bracket-spacing': [2, 'never']
194 197
   }
195 198
 }
196
-

+ 1 - 0
.gitignore

@@ -5,6 +5,7 @@ npm-debug.log*
5 5
 yarn-debug.log*
6 6
 yarn-error.log*
7 7
 package-lock.json
8
+tests/**/coverage/
8 9
 
9 10
 # Editor directories and files
10 11
 .idea

+ 2 - 4
.postcssrc.js

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

+ 32 - 32
README-zh.md

@@ -1,19 +1,17 @@
1 1
 # vue-admin-template
2 2
 
3
-> 这是一个 极简的 vue admin 管理后台 它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
3
+> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
4 4
 
5 5
 [线上地址](http://panjiachen.github.io/vue-admin-template)
6 6
 
7 7
 [国内访问](https://panjiachen.gitee.io/vue-admin-template)
8 8
 
9
+目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若发现问题,欢迎提 issue。若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
10
+
9 11
 ## Extra
10 12
 
11 13
 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
12 14
 
13
-本项目基于`webpack4`开发,若还想使用`webpack3`开发,请使用该分支[webpack3](https://github.com/PanJiaChen/vue-admin-template/tree/webpack3)
14
-
15
-如果你想使用基于 vue + typescript 的管理后台, 可以看看这个项目: [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (鸣谢: [@Armour](https://github.com/Armour))
16
-
17 15
 ## 相关项目
18 16
 
19 17
 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
@@ -33,53 +31,55 @@
33 31
 ## Build Setup
34 32
 
35 33
 ```bash
36
-# Clone project
34
+# 克隆项目
37 35
 git clone https://github.com/PanJiaChen/vue-admin-template.git
38 36
 
39
-# Install dependencies
37
+# 进入项目目录
38
+cd vue-admin-template
39
+
40
+# 安装依赖
40 41
 npm install
41 42
 
42
-# 建议不要用cnpm  安装有各种诡异的bug 可以通过如下操作解决npm速度慢的问题
43
+# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
43 44
 npm install --registry=https://registry.npm.taobao.org
44 45
 
45
-# Serve with hot reload at localhost:9528
46
+# 启动服务
46 47
 npm run dev
48
+```
47 49
 
48
-# Build for production with minification
49
-npm run build
50
+浏览器访问 [http://localhost:9528](http://localhost:9528)
50 51
 
51
-# Build for production and view the bundle analyzer report
52
-npm run build --report
53
-```
52
+## 发布
54 53
 
55
-## Demo
54
+```bash
55
+# 构建测试环境
56
+npm run build:stage
56 57
 
57
-![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
58
+# 构建生产环境
59
+npm run build:prod
60
+```
58 61
 
59
-### Element-Ui 使用 cdn 教程
62
+## 其它
60 63
 
61
-首先找到 `index.html` ([根目录下](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/index.html))
64
+```bash
65
+# 预览发布环境效果
66
+npm run preview
62 67
 
63
-引入 Element 的 css 和 js ,并且引入 vue 。因为 Element-Ui 是依赖 vue 的,所以必须在它之前引入 vue 。
68
+# 预览发布环境效果 + 静态资源分析
69
+npm run preview -- --report
64 70
 
65
-之后找到 [webpack.base.conf.js](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/build/webpack.base.conf.js) 加入 `externals` 让 webpack 不打包 vue 和 element
71
+# 代码格式检查
72
+npm run lint
66 73
 
67
-```
68
-externals: {
69
-  vue: 'Vue',
70
-  'element-ui':'ELEMENT'
71
-}
74
+# 代码格式检查并自动修复
75
+npm run lint -- --fix
72 76
 ```
73 77
 
74
-之后还有一个小细节是如果你用了全局对象方式引入 vue,就不需要 手动 `Vue.use(Vuex)` ,它会自动挂载,具体见 [issue](https://github.com/vuejs/vuex/issues/731)
78
+更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
75 79
 
76
-最终你可以使用 `npm run build --report` 查看效果
77
-如图:
78
-![demo](https://panjiachen.github.io/images/element-cdn.png)
79
-
80
-**[具体代码](https://github.com/PanJiaChen/vue-admin-template/commit/746aff560932704ae821f82f10b8b2a9681d5177)**
80
+## Demo
81 81
 
82
-**[对应分支](https://github.com/PanJiaChen/vue-admin-template/tree/element-ui-cdn)**
82
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
83 83
 
84 84
 ## Browsers support
85 85
 

+ 40 - 39
README.md

@@ -1,30 +1,61 @@
1 1
 # vue-admin-template
2 2
 
3
+English | [简体中文](./README.zh-CN.md)
4
+
3 5
 > A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
4 6
 
5 7
 **Live demo:** http://panjiachen.github.io/vue-admin-template
6 8
 
7
-[中文文档](https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md)
9
+
10
+**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli'**
8 11
 
9 12
 ## Build Setup
10 13
 
14
+
11 15
 ```bash
12
-# Clone project
13
-git clone https://github.com/PanJiaChen/vue-admin-template.git
16
+# clone the project
17
+git clone https://github.com/PanJiaChen/vue-element-admin.git
18
+
19
+# enter the project directory
20
+cd vue-element-admin
14 21
 
15
-# Install dependencies
22
+# install dependency
16 23
 npm install
17 24
 
18
-# Serve with hot reload at localhost:9528
25
+# develop
19 26
 npm run dev
27
+```
28
+
29
+This will automatically open http://localhost:9527
30
+
31
+## Build
32
+
33
+```bash
34
+# build for test environment
35
+npm run build:stage
36
+
37
+# build for production environment
38
+npm run build:prod
39
+```
40
+
41
+## Advanced
42
+
43
+```bash
44
+# preview the release environment effect
45
+npm run preview
46
+
47
+# preview the release environment effect + static resource analysis
48
+npm run preview -- --report
20 49
 
21
-# Build for production with minification
22
-npm run build
50
+# code format check
51
+npm run lint
23 52
 
24
-# Build for production and view the bundle analyzer report
25
-npm run build --report
53
+# code format check and auto fix
54
+npm run lint -- --fix
26 55
 ```
27 56
 
57
+Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
58
+
28 59
 ## Demo
29 60
 
30 61
 ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
@@ -33,8 +64,6 @@ npm run build --report
33 64
 
34 65
 If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
35 66
 
36
-This project is based on `webpack4` development. If you want to use `webpack3` development, please use this branch [webpack3](https://github.com/PanJiaChen/vue-admin-template/tree/webpack3)
37
-
38 67
 For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
39 68
 
40 69
 ## Related Project
@@ -45,34 +74,6 @@ For `typescript` version, you can use [vue-typescript-admin-template](https://gi
45 74
 
46 75
 [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
47 76
 
48
-### Element-Ui using cdn tutorial
49
-
50
-First find `index.html`([root directory](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/index.html))
51
-
52
-Import css and js of `Element`, and then import vue. Because `Element` is vue-dependent, vue must be import before it.
53
-
54
-Then find [webpack.base.conf.js](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/build/webpack.base.conf.js)
55
-Add `externals` to make webpack not package vue and element.
56
-
57
-```
58
-externals: {
59
-  vue: 'Vue',
60
-  'element-ui':'ELEMENT'
61
-}
62
-```
63
-
64
-Finally there is a small detail to pay attention to that if you import vue in global, you don't need to manually `Vue.use(Vuex)`, it will be automatically mounted, see
65
-[issue](https://github.com/vuejs/vuex/issues/731)
66
-
67
-And you can use `npm run build --report` to see the effect
68
-
69
-Pictured:
70
-![demo](https://panjiachen.github.io/images/element-cdn.png)
71
-
72
-**[Detailed code](https://github.com/PanJiaChen/vue-admin-template/commit/746aff560932704ae821f82f10b8b2a9681d5177)**
73
-
74
-**[Branch](https://github.com/PanJiaChen/vue-admin-template/tree/element-ui-cdn)**
75
-
76 77
 ## Browsers support
77 78
 
78 79
 Modern browsers and Internet Explorer 10+.

+ 5 - 0
babel.config.js

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

+ 0 - 45
build/build.js

@@ -1,45 +0,0 @@
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(
23
-      stats.toString({
24
-        colors: true,
25
-        modules: false,
26
-        children: false,
27
-        chunks: false,
28
-        chunkModules: false
29
-      }) + '\n\n'
30
-    )
31
-
32
-    if (stats.hasErrors()) {
33
-      console.log(chalk.red('  Build failed with errors.\n'))
34
-      process.exit(1)
35
-    }
36
-
37
-    console.log(chalk.cyan('  Build complete.\n'))
38
-    console.log(
39
-      chalk.yellow(
40
-        '  Tip: built files are meant to be served over an HTTP server.\n' +
41
-          "  Opening index.html over file:// won't work.\n"
42
-      )
43
-    )
44
-  })
45
-})

+ 0 - 64
build/check-versions.js

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

+ 35 - 0
build/index.js

@@ -0,0 +1,35 @@
1
+const { run } = require('runjs')
2
+const chalk = require('chalk')
3
+const config = require('../vue.config.js')
4
+const rawArgv = process.argv.slice(2)
5
+const args = rawArgv.join(' ')
6
+
7
+if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
8
+  const report = rawArgv.includes('--report')
9
+
10
+  run(`vue-cli-service build ${args}`)
11
+
12
+  const port = 9526
13
+  const publicPath = config.publicPath
14
+
15
+  var connect = require('connect')
16
+  var serveStatic = require('serve-static')
17
+  const app = connect()
18
+
19
+  app.use(
20
+    publicPath,
21
+    serveStatic('./dist', {
22
+      index: ['index.html', '/']
23
+    })
24
+  )
25
+
26
+  app.listen(port, function () {
27
+    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
28
+    if (report) {
29
+      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
30
+    }
31
+
32
+  })
33
+} else {
34
+  run(`vue-cli-service build ${args}`)
35
+}

BIN
build/logo.png


+ 0 - 108
build/utils.js

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

+ 0 - 5
build/vue-loader.conf.js

@@ -1,5 +0,0 @@
1
-'use strict'
2
-
3
-module.exports = {
4
-  //You can set the vue-loader configuration by yourself.
5
-}

+ 0 - 108
build/webpack.base.conf.js

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

+ 0 - 95
build/webpack.dev.conf.js

@@ -1,95 +0,0 @@
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
-  mode: 'development',
21
-  module: {
22
-    rules: utils.styleLoaders({
23
-      sourceMap: config.dev.cssSourceMap,
24
-      usePostCSS: true
25
-    })
26
-  },
27
-  // cheap-module-eval-source-map is faster for development
28
-  devtool: config.dev.devtool,
29
-
30
-  // these devServer options should be customized in /config/index.js
31
-  devServer: {
32
-    clientLogLevel: 'warning',
33
-    historyApiFallback: true,
34
-    hot: true,
35
-    compress: true,
36
-    host: HOST || config.dev.host,
37
-    port: PORT || config.dev.port,
38
-    open: config.dev.autoOpenBrowser,
39
-    overlay: config.dev.errorOverlay
40
-      ? { warnings: false, errors: true }
41
-      : false,
42
-    publicPath: config.dev.assetsPublicPath,
43
-    proxy: config.dev.proxyTable,
44
-    quiet: true, // necessary for FriendlyErrorsPlugin
45
-    watchOptions: {
46
-      poll: config.dev.poll
47
-    }
48
-  },
49
-  plugins: [
50
-    new webpack.DefinePlugin({
51
-      'process.env': require('../config/dev.env')
52
-    }),
53
-    new webpack.HotModuleReplacementPlugin(),
54
-    // https://github.com/ampedandwired/html-webpack-plugin
55
-    new HtmlWebpackPlugin({
56
-      filename: 'index.html',
57
-      template: 'index.html',
58
-      inject: true,
59
-      favicon: resolve('favicon.ico'),
60
-      title: 'vue-admin-template'
61
-    })
62
-  ]
63
-})
64
-
65
-module.exports = new Promise((resolve, reject) => {
66
-  portfinder.basePort = process.env.PORT || config.dev.port
67
-  portfinder.getPort((err, port) => {
68
-    if (err) {
69
-      reject(err)
70
-    } else {
71
-      // publish the new Port, necessary for e2e tests
72
-      process.env.PORT = port
73
-      // add port to devServer config
74
-      devWebpackConfig.devServer.port = port
75
-
76
-      // Add FriendlyErrorsPlugin
77
-      devWebpackConfig.plugins.push(
78
-        new FriendlyErrorsPlugin({
79
-          compilationSuccessInfo: {
80
-            messages: [
81
-              `Your application is running here: http://${
82
-                devWebpackConfig.devServer.host
83
-              }:${port}`
84
-            ]
85
-          },
86
-          onErrors: config.dev.notifyOnErrors
87
-            ? utils.createNotifierCallback()
88
-            : undefined
89
-        })
90
-      )
91
-
92
-      resolve(devWebpackConfig)
93
-    }
94
-  })
95
-})

+ 0 - 177
build/webpack.prod.conf.js

@@ -1,177 +0,0 @@
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 ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
11
-const MiniCssExtractPlugin = require('mini-css-extract-plugin')
12
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
13
-const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
14
-
15
-function resolve(dir) {
16
-  return path.join(__dirname, '..', dir)
17
-}
18
-
19
-const env = require('../config/prod.env')
20
-
21
-// For NamedChunksPlugin
22
-const seen = new Set()
23
-const nameLength = 4
24
-
25
-const webpackConfig = merge(baseWebpackConfig, {
26
-  mode: 'production',
27
-  module: {
28
-    rules: utils.styleLoaders({
29
-      sourceMap: config.build.productionSourceMap,
30
-      extract: true,
31
-      usePostCSS: true
32
-    })
33
-  },
34
-  devtool: config.build.productionSourceMap ? config.build.devtool : false,
35
-  output: {
36
-    path: config.build.assetsRoot,
37
-    filename: utils.assetsPath('js/[name].[chunkhash:8].js'),
38
-    chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js')
39
-  },
40
-  plugins: [
41
-    // http://vuejs.github.io/vue-loader/en/workflow/production.html
42
-    new webpack.DefinePlugin({
43
-      'process.env': env
44
-    }),
45
-    // extract css into its own file
46
-    new MiniCssExtractPlugin({
47
-      filename: utils.assetsPath('css/[name].[contenthash:8].css'),
48
-      chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css')
49
-    }),
50
-    // generate dist index.html with correct asset hash for caching.
51
-    // you can customize output by editing /index.html
52
-    // see https://github.com/ampedandwired/html-webpack-plugin
53
-    new HtmlWebpackPlugin({
54
-      filename: config.build.index,
55
-      template: 'index.html',
56
-      inject: true,
57
-      favicon: resolve('favicon.ico'),
58
-      title: 'vue-admin-template',
59
-      minify: {
60
-        removeComments: true,
61
-        collapseWhitespace: true,
62
-        removeAttributeQuotes: true
63
-        // more options:
64
-        // https://github.com/kangax/html-minifier#options-quick-reference
65
-      }
66
-      // default sort mode uses toposort which cannot handle cyclic deps
67
-      // in certain cases, and in webpack 4, chunk order in HTML doesn't
68
-      // matter anyway
69
-    }),
70
-    new ScriptExtHtmlWebpackPlugin({
71
-      //`runtime` must same as runtimeChunk name. default is `runtime`
72
-      inline: /runtime\..*\.js$/
73
-    }),
74
-    // keep chunk.id stable when chunk has no name
75
-    new webpack.NamedChunksPlugin(chunk => {
76
-      if (chunk.name) {
77
-        return chunk.name
78
-      }
79
-      const modules = Array.from(chunk.modulesIterable)
80
-      if (modules.length > 1) {
81
-        const hash = require('hash-sum')
82
-        const joinedHash = hash(modules.map(m => m.id).join('_'))
83
-        let len = nameLength
84
-        while (seen.has(joinedHash.substr(0, len))) len++
85
-        seen.add(joinedHash.substr(0, len))
86
-        return `chunk-${joinedHash.substr(0, len)}`
87
-      } else {
88
-        return modules[0].id
89
-      }
90
-    }),
91
-    // keep module.id stable when vender modules does not change
92
-    new webpack.HashedModuleIdsPlugin(),
93
-    // copy custom static assets
94
-    new CopyWebpackPlugin([
95
-      {
96
-        from: path.resolve(__dirname, '../static'),
97
-        to: config.build.assetsSubDirectory,
98
-        ignore: ['.*']
99
-      }
100
-    ])
101
-  ],
102
-  optimization: {
103
-    splitChunks: {
104
-      chunks: 'all',
105
-      cacheGroups: {
106
-        libs: {
107
-          name: 'chunk-libs',
108
-          test: /[\\/]node_modules[\\/]/,
109
-          priority: 10,
110
-          chunks: 'initial' // 只打包初始时依赖的第三方
111
-        },
112
-        elementUI: {
113
-          name: 'chunk-elementUI', // 单独将 elementUI 拆包
114
-          priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
115
-          test: /[\\/]node_modules[\\/]element-ui[\\/]/
116
-        }
117
-      }
118
-    },
119
-    runtimeChunk: 'single',
120
-    minimizer: [
121
-      new UglifyJsPlugin({
122
-        uglifyOptions: {
123
-          mangle: {
124
-            safari10: true
125
-          }
126
-        },
127
-        sourceMap: config.build.productionSourceMap,
128
-        cache: true,
129
-        parallel: true
130
-      }),
131
-      // Compress extracted CSS. We are using this plugin so that possible
132
-      // duplicated CSS from different components can be deduped.
133
-      new OptimizeCSSAssetsPlugin()
134
-    ]
135
-  }
136
-})
137
-
138
-if (config.build.productionGzip) {
139
-  const CompressionWebpackPlugin = require('compression-webpack-plugin')
140
-
141
-  webpackConfig.plugins.push(
142
-    new CompressionWebpackPlugin({
143
-      algorithm: 'gzip',
144
-      test: new RegExp(
145
-        '\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
146
-      ),
147
-      threshold: 10240,
148
-      minRatio: 0.8
149
-    })
150
-  )
151
-}
152
-
153
-if (config.build.generateAnalyzerReport || config.build.bundleAnalyzerReport) {
154
-  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
155
-    .BundleAnalyzerPlugin
156
-
157
-  if (config.build.bundleAnalyzerReport) {
158
-    webpackConfig.plugins.push(
159
-      new BundleAnalyzerPlugin({
160
-        analyzerPort: 8080,
161
-        generateStatsFile: false
162
-      })
163
-    )
164
-  }
165
-
166
-  if (config.build.generateAnalyzerReport) {
167
-    webpackConfig.plugins.push(
168
-      new BundleAnalyzerPlugin({
169
-        analyzerMode: 'static',
170
-        reportFilename: 'bundle-report.html',
171
-        openAnalyzer: false
172
-      })
173
-    )
174
-  }
175
-}
176
-
177
-module.exports = webpackConfig

+ 0 - 8
config/dev.env.js

@@ -1,8 +0,0 @@
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
-})

+ 0 - 86
config/index.js

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

+ 0 - 5
config/prod.env.js

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

+ 0 - 12
index.html

@@ -1,12 +0,0 @@
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>

+ 24 - 0
jest.config.js

@@ -0,0 +1,24 @@
1
+module.exports = {
2
+  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
3
+  transform: {
4
+    '^.+\\.vue$': 'vue-jest',
5
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
6
+      'jest-transform-stub',
7
+    '^.+\\.jsx?$': 'babel-jest'
8
+  },
9
+  moduleNameMapper: {
10
+    '^@/(.*)$': '<rootDir>/src/$1'
11
+  },
12
+  snapshotSerializers: ['jest-serializer-vue'],
13
+  testMatch: [
14
+    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
15
+  ],
16
+  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
17
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
18
+  // 'collectCoverage': true,
19
+  'coverageReporters': [
20
+    'lcov',
21
+    'text-summary'
22
+  ],
23
+  testURL: 'http://localhost/'
24
+}

+ 61 - 21
mock/index.js

@@ -1,26 +1,66 @@
1 1
 import Mock from 'mockjs'
2
-import userAPI from './user'
3
-import tableAPI from './table'
4
-
5
-// Fix an issue with setting withCredentials = true, cross-domain request lost cookies
6
-// https://github.com/nuysoft/Mock/issues/300
7
-Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
8
-Mock.XHR.prototype.send = function() {
9
-  if (this.custom.xhr) {
10
-    this.custom.xhr.withCredentials = this.withCredentials || false
2
+import { param2Obj } from '../src/utils'
3
+
4
+import user from './user'
5
+import table from './table'
6
+
7
+const mocks = [
8
+  ...user,
9
+  ...table
10
+]
11
+
12
+// for front mock
13
+// please use it cautiously, it will redefine XMLHttpRequest,
14
+// which will cause many of your third-party libraries to be invalidated(like progress event).
15
+export function mockXHR() {
16
+  // mock patch
17
+  // https://github.com/nuysoft/Mock/issues/300
18
+  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
19
+  Mock.XHR.prototype.send = function() {
20
+    if (this.custom.xhr) {
21
+      this.custom.xhr.withCredentials = this.withCredentials || false
22
+
23
+      if (this.responseType) {
24
+        this.custom.xhr.responseType = this.responseType
25
+      }
26
+    }
27
+    this.proxy_send(...arguments)
11 28
   }
12
-  this.proxy_send(...arguments)
13
-}
14
-// Mock.setup({
15
-//   timeout: '350-600'
16
-// })
17 29
 
18
-// User
19
-Mock.mock(/\/user\/login/, 'post', userAPI.login)
20
-Mock.mock(/\/user\/info/, 'get', userAPI.getInfo)
21
-Mock.mock(/\/user\/logout/, 'post', userAPI.logout)
30
+  function XHR2ExpressReqWrap(respond) {
31
+    return function(options) {
32
+      let result = null
33
+      if (respond instanceof Function) {
34
+        const { body, type, url } = options
35
+        // https://expressjs.com/en/4x/api.html#req
36
+        result = respond({
37
+          method: type,
38
+          body: JSON.parse(body),
39
+          query: param2Obj(url)
40
+        })
41
+      } else {
42
+        result = respond
43
+      }
44
+      return Mock.mock(result)
45
+    }
46
+  }
22 47
 
23
-// Table
24
-Mock.mock(/\/table\/list/, 'get', tableAPI.list)
48
+  for (const i of mocks) {
49
+    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
50
+  }
51
+}
52
+
53
+// for mock server
54
+const responseFake = (url, type, respond) => {
55
+  return {
56
+    url: new RegExp(`/mock${url}`),
57
+    type: type || 'get',
58
+    response(req, res) {
59
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
60
+    }
61
+  }
62
+}
25 63
 
26
-export default Mock
64
+export default mocks.map(route => {
65
+  return responseFake(route.url, route.type, route.response)
66
+})

+ 64 - 0
mock/mock-server.js

@@ -0,0 +1,64 @@
1
+const chokidar = require('chokidar')
2
+const bodyParser = require('body-parser')
3
+const chalk = require('chalk')
4
+const path = require('path')
5
+
6
+const mockDir = path.join(process.cwd(), 'mock')
7
+
8
+function registerRoutes(app) {
9
+  let mockLastIndex
10
+  const { default: mocks } = require('./index.js')
11
+  for (const mock of mocks) {
12
+    app[mock.type](mock.url, mock.response)
13
+    mockLastIndex = app._router.stack.length
14
+  }
15
+  const mockRoutesLength = Object.keys(mocks).length
16
+  return {
17
+    mockRoutesLength: mockRoutesLength,
18
+    mockStartIndex: mockLastIndex - mockRoutesLength
19
+  }
20
+}
21
+
22
+function unregisterRoutes() {
23
+  Object.keys(require.cache).forEach(i => {
24
+    if (i.includes(mockDir)) {
25
+      delete require.cache[require.resolve(i)]
26
+    }
27
+  })
28
+}
29
+
30
+module.exports = app => {
31
+  // es6 polyfill
32
+  require('@babel/register')
33
+
34
+  // parse app.body
35
+  // https://expressjs.com/en/4x/api.html#req.body
36
+  app.use(bodyParser.json())
37
+  app.use(bodyParser.urlencoded({
38
+    extended: true
39
+  }))
40
+
41
+  const mockRoutes = registerRoutes(app)
42
+  var mockRoutesLength = mockRoutes.mockRoutesLength
43
+  var mockStartIndex = mockRoutes.mockStartIndex
44
+
45
+  // watch files, hot reload mock server
46
+  chokidar.watch(mockDir, {
47
+    ignored: /mock-server/,
48
+    ignoreInitial: true
49
+  }).on('all', (event, path) => {
50
+    if (event === 'change' || event === 'add') {
51
+      // remove mock routes stack
52
+      app._router.stack.splice(mockStartIndex, mockRoutesLength)
53
+
54
+      // clear routes cache
55
+      unregisterRoutes()
56
+
57
+      const mockRoutes = registerRoutes(app)
58
+      mockRoutesLength = mockRoutes.mockRoutesLength
59
+      mockStartIndex = mockRoutes.mockStartIndex
60
+
61
+      console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
62
+    }
63
+  })
64
+}

+ 25 - 16
mock/table.js

@@ -1,20 +1,29 @@
1 1
 import Mock from 'mockjs'
2 2
 
3
-export default {
4
-  list: () => {
5
-    const items = Mock.mock({
6
-      'items|30': [{
7
-        id: '@id',
8
-        title: '@sentence(10, 20)',
9
-        'status|1': ['published', 'draft', 'deleted'],
10
-        author: 'name',
11
-        display_time: '@datetime',
12
-        pageviews: '@integer(300, 5000)'
13
-      }]
14
-    })
15
-    return {
16
-      code: 20000,
17
-      data: items
3
+const data = Mock.mock({
4
+  'items|30': [{
5
+    id: '@id',
6
+    title: '@sentence(10, 20)',
7
+    'status|1': ['published', 'draft', 'deleted'],
8
+    author: 'name',
9
+    display_time: '@datetime',
10
+    pageviews: '@integer(300, 5000)'
11
+  }]
12
+})
13
+
14
+export default [
15
+  {
16
+    url: '/table/list',
17
+    type: 'get',
18
+    response: config => {
19
+      const items = data.items
20
+      return {
21
+        code: 20000,
22
+        data: {
23
+          total: items.length,
24
+          items: items
25
+        }
26
+      }
18 27
     }
19 28
   }
20
-}
29
+]

+ 44 - 24
mock/user.js

@@ -1,4 +1,3 @@
1
-import { param2Obj } from './utils'
2 1
 
3 2
 const tokens = {
4 3
   admin: {
@@ -24,41 +23,62 @@ const users = {
24 23
   }
25 24
 }
26 25
 
27
-export default {
28
-  login: res => {
29
-    const { username } = JSON.parse(res.body)
30
-    const data = tokens[username]
26
+export default [
27
+  // user login
28
+  {
29
+    url: '/user/login',
30
+    type: 'post',
31
+    response: config => {
32
+      const { username } = config.body
33
+      const token = tokens[username]
34
+
35
+      // mock error
36
+      if (!token) {
37
+        return {
38
+          code: 60204,
39
+          message: 'Account and password are incorrect.'
40
+        }
41
+      }
31 42
 
32
-    if (data) {
33 43
       return {
34 44
         code: 20000,
35
-        data
45
+        data: token
36 46
       }
37 47
     }
38
-    return {
39
-      code: 60204,
40
-      message: 'Account and password are incorrect.'
41
-    }
42 48
   },
43
-  getInfo: res => {
44
-    const { token } = param2Obj(res.url)
45
-    const info = users[token]
46 49
 
47
-    if (info) {
50
+  // get user info
51
+  {
52
+    url: '/user/info\.*',
53
+    type: 'get',
54
+    response: config => {
55
+      const { token } = config.query
56
+      const info = users[token]
57
+
58
+      // mock error
59
+      if (!info) {
60
+        return {
61
+          code: 50008,
62
+          message: 'Login failed, unable to get user details.'
63
+        }
64
+      }
65
+
48 66
       return {
49 67
         code: 20000,
50 68
         data: info
51 69
       }
52 70
     }
53
-    return {
54
-      code: 50008,
55
-      message: 'Login failed, unable to get user details.'
56
-    }
57 71
   },
58
-  logout: () => {
59
-    return {
60
-      code: 20000,
61
-      data: 'success'
72
+
73
+  // user logout
74
+  {
75
+    url: '/user/logout',
76
+    type: 'post',
77
+    response: _ => {
78
+      return {
79
+        code: 20000,
80
+        data: 'success'
81
+      }
62 82
     }
63 83
   }
64
-}
84
+]

+ 0 - 14
mock/utils.js

@@ -1,14 +0,0 @@
1
-export function param2Obj(url) {
2
-  const search = url.split('?')[1]
3
-  if (!search) {
4
-    return {}
5
-  }
6
-  return JSON.parse(
7
-    '{"' +
8
-      decodeURIComponent(search)
9
-        .replace(/"/g, '\\"')
10
-        .replace(/&/g, '","')
11
-        .replace(/=/g, '":"') +
12
-      '"}'
13
-  )
14
-}

+ 39 - 62
package.json

@@ -1,82 +1,59 @@
1 1
 {
2 2
   "name": "vue-admin-template",
3
-  "version": "3.9.0",
4
-  "license": "MIT",
3
+  "version": "4.1.0",
5 4
   "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
6 5
   "author": "Pan <panfree23@gmail.com>",
6
+  "license": "MIT",
7 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 npm run build",
8
+    "dev": "vue-cli-service serve",
9
+    "build:prod": "vue-cli-service build",
10
+    "build:stage": "vue-cli-service build --mode staging",
11
+    "preview": "node build/index.js --preview",
12 12
     "lint": "eslint --ext .js,.vue src",
13
-    "test": "npm run lint",
13
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
14
+    "test:ci": "npm run lint && npm run test:unit",
14 15
     "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
15 16
   },
16 17
   "dependencies": {
17 18
     "axios": "0.18.0",
18
-    "element-ui": "2.4.6",
19
+    "element-ui": "2.7.2",
19 20
     "js-cookie": "2.2.0",
20
-    "mockjs": "1.0.1-beta3",
21 21
     "normalize.css": "7.0.0",
22 22
     "nprogress": "0.2.0",
23
-    "vue": "2.5.17",
24
-    "vue-router": "3.0.1",
25
-    "vuex": "3.0.1"
23
+    "path-to-regexp": "2.4.0",
24
+    "vue": "2.6.10",
25
+    "vue-router": "3.0.6",
26
+    "vuex": "3.1.0"
26 27
   },
27 28
   "devDependencies": {
28
-    "autoprefixer": "8.5.0",
29
-    "babel-core": "6.26.0",
30
-    "babel-eslint": "8.2.6",
31
-    "babel-helper-vue-jsx-merge-props": "2.0.3",
32
-    "babel-loader": "7.1.5",
33
-    "babel-plugin-syntax-jsx": "6.18.0",
34
-    "babel-plugin-transform-runtime": "6.23.0",
35
-    "babel-plugin-transform-vue-jsx": "3.7.0",
36
-    "babel-preset-env": "1.7.0",
37
-    "babel-preset-stage-2": "6.24.1",
38
-    "chalk": "2.4.1",
39
-    "compression-webpack-plugin": "2.0.0",
40
-    "copy-webpack-plugin": "4.5.2",
41
-    "css-loader": "1.0.0",
42
-    "eslint": "4.19.1",
43
-    "eslint-friendly-formatter": "4.0.1",
44
-    "eslint-loader": "2.0.0",
45
-    "eslint-plugin-vue": "4.7.1",
46
-    "eventsource-polyfill": "0.9.6",
47
-    "file-loader": "1.1.11",
48
-    "friendly-errors-webpack-plugin": "1.7.0",
49
-    "html-webpack-plugin": "4.0.0-alpha",
50
-    "mini-css-extract-plugin": "0.4.1",
51
-    "node-notifier": "5.2.1",
52
-    "node-sass": "^4.7.2",
53
-    "optimize-css-assets-webpack-plugin": "5.0.0",
54
-    "ora": "3.0.0",
55
-    "path-to-regexp": "2.4.0",
56
-    "portfinder": "1.0.16",
57
-    "postcss-import": "12.0.0",
58
-    "postcss-loader": "2.1.6",
59
-    "postcss-url": "7.3.2",
60
-    "rimraf": "2.6.2",
61
-    "sass-loader": "7.0.3",
62
-    "script-ext-html-webpack-plugin": "2.0.1",
63
-    "semver": "5.5.0",
64
-    "shelljs": "0.8.2",
65
-    "svg-sprite-loader": "3.8.0",
66
-    "svgo": "1.0.5",
67
-    "uglifyjs-webpack-plugin": "1.2.7",
68
-    "url-loader": "1.0.1",
69
-    "vue-loader": "15.3.0",
70
-    "vue-style-loader": "4.1.2",
71
-    "vue-template-compiler": "2.5.17",
72
-    "webpack": "4.16.5",
73
-    "webpack-bundle-analyzer": "2.13.1",
74
-    "webpack-cli": "3.1.0",
75
-    "webpack-dev-server": "3.1.14",
76
-    "webpack-merge": "4.1.4"
29
+    "@babel/core": "7.0.0",
30
+    "@babel/register": "7.0.0",
31
+    "@vue/cli-plugin-babel": "3.6.0",
32
+    "@vue/cli-plugin-eslint": "3.6.0",
33
+    "@vue/cli-plugin-unit-jest": "3.6.3",
34
+    "@vue/cli-service": "3.6.0",
35
+    "@vue/test-utils": "1.0.0-beta.29",
36
+    "babel-core": "7.0.0-bridge.0",
37
+    "babel-eslint": "10.0.1",
38
+    "babel-jest": "23.6.0",
39
+    "chalk": "2.4.2",
40
+    "connect": "3.6.6",
41
+    "eslint": "5.15.3",
42
+    "eslint-plugin-vue": "5.2.2",
43
+    "html-webpack-plugin": "3.2.0",
44
+    "mockjs": "1.0.1-beta3",
45
+    "node-sass": "^4.9.0",
46
+    "runjs": "^4.3.2",
47
+    "sass-loader": "^7.1.0",
48
+    "script-ext-html-webpack-plugin": "2.1.3",
49
+    "script-loader": "0.7.2",
50
+    "serve-static": "^1.13.2",
51
+    "svg-sprite-loader": "4.1.3",
52
+    "svgo": "1.2.2",
53
+    "vue-template-compiler": "2.6.10"
77 54
   },
78 55
   "engines": {
79
-    "node": ">= 6.0.0",
56
+    "node": ">=8.9",
80 57
     "npm": ">= 3.0.0"
81 58
   },
82 59
   "browserslist": [

favicon.ico → public/favicon.ico


+ 17 - 0
public/index.html

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

+ 1 - 1
src/App.vue

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
   <div id="app">
3
-    <router-view/>
3
+    <router-view />
4 4
   </div>
5 5
 </template>
6 6
 

+ 2 - 5
src/api/login.js

@@ -1,13 +1,10 @@
1 1
 import request from '@/utils/request'
2 2
 
3
-export function login(username, password) {
3
+export function login(data) {
4 4
   return request({
5 5
     url: '/user/login',
6 6
     method: 'post',
7
-    data: {
8
-      username,
9
-      password
10
-    }
7
+    data
11 8
   })
12 9
 }
13 10
 

+ 23 - 14
src/components/Breadcrumb/index.vue

@@ -2,7 +2,7 @@
2 2
   <el-breadcrumb class="app-breadcrumb" separator="/">
3 3
     <transition-group name="breadcrumb">
4 4
       <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
5
-        <span v-if="item.redirect==='noredirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
5
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
6 6
         <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
7 7
       </el-breadcrumb-item>
8 8
     </transition-group>
@@ -28,15 +28,23 @@ export default {
28 28
   },
29 29
   methods: {
30 30
     getBreadcrumb() {
31
-      let matched = this.$route.matched.filter(item => item.name)
32
-
31
+      // only show routes with meta.title
32
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
33 33
       const first = matched[0]
34
-      if (first && first.name !== 'dashboard') {
34
+
35
+      if (!this.isDashboard(first)) {
35 36
         matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
36 37
       }
37 38
 
38 39
       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
39 40
     },
41
+    isDashboard(route) {
42
+      const name = route && route.name
43
+      if (!name) {
44
+        return false
45
+      }
46
+      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
47
+    },
40 48
     pathCompile(path) {
41 49
       // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
42 50
       const { params } = this.$route
@@ -55,15 +63,16 @@ export default {
55 63
 }
56 64
 </script>
57 65
 
58
-<style rel="stylesheet/scss" lang="scss" scoped>
59
-  .app-breadcrumb.el-breadcrumb {
60
-    display: inline-block;
61
-    font-size: 14px;
62
-    line-height: 50px;
63
-    margin-left: 10px;
64
-    .no-redirect {
65
-      color: #97a8be;
66
-      cursor: text;
67
-    }
66
+<style lang="scss" scoped>
67
+.app-breadcrumb.el-breadcrumb {
68
+  display: inline-block;
69
+  font-size: 14px;
70
+  line-height: 50px;
71
+  margin-left: 8px;
72
+
73
+  .no-redirect {
74
+    color: #97a8be;
75
+    cursor: text;
68 76
   }
77
+}
69 78
 </style>

+ 9 - 7
src/components/Hamburger/index.vue

@@ -1,5 +1,5 @@
1 1
 <template>
2
-  <div>
2
+  <div style="padding: 0 15px;" @click="toggleClick">
3 3
     <svg
4 4
       :class="{'is-active':isActive}"
5 5
       class="hamburger"
@@ -7,7 +7,7 @@
7 7
       xmlns="http://www.w3.org/2000/svg"
8 8
       width="64"
9 9
       height="64"
10
-      @click="toggleClick">
10
+    >
11 11
       <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
12 12
     </svg>
13 13
   </div>
@@ -20,10 +20,11 @@ export default {
20 20
     isActive: {
21 21
       type: Boolean,
22 22
       default: false
23
-    },
24
-    toggleClick: {
25
-      type: Function,
26
-      default: null
23
+    }
24
+  },
25
+  methods: {
26
+    toggleClick() {
27
+      this.$emit('toggleClick')
27 28
     }
28 29
   }
29 30
 }
@@ -32,10 +33,11 @@ export default {
32 33
 <style scoped>
33 34
 .hamburger {
34 35
   display: inline-block;
35
-  cursor: pointer;
36
+  vertical-align: middle;
36 37
   width: 20px;
37 38
   height: 20px;
38 39
 }
40
+
39 41
 .hamburger.is-active {
40 42
   transform: rotate(180deg);
41 43
 }

+ 1 - 1
src/components/SvgIcon/index.vue

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
   <svg :class="svgClass" aria-hidden="true" v-on="$listeners">
3
-    <use :xlink:href="iconName"/>
3
+    <use :xlink:href="iconName" />
4 4
   </svg>
5 5
 </template>
6 6
 

+ 2 - 2
src/icons/index.js

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
src/icons/svg/dashboard.svg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
src/icons/svg/eye-open.svg


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

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
src/icons/svg/table.svg


+ 8 - 6
src/views/layout/components/AppMain.vue

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

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

@@ -0,0 +1,139 @@
1
+<template>
2
+  <div class="navbar">
3
+    <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
4
+
5
+    <breadcrumb class="breadcrumb-container" />
6
+
7
+    <div class="right-menu">
8
+      <el-dropdown class="avatar-container" trigger="click">
9
+        <div class="avatar-wrapper">
10
+          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
11
+          <i class="el-icon-caret-bottom" />
12
+        </div>
13
+        <el-dropdown-menu slot="dropdown" class="user-dropdown">
14
+          <router-link to="/">
15
+            <el-dropdown-item>
16
+              Home
17
+            </el-dropdown-item>
18
+          </router-link>
19
+          <a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
20
+            <el-dropdown-item>Github</el-dropdown-item>
21
+          </a>
22
+          <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
23
+            <el-dropdown-item>Docs</el-dropdown-item>
24
+          </a>
25
+          <el-dropdown-item divided>
26
+            <span style="display:block;" @click="logout">Log Out</span>
27
+          </el-dropdown-item>
28
+        </el-dropdown-menu>
29
+      </el-dropdown>
30
+    </div>
31
+  </div>
32
+</template>
33
+
34
+<script>
35
+import { mapGetters } from 'vuex'
36
+import Breadcrumb from '@/components/Breadcrumb'
37
+import Hamburger from '@/components/Hamburger'
38
+
39
+export default {
40
+  components: {
41
+    Breadcrumb,
42
+    Hamburger
43
+  },
44
+  computed: {
45
+    ...mapGetters([
46
+      'sidebar',
47
+      'avatar'
48
+    ])
49
+  },
50
+  methods: {
51
+    toggleSideBar() {
52
+      this.$store.dispatch('app/toggleSideBar')
53
+    },
54
+    async logout() {
55
+      await this.$store.dispatch('user/logout')
56
+      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
57
+    }
58
+  }
59
+}
60
+</script>
61
+
62
+<style lang="scss" scoped>
63
+.navbar {
64
+  height: 50px;
65
+  overflow: hidden;
66
+  position: relative;
67
+  background: #fff;
68
+  box-shadow: 0 1px 4px rgba(0,21,41,.08);
69
+
70
+  .hamburger-container {
71
+    line-height: 46px;
72
+    height: 100%;
73
+    float: left;
74
+    cursor: pointer;
75
+    transition: background .3s;
76
+    -webkit-tap-highlight-color:transparent;
77
+
78
+    &:hover {
79
+      background: rgba(0, 0, 0, .025)
80
+    }
81
+  }
82
+
83
+  .breadcrumb-container {
84
+    float: left;
85
+  }
86
+
87
+  .right-menu {
88
+    float: right;
89
+    height: 100%;
90
+    line-height: 50px;
91
+
92
+    &:focus {
93
+      outline: none;
94
+    }
95
+
96
+    .right-menu-item {
97
+      display: inline-block;
98
+      padding: 0 8px;
99
+      height: 100%;
100
+      font-size: 18px;
101
+      color: #5a5e66;
102
+      vertical-align: text-bottom;
103
+
104
+      &.hover-effect {
105
+        cursor: pointer;
106
+        transition: background .3s;
107
+
108
+        &:hover {
109
+          background: rgba(0, 0, 0, .025)
110
+        }
111
+      }
112
+    }
113
+
114
+    .avatar-container {
115
+      margin-right: 30px;
116
+
117
+      .avatar-wrapper {
118
+        margin-top: 5px;
119
+        position: relative;
120
+
121
+        .user-avatar {
122
+          cursor: pointer;
123
+          width: 40px;
124
+          height: 40px;
125
+          border-radius: 10px;
126
+        }
127
+
128
+        .el-icon-caret-bottom {
129
+          cursor: pointer;
130
+          position: absolute;
131
+          right: -20px;
132
+          top: 25px;
133
+          font-size: 12px;
134
+        }
135
+      }
136
+    }
137
+  }
138
+}
139
+</style>

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

@@ -0,0 +1,26 @@
1
+export default {
2
+  computed: {
3
+    device() {
4
+      return this.$store.state.app.device
5
+    }
6
+  },
7
+  mounted() {
8
+    // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9
+    // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10
+    this.fixBugIniOS()
11
+  },
12
+  methods: {
13
+    fixBugIniOS() {
14
+      const $subMenu = this.$refs.subMenu
15
+      if ($subMenu) {
16
+        const handleMouseleave = $subMenu.handleMouseleave
17
+        $subMenu.handleMouseleave = (e) => {
18
+          if (this.device === 'mobile') {
19
+            return
20
+          }
21
+          handleMouseleave(e)
22
+        }
23
+      }
24
+    }
25
+  }
26
+}

+ 8 - 9
src/views/layout/components/Sidebar/Item.vue

@@ -3,18 +3,17 @@ export default {
3 3
   name: 'MenuItem',
4 4
   functional: true,
5 5
   props: {
6
-    meta: {
7
-      type: Object,
8
-      default: () => {
9
-        return {
10
-          title: '',
11
-          icon: ''
12
-        }
13
-      }
6
+    icon: {
7
+      type: String,
8
+      default: ''
9
+    },
10
+    title: {
11
+      type: String,
12
+      default: ''
14 13
     }
15 14
   },
16 15
   render(h, context) {
17
-    const { icon, title } = context.props.meta
16
+    const { icon, title } = context.props
18 17
     const vnodes = []
19 18
 
20 19
     if (icon) {

+ 1 - 1
src/views/layout/components/Sidebar/Link.vue

@@ -2,7 +2,7 @@
2 2
 <template>
3 3
   <!-- eslint-disable vue/require-component-is -->
4 4
   <component v-bind="linkProps(to)">
5
-    <slot/>
5
+    <slot />
6 6
   </component>
7 7
 </template>
8 8
 

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

@@ -0,0 +1,82 @@
1
+<template>
2
+  <div class="sidebar-logo-container" :class="{'collapse':collapse}">
3
+    <transition name="sidebarLogoFade">
4
+      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
5
+        <img v-if="logo" :src="logo" class="sidebar-logo">
6
+        <h1 v-else class="sidebar-title">{{ title }} </h1>
7
+      </router-link>
8
+      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
9
+        <img v-if="logo" :src="logo" class="sidebar-logo">
10
+        <h1 class="sidebar-title">{{ title }} </h1>
11
+      </router-link>
12
+    </transition>
13
+  </div>
14
+</template>
15
+
16
+<script>
17
+export default {
18
+  name: 'SidebarLogo',
19
+  props: {
20
+    collapse: {
21
+      type: Boolean,
22
+      required: true
23
+    }
24
+  },
25
+  data() {
26
+    return {
27
+      title: 'Vue Admin Template',
28
+      logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
29
+    }
30
+  }
31
+}
32
+</script>
33
+
34
+<style lang="scss" scoped>
35
+.sidebarLogoFade-enter-active {
36
+  transition: opacity 1.5s;
37
+}
38
+
39
+.sidebarLogoFade-enter,
40
+.sidebarLogoFade-leave-to {
41
+  opacity: 0;
42
+}
43
+
44
+.sidebar-logo-container {
45
+  position: relative;
46
+  width: 100%;
47
+  height: 50px;
48
+  line-height: 50px;
49
+  background: #2b2f3a;
50
+  text-align: center;
51
+  overflow: hidden;
52
+
53
+  & .sidebar-logo-link {
54
+    height: 100%;
55
+    width: 100%;
56
+
57
+    & .sidebar-logo {
58
+      width: 32px;
59
+      height: 32px;
60
+      vertical-align: middle;
61
+      margin-right: 12px;
62
+    }
63
+
64
+    & .sidebar-title {
65
+      display: inline-block;
66
+      margin: 0;
67
+      color: #fff;
68
+      font-weight: 600;
69
+      line-height: 50px;
70
+      font-size: 14px;
71
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
72
+      vertical-align: middle;
73
+    }
74
+  }
75
+
76
+  &.collapse {
77
+    .sidebar-logo {
78
+      margin-right: 0px;
79
+    }
80
+  }
81
+}
82
+</style>

+ 11 - 7
src/views/layout/components/Sidebar/SidebarItem.vue

@@ -1,27 +1,26 @@
1 1
 <template>
2 2
   <div v-if="!item.hidden" class="menu-wrapper">
3
-
4 3
     <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
5
-      <app-link :to="resolvePath(onlyOneChild.path)">
4
+      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
6 5
         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
7
-          <item :meta="Object.assign({},item.meta,onlyOneChild.meta)" />
6
+          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
8 7
         </el-menu-item>
9 8
       </app-link>
10 9
     </template>
11 10
 
12 11
     <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
13 12
       <template slot="title">
14
-        <item :meta="item.meta" />
13
+        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
15 14
       </template>
16 15
       <sidebar-item
17 16
         v-for="child in item.children"
17
+        :key="child.path"
18 18
         :is-nest="true"
19 19
         :item="child"
20
-        :key="child.path"
21 20
         :base-path="resolvePath(child.path)"
22
-        class="nest-menu" />
21
+        class="nest-menu"
22
+      />
23 23
     </el-submenu>
24
-
25 24
   </div>
26 25
 </template>
27 26
 
@@ -30,10 +29,12 @@ import path from 'path'
30 29
 import { isExternal } from '@/utils/validate'
31 30
 import Item from './Item'
32 31
 import AppLink from './Link'
32
+import FixiOSBug from './FixiOSBug'
33 33
 
34 34
 export default {
35 35
   name: 'SidebarItem',
36 36
   components: { Item, AppLink },
37
+  mixins: [FixiOSBug],
37 38
   props: {
38 39
     // route object
39 40
     item: {
@@ -84,6 +85,9 @@ export default {
84 85
       if (isExternal(routePath)) {
85 86
         return routePath
86 87
       }
88
+      if (isExternal(this.basePath)) {
89
+        return this.basePath
90
+      }
87 91
       return path.resolve(this.basePath, routePath)
88 92
     }
89 93
   }

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

@@ -0,0 +1,54 @@
1
+<template>
2
+  <div :class="{'has-logo':showLogo}">
3
+    <logo v-if="showLogo" :collapse="isCollapse" />
4
+    <el-scrollbar wrap-class="scrollbar-wrapper">
5
+      <el-menu
6
+        :default-active="activeMenu"
7
+        :collapse="isCollapse"
8
+        :background-color="variables.menuBg"
9
+        :text-color="variables.menuText"
10
+        :unique-opened="false"
11
+        :active-text-color="variables.menuActiveText"
12
+        :collapse-transition="false"
13
+        mode="vertical"
14
+      >
15
+        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
16
+      </el-menu>
17
+    </el-scrollbar>
18
+  </div>
19
+</template>
20
+
21
+<script>
22
+import { mapGetters } from 'vuex'
23
+import Logo from './Logo'
24
+import SidebarItem from './SidebarItem'
25
+import variables from '@/styles/variables.scss'
26
+
27
+export default {
28
+  components: { SidebarItem, Logo },
29
+  computed: {
30
+    ...mapGetters([
31
+      'permission_routes',
32
+      'sidebar'
33
+    ]),
34
+    activeMenu() {
35
+      const route = this.$route
36
+      const { meta, path } = route
37
+      // if set path, the sidebar will highlight the path you set
38
+      if (meta.activeMenu) {
39
+        return meta.activeMenu
40
+      }
41
+      return path
42
+    },
43
+    showLogo() {
44
+      return this.$store.state.settings.sidebarLogo
45
+    },
46
+    variables() {
47
+      return variables
48
+    },
49
+    isCollapse() {
50
+      return !this.sidebar.opened
51
+    }
52
+  }
53
+}
54
+</script>

src/views/layout/components/index.js → src/layout/components/index.js


+ 30 - 6
src/views/layout/Layout.vue

@@ -1,10 +1,12 @@
1 1
 <template>
2 2
   <div :class="classObj" class="app-wrapper">
3
-    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
4
-    <sidebar class="sidebar-container"/>
3
+    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
4
+    <sidebar class="sidebar-container" />
5 5
     <div class="main-container">
6
-      <navbar/>
7
-      <app-main/>
6
+      <div :class="{'fixed-header':fixedHeader}">
7
+        <navbar />
8
+      </div>
9
+      <app-main />
8 10
     </div>
9 11
   </div>
10 12
 </template>
@@ -28,6 +30,9 @@ export default {
28 30
     device() {
29 31
       return this.$store.state.app.device
30 32
     },
33
+    fixedHeader() {
34
+      return this.$store.state.settings.fixedHeader
35
+    },
31 36
     classObj() {
32 37
       return {
33 38
         hideSidebar: !this.sidebar.opened,
@@ -45,8 +50,10 @@ export default {
45 50
 }
46 51
 </script>
47 52
 
48
-<style rel="stylesheet/scss" lang="scss" scoped>
49
-  @import "src/styles/mixin.scss";
53
+<style lang="scss" scoped>
54
+  @import "~@/styles/mixin.scss";
55
+  @import "~@/styles/variables.scss";
56
+
50 57
   .app-wrapper {
51 58
     @include clearfix;
52 59
     position: relative;
@@ -66,4 +73,21 @@ export default {
66 73
     position: absolute;
67 74
     z-index: 999;
68 75
   }
76
+
77
+  .fixed-header {
78
+    position: fixed;
79
+    top: 0;
80
+    right: 0;
81
+    z-index: 9;
82
+    width: calc(100% - #{$sideBarWidth});
83
+    transition: width 0.28s;
84
+  }
85
+
86
+  .hideSidebar .fixed-header {
87
+    width: calc(100% - 54px)
88
+  }
89
+
90
+  .mobile .fixed-header {
91
+    width: 100%;
92
+  }
69 93
 </style>

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

@@ -0,0 +1,45 @@
1
+import store from '@/store'
2
+
3
+const { body } = document
4
+const WIDTH = 992 // refer to Bootstrap's responsive design
5
+
6
+export default {
7
+  watch: {
8
+    $route(route) {
9
+      if (this.device === 'mobile' && this.sidebar.opened) {
10
+        store.dispatch('app/closeSideBar', { withoutAnimation: false })
11
+      }
12
+    }
13
+  },
14
+  beforeMount() {
15
+    window.addEventListener('resize', this.$_resizeHandler)
16
+  },
17
+  beforeDestroy() {
18
+    window.removeEventListener('resize', this.$_resizeHandler)
19
+  },
20
+  mounted() {
21
+    const isMobile = this.$_isMobile()
22
+    if (isMobile) {
23
+      store.dispatch('app/toggleDevice', 'mobile')
24
+      store.dispatch('app/closeSideBar', { withoutAnimation: true })
25
+    }
26
+  },
27
+  methods: {
28
+    // use $_ for mixins properties
29
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30
+    $_isMobile() {
31
+      const rect = body.getBoundingClientRect()
32
+      return rect.width - 1 < WIDTH
33
+    },
34
+    $_resizeHandler() {
35
+      if (!document.hidden) {
36
+        const isMobile = this.$_isMobile()
37
+        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
38
+
39
+        if (isMobile) {
40
+          store.dispatch('app/closeSideBar', { withoutAnimation: true })
41
+        }
42
+      }
43
+    }
44
+  }
45
+}

+ 7 - 7
src/main.js

@@ -16,15 +16,15 @@ import '@/icons' // icon
16 16
 import '@/permission' // permission control
17 17
 
18 18
 /**
19
- * This project originally used easy-mock to simulate data,
20
- * but its official service is very unstable,
21
- * and you can build your own service if you need it.
22
- * So here I use Mock.js for local emulation,
23
- * it will intercept your request, so you won't see the request in the network.
24
- * If you remove `../mock` it will automatically request easy-mock data.
19
+ * If you don't want to use mock-server
20
+ * you want to use mockjs for request interception
21
+ * you can execute:
22
+ *
23
+ * import { mockXHR } from '../mock'
24
+ * mockXHR()
25 25
  */
26
-import '../mock' // simulation data
27 26
 
27
+// set ElementUI lang to EN
28 28
 Vue.use(ElementUI, { locale })
29 29
 
30 30
 Vue.config.productionTip = false

+ 50 - 19
src/permission.js

@@ -1,43 +1,74 @@
1 1
 import router from './router'
2 2
 import store from './store'
3
+import { Message } from 'element-ui'
3 4
 import NProgress from 'nprogress' // progress bar
4 5
 import 'nprogress/nprogress.css' // progress bar style
5
-import { Message } from 'element-ui'
6
-import { getToken } from '@/utils/auth' // getToken from cookie
6
+import { getToken } from '@/utils/auth' // get token from cookie
7
+import getPageTitle from '@/utils/get-page-title'
7 8
 
8
-NProgress.configure({ showSpinner: false })// NProgress configuration
9
+NProgress.configure({ showSpinner: false }) // NProgress Configuration
9 10
 
10
-const whiteList = ['/login'] // 不重定向白名单
11
-router.beforeEach((to, from, next) => {
11
+const whiteList = ['/login'] // no redirect whitelist
12
+
13
+router.beforeEach(async(to, from, next) => {
14
+  // start progress bar
12 15
   NProgress.start()
13
-  if (getToken()) {
16
+
17
+  // set page title
18
+  document.title = getPageTitle(to.meta.title)
19
+
20
+  // determine whether the user has logged in
21
+  const hasToken = getToken()
22
+
23
+  if (hasToken) {
14 24
     if (to.path === '/login') {
25
+      // if is logged in, redirect to the home page
15 26
       next({ path: '/' })
16
-      NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
27
+      NProgress.done()
17 28
     } else {
18
-      if (store.getters.roles.length === 0) {
19
-        store.dispatch('GetInfo').then(res => { // 拉取用户信息
20
-          next()
21
-        }).catch((err) => {
22
-          store.dispatch('FedLogOut').then(() => {
23
-            Message.error(err || 'Verification failed, please login again')
24
-            next({ path: '/' })
25
-          })
26
-        })
27
-      } else {
29
+      // determine whether the user has obtained his permission roles through getInfo
30
+      const hasRoles = store.getters.roles && store.getters.roles.length > 0
31
+      if (hasRoles) {
28 32
         next()
33
+      } else {
34
+        try {
35
+          // get user info
36
+          // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
37
+          const { roles } = await store.dispatch('user/getInfo')
38
+
39
+          // generate accessible routes map based on roles
40
+          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
41
+
42
+          // dynamically add accessible routes
43
+          router.addRoutes(accessRoutes)
44
+
45
+          // hack method to ensure that addRoutes is complete
46
+          // set the replace: true, so the navigation will not leave a history record
47
+          next({ ...to, replace: true })
48
+        } catch (error) {
49
+          // remove token and go to login page to re-login
50
+          await store.dispatch('user/resetToken')
51
+          Message.error(error || 'Has Error')
52
+          next(`/login?redirect=${to.path}`)
53
+          NProgress.done()
54
+        }
29 55
       }
30 56
     }
31 57
   } else {
58
+    /* has no token*/
59
+
32 60
     if (whiteList.indexOf(to.path) !== -1) {
61
+      // in the free login whitelist, go directly
33 62
       next()
34 63
     } else {
35
-      next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
64
+      // other pages that do not have permission to access are redirected to the login page.
65
+      next(`/login?redirect=${to.path}`)
36 66
       NProgress.done()
37 67
     }
38 68
   }
39 69
 })
40 70
 
41 71
 router.afterEach(() => {
42
-  NProgress.done() // 结束Progress
72
+  // finish progress bar
73
+  NProgress.done()
43 74
 })

+ 58 - 24
src/router/index.js

@@ -1,40 +1,57 @@
1 1
 import Vue from 'vue'
2 2
 import Router from 'vue-router'
3 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 4
 Vue.use(Router)
8 5
 
9 6
 /* Layout */
10
-import Layout from '../views/layout/Layout'
7
+import Layout from '@/layout'
11 8
 
12 9
 /**
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 redirect 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)
10
+ * Note: sub-menu only appear when route children.length >= 1
11
+ * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
12
+ *
13
+ * hidden: true                   if set true, item will not show in the sidebar(default is false)
14
+ * alwaysShow: true               if set true, will always show the root menu
15
+ *                                if not set alwaysShow, when item has more than one children route,
16
+ *                                it will becomes nested mode, otherwise not show the root menu
17
+ * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb
18
+ * name:'router-name'             the name is used by <keep-alive> (must set!!!)
19
+ * meta : {
20
+    roles: ['admin','editor']    control the page roles (you can set multiple roles)
21
+    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
21 22
     icon: 'svg-name'             the icon show in the sidebar
22
-    breadcrumb: false            if false, the item will hidden in breadcrumb(default is true)
23
+    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
24
+    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
23 25
   }
24
-**/
25
-export const constantRouterMap = [
26
-  { path: '/login', component: () => import('@/views/login/index'), hidden: true },
27
-  { path: '/404', component: () => import('@/views/404'), hidden: true },
26
+ */
27
+
28
+/**
29
+ * constantRoutes
30
+ * a base page that does not have permission requirements
31
+ * all roles can be accessed
32
+ */
33
+export const constantRoutes = [
34
+  {
35
+    path: '/login',
36
+    component: () => import('@/views/login/index'),
37
+    hidden: true
38
+  },
39
+
40
+  {
41
+    path: '/404',
42
+    component: () => import('@/views/404'),
43
+    hidden: true
44
+  },
28 45
 
29 46
   {
30 47
     path: '/',
31 48
     component: Layout,
32 49
     redirect: '/dashboard',
33
-    name: 'Dashboard',
34
-    hidden: true,
35 50
     children: [{
36 51
       path: 'dashboard',
37
-      component: () => import('@/views/dashboard/index')
52
+      name: 'Dashboard',
53
+      component: () => import('@/views/dashboard/index'),
54
+      meta: { title: 'Dashboard', icon: 'dashboard' }
38 55
     }]
39 56
   },
40 57
 
@@ -71,8 +88,14 @@ export const constantRouterMap = [
71 88
         meta: { title: 'Form', icon: 'form' }
72 89
       }
73 90
     ]
74
-  },
91
+  }
92
+]
75 93
 
94
+/**
95
+ * asyncRoutes
96
+ * the routes that need to be dynamically loaded based on user roles
97
+ */
98
+export const asyncRoutes = [
76 99
   {
77 100
     path: '/nested',
78 101
     component: Layout,
@@ -142,11 +165,22 @@ export const constantRouterMap = [
142 165
     ]
143 166
   },
144 167
 
168
+  // 404 page must be placed at the end !!!
145 169
   { path: '*', redirect: '/404', hidden: true }
146 170
 ]
147 171
 
148
-export default new Router({
149
-  // mode: 'history', //后端支持可开
172
+const createRouter = () => new Router({
173
+  // mode: 'history', // require service support
150 174
   scrollBehavior: () => ({ y: 0 }),
151
-  routes: constantRouterMap
175
+  routes: constantRoutes
152 176
 })
177
+
178
+const router = createRouter()
179
+
180
+// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
181
+export function resetRouter() {
182
+  const newRouter = createRouter()
183
+  router.matcher = newRouter.matcher // reset router
184
+}
185
+
186
+export default router

+ 16 - 0
src/settings.js

@@ -0,0 +1,16 @@
1
+module.exports = {
2
+
3
+  title: 'Vue Admin Template',
4
+
5
+  /**
6
+   * @type {boolean} true | false
7
+   * @description Whether fix the header
8
+   */
9
+  fixedHeader: false,
10
+
11
+  /**
12
+   * @type {boolean} true | false
13
+   * @description Whether show the logo in sidebar
14
+   */
15
+  sidebarLogo: false
16
+}

+ 2 - 1
src/store/getters.js

@@ -4,6 +4,7 @@ const getters = {
4 4
   token: state => state.user.token,
5 5
   avatar: state => state.user.avatar,
6 6
   name: state => state.user.name,
7
-  roles: state => state.user.roles
7
+  roles: state => state.user.roles,
8
+  permission_routes: state => state.permission.routes
8 9
 }
9 10
 export default getters

+ 5 - 1
src/store/index.js

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

+ 39 - 34
src/store/modules/app.js

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

+ 69 - 0
src/store/modules/permission.js

@@ -0,0 +1,69 @@
1
+import { asyncRoutes, constantRoutes } from '@/router'
2
+
3
+/**
4
+ * Use meta.role to determine if the current user has permission
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.includes(role))
11
+  } else {
12
+    return true
13
+  }
14
+}
15
+
16
+/**
17
+ * Filter asynchronous routing tables by recursion
18
+ * @param routes asyncRoutes
19
+ * @param roles
20
+ */
21
+export function filterAsyncRoutes(routes, roles) {
22
+  const res = []
23
+
24
+  routes.forEach(route => {
25
+    const tmp = { ...route }
26
+    if (hasPermission(roles, tmp)) {
27
+      if (tmp.children) {
28
+        tmp.children = filterAsyncRoutes(tmp.children, roles)
29
+      }
30
+      res.push(tmp)
31
+    }
32
+  })
33
+
34
+  return res
35
+}
36
+
37
+const state = {
38
+  routes: [],
39
+  addRoutes: []
40
+}
41
+
42
+const mutations = {
43
+  SET_ROUTES: (state, routes) => {
44
+    state.addRoutes = routes
45
+    state.routes = constantRoutes.concat(routes)
46
+  }
47
+}
48
+
49
+const actions = {
50
+  generateRoutes({ commit }, roles) {
51
+    return new Promise(resolve => {
52
+      let accessedRoutes
53
+      if (roles.includes('admin')) {
54
+        accessedRoutes = asyncRoutes || []
55
+      } else {
56
+        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
57
+      }
58
+      commit('SET_ROUTES', accessedRoutes)
59
+      resolve(accessedRoutes)
60
+    })
61
+  }
62
+}
63
+
64
+export default {
65
+  namespaced: true,
66
+  state,
67
+  mutations,
68
+  actions
69
+}

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

@@ -0,0 +1,31 @@
1
+import defaultSettings from '@/settings'
2
+
3
+const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
4
+
5
+const state = {
6
+  showSettings: showSettings,
7
+  fixedHeader: fixedHeader,
8
+  sidebarLogo: sidebarLogo
9
+}
10
+
11
+const mutations = {
12
+  CHANGE_SETTING: (state, { key, value }) => {
13
+    if (state.hasOwnProperty(key)) {
14
+      state[key] = value
15
+    }
16
+  }
17
+}
18
+
19
+const actions = {
20
+  changeSetting({ commit }, data) {
21
+    commit('CHANGE_SETTING', data)
22
+  }
23
+}
24
+
25
+export default {
26
+  namespaced: true,
27
+  state,
28
+  mutations,
29
+  actions
30
+}
31
+

+ 85 - 70
src/store/modules/user.js

@@ -1,87 +1,102 @@
1
-import { login, logout, getInfo } from '@/api/login'
1
+import { login, logout, getInfo } from '@/api/user'
2 2
 import { getToken, setToken, removeToken } from '@/utils/auth'
3
+import { resetRouter } from '@/router'
3 4
 
4
-const user = {
5
-  state: {
6
-    token: getToken(),
7
-    name: '',
8
-    avatar: '',
9
-    roles: []
10
-  },
5
+const state = {
6
+  token: getToken(),
7
+  name: '',
8
+  avatar: '',
9
+  roles: []
10
+}
11 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
-    }
12
+const mutations = {
13
+  SET_TOKEN: (state, token) => {
14
+    state.token = token
15
+  },
16
+  SET_NAME: (state, name) => {
17
+    state.name = name
25 18
   },
19
+  SET_AVATAR: (state, avatar) => {
20
+    state.avatar = avatar
21
+  },
22
+  SET_ROLES: (state, roles) => {
23
+    state.roles = roles
24
+  }
25
+}
26 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
-        })
27
+const actions = {
28
+  // user login
29
+  login({ commit }, userInfo) {
30
+    const { username, password } = userInfo
31
+    return new Promise((resolve, reject) => {
32
+      login({ username: username.trim(), password: password }).then(response => {
33
+        const { data } = response
34
+        commit('SET_TOKEN', data.token)
35
+        setToken(data.token)
36
+        resolve()
37
+      }).catch(error => {
38
+        reject(error)
40 39
       })
41
-    },
40
+    })
41
+  },
42 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
-    },
43
+  // get user info
44
+  getInfo({ commit, state }) {
45
+    return new Promise((resolve, reject) => {
46
+      getInfo(state.token).then(response => {
47
+        const { data } = response
61 48
 
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
-        })
49
+        if (!data) {
50
+          reject('Verification failed, please Login again.')
51
+        }
52
+
53
+        const { roles, name, avatar } = data
54
+
55
+        // roles must be a non-empty array
56
+        if (!roles || roles.length <= 0) {
57
+          reject('getInfo: roles must be a non-null array!')
58
+        }
59
+
60
+        commit('SET_ROLES', roles)
61
+        commit('SET_NAME', name)
62
+        commit('SET_AVATAR', avatar)
63
+        resolve(data)
64
+      }).catch(error => {
65
+        reject(error)
73 66
       })
74
-    },
67
+    })
68
+  },
75 69
 
76
-    // 前端 登出
77
-    FedLogOut({ commit }) {
78
-      return new Promise(resolve => {
70
+  // user logout
71
+  logout({ commit, state }) {
72
+    return new Promise((resolve, reject) => {
73
+      logout(state.token).then(() => {
79 74
         commit('SET_TOKEN', '')
75
+        commit('SET_ROLES', [])
80 76
         removeToken()
77
+        resetRouter()
81 78
         resolve()
79
+      }).catch(error => {
80
+        reject(error)
82 81
       })
83
-    }
82
+    })
83
+  },
84
+
85
+  // remove token
86
+  resetToken({ commit }) {
87
+    return new Promise(resolve => {
88
+      commit('SET_TOKEN', '')
89
+      commit('SET_ROLES', [])
90
+      removeToken()
91
+      resolve()
92
+    })
84 93
   }
85 94
 }
86 95
 
87
-export default user
96
+export default {
97
+  namespaced: true,
98
+  state,
99
+  mutations,
100
+  actions
101
+}
102
+

+ 17 - 3
src/styles/element-ui.scss

@@ -1,4 +1,10 @@
1
-//to reset element-ui default css
1
+// cover some element-ui styles
2
+
3
+.el-breadcrumb__inner,
4
+.el-breadcrumb__inner a {
5
+  font-weight: 400 !important;
6
+}
7
+
2 8
 .el-upload {
3 9
   input[type="file"] {
4 10
     display: none !important;
@@ -9,7 +15,8 @@
9 15
   display: none;
10 16
 }
11 17
 
12
-//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
18
+
19
+// to fixed https://github.com/ElemeFE/element/issues/2461
13 20
 .el-dialog {
14 21
   transform: none;
15 22
   left: 0;
@@ -17,7 +24,7 @@
17 24
   margin: 0 auto;
18 25
 }
19 26
 
20
-//element ui upload
27
+// refine element ui upload
21 28
 .upload-container {
22 29
   .el-upload {
23 30
     width: 100%;
@@ -28,3 +35,10 @@
28 35
     }
29 36
   }
30 37
 }
38
+
39
+// dropdown
40
+.el-dropdown-menu {
41
+  a {
42
+    display: block
43
+  }
44
+}

+ 5 - 18
src/styles/index.scss

@@ -31,19 +31,6 @@ html {
31 31
   box-sizing: inherit;
32 32
 }
33 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 34
 a:focus,
48 35
 a:active {
49 36
   outline: none;
@@ -57,6 +44,10 @@ a:hover {
57 44
   text-decoration: none;
58 45
 }
59 46
 
47
+div:focus {
48
+  outline: none;
49
+}
50
+
60 51
 .clearfix {
61 52
   &:after {
62 53
     visibility: hidden;
@@ -68,11 +59,7 @@ a:hover {
68 59
   }
69 60
 }
70 61
 
71
-//main-container全局样式
72
-.app-main {
73
-  min-height: 100%
74
-}
75
-
62
+// main-container global css
76 63
 .app-container {
77 64
   padding: 20px;
78 65
 }

+ 15 - 8
src/styles/sidebar.scss

@@ -1,6 +1,5 @@
1 1
 #app {
2 2
 
3
-  // 主体区域 Main container
4 3
   .main-container {
5 4
     min-height: 100%;
6 5
     transition: margin-left .28s;
@@ -8,10 +7,10 @@
8 7
     position: relative;
9 8
   }
10 9
 
11
-  // 侧边栏 Sidebar container
12 10
   .sidebar-container {
13 11
     transition: width 0.28s;
14 12
     width: $sideBarWidth !important;
13
+    background-color: $menuBg;
15 14
     height: 100%;
16 15
     position: fixed;
17 16
     font-size: 0px;
@@ -21,23 +20,29 @@
21 20
     z-index: 1001;
22 21
     overflow: hidden;
23 22
 
24
-    //reset element-ui css
23
+    // reset element-ui css
25 24
     .horizontal-collapse-transition {
26 25
       transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
27 26
     }
28 27
 
29 28
     .scrollbar-wrapper {
30 29
       overflow-x: hidden !important;
31
-
32
-      .el-scrollbar__view {
33
-        height: 100%;
34
-      }
35 30
     }
36 31
 
37 32
     .el-scrollbar__bar.is-vertical {
38 33
       right: 0px;
39 34
     }
40 35
 
36
+    .el-scrollbar {
37
+      height: 100%;
38
+    }
39
+
40
+    &.has-logo {
41
+      .el-scrollbar {
42
+        height: calc(100% - 50px);
43
+      }
44
+    }
45
+
41 46
     .is-horizontal {
42 47
       display: none;
43 48
     }
@@ -100,6 +105,7 @@
100 105
 
101 106
       .el-tooltip {
102 107
         padding: 0 !important;
108
+
103 109
         .svg-icon {
104 110
           margin-left: 20px;
105 111
         }
@@ -111,6 +117,7 @@
111 117
 
112 118
       &>.el-submenu__title {
113 119
         padding: 0 !important;
120
+
114 121
         .svg-icon {
115 122
           margin-left: 20px;
116 123
         }
@@ -140,7 +147,7 @@
140 147
     min-width: $sideBarWidth !important;
141 148
   }
142 149
 
143
-  // 适配移动端, Mobile responsive
150
+  // mobile responsive
144 151
   .mobile {
145 152
     .main-container {
146 153
       margin-left: 0px;

+ 4 - 4
src/styles/transition.scss

@@ -1,6 +1,6 @@
1
-//globl transition css
1
+// global transition css
2 2
 
3
-/*fade*/
3
+/* fade */
4 4
 .fade-enter-active,
5 5
 .fade-leave-active {
6 6
   transition: opacity 0.28s;
@@ -11,7 +11,7 @@
11 11
   opacity: 0;
12 12
 }
13 13
 
14
-/*fade-transform*/
14
+/* fade-transform */
15 15
 .fade-transform-leave-active,
16 16
 .fade-transform-enter-active {
17 17
   transition: all .5s;
@@ -27,7 +27,7 @@
27 27
   transform: translateX(30px);
28 28
 }
29 29
 
30
-/*fade*/
30
+/* breadcrumb transition */
31 31
 .breadcrumb-enter-active,
32 32
 .breadcrumb-leave-active {
33 33
   transition: all .5s;

+ 1 - 1
src/styles/variables.scss

@@ -1,4 +1,4 @@
1
-//sidebar
1
+// sidebar
2 2
 $menuText:#bfcbd9;
3 3
 $menuActiveText:#409EFF;
4 4
 $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951

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

@@ -0,0 +1,10 @@
1
+import defaultSettings from '@/settings'
2
+
3
+const title = defaultSettings.title || 'Vue Admin Template'
4
+
5
+export default function getPageTitle(pageTitle) {
6
+  if (pageTitle) {
7
+    return `${pageTitle} - ${title}`
8
+  }
9
+  return `${title}`
10
+}

+ 90 - 0
src/utils/index.js

@@ -0,0 +1,90 @@
1
+/**
2
+ * Created by PanJiaChen on 16/11/18.
3
+ */
4
+
5
+/**
6
+ * Parse the time to string
7
+ * @param {(Object|string|number)} time
8
+ * @param {string} cFormat
9
+ * @returns {string}
10
+ */
11
+export function parseTime(time, cFormat) {
12
+  if (arguments.length === 0) {
13
+    return null
14
+  }
15
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
16
+  let date
17
+  if (typeof time === 'object') {
18
+    date = time
19
+  } else {
20
+    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
21
+      time = parseInt(time)
22
+    }
23
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
24
+      time = time * 1000
25
+    }
26
+    date = new Date(time)
27
+  }
28
+  const formatObj = {
29
+    y: date.getFullYear(),
30
+    m: date.getMonth() + 1,
31
+    d: date.getDate(),
32
+    h: date.getHours(),
33
+    i: date.getMinutes(),
34
+    s: date.getSeconds(),
35
+    a: date.getDay()
36
+  }
37
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
38
+    let value = formatObj[key]
39
+    // Note: getDay() returns 0 on Sunday
40
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
41
+    if (result.length > 0 && value < 10) {
42
+      value = '0' + value
43
+    }
44
+    return value || 0
45
+  })
46
+  return time_str
47
+}
48
+
49
+/**
50
+ * @param {number} time
51
+ * @param {string} option
52
+ * @returns {string}
53
+ */
54
+export function formatTime(time, option) {
55
+  if (('' + time).length === 10) {
56
+    time = parseInt(time) * 1000
57
+  } else {
58
+    time = +time
59
+  }
60
+  const d = new Date(time)
61
+  const now = Date.now()
62
+
63
+  const diff = (now - d) / 1000
64
+
65
+  if (diff < 30) {
66
+    return '刚刚'
67
+  } else if (diff < 3600) {
68
+    // less 1 hour
69
+    return Math.ceil(diff / 60) + '分钟前'
70
+  } else if (diff < 3600 * 24) {
71
+    return Math.ceil(diff / 3600) + '小时前'
72
+  } else if (diff < 3600 * 24 * 2) {
73
+    return '1天前'
74
+  }
75
+  if (option) {
76
+    return parseTime(time, option)
77
+  } else {
78
+    return (
79
+      d.getMonth() +
80
+      1 +
81
+      '月' +
82
+      d.getDate() +
83
+      '日' +
84
+      d.getHours() +
85
+      '时' +
86
+      d.getMinutes() +
87
+      '分'
88
+    )
89
+  }
90
+}

+ 40 - 28
src/utils/request.js

@@ -1,62 +1,74 @@
1 1
 import axios from 'axios'
2
-import { Message, MessageBox } from 'element-ui'
3
-import store from '../store'
2
+import { MessageBox, Message } from 'element-ui'
3
+import store from '@/store'
4 4
 import { getToken } from '@/utils/auth'
5 5
 
6
-// 创建axios实例
6
+// create an axios instance
7 7
 const service = axios.create({
8
-  baseURL: process.env.BASE_API, // api 的 base_url
9
-  timeout: 5000 // 请求超时时间
8
+  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
9
+  withCredentials: true, // send cookies when cross-domain requests
10
+  timeout: 5000 // request timeout
10 11
 })
11 12
 
12
-// request拦截器
13
+// request interceptor
13 14
 service.interceptors.request.use(
14 15
   config => {
16
+    // do something before request is sent
17
+
15 18
     if (store.getters.token) {
16
-      config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
19
+      // let each request carry token
20
+      // ['X-Token'] is a custom headers key
21
+      // please modify it according to the actual situation
22
+      config.headers['X-Token'] = getToken()
17 23
     }
18 24
     return config
19 25
   },
20 26
   error => {
21
-    // Do something with request error
27
+    // do something with request error
22 28
     console.log(error) // for debug
23
-    Promise.reject(error)
29
+    return Promise.reject(error)
24 30
   }
25 31
 )
26 32
 
27
-// response 拦截器
33
+// response interceptor
28 34
 service.interceptors.response.use(
35
+  /**
36
+   * If you want to get http information such as headers or status
37
+   * Please return  response => response
38
+  */
39
+
40
+  /**
41
+   * Determine the request status by custom code
42
+   * Here is just an example
43
+   * You can also judge the status by HTTP Status Code
44
+   */
29 45
   response => {
30
-    /**
31
-     * code为非20000是抛错 可结合自己业务进行修改
32
-     */
33 46
     const res = response.data
47
+
48
+    // if the custom code is not 20000, it is judged as an error.
34 49
     if (res.code !== 20000) {
35 50
       Message({
36
-        message: res.message,
51
+        message: res.message || 'error',
37 52
         type: 'error',
38 53
         duration: 5 * 1000
39 54
       })
40 55
 
41
-      // 50008:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
56
+      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
42 57
       if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
43
-        MessageBox.confirm(
44
-          '你已被登出,可以取消继续留在该页面,或者重新登录',
45
-          '确定登出',
46
-          {
47
-            confirmButtonText: '重新登录',
48
-            cancelButtonText: '取消',
49
-            type: 'warning'
50
-          }
51
-        ).then(() => {
52
-          store.dispatch('FedLogOut').then(() => {
53
-            location.reload() // 为了重新实例化vue-router对象 避免bug
58
+        // to re-login
59
+        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
60
+          confirmButtonText: 'Re-Login',
61
+          cancelButtonText: 'Cancel',
62
+          type: 'warning'
63
+        }).then(() => {
64
+          store.dispatch('user/resetToken').then(() => {
65
+            location.reload()
54 66
           })
55 67
         })
56 68
       }
57
-      return Promise.reject('error')
69
+      return Promise.reject(res.message || 'error')
58 70
     } else {
59
-      return response.data
71
+      return res
60 72
     }
61 73
   },
62 74
   error => {

+ 14 - 6
src/utils/validate.js

@@ -1,12 +1,20 @@
1 1
 /**
2
- * Created by jiachenpan on 16/11/18.
2
+ * Created by PanJiaChen on 16/11/18.
3 3
  */
4 4
 
5
-export function isvalidUsername(str) {
6
-  const valid_map = ['admin', 'editor']
7
-  return valid_map.indexOf(str.trim()) >= 0
8
-}
9
-
5
+/**
6
+ * @param {string} path
7
+ * @returns {Boolean}
8
+ */
10 9
 export function isExternal(path) {
11 10
   return /^(https?:|mailto:|tel:)/.test(path)
12 11
 }
12
+
13
+/**
14
+ * @param {string} str
15
+ * @returns {Boolean}
16
+ */
17
+export function validUsername(str) {
18
+  const valid_map = ['admin', 'editor']
19
+  return valid_map.indexOf(str.trim()) >= 0
20
+}

+ 6 - 6
src/views/404.vue

@@ -9,12 +9,12 @@
9 9
       </div>
10 10
       <div class="bullshit">
11 11
         <div class="bullshit__oops">OOPS!</div>
12
-        <div class="bullshit__info">版权所有
13
-          <a class="link-type" href="https://wallstreetcn.com" target="_blank">华尔街见闻</a>
12
+        <div class="bullshit__info">All rights reserved
13
+          <a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
14 14
         </div>
15 15
         <div class="bullshit__headline">{{ message }}</div>
16
-        <div class="bullshit__info">请检查您输入的网址是否正确,请点击以下按钮返回主页或者发送错误报告</div>
17
-        <a href="" class="bullshit__return-home">返回首页</a>
16
+        <div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
17
+        <a href="" class="bullshit__return-home">Back to home</a>
18 18
       </div>
19 19
     </div>
20 20
   </div>
@@ -26,13 +26,13 @@ export default {
26 26
   name: 'Page404',
27 27
   computed: {
28 28
     message() {
29
-      return '网管说这个页面你不能进......'
29
+      return 'The webmaster said that you can not enter this page...'
30 30
     }
31 31
   }
32 32
 }
33 33
 </script>
34 34
 
35
-<style rel="stylesheet/scss" lang="scss" scoped>
35
+<style lang="scss" scoped>
36 36
 .wscn-http404-container{
37 37
   transform: translate(-50%,-50%);
38 38
   position: absolute;

+ 3 - 3
src/views/dashboard/index.vue

@@ -1,7 +1,7 @@
1 1
 <template>
2 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>
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 5
   </div>
6 6
 </template>
7 7
 
@@ -19,7 +19,7 @@ export default {
19 19
 }
20 20
 </script>
21 21
 
22
-<style rel="stylesheet/scss" lang="scss" scoped>
22
+<style lang="scss" scoped>
23 23
 .dashboard {
24 24
   &-container {
25 25
     margin: 30px;

+ 13 - 13
src/views/form/index.vue

@@ -2,42 +2,42 @@
2 2
   <div class="app-container">
3 3
     <el-form ref="form" :model="form" label-width="120px">
4 4
       <el-form-item label="Activity name">
5
-        <el-input v-model="form.name"/>
5
+        <el-input v-model="form.name" />
6 6
       </el-form-item>
7 7
       <el-form-item label="Activity zone">
8 8
         <el-select v-model="form.region" placeholder="please select your zone">
9
-          <el-option label="Zone one" value="shanghai"/>
10
-          <el-option label="Zone two" value="beijing"/>
9
+          <el-option label="Zone one" value="shanghai" />
10
+          <el-option label="Zone two" value="beijing" />
11 11
         </el-select>
12 12
       </el-form-item>
13 13
       <el-form-item label="Activity time">
14 14
         <el-col :span="11">
15
-          <el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;"/>
15
+          <el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;" />
16 16
         </el-col>
17 17
         <el-col :span="2" class="line">-</el-col>
18 18
         <el-col :span="11">
19
-          <el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;"/>
19
+          <el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;" />
20 20
         </el-col>
21 21
       </el-form-item>
22 22
       <el-form-item label="Instant delivery">
23
-        <el-switch v-model="form.delivery"/>
23
+        <el-switch v-model="form.delivery" />
24 24
       </el-form-item>
25 25
       <el-form-item label="Activity type">
26 26
         <el-checkbox-group v-model="form.type">
27
-          <el-checkbox label="Online activities" name="type"/>
28
-          <el-checkbox label="Promotion activities" name="type"/>
29
-          <el-checkbox label="Offline activities" name="type"/>
30
-          <el-checkbox label="Simple brand exposure" name="type"/>
27
+          <el-checkbox label="Online activities" name="type" />
28
+          <el-checkbox label="Promotion activities" name="type" />
29
+          <el-checkbox label="Offline activities" name="type" />
30
+          <el-checkbox label="Simple brand exposure" name="type" />
31 31
         </el-checkbox-group>
32 32
       </el-form-item>
33 33
       <el-form-item label="Resources">
34 34
         <el-radio-group v-model="form.resource">
35
-          <el-radio label="Sponsor"/>
36
-          <el-radio label="Venue"/>
35
+          <el-radio label="Sponsor" />
36
+          <el-radio label="Venue" />
37 37
         </el-radio-group>
38 38
       </el-form-item>
39 39
       <el-form-item label="Activity form">
40
-        <el-input v-model="form.desc" type="textarea"/>
40
+        <el-input v-model="form.desc" type="textarea" />
41 41
       </el-form-item>
42 42
       <el-form-item>
43 43
         <el-button type="primary" @click="onSubmit">Create</el-button>

+ 0 - 95
src/views/layout/components/Navbar.vue

@@ -1,95 +0,0 @@
1
-<template>
2
-  <div class="navbar">
3
-    <hamburger :toggle-click="toggleSideBar" :is-active="sidebar.opened" class="hamburger-container"/>
4
-    <breadcrumb />
5
-    <el-dropdown class="avatar-container" trigger="click">
6
-      <div class="avatar-wrapper">
7
-        <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
8
-        <i class="el-icon-caret-bottom"/>
9
-      </div>
10
-      <el-dropdown-menu slot="dropdown" class="user-dropdown">
11
-        <router-link class="inlineBlock" to="/">
12
-          <el-dropdown-item>
13
-            Home
14
-          </el-dropdown-item>
15
-        </router-link>
16
-        <el-dropdown-item divided>
17
-          <span style="display:block;" @click="logout">LogOut</span>
18
-        </el-dropdown-item>
19
-      </el-dropdown-menu>
20
-    </el-dropdown>
21
-  </div>
22
-</template>
23
-
24
-<script>
25
-import { mapGetters } from 'vuex'
26
-import Breadcrumb from '@/components/Breadcrumb'
27
-import Hamburger from '@/components/Hamburger'
28
-
29
-export default {
30
-  components: {
31
-    Breadcrumb,
32
-    Hamburger
33
-  },
34
-  computed: {
35
-    ...mapGetters([
36
-      'sidebar',
37
-      'avatar'
38
-    ])
39
-  },
40
-  methods: {
41
-    toggleSideBar() {
42
-      this.$store.dispatch('ToggleSideBar')
43
-    },
44
-    logout() {
45
-      this.$store.dispatch('LogOut').then(() => {
46
-        location.reload() // 为了重新实例化vue-router对象 避免bug
47
-      })
48
-    }
49
-  }
50
-}
51
-</script>
52
-
53
-<style rel="stylesheet/scss" lang="scss" scoped>
54
-.navbar {
55
-  height: 50px;
56
-  line-height: 50px;
57
-  box-shadow: 0 1px 3px 0 rgba(0,0,0,.12), 0 0 3px 0 rgba(0,0,0,.04);
58
-  .hamburger-container {
59
-    line-height: 58px;
60
-    height: 50px;
61
-    float: left;
62
-    padding: 0 10px;
63
-  }
64
-  .screenfull {
65
-    position: absolute;
66
-    right: 90px;
67
-    top: 16px;
68
-    color: red;
69
-  }
70
-  .avatar-container {
71
-    height: 50px;
72
-    display: inline-block;
73
-    position: absolute;
74
-    right: 35px;
75
-    .avatar-wrapper {
76
-      cursor: pointer;
77
-      margin-top: 5px;
78
-      position: relative;
79
-      line-height: initial;
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
-}
94
-</style>
95
-

+ 0 - 39
src/views/layout/components/Sidebar/index.vue

@@ -1,39 +0,0 @@
1
-<template>
2
-  <el-scrollbar wrap-class="scrollbar-wrapper">
3
-    <el-menu
4
-      :default-active="$route.path"
5
-      :collapse="isCollapse"
6
-      :background-color="variables.menuBg"
7
-      :text-color="variables.menuText"
8
-      :active-text-color="variables.menuActiveText"
9
-      :collapse-transition="false"
10
-      mode="vertical"
11
-    >
12
-      <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path"/>
13
-    </el-menu>
14
-  </el-scrollbar>
15
-</template>
16
-
17
-<script>
18
-import { mapGetters } from 'vuex'
19
-import variables from '@/styles/variables.scss'
20
-import SidebarItem from './SidebarItem'
21
-
22
-export default {
23
-  components: { SidebarItem },
24
-  computed: {
25
-    ...mapGetters([
26
-      'sidebar'
27
-    ]),
28
-    routes() {
29
-      return this.$router.options.routes
30
-    },
31
-    variables() {
32
-      return variables
33
-    },
34
-    isCollapse() {
35
-      return !this.sidebar.opened
36
-    }
37
-  }
38
-}
39
-</script>

+ 0 - 40
src/views/layout/mixin/ResizeHandler.js

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

+ 88 - 47
src/views/login/index.vue

@@ -1,57 +1,73 @@
1 1
 <template>
2 2
   <div class="login-container">
3 3
     <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
4
-      <h3 class="title">vue-admin-template</h3>
4
+
5
+      <div class="title-container">
6
+        <h3 class="title">Login Form</h3>
7
+      </div>
8
+
5 9
       <el-form-item prop="username">
6 10
         <span class="svg-container">
7 11
           <svg-icon icon-class="user" />
8 12
         </span>
9
-        <el-input v-model="loginForm.username" name="username" type="text" auto-complete="on" placeholder="username" />
13
+        <el-input
14
+          ref="username"
15
+          v-model="loginForm.username"
16
+          placeholder="Username"
17
+          name="username"
18
+          type="text"
19
+          tabindex="1"
20
+          auto-complete="on"
21
+        />
10 22
       </el-form-item>
23
+
11 24
       <el-form-item prop="password">
12 25
         <span class="svg-container">
13 26
           <svg-icon icon-class="password" />
14 27
         </span>
15 28
         <el-input
16
-          :type="pwdType"
29
+          :key="passwordType"
30
+          ref="password"
17 31
           v-model="loginForm.password"
32
+          :type="passwordType"
33
+          placeholder="Password"
18 34
           name="password"
35
+          tabindex="2"
19 36
           auto-complete="on"
20
-          placeholder="password"
21
-          @keyup.enter.native="handleLogin" />
37
+          @keyup.enter.native="handleLogin"
38
+        />
22 39
         <span class="show-pwd" @click="showPwd">
23
-          <svg-icon :icon-class="pwdType === 'password' ? 'eye' : 'eye-open'" />
40
+          <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
24 41
         </span>
25 42
       </el-form-item>
26
-      <el-form-item>
27
-        <el-button :loading="loading" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
28
-          Sign in
29
-        </el-button>
30
-      </el-form-item>
43
+
44
+      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
45
+
31 46
       <div class="tips">
32 47
         <span style="margin-right:20px;">username: admin</span>
33
-        <span> password: admin</span>
48
+        <span> password: any</span>
34 49
       </div>
50
+
35 51
     </el-form>
36 52
   </div>
37 53
 </template>
38 54
 
39 55
 <script>
40
-import { isvalidUsername } from '@/utils/validate'
56
+import { validUsername } from '@/utils/validate'
41 57
 
42 58
 export default {
43 59
   name: 'Login',
44 60
   data() {
45 61
     const validateUsername = (rule, value, callback) => {
46
-      if (!isvalidUsername(value)) {
47
-        callback(new Error('请输入正确的用户名'))
62
+      if (!validUsername(value)) {
63
+        callback(new Error('Please enter the correct user name'))
48 64
       } else {
49 65
         callback()
50 66
       }
51 67
     }
52
-    const validatePass = (rule, value, callback) => {
53
-      if (value.length < 5) {
54
-        callback(new Error('密码不能小于5位'))
68
+    const validatePassword = (rule, value, callback) => {
69
+      if (value.length < 6) {
70
+        callback(new Error('The password can not be less than 6 digits'))
55 71
       } else {
56 72
         callback()
57 73
       }
@@ -59,14 +75,14 @@ export default {
59 75
     return {
60 76
       loginForm: {
61 77
         username: 'admin',
62
-        password: 'admin'
78
+        password: '111111'
63 79
       },
64 80
       loginRules: {
65 81
         username: [{ required: true, trigger: 'blur', validator: validateUsername }],
66
-        password: [{ required: true, trigger: 'blur', validator: validatePass }]
82
+        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
67 83
       },
68 84
       loading: false,
69
-      pwdType: 'password',
85
+      passwordType: 'password',
70 86
       redirect: undefined
71 87
     }
72 88
   },
@@ -80,19 +96,22 @@ export default {
80 96
   },
81 97
   methods: {
82 98
     showPwd() {
83
-      if (this.pwdType === 'password') {
84
-        this.pwdType = ''
99
+      if (this.passwordType === 'password') {
100
+        this.passwordType = ''
85 101
       } else {
86
-        this.pwdType = 'password'
102
+        this.passwordType = 'password'
87 103
       }
104
+      this.$nextTick(() => {
105
+        this.$refs.password.focus()
106
+      })
88 107
     },
89 108
     handleLogin() {
90 109
       this.$refs.loginForm.validate(valid => {
91 110
         if (valid) {
92 111
           this.loading = true
93
-          this.$store.dispatch('Login', this.loginForm).then(() => {
94
-            this.loading = false
112
+          this.$store.dispatch('user/login', this.loginForm).then(() => {
95 113
             this.$router.push({ path: this.redirect || '/' })
114
+            this.loading = false
96 115
           }).catch(() => {
97 116
             this.loading = false
98 117
           })
@@ -106,9 +125,19 @@ export default {
106 125
 }
107 126
 </script>
108 127
 
109
-<style rel="stylesheet/scss" lang="scss">
110
-$bg:#2d3a4b;
111
-$light_gray:#eee;
128
+<style lang="scss">
129
+/* 修复input 背景不协调 和光标变色 */
130
+/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
131
+
132
+$bg:#283443;
133
+$light_gray:#fff;
134
+$cursor: #fff;
135
+
136
+@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
137
+  .login-container .el-input input {
138
+    color: $cursor;
139
+  }
140
+}
112 141
 
113 142
 /* reset element-ui css */
114 143
 .login-container {
@@ -116,6 +145,7 @@ $light_gray:#eee;
116 145
     display: inline-block;
117 146
     height: 47px;
118 147
     width: 85%;
148
+
119 149
     input {
120 150
       background: transparent;
121 151
       border: 0px;
@@ -124,12 +154,15 @@ $light_gray:#eee;
124 154
       padding: 12px 5px 12px 15px;
125 155
       color: $light_gray;
126 156
       height: 47px;
157
+      caret-color: $cursor;
158
+
127 159
       &:-webkit-autofill {
128
-        -webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
129
-        -webkit-text-fill-color: #fff !important;
160
+        box-shadow: 0 0 0px 1000px $bg inset !important;
161
+        -webkit-text-fill-color: $cursor !important;
130 162
       }
131 163
     }
132 164
   }
165
+
133 166
   .el-form-item {
134 167
     border: 1px solid rgba(255, 255, 255, 0.1);
135 168
     background: rgba(0, 0, 0, 0.1);
@@ -137,37 +170,40 @@ $light_gray:#eee;
137 170
     color: #454545;
138 171
   }
139 172
 }
140
-
141 173
 </style>
142 174
 
143
-<style rel="stylesheet/scss" lang="scss" scoped>
175
+<style lang="scss" scoped>
144 176
 $bg:#2d3a4b;
145 177
 $dark_gray:#889aa4;
146 178
 $light_gray:#eee;
179
+
147 180
 .login-container {
148
-  position: fixed;
149
-  height: 100%;
181
+  min-height: 100%;
150 182
   width: 100%;
151 183
   background-color: $bg;
184
+  overflow: hidden;
185
+
152 186
   .login-form {
153
-    position: absolute;
154
-    left: 0;
155
-    right: 0;
187
+    position: relative;
156 188
     width: 520px;
157 189
     max-width: 100%;
158
-    padding: 35px 35px 15px 35px;
159
-    margin: 120px auto;
190
+    padding: 160px 35px 0;
191
+    margin: 0 auto;
192
+    overflow: hidden;
160 193
   }
194
+
161 195
   .tips {
162 196
     font-size: 14px;
163 197
     color: #fff;
164 198
     margin-bottom: 10px;
199
+
165 200
     span {
166 201
       &:first-of-type {
167 202
         margin-right: 16px;
168 203
       }
169 204
     }
170 205
   }
206
+
171 207
   .svg-container {
172 208
     padding: 6px 5px 6px 15px;
173 209
     color: $dark_gray;
@@ -175,14 +211,19 @@ $light_gray:#eee;
175 211
     width: 30px;
176 212
     display: inline-block;
177 213
   }
178
-  .title {
179
-    font-size: 26px;
180
-    font-weight: 400;
181
-    color: $light_gray;
182
-    margin: 0px auto 40px auto;
183
-    text-align: center;
184
-    font-weight: bold;
214
+
215
+  .title-container {
216
+    position: relative;
217
+
218
+    .title {
219
+      font-size: 26px;
220
+      color: $light_gray;
221
+      margin: 0px auto 40px auto;
222
+      text-align: center;
223
+      font-weight: bold;
224
+    }
185 225
   }
226
+
186 227
   .show-pwd {
187 228
     position: absolute;
188 229
     right: 10px;

+ 1 - 1
src/views/nested/menu1/index.vue

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

+ 1 - 1
src/views/nested/menu1/menu1-1/index.vue

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

+ 4 - 3
src/views/table/index.vue

@@ -6,7 +6,8 @@
6 6
       element-loading-text="Loading"
7 7
       border
8 8
       fit
9
-      highlight-current-row>
9
+      highlight-current-row
10
+    >
10 11
       <el-table-column align="center" label="ID" width="95">
11 12
         <template slot-scope="scope">
12 13
           {{ scope.$index }}
@@ -34,7 +35,7 @@
34 35
       </el-table-column>
35 36
       <el-table-column align="center" prop="created_at" label="Display_time" width="200">
36 37
         <template slot-scope="scope">
37
-          <i class="el-icon-time"/>
38
+          <i class="el-icon-time" />
38 39
           <span>{{ scope.row.display_time }}</span>
39 40
         </template>
40 41
       </el-table-column>
@@ -68,7 +69,7 @@ export default {
68 69
   methods: {
69 70
     fetchData() {
70 71
       this.listLoading = true
71
-      getList(this.listQuery).then(response => {
72
+      getList().then(response => {
72 73
         this.list = response.data.items
73 74
         this.listLoading = false
74 75
       })

+ 0 - 0
static/.gitkeep


+ 5 - 0
tests/unit/.eslintrc.js

@@ -0,0 +1,5 @@
1
+module.exports = {
2
+  env: {
3
+    jest: true
4
+  }
5
+}

+ 98 - 0
tests/unit/components/Breadcrumb.spec.js

@@ -0,0 +1,98 @@
1
+import { mount, createLocalVue } from '@vue/test-utils'
2
+import VueRouter from 'vue-router'
3
+import ElementUI from 'element-ui'
4
+import Breadcrumb from '@/components/Breadcrumb/index.vue'
5
+
6
+const localVue = createLocalVue()
7
+localVue.use(VueRouter)
8
+localVue.use(ElementUI)
9
+
10
+const routes = [
11
+  {
12
+    path: '/',
13
+    name: 'home',
14
+    children: [{
15
+      path: 'dashboard',
16
+      name: 'dashboard'
17
+    }]
18
+  },
19
+  {
20
+    path: '/menu',
21
+    name: 'menu',
22
+    children: [{
23
+      path: 'menu1',
24
+      name: 'menu1',
25
+      meta: { title: 'menu1' },
26
+      children: [{
27
+        path: 'menu1-1',
28
+        name: 'menu1-1',
29
+        meta: { title: 'menu1-1' }
30
+      },
31
+      {
32
+        path: 'menu1-2',
33
+        name: 'menu1-2',
34
+        redirect: 'noredirect',
35
+        meta: { title: 'menu1-2' },
36
+        children: [{
37
+          path: 'menu1-2-1',
38
+          name: 'menu1-2-1',
39
+          meta: { title: 'menu1-2-1' }
40
+        },
41
+        {
42
+          path: 'menu1-2-2',
43
+          name: 'menu1-2-2'
44
+        }]
45
+      }]
46
+    }]
47
+  }]
48
+
49
+const router = new VueRouter({
50
+  routes
51
+})
52
+
53
+describe('Breadcrumb.vue', () => {
54
+  const wrapper = mount(Breadcrumb, {
55
+    localVue,
56
+    router
57
+  })
58
+  it('dashboard', () => {
59
+    router.push('/dashboard')
60
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
61
+    expect(len).toBe(1)
62
+  })
63
+  it('normal route', () => {
64
+    router.push('/menu/menu1')
65
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
66
+    expect(len).toBe(2)
67
+  })
68
+  it('nested route', () => {
69
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
70
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
71
+    expect(len).toBe(4)
72
+  })
73
+  it('no meta.title', () => {
74
+    router.push('/menu/menu1/menu1-2/menu1-2-2')
75
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
76
+    expect(len).toBe(3)
77
+  })
78
+  // it('click link', () => {
79
+  //   router.push('/menu/menu1/menu1-2/menu1-2-2')
80
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
81
+  //   const second = breadcrumbArray.at(1)
82
+  //   console.log(breadcrumbArray)
83
+  //   const href = second.find('a').attributes().href
84
+  //   expect(href).toBe('#/menu/menu1')
85
+  // })
86
+  // it('noRedirect', () => {
87
+  //   router.push('/menu/menu1/menu1-2/menu1-2-1')
88
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
89
+  //   const redirectBreadcrumb = breadcrumbArray.at(2)
90
+  //   expect(redirectBreadcrumb.contains('a')).toBe(false)
91
+  // })
92
+  it('last breadcrumb', () => {
93
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
94
+    const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
95
+    const redirectBreadcrumb = breadcrumbArray.at(3)
96
+    expect(redirectBreadcrumb.contains('a')).toBe(false)
97
+  })
98
+})

+ 18 - 0
tests/unit/components/Hamburger.spec.js

@@ -0,0 +1,18 @@
1
+import { shallowMount } from '@vue/test-utils'
2
+import Hamburger from '@/components/Hamburger/index.vue'
3
+describe('Hamburger.vue', () => {
4
+  it('toggle click', () => {
5
+    const wrapper = shallowMount(Hamburger)
6
+    const mockFn = jest.fn()
7
+    wrapper.vm.$on('toggleClick', mockFn)
8
+    wrapper.find('.hamburger').trigger('click')
9
+    expect(mockFn).toBeCalled()
10
+  })
11
+  it('prop isActive', () => {
12
+    const wrapper = shallowMount(Hamburger)
13
+    wrapper.setProps({ isActive: true })
14
+    expect(wrapper.contains('.is-active')).toBe(true)
15
+    wrapper.setProps({ isActive: false })
16
+    expect(wrapper.contains('.is-active')).toBe(false)
17
+  })
18
+})

+ 22 - 0
tests/unit/components/SvgIcon.spec.js

@@ -0,0 +1,22 @@
1
+import { shallowMount } from '@vue/test-utils'
2
+import SvgIcon from '@/components/SvgIcon/index.vue'
3
+describe('SvgIcon.vue', () => {
4
+  it('iconClass', () => {
5
+    const wrapper = shallowMount(SvgIcon, {
6
+      propsData: {
7
+        iconClass: 'test'
8
+      }
9
+    })
10
+    expect(wrapper.find('use').attributes().href).toBe('#icon-test')
11
+  })
12
+  it('className', () => {
13
+    const wrapper = shallowMount(SvgIcon, {
14
+      propsData: {
15
+        iconClass: 'test'
16
+      }
17
+    })
18
+    expect(wrapper.classes().length).toBe(1)
19
+    wrapper.setProps({ className: 'test' })
20
+    expect(wrapper.classes().includes('test')).toBe(true)
21
+  })
22
+})

+ 30 - 0
tests/unit/utils/formatTime.spec.js

@@ -0,0 +1,30 @@
1
+import { formatTime } from '@/utils/index.js'
2
+
3
+describe('Utils:formatTime', () => {
4
+  const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5
+  const retrofit = 5 * 1000
6
+
7
+  it('ten digits timestamp', () => {
8
+    expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
9
+  })
10
+  it('test now', () => {
11
+    expect(formatTime(+new Date() - 1)).toBe('刚刚')
12
+  })
13
+  it('less two minute', () => {
14
+    expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
15
+  })
16
+  it('less two hour', () => {
17
+    expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
18
+  })
19
+  it('less one day', () => {
20
+    expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
21
+  })
22
+  it('more than one day', () => {
23
+    expect(formatTime(d)).toBe('7月13日17时54分')
24
+  })
25
+  it('format', () => {
26
+    expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
27
+    expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
28
+    expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
29
+  })
30
+})

+ 28 - 0
tests/unit/utils/parseTime.spec.js

@@ -0,0 +1,28 @@
1
+import { parseTime } from '@/utils/index.js'
2
+
3
+describe('Utils:parseTime', () => {
4
+  const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5
+  it('timestamp', () => {
6
+    expect(parseTime(d)).toBe('2018-07-13 17:54:01')
7
+  })
8
+  it('ten digits timestamp', () => {
9
+    expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
10
+  })
11
+  it('new Date', () => {
12
+    expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
13
+  })
14
+  it('format', () => {
15
+    expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
16
+    expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
17
+    expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
18
+  })
19
+  it('get the day of the week', () => {
20
+    expect(parseTime(d, '{a}')).toBe('五') // 星期五
21
+  })
22
+  it('get the day of the week', () => {
23
+    expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
24
+  })
25
+  it('empty argument', () => {
26
+    expect(parseTime()).toBeNull()
27
+  })
28
+})

+ 17 - 0
tests/unit/utils/validate.spec.js

@@ -0,0 +1,17 @@
1
+import { validUsername, isExternal } from '@/utils/validate.js'
2
+
3
+describe('Utils:validate', () => {
4
+  it('validUsername', () => {
5
+    expect(validUsername('admin')).toBe(true)
6
+    expect(validUsername('editor')).toBe(true)
7
+    expect(validUsername('xxxx')).toBe(false)
8
+  })
9
+  it('isExternal', () => {
10
+    expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11
+    expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
12
+    expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
13
+    expect(isExternal('/dashboard')).toBe(false)
14
+    expect(isExternal('./dashboard')).toBe(false)
15
+    expect(isExternal('dashboard')).toBe(false)
16
+  })
17
+})

+ 133 - 0
vue.config.js

@@ -0,0 +1,133 @@
1
+'use strict'
2
+const path = require('path')
3
+const defaultSettings = require('./src/settings.js')
4
+
5
+function resolve(dir) {
6
+  return path.join(__dirname, dir)
7
+}
8
+
9
+const name = defaultSettings.title || 'vue Admin Template' // page title
10
+const port = 9528 // dev port
11
+
12
+// All configuration item explanations can be find in https://cli.vuejs.org/config/
13
+module.exports = {
14
+  /**
15
+   * You will need to set publicPath if you plan to deploy your site under a sub path,
16
+   * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
17
+   * then publicPath should be set to "/bar/".
18
+   * In most cases please use '/' !!!
19
+   * Detail: https://cli.vuejs.org/config/#publicpath
20
+   */
21
+  publicPath: '/',
22
+  outputDir: 'dist',
23
+  assetsDir: 'static',
24
+  lintOnSave: process.env.NODE_ENV === 'development',
25
+  productionSourceMap: false,
26
+  devServer: {
27
+    port: port,
28
+    open: true,
29
+    overlay: {
30
+      warnings: false,
31
+      errors: true
32
+    },
33
+    proxy: {
34
+      // change xxx-api/login => mock/login
35
+      // detail: https://cli.vuejs.org/config/#devserver-proxy
36
+      [process.env.VUE_APP_BASE_API]: {
37
+        target: `http://localhost:${port}/mock`,
38
+        changeOrigin: true,
39
+        pathRewrite: {
40
+          ['^' + process.env.VUE_APP_BASE_API]: ''
41
+        }
42
+      }
43
+    },
44
+    after: require('./mock/mock-server.js')
45
+  },
46
+  configureWebpack: {
47
+    // provide the app's title in webpack's name field, so that
48
+    // it can be accessed in index.html to inject the correct title.
49
+    name: name,
50
+    resolve: {
51
+      alias: {
52
+        '@': resolve('src')
53
+      }
54
+    }
55
+  },
56
+  chainWebpack(config) {
57
+    config.plugins.delete('preload') // TODO: need test
58
+    config.plugins.delete('prefetch') // TODO: need test
59
+
60
+    // set svg-sprite-loader
61
+    config.module
62
+      .rule('svg')
63
+      .exclude.add(resolve('src/icons'))
64
+      .end()
65
+    config.module
66
+      .rule('icons')
67
+      .test(/\.svg$/)
68
+      .include.add(resolve('src/icons'))
69
+      .end()
70
+      .use('svg-sprite-loader')
71
+      .loader('svg-sprite-loader')
72
+      .options({
73
+        symbolId: 'icon-[name]'
74
+      })
75
+      .end()
76
+
77
+    // set preserveWhitespace
78
+    config.module
79
+      .rule('vue')
80
+      .use('vue-loader')
81
+      .loader('vue-loader')
82
+      .tap(options => {
83
+        options.compilerOptions.preserveWhitespace = true
84
+        return options
85
+      })
86
+      .end()
87
+
88
+    config
89
+    // https://webpack.js.org/configuration/devtool/#development
90
+      .when(process.env.NODE_ENV === 'development',
91
+        config => config.devtool('cheap-source-map')
92
+      )
93
+
94
+    config
95
+      .when(process.env.NODE_ENV !== 'development',
96
+        config => {
97
+          config
98
+            .plugin('ScriptExtHtmlWebpackPlugin')
99
+            .after('html')
100
+            .use('script-ext-html-webpack-plugin', [{
101
+            // `runtime` must same as runtimeChunk name. default is `runtime`
102
+              inline: /runtime\..*\.js$/
103
+            }])
104
+            .end()
105
+          config
106
+            .optimization.splitChunks({
107
+              chunks: 'all',
108
+              cacheGroups: {
109
+                libs: {
110
+                  name: 'chunk-libs',
111
+                  test: /[\\/]node_modules[\\/]/,
112
+                  priority: 10,
113
+                  chunks: 'initial' // only package third parties that are initially dependent
114
+                },
115
+                elementUI: {
116
+                  name: 'chunk-elementUI', // split elementUI into a single package
117
+                  priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
118
+                  test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
119
+                },
120
+                commons: {
121
+                  name: 'chunk-commons',
122
+                  test: resolve('src/components'), // can customize your rules
123
+                  minChunks: 3, //  minimum common number
124
+                  priority: 5,
125
+                  reuseExistingChunk: true
126
+                }
127
+              }
128
+            })
129
+          config.optimization.runtimeChunk('single')
130
+        }
131
+      )
132
+  }
133
+}