Commit 80a28914e89969ed20c58e90dd89dc8ea24b5c68

Authored by 吉鹏
0 parents
Exists in master

init

Showing 96 changed files with 4033 additions and 0 deletions   Show diff stats
File was created 1 # http://editorconfig.org
2 root = true
3
4 [*]
5 charset = utf-8
6 indent_style = space
7 indent_size = 2
8 end_of_line = lf
9 insert_final_newline = true
10 trim_trailing_whitespace = true
11
12 [*.md]
13 insert_final_newline = false
14 trim_trailing_whitespace = false
15
File was created 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
15
File was created 1 # just a flag
2 ENV = 'production'
3
4 # base api
5 VUE_APP_BASE_API = '/prod-api'
6
7
File was created 1 NODE_ENV = production
2
3 # just a flag
4 ENV = 'staging'
5
6 # base api
7 VUE_APP_BASE_API = '/stage-api'
8
9
File was created 1 build/*.js
2 src/assets
3 public
4 dist
5
File was created 1 module.exports = {
2 root: true,
3 parserOptions: {
4 parser: 'babel-eslint',
5 sourceType: 'module'
6 },
7 env: {
8 browser: true,
9 node: true,
10 es6: true,
11 },
12 extends: ['plugin:vue/recommended', 'eslint:recommended'],
13
14 // add your custom rules here
15 //it is base on https://github.com/vuejs/eslint-config-vue
16 rules: {
17 "vue/max-attributes-per-line": [2, {
18 "singleline": 10,
19 "multiline": {
20 "max": 1,
21 "allowFirstLine": false
22 }
23 }],
24 "vue/singleline-html-element-content-newline": "off",
25 "vue/multiline-html-element-content-newline":"off",
26 "vue/name-property-casing": ["error", "PascalCase"],
27 "vue/no-v-html": "off",
28 'accessor-pairs': 2,
29 'arrow-spacing': [2, {
30 'before': true,
31 'after': true
32 }],
33 'block-spacing': [2, 'always'],
34 'brace-style': [2, '1tbs', {
35 'allowSingleLine': true
36 }],
37 'camelcase': [0, {
38 'properties': 'always'
39 }],
40 'comma-dangle': [2, 'never'],
41 'comma-spacing': [2, {
42 'before': false,
43 'after': true
44 }],
45 'comma-style': [2, 'last'],
46 'constructor-super': 2,
47 'curly': [2, 'multi-line'],
48 'dot-location': [2, 'property'],
49 'eol-last': 2,
50 'eqeqeq': ["error", "always", {"null": "ignore"}],
51 'generator-star-spacing': [2, {
52 'before': true,
53 'after': true
54 }],
55 'handle-callback-err': [2, '^(err|error)$'],
56 'indent': [2, 2, {
57 'SwitchCase': 1
58 }],
59 'jsx-quotes': [2, 'prefer-single'],
60 'key-spacing': [2, {
61 'beforeColon': false,
62 'afterColon': true
63 }],
64 'keyword-spacing': [2, {
65 'before': true,
66 'after': true
67 }],
68 'new-cap': [2, {
69 'newIsCap': true,
70 'capIsNew': false
71 }],
72 'new-parens': 2,
73 'no-array-constructor': 2,
74 'no-caller': 2,
75 'no-console': 'off',
76 'no-class-assign': 2,
77 'no-cond-assign': 2,
78 'no-const-assign': 2,
79 'no-control-regex': 0,
80 'no-delete-var': 2,
81 'no-dupe-args': 2,
82 'no-dupe-class-members': 2,
83 'no-dupe-keys': 2,
84 'no-duplicate-case': 2,
85 'no-empty-character-class': 2,
86 'no-empty-pattern': 2,
87 'no-eval': 2,
88 'no-ex-assign': 2,
89 'no-extend-native': 2,
90 'no-extra-bind': 2,
91 'no-extra-boolean-cast': 2,
92 'no-extra-parens': [2, 'functions'],
93 'no-fallthrough': 2,
94 'no-floating-decimal': 2,
95 'no-func-assign': 2,
96 'no-implied-eval': 2,
97 'no-inner-declarations': [2, 'functions'],
98 'no-invalid-regexp': 2,
99 'no-irregular-whitespace': 2,
100 'no-iterator': 2,
101 'no-label-var': 2,
102 'no-labels': [2, {
103 'allowLoop': false,
104 'allowSwitch': false
105 }],
106 'no-lone-blocks': 2,
107 'no-mixed-spaces-and-tabs': 2,
108 'no-multi-spaces': 2,
109 'no-multi-str': 2,
110 'no-multiple-empty-lines': [2, {
111 'max': 1
112 }],
113 'no-native-reassign': 2,
114 'no-negated-in-lhs': 2,
115 'no-new-object': 2,
116 'no-new-require': 2,
117 'no-new-symbol': 2,
118 'no-new-wrappers': 2,
119 'no-obj-calls': 2,
120 'no-octal': 2,
121 'no-octal-escape': 2,
122 'no-path-concat': 2,
123 'no-proto': 2,
124 'no-redeclare': 2,
125 'no-regex-spaces': 2,
126 'no-return-assign': [2, 'except-parens'],
127 'no-self-assign': 2,
128 'no-self-compare': 2,
129 'no-sequences': 2,
130 'no-shadow-restricted-names': 2,
131 'no-spaced-func': 2,
132 'no-sparse-arrays': 2,
133 'no-this-before-super': 2,
134 'no-throw-literal': 2,
135 'no-trailing-spaces': 2,
136 'no-undef': 2,
137 'no-undef-init': 2,
138 'no-unexpected-multiline': 2,
139 'no-unmodified-loop-condition': 2,
140 'no-unneeded-ternary': [2, {
141 'defaultAssignment': false
142 }],
143 'no-unreachable': 2,
144 'no-unsafe-finally': 2,
145 'no-unused-vars': [2, {
146 'vars': 'all',
147 'args': 'none'
148 }],
149 'no-useless-call': 2,
150 'no-useless-computed-key': 2,
151 'no-useless-constructor': 2,
152 'no-useless-escape': 0,
153 'no-whitespace-before-property': 2,
154 'no-with': 2,
155 'one-var': [2, {
156 'initialized': 'never'
157 }],
158 'operator-linebreak': [2, 'after', {
159 'overrides': {
160 '?': 'before',
161 ':': 'before'
162 }
163 }],
164 'padded-blocks': [2, 'never'],
165 'quotes': [2, 'single', {
166 'avoidEscape': true,
167 'allowTemplateLiterals': true
168 }],
169 'semi': [2, 'never'],
170 'semi-spacing': [2, {
171 'before': false,
172 'after': true
173 }],
174 'space-before-blocks': [2, 'always'],
175 'space-before-function-paren': [2, 'never'],
176 'space-in-parens': [2, 'never'],
177 'space-infix-ops': 2,
178 'space-unary-ops': [2, {
179 'words': true,
180 'nonwords': false
181 }],
182 'spaced-comment': [2, 'always', {
183 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
184 }],
185 'template-curly-spacing': [2, 'never'],
186 'use-isnan': 2,
187 'valid-typeof': 2,
188 'wrap-iife': [2, 'any'],
189 'yield-star-spacing': [2, 'both'],
190 'yoda': [2, 'never'],
191 'prefer-const': 2,
192 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
193 'object-curly-spacing': [2, 'always', {
194 objectsInObjects: false
195 }],
196 'array-bracket-spacing': [2, 'never']
197 }
198 }
199
File was created 1 .DS_Store
2 node_modules/
3 dist/
4 npm-debug.log*
5 yarn-debug.log*
6 yarn-error.log*
7 package-lock.json
8 tests/**/coverage/
9
10 # Editor directories and files
11 .idea
12 .vscode
13 *.suo
14 *.ntvs*
15 *.njsproj
16 *.sln
17
File was created 1 language: node_js
2 node_js: 10
3 script: npm run test
4 notifications:
5 email: false
6
File was created 1 MIT License
2
3 Copyright (c) 2017-present PanJiaChen
4
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included in all
13 copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 SOFTWARE.
22
File was created 1
2 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
3
4 ## 相关项目
5
6 - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
7
8 - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
9
10 - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
11
12 - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
13
14 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
15
16 - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
17 - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
18 - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
19 - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
20 - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
21
22 ## Build Setup
23
24 ```bash
25
26 # 进入项目目录
27 cd gulu-admin
28
29 # 安装依赖
30 npm install
31
32 # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
33 npm install --registry=https://registry.npm.taobao.org
34
35 # 启动服务
36 npm run dev
37 ```
38
39 浏览器访问 [http://localhost:9528](http://localhost:9528)
40
41 ## 发布
42
43 ```bash
44 # 构建测试环境
45 npm run build:stage
46
47 # 构建生产环境
48 npm run build:prod
49 ```
50
51 ## 其它
52
53 ```bash
54 # 预览发布环境效果
55 npm run preview
56
57 # 预览发布环境效果 + 静态资源分析
58 npm run preview -- --report
59
60 # 代码格式检查
61 npm run lint
62
63 # 代码格式检查并自动修复
64 npm run lint -- --fix
65 ```
66
67
File was created 1
2 ```bash
3
4 # enter the project directory
5 cd gulu-admin
6
7 # install dependency
8 npm install
9
10 # develop
11 npm run dev
12 ```
13
14 This will automatically open http://localhost:9528
15
16 ## Build
17
18 ```bash
19 # build for test environment
20 npm run build:stage
21
22 # build for production environment
23 npm run build:prod
24 ```
25
26 ## Advanced
27
28 ```bash
29 # preview the release environment effect
30 npm run preview
31
32 # preview the release environment effect + static resource analysis
33 npm run preview -- --report
34
35 # code format check
36 npm run lint
37
38 # code format check and auto fix
39 npm run lint -- --fix
40 ```
41
42 Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
43
44 ## Demo
45
46 ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
47
48 ## Extra
49
50 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)
51
52 For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
53
54 ## Related Project
55
56 - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
57
58 - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
59
60 - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
61
62 - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
63
64 ## Browsers support
65
66 Modern browsers and Internet Explorer 10+.
67
File was created 1 module.exports = {
2 presets: [
3 '@vue/app'
4 ]
5 }
6
File was created 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 }
36
File was created 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 }
25
File was created 1 {
2 "compilerOptions": {
3 "baseUrl": "./",
4 "paths": {
5 "@/*": ["src/*"]
6 }
7 },
8 "exclude": ["node_modules", "dist"]
9 }
10
File was created 1 import Mock from 'mockjs'
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)
28 }
29
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 }
47
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(`${process.env.VUE_APP_BASE_API}${url}`),
57 type: type || 'get',
58 response(req, res) {
59 console.log('request invoke:' + req.path)
60 res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
61 }
62 }
63 }
64
65 export default mocks.map(route => {
66 return responseFake(route.url, route.type, route.response)
67 })
68
mock/mock-server.js
File was created 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 try {
52 // remove mock routes stack
53 app._router.stack.splice(mockStartIndex, mockRoutesLength)
54
55 // clear routes cache
56 unregisterRoutes()
57
58 const mockRoutes = registerRoutes(app)
59 mockRoutesLength = mockRoutes.mockRoutesLength
60 mockStartIndex = mockRoutes.mockStartIndex
61
62 console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
63 } catch (error) {
64 console.log(chalk.redBright(error))
65 }
66 }
67 })
68 }
69
File was created 1 import Mock from 'mockjs'
2
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: '/vue-admin-template/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 }
27 }
28 }
29 ]
30
File was created 1
2 const tokens = {
3 admin: {
4 token: 'admin-token'
5 },
6 editor: {
7 token: 'editor-token'
8 }
9 }
10
11 const users = {
12 'admin-token': {
13 roles: ['admin'],
14 introduction: 'I am a super administrator',
15 avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
16 name: 'Super Admin'
17 },
18 'editor-token': {
19 roles: ['editor'],
20 introduction: 'I am an editor',
21 avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
22 name: 'Normal Editor'
23 }
24 }
25
26 export default [
27 // user login
28 {
29 url: '/vue-admin-template/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 }
42
43 return {
44 code: 20000,
45 data: token
46 }
47 }
48 },
49
50 // get user info
51 {
52 url: '/vue-admin-template/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
66 return {
67 code: 20000,
68 data: info
69 }
70 }
71 },
72
73 // user logout
74 {
75 url: '/vue-admin-template/user/logout',
76 type: 'post',
77 response: _ => {
78 return {
79 code: 20000,
80 data: 'success'
81 }
82 }
83 }
84 ]
85
File was created 1 {
2 "name": "vue-admin-template",
3 "version": "4.2.1",
4 "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
5 "author": "Pan <panfree23@gmail.com>",
6 "license": "MIT",
7 "scripts": {
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 "lint": "eslint --ext .js,.vue src",
13 "test:unit": "jest --clearCache && vue-cli-service test:unit",
14 "test:ci": "npm run lint && npm run test:unit",
15 "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
16 },
17 "dependencies": {
18 "axios": "0.18.1",
19 "element-ui": "2.13.0",
20 "js-cookie": "2.2.0",
21 "normalize.css": "7.0.0",
22 "nprogress": "0.2.0",
23 "path-to-regexp": "2.4.0",
24 "vue": "2.6.10",
25 "vue-router": "3.0.6",
26 "vuex": "3.1.0"
27 },
28 "devDependencies": {
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.9.1",
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 "autoprefixer": "^9.5.1",
37 "babel-core": "7.0.0-bridge.0",
38 "babel-eslint": "10.0.1",
39 "babel-jest": "23.6.0",
40 "chalk": "2.4.2",
41 "connect": "3.6.6",
42 "eslint": "5.15.3",
43 "eslint-plugin-vue": "5.2.2",
44 "html-webpack-plugin": "3.2.0",
45 "mockjs": "1.0.1-beta3",
46 "node-sass": "^4.9.0",
47 "runjs": "^4.3.2",
48 "sass-loader": "^7.1.0",
49 "script-ext-html-webpack-plugin": "2.1.3",
50 "script-loader": "0.7.2",
51 "serve-static": "^1.13.2",
52 "svg-sprite-loader": "4.1.3",
53 "svgo": "1.2.2",
54 "vue-template-compiler": "2.6.10"
55 },
56 "engines": {
57 "node": ">=8.9",
58 "npm": ">= 3.0.0"
59 },
60 "browserslist": [
61 "> 1%",
62 "last 2 versions"
63 ]
64 }
65
postcss.config.js
File was created 1 // https://github.com/michael-ciniawsky/postcss-load-config
2
3 module.exports = {
4 'plugins': {
5 // to edit target browsers: use "browserslist" field in package.json
6 'autoprefixer': {}
7 }
8 }
9
public/favicon.ico
No preview for this file type
public/index.html
File was created 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>
18
File was created 1 <template>
2 <div id="app">
3 <router-view />
4 </div>
5 </template>
6
7 <script>
8 export default {
9 name: 'App'
10 }
11 </script>
12
File was created 1 import request from '@/utils/request'
2
3 export function getList(params) {
4 return request({
5 url: '/vue-admin-template/table/list',
6 method: 'get',
7 params
8 })
9 }
10
File was created 1 import request from '@/utils/request'
2
3 export function login(data) {
4 return request({
5 url: '/vue-admin-template/user/login',
6 method: 'post',
7 data
8 })
9 }
10
11 export function getInfo(token) {
12 return request({
13 url: '/vue-admin-template/user/info',
14 method: 'get',
15 params: { token }
16 })
17 }
18
19 export function logout() {
20 return request({
21 url: '/vue-admin-template/user/logout',
22 method: 'post'
23 })
24 }
25
src/assets/404_images/404.png

95.8 KB

src/assets/404_images/404_cloud.png

4.65 KB

src/components/Breadcrumb/index.vue
File was created 1 <template>
2 <el-breadcrumb class="app-breadcrumb" separator="/">
3 <transition-group name="breadcrumb">
4 <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
5 <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
6 <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
7 </el-breadcrumb-item>
8 </transition-group>
9 </el-breadcrumb>
10 </template>
11
12 <script>
13 import pathToRegexp from 'path-to-regexp'
14
15 export default {
16 data() {
17 return {
18 levelList: null
19 }
20 },
21 watch: {
22 $route() {
23 this.getBreadcrumb()
24 }
25 },
26 created() {
27 this.getBreadcrumb()
28 },
29 methods: {
30 getBreadcrumb() {
31 // only show routes with meta.title
32 let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
33 const first = matched[0]
34
35 if (!this.isDashboard(first)) {
36 matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
37 }
38
39 this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
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 },
48 pathCompile(path) {
49 // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
50 const { params } = this.$route
51 var toPath = pathToRegexp.compile(path)
52 return toPath(params)
53 },
54 handleLink(item) {
55 const { redirect, path } = item
56 if (redirect) {
57 this.$router.push(redirect)
58 return
59 }
60 this.$router.push(this.pathCompile(path))
61 }
62 }
63 }
64 </script>
65
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;
76 }
77 }
78 </style>
79
src/components/Hamburger/index.vue
File was created 1 <template>
2 <div style="padding: 0 15px;" @click="toggleClick">
3 <svg
4 :class="{'is-active':isActive}"
5 class="hamburger"
6 viewBox="0 0 1024 1024"
7 xmlns="http://www.w3.org/2000/svg"
8 width="64"
9 height="64"
10 >
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 </svg>
13 </div>
14 </template>
15
16 <script>
17 export default {
18 name: 'Hamburger',
19 props: {
20 isActive: {
21 type: Boolean,
22 default: false
23 }
24 },
25 methods: {
26 toggleClick() {
27 this.$emit('toggleClick')
28 }
29 }
30 }
31 </script>
32
33 <style scoped>
34 .hamburger {
35 display: inline-block;
36 vertical-align: middle;
37 width: 20px;
38 height: 20px;
39 }
40
41 .hamburger.is-active {
42 transform: rotate(180deg);
43 }
44 </style>
45
src/components/SvgIcon/index.vue
File was created 1 <template>
2 <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
3 <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
4 <use :href="iconName" />
5 </svg>
6 </template>
7
8 <script>
9 // doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
10 import { isExternal } from '@/utils/validate'
11
12 export default {
13 name: 'SvgIcon',
14 props: {
15 iconClass: {
16 type: String,
17 required: true
18 },
19 className: {
20 type: String,
21 default: ''
22 }
23 },
24 computed: {
25 isExternal() {
26 return isExternal(this.iconClass)
27 },
28 iconName() {
29 return `#icon-${this.iconClass}`
30 },
31 svgClass() {
32 if (this.className) {
33 return 'svg-icon ' + this.className
34 } else {
35 return 'svg-icon'
36 }
37 },
38 styleExternalIcon() {
39 return {
40 mask: `url(${this.iconClass}) no-repeat 50% 50%`,
41 '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
42 }
43 }
44 }
45 }
46 </script>
47
48 <style scoped>
49 .svg-icon {
50 width: 1em;
51 height: 1em;
52 vertical-align: -0.15em;
53 fill: currentColor;
54 overflow: hidden;
55 }
56
57 .svg-external-icon {
58 background-color: currentColor;
59 mask-size: cover!important;
60 display: inline-block;
61 }
62 </style>
63
src/icons/index.js
File was created 1 import Vue from 'vue'
2 import SvgIcon from '@/components/SvgIcon'// svg component
3
4 // register globally
5 Vue.component('svg-icon', SvgIcon)
6
7 const req = require.context('./svg', false, /\.svg$/)
8 const requireAll = requireContext => requireContext.keys().map(requireContext)
9 requireAll(req)
10
src/icons/svg/dashboard.svg
File was created 1 <svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
src/icons/svg/example.svg
File was created 1 <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>
src/icons/svg/eye-open.svg
File was created 1 <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
src/icons/svg/eye.svg
File was created 1 <svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
src/icons/svg/form.svg
File was created 1 <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>
src/icons/svg/link.svg
File was created 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>
src/icons/svg/nested.svg
File was created 1 <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>
src/icons/svg/password.svg
File was created 1 <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>
src/icons/svg/table.svg
File was created 1 <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>
src/icons/svg/tree.svg
File was created 1 <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
src/icons/svg/user.svg
File was created 1 <svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
src/icons/svgo.yml
File was created 1 # replace default config
2
3 # multipass: true
4 # full: true
5
6 plugins:
7
8 # - name
9 #
10 # or:
11 # - name: false
12 # - name: true
13 #
14 # or:
15 # - name:
16 # param1: 1
17 # param2: 2
18
19 - removeAttrs:
20 attrs:
21 - 'fill'
22 - 'fill-rule'
23
src/layout/components/AppMain.vue
File was created 1 <template>
2 <section class="app-main">
3 <transition name="fade-transform" mode="out-in">
4 <router-view :key="key" />
5 </transition>
6 </section>
7 </template>
8
9 <script>
10 export default {
11 name: 'AppMain',
12 computed: {
13 key() {
14 return this.$route.path
15 }
16 }
17 }
18 </script>
19
20 <style scoped>
21 .app-main {
22 /*50 = navbar */
23 min-height: calc(100vh - 50px);
24 width: 100%;
25 position: relative;
26 overflow: hidden;
27 }
28 .fixed-header+.app-main {
29 padding-top: 50px;
30 }
31 </style>
32
33 <style lang="scss">
34 // fix css style bug in open el-dialog
35 .el-popup-parent--hidden {
36 .fixed-header {
37 padding-right: 15px;
38 }
39 }
40 </style>
41
src/layout/components/Navbar.vue
File was created 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 @click.native="logout">
26 <span style="display:block;">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>
140
src/layout/components/Sidebar/FixiOSBug.js
File was created 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 }
27
src/layout/components/Sidebar/Item.vue
File was created 1 <script>
2 export default {
3 name: 'MenuItem',
4 functional: true,
5 props: {
6 icon: {
7 type: String,
8 default: ''
9 },
10 title: {
11 type: String,
12 default: ''
13 }
14 },
15 render(h, context) {
16 const { icon, title } = context.props
17 const vnodes = []
18
19 if (icon) {
20 vnodes.push(<svg-icon icon-class={icon}/>)
21 }
22
23 if (title) {
24 vnodes.push(<span slot='title'>{(title)}</span>)
25 }
26 return vnodes
27 }
28 }
29 </script>
30
src/layout/components/Sidebar/Link.vue
File was created 1
2 <template>
3 <!-- eslint-disable vue/require-component-is -->
4 <component v-bind="linkProps(to)">
5 <slot />
6 </component>
7 </template>
8
9 <script>
10 import { isExternal } from '@/utils/validate'
11
12 export default {
13 props: {
14 to: {
15 type: String,
16 required: true
17 }
18 },
19 methods: {
20 linkProps(url) {
21 if (isExternal(url)) {
22 return {
23 is: 'a',
24 href: url,
25 target: '_blank',
26 rel: 'noopener'
27 }
28 }
29 return {
30 is: 'router-link',
31 to: url
32 }
33 }
34 }
35 }
36 </script>
37
src/layout/components/Sidebar/Logo.vue
File was created 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>
83
src/layout/components/Sidebar/SidebarItem.vue
File was created 1 <template>
2 <div v-if="!item.hidden">
3 <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
4 <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
5 <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
6 <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
7 </el-menu-item>
8 </app-link>
9 </template>
10
11 <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
12 <template slot="title">
13 <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
14 </template>
15 <sidebar-item
16 v-for="child in item.children"
17 :key="child.path"
18 :is-nest="true"
19 :item="child"
20 :base-path="resolvePath(child.path)"
21 class="nest-menu"
22 />
23 </el-submenu>
24 </div>
25 </template>
26
27 <script>
28 import path from 'path'
29 import { isExternal } from '@/utils/validate'
30 import Item from './Item'
31 import AppLink from './Link'
32 import FixiOSBug from './FixiOSBug'
33
34 export default {
35 name: 'SidebarItem',
36 components: { Item, AppLink },
37 mixins: [FixiOSBug],
38 props: {
39 // route object
40 item: {
41 type: Object,
42 required: true
43 },
44 isNest: {
45 type: Boolean,
46 default: false
47 },
48 basePath: {
49 type: String,
50 default: ''
51 }
52 },
53 data() {
54 // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
55 // TODO: refactor with render function
56 this.onlyOneChild = null
57 return {}
58 },
59 methods: {
60 hasOneShowingChild(children = [], parent) {
61 const showingChildren = children.filter(item => {
62 if (item.hidden) {
63 return false
64 } else {
65 // Temp set(will be used if only has one showing child)
66 this.onlyOneChild = item
67 return true
68 }
69 })
70
71 // When there is only one child router, the child router is displayed by default
72 if (showingChildren.length === 1) {
73 return true
74 }
75
76 // Show parent if there are no child router to display
77 if (showingChildren.length === 0) {
78 this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
79 return true
80 }
81
82 return false
83 },
84 resolvePath(routePath) {
85 if (isExternal(routePath)) {
86 return routePath
87 }
88 if (isExternal(this.basePath)) {
89 return this.basePath
90 }
91 return path.resolve(this.basePath, routePath)
92 }
93 }
94 }
95 </script>
96
src/layout/components/Sidebar/index.vue
File was created 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 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 'sidebar'
32 ]),
33 routes() {
34 return this.$router.options.routes
35 },
36 activeMenu() {
37 const route = this.$route
38 const { meta, path } = route
39 // if set path, the sidebar will highlight the path you set
40 if (meta.activeMenu) {
41 return meta.activeMenu
42 }
43 return path
44 },
45 showLogo() {
46 return this.$store.state.settings.sidebarLogo
47 },
48 variables() {
49 return variables
50 },
51 isCollapse() {
52 return !this.sidebar.opened
53 }
54 }
55 }
56 </script>
57
src/layout/components/index.js
File was created 1 export { default as Navbar } from './Navbar'
2 export { default as Sidebar } from './Sidebar'
3 export { default as AppMain } from './AppMain'
4
src/layout/index.vue
File was created 1 <template>
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" />
5 <div class="main-container">
6 <div :class="{'fixed-header':fixedHeader}">
7 <navbar />
8 </div>
9 <app-main />
10 </div>
11 </div>
12 </template>
13
14 <script>
15 import { Navbar, Sidebar, AppMain } from './components'
16 import ResizeMixin from './mixin/ResizeHandler'
17
18 export default {
19 name: 'Layout',
20 components: {
21 Navbar,
22 Sidebar,
23 AppMain
24 },
25 mixins: [ResizeMixin],
26 computed: {
27 sidebar() {
28 return this.$store.state.app.sidebar
29 },
30 device() {
31 return this.$store.state.app.device
32 },
33 fixedHeader() {
34 return this.$store.state.settings.fixedHeader
35 },
36 classObj() {
37 return {
38 hideSidebar: !this.sidebar.opened,
39 openSidebar: this.sidebar.opened,
40 withoutAnimation: this.sidebar.withoutAnimation,
41 mobile: this.device === 'mobile'
42 }
43 }
44 },
45 methods: {
46 handleClickOutside() {
47 this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
48 }
49 }
50 }
51 </script>
52
53 <style lang="scss" scoped>
54 @import "~@/styles/mixin.scss";
55 @import "~@/styles/variables.scss";
56
57 .app-wrapper {
58 @include clearfix;
59 position: relative;
60 height: 100%;
61 width: 100%;
62 &.mobile.openSidebar{
63 position: fixed;
64 top: 0;
65 }
66 }
67 .drawer-bg {
68 background: #000;
69 opacity: 0.3;
70 width: 100%;
71 top: 0;
72 height: 100%;
73 position: absolute;
74 z-index: 999;
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 }
93 </style>
94
src/layout/mixin/ResizeHandler.js
File was created 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 }
46
File was created 1 import Vue from 'vue'
2
3 import 'normalize.css/normalize.css' // A modern alternative to CSS resets
4
5 import ElementUI from 'element-ui'
6 import 'element-ui/lib/theme-chalk/index.css'
7 import locale from 'element-ui/lib/locale/lang/en' // lang i18n
8
9 import '@/styles/index.scss' // global css
10
11 import App from './App'
12 import store from './store'
13 import router from './router'
14
15 import '@/icons' // icon
16 import '@/permission' // permission control
17
18 /**
19 * If you don't want to use mock-server
20 * you want to use MockJs for mock api
21 * you can execute: mockXHR()
22 *
23 * Currently MockJs will be used in the production environment,
24 * please remove it before going online ! ! !
25 */
26 if (process.env.NODE_ENV === 'production') {
27 const { mockXHR } = require('../mock')
28 mockXHR()
29 }
30
31 // set ElementUI lang to EN
32 Vue.use(ElementUI, { locale })
33 // 如果想要中文版 element-ui,按如下方式声明
34 // Vue.use(ElementUI)
35
36 Vue.config.productionTip = false
37
38 new Vue({
39 el: '#app',
40 router,
41 store,
42 render: h => h(App)
43 })
44
src/permission.js
File was created 1 import router from './router'
2 import store from './store'
3 import { Message } from 'element-ui'
4 import NProgress from 'nprogress' // progress bar
5 import 'nprogress/nprogress.css' // progress bar style
6 import { getToken } from '@/utils/auth' // get token from cookie
7 import getPageTitle from '@/utils/get-page-title'
8
9 NProgress.configure({ showSpinner: false }) // NProgress Configuration
10
11 const whiteList = ['/login'] // no redirect whitelist
12
13 router.beforeEach(async(to, from, next) => {
14 // start progress bar
15 NProgress.start()
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) {
24 if (to.path === '/login') {
25 // if is logged in, redirect to the home page
26 next({ path: '/' })
27 NProgress.done()
28 } else {
29 const hasGetUserInfo = store.getters.name
30 if (hasGetUserInfo) {
31 next()
32 } else {
33 try {
34 // get user info
35 await store.dispatch('user/getInfo')
36
37 next()
38 } catch (error) {
39 // remove token and go to login page to re-login
40 await store.dispatch('user/resetToken')
41 Message.error(error || 'Has Error')
42 next(`/login?redirect=${to.path}`)
43 NProgress.done()
44 }
45 }
46 }
47 } else {
48 /* has no token*/
49
50 if (whiteList.indexOf(to.path) !== -1) {
51 // in the free login whitelist, go directly
52 next()
53 } else {
54 // other pages that do not have permission to access are redirected to the login page.
55 next(`/login?redirect=${to.path}`)
56 NProgress.done()
57 }
58 }
59 })
60
61 router.afterEach(() => {
62 // finish progress bar
63 NProgress.done()
64 })
65
src/router/index.js
File was created 1 import Vue from 'vue'
2 import Router from 'vue-router'
3
4 Vue.use(Router)
5
6 /* Layout */
7 import Layout from '@/layout'
8
9 /**
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)
22 icon: 'svg-name' the icon show in the sidebar
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
25 }
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 },
45
46 {
47 path: '/',
48 component: Layout,
49 redirect: '/dashboard',
50 children: [{
51 path: 'dashboard',
52 name: 'Dashboard',
53 component: () => import('@/views/dashboard/index'),
54 meta: { title: 'Dashboard', icon: 'dashboard' }
55 }]
56 },
57
58 {
59 path: '/example',
60 component: Layout,
61 redirect: '/example/table',
62 name: 'Example',
63 meta: { title: 'Example', icon: 'example' },
64 children: [
65 {
66 path: 'table',
67 name: 'Table',
68 component: () => import('@/views/table/index'),
69 meta: { title: 'Table', icon: 'table' }
70 },
71 {
72 path: 'tree',
73 name: 'Tree',
74 component: () => import('@/views/tree/index'),
75 meta: { title: 'Tree', icon: 'tree' }
76 }
77 ]
78 },
79
80 {
81 path: '/form',
82 component: Layout,
83 children: [
84 {
85 path: 'index',
86 name: 'Form',
87 component: () => import('@/views/form/index'),
88 meta: { title: 'Form', icon: 'form' }
89 }
90 ]
91 },
92
93 {
94 path: '/nested',
95 component: Layout,
96 redirect: '/nested/menu1',
97 name: 'Nested',
98 meta: {
99 title: 'Nested',
100 icon: 'nested'
101 },
102 children: [
103 {
104 path: 'menu1',
105 component: () => import('@/views/nested/menu1/index'), // Parent router-view
106 name: 'Menu1',
107 meta: { title: 'Menu1' },
108 children: [
109 {
110 path: 'menu1-1',
111 component: () => import('@/views/nested/menu1/menu1-1'),
112 name: 'Menu1-1',
113 meta: { title: 'Menu1-1' }
114 },
115 {
116 path: 'menu1-2',
117 component: () => import('@/views/nested/menu1/menu1-2'),
118 name: 'Menu1-2',
119 meta: { title: 'Menu1-2' },
120 children: [
121 {
122 path: 'menu1-2-1',
123 component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
124 name: 'Menu1-2-1',
125 meta: { title: 'Menu1-2-1' }
126 },
127 {
128 path: 'menu1-2-2',
129 component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
130 name: 'Menu1-2-2',
131 meta: { title: 'Menu1-2-2' }
132 }
133 ]
134 },
135 {
136 path: 'menu1-3',
137 component: () => import('@/views/nested/menu1/menu1-3'),
138 name: 'Menu1-3',
139 meta: { title: 'Menu1-3' }
140 }
141 ]
142 },
143 {
144 path: 'menu2',
145 component: () => import('@/views/nested/menu2/index'),
146 meta: { title: 'menu2' }
147 }
148 ]
149 },
150
151 {
152 path: 'external-link',
153 component: Layout,
154 children: [
155 {
156 path: 'https://panjiachen.github.io/vue-element-admin-site/#/',
157 meta: { title: 'External Link', icon: 'link' }
158 }
159 ]
160 },
161
162 // 404 page must be placed at the end !!!
163 { path: '*', redirect: '/404', hidden: true }
164 ]
165
166 const createRouter = () => new Router({
167 // mode: 'history', // require service support
168 scrollBehavior: () => ({ y: 0 }),
169 routes: constantRoutes
170 })
171
172 const router = createRouter()
173
174 // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
175 export function resetRouter() {
176 const newRouter = createRouter()
177 router.matcher = newRouter.matcher // reset router
178 }
179
180 export default router
181
File was created 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 }
17
src/store/getters.js
File was created 1 const getters = {
2 sidebar: state => state.app.sidebar,
3 device: state => state.app.device,
4 token: state => state.user.token,
5 avatar: state => state.user.avatar,
6 name: state => state.user.name
7 }
8 export default getters
9
src/store/index.js
File was created 1 import Vue from 'vue'
2 import Vuex from 'vuex'
3 import getters from './getters'
4 import app from './modules/app'
5 import settings from './modules/settings'
6 import user from './modules/user'
7
8 Vue.use(Vuex)
9
10 const store = new Vuex.Store({
11 modules: {
12 app,
13 settings,
14 user
15 },
16 getters
17 })
18
19 export default store
20
src/store/modules/app.js
File was created 1 import Cookies from 'js-cookie'
2
3 const state = {
4 sidebar: {
5 opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
6 withoutAnimation: false
7 },
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) {
16 Cookies.set('sidebarStatus', 1)
17 } else {
18 Cookies.set('sidebarStatus', 0)
19 }
20 },
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 }
41 }
42
43 export default {
44 namespaced: true,
45 state,
46 mutations,
47 actions
48 }
49
src/store/modules/settings.js
File was created 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
32
src/store/modules/user.js
File was created 1 import { login, logout, getInfo } from '@/api/user'
2 import { getToken, setToken, removeToken } from '@/utils/auth'
3 import { resetRouter } from '@/router'
4
5 const getDefaultState = () => {
6 return {
7 token: getToken(),
8 name: '',
9 avatar: ''
10 }
11 }
12
13 const state = getDefaultState()
14
15 const mutations = {
16 RESET_STATE: (state) => {
17 Object.assign(state, getDefaultState())
18 },
19 SET_TOKEN: (state, token) => {
20 state.token = token
21 },
22 SET_NAME: (state, name) => {
23 state.name = name
24 },
25 SET_AVATAR: (state, avatar) => {
26 state.avatar = avatar
27 }
28 }
29
30 const actions = {
31 // user login
32 login({ commit }, userInfo) {
33 const { username, password } = userInfo
34 return new Promise((resolve, reject) => {
35 login({ username: username.trim(), password: password }).then(response => {
36 const { data } = response
37 commit('SET_TOKEN', data.token)
38 setToken(data.token)
39 resolve()
40 }).catch(error => {
41 reject(error)
42 })
43 })
44 },
45
46 // get user info
47 getInfo({ commit, state }) {
48 return new Promise((resolve, reject) => {
49 getInfo(state.token).then(response => {
50 const { data } = response
51
52 if (!data) {
53 reject('Verification failed, please Login again.')
54 }
55
56 const { name, avatar } = data
57
58 commit('SET_NAME', name)
59 commit('SET_AVATAR', avatar)
60 resolve(data)
61 }).catch(error => {
62 reject(error)
63 })
64 })
65 },
66
67 // user logout
68 logout({ commit, state }) {
69 return new Promise((resolve, reject) => {
70 logout(state.token).then(() => {
71 removeToken() // must remove token first
72 resetRouter()
73 commit('RESET_STATE')
74 resolve()
75 }).catch(error => {
76 reject(error)
77 })
78 })
79 },
80
81 // remove token
82 resetToken({ commit }) {
83 return new Promise(resolve => {
84 removeToken() // must remove token first
85 commit('RESET_STATE')
86 resolve()
87 })
88 }
89 }
90
91 export default {
92 namespaced: true,
93 state,
94 mutations,
95 actions
96 }
97
98
src/styles/element-ui.scss
File was created 1 // cover some element-ui styles
2
3 .el-breadcrumb__inner,
4 .el-breadcrumb__inner a {
5 font-weight: 400 !important;
6 }
7
8 .el-upload {
9 input[type="file"] {
10 display: none !important;
11 }
12 }
13
14 .el-upload__input {
15 display: none;
16 }
17
18
19 // to fixed https://github.com/ElemeFE/element/issues/2461
20 .el-dialog {
21 transform: none;
22 left: 0;
23 position: relative;
24 margin: 0 auto;
25 }
26
27 // refine element ui upload
28 .upload-container {
29 .el-upload {
30 width: 100%;
31
32 .el-upload-dragger {
33 width: 100%;
34 height: 200px;
35 }
36 }
37 }
38
39 // dropdown
40 .el-dropdown-menu {
41 a {
42 display: block
43 }
44 }
45
46 // to fix el-date-picker css style
47 .el-range-separator {
48 box-sizing: content-box;
49 }
50
src/styles/index.scss
File was created 1 @import './variables.scss';
2 @import './mixin.scss';
3 @import './transition.scss';
4 @import './element-ui.scss';
5 @import './sidebar.scss';
6
7 body {
8 height: 100%;
9 -moz-osx-font-smoothing: grayscale;
10 -webkit-font-smoothing: antialiased;
11 text-rendering: optimizeLegibility;
12 font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
13 }
14
15 label {
16 font-weight: 700;
17 }
18
19 html {
20 height: 100%;
21 box-sizing: border-box;
22 }
23
24 #app {
25 height: 100%;
26 }
27
28 *,
29 *:before,
30 *:after {
31 box-sizing: inherit;
32 }
33
34 a:focus,
35 a:active {
36 outline: none;
37 }
38
39 a,
40 a:focus,
41 a:hover {
42 cursor: pointer;
43 color: inherit;
44 text-decoration: none;
45 }
46
47 div:focus {
48 outline: none;
49 }
50
51 .clearfix {
52 &:after {
53 visibility: hidden;
54 display: block;
55 font-size: 0;
56 content: " ";
57 clear: both;
58 height: 0;
59 }
60 }
61
62 // main-container global css
63 .app-container {
64 padding: 20px;
65 }
66
src/styles/mixin.scss
File was created 1 @mixin clearfix {
2 &:after {
3 content: "";
4 display: table;
5 clear: both;
6 }
7 }
8
9 @mixin scrollBar {
10 &::-webkit-scrollbar-track-piece {
11 background: #d3dce6;
12 }
13
14 &::-webkit-scrollbar {
15 width: 6px;
16 }
17
18 &::-webkit-scrollbar-thumb {
19 background: #99a9bf;
20 border-radius: 20px;
21 }
22 }
23
24 @mixin relative {
25 position: relative;
26 width: 100%;
27 height: 100%;
28 }
29
src/styles/sidebar.scss
File was created 1 #app {
2
3 .main-container {
4 min-height: 100%;
5 transition: margin-left .28s;
6 margin-left: $sideBarWidth;
7 position: relative;
8 }
9
10 .sidebar-container {
11 transition: width 0.28s;
12 width: $sideBarWidth !important;
13 background-color: $menuBg;
14 height: 100%;
15 position: fixed;
16 font-size: 0px;
17 top: 0;
18 bottom: 0;
19 left: 0;
20 z-index: 1001;
21 overflow: hidden;
22
23 // reset element-ui css
24 .horizontal-collapse-transition {
25 transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 }
27
28 .scrollbar-wrapper {
29 overflow-x: hidden !important;
30 }
31
32 .el-scrollbar__bar.is-vertical {
33 right: 0px;
34 }
35
36 .el-scrollbar {
37 height: 100%;
38 }
39
40 &.has-logo {
41 .el-scrollbar {
42 height: calc(100% - 50px);
43 }
44 }
45
46 .is-horizontal {
47 display: none;
48 }
49
50 a {
51 display: inline-block;
52 width: 100%;
53 overflow: hidden;
54 }
55
56 .svg-icon {
57 margin-right: 16px;
58 }
59
60 .el-menu {
61 border: none;
62 height: 100%;
63 width: 100% !important;
64 }
65
66 // menu hover
67 .submenu-title-noDropdown,
68 .el-submenu__title {
69 &:hover {
70 background-color: $menuHover !important;
71 }
72 }
73
74 .is-active>.el-submenu__title {
75 color: $subMenuActiveText !important;
76 }
77
78 & .nest-menu .el-submenu>.el-submenu__title,
79 & .el-submenu .el-menu-item {
80 min-width: $sideBarWidth !important;
81 background-color: $subMenuBg !important;
82
83 &:hover {
84 background-color: $subMenuHover !important;
85 }
86 }
87 }
88
89 .hideSidebar {
90 .sidebar-container {
91 width: 54px !important;
92 }
93
94 .main-container {
95 margin-left: 54px;
96 }
97
98 .submenu-title-noDropdown {
99 padding: 0 !important;
100 position: relative;
101
102 .el-tooltip {
103 padding: 0 !important;
104
105 .svg-icon {
106 margin-left: 20px;
107 }
108 }
109 }
110
111 .el-submenu {
112 overflow: hidden;
113
114 &>.el-submenu__title {
115 padding: 0 !important;
116
117 .svg-icon {
118 margin-left: 20px;
119 }
120
121 .el-submenu__icon-arrow {
122 display: none;
123 }
124 }
125 }
126
127 .el-menu--collapse {
128 .el-submenu {
129 &>.el-submenu__title {
130 &>span {
131 height: 0;
132 width: 0;
133 overflow: hidden;
134 visibility: hidden;
135 display: inline-block;
136 }
137 }
138 }
139 }
140 }
141
142 .el-menu--collapse .el-menu .el-submenu {
143 min-width: $sideBarWidth !important;
144 }
145
146 // mobile responsive
147 .mobile {
148 .main-container {
149 margin-left: 0px;
150 }
151
152 .sidebar-container {
153 transition: transform .28s;
154 width: $sideBarWidth !important;
155 }
156
157 &.hideSidebar {
158 .sidebar-container {
159 pointer-events: none;
160 transition-duration: 0.3s;
161 transform: translate3d(-$sideBarWidth, 0, 0);
162 }
163 }
164 }
165
166 .withoutAnimation {
167
168 .main-container,
169 .sidebar-container {
170 transition: none;
171 }
172 }
173 }
174
175 // when menu collapsed
176 .el-menu--vertical {
177 &>.el-menu {
178 .svg-icon {
179 margin-right: 16px;
180 }
181 }
182
183 .nest-menu .el-submenu>.el-submenu__title,
184 .el-menu-item {
185 &:hover {
186 // you can use $subMenuHover
187 background-color: $menuHover !important;
188 }
189 }
190
191 // the scroll bar appears when the subMenu is too long
192 >.el-menu--popup {
193 max-height: 100vh;
194 overflow-y: auto;
195
196 &::-webkit-scrollbar-track-piece {
197 background: #d3dce6;
198 }
199
200 &::-webkit-scrollbar {
201 width: 6px;
202 }
203
204 &::-webkit-scrollbar-thumb {
205 background: #99a9bf;
206 border-radius: 20px;
207 }
208 }
209 }
210
src/styles/transition.scss
File was created 1 // global transition css
2
3 /* fade */
4 .fade-enter-active,
5 .fade-leave-active {
6 transition: opacity 0.28s;
7 }
8
9 .fade-enter,
10 .fade-leave-active {
11 opacity: 0;
12 }
13
14 /* fade-transform */
15 .fade-transform-leave-active,
16 .fade-transform-enter-active {
17 transition: all .5s;
18 }
19
20 .fade-transform-enter {
21 opacity: 0;
22 transform: translateX(-30px);
23 }
24
25 .fade-transform-leave-to {
26 opacity: 0;
27 transform: translateX(30px);
28 }
29
30 /* breadcrumb transition */
31 .breadcrumb-enter-active,
32 .breadcrumb-leave-active {
33 transition: all .5s;
34 }
35
36 .breadcrumb-enter,
37 .breadcrumb-leave-active {
38 opacity: 0;
39 transform: translateX(20px);
40 }
41
42 .breadcrumb-move {
43 transition: all .5s;
44 }
45
46 .breadcrumb-leave-active {
47 position: absolute;
48 }
49
src/styles/variables.scss
File was created 1 // sidebar
2 $menuText:#bfcbd9;
3 $menuActiveText:#409EFF;
4 $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
5
6 $menuBg:#304156;
7 $menuHover:#263445;
8
9 $subMenuBg:#1f2d3d;
10 $subMenuHover:#001528;
11
12 $sideBarWidth: 210px;
13
14 // the :export directive is the magic sauce for webpack
15 // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 :export {
17 menuText: $menuText;
18 menuActiveText: $menuActiveText;
19 subMenuActiveText: $subMenuActiveText;
20 menuBg: $menuBg;
21 menuHover: $menuHover;
22 subMenuBg: $subMenuBg;
23 subMenuHover: $subMenuHover;
24 sideBarWidth: $sideBarWidth;
25 }
26
src/utils/auth.js
File was created 1 import Cookies from 'js-cookie'
2
3 const TokenKey = 'vue_admin_template_token'
4
5 export function getToken() {
6 return Cookies.get(TokenKey)
7 }
8
9 export function setToken(token) {
10 return Cookies.set(TokenKey, token)
11 }
12
13 export function removeToken() {
14 return Cookies.remove(TokenKey)
15 }
16
src/utils/get-page-title.js
File was created 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 }
11
src/utils/index.js
File was created 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 | null}
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(/{([ymdhisa])+}/g, (result, key) => {
38 const value = formatObj[key]
39 // Note: getDay() returns 0 on Sunday
40 if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
41 return value.toString().padStart(2, '0')
42 })
43 return time_str
44 }
45
46 /**
47 * @param {number} time
48 * @param {string} option
49 * @returns {string}
50 */
51 export function formatTime(time, option) {
52 if (('' + time).length === 10) {
53 time = parseInt(time) * 1000
54 } else {
55 time = +time
56 }
57 const d = new Date(time)
58 const now = Date.now()
59
60 const diff = (now - d) / 1000
61
62 if (diff < 30) {
63 return '刚刚'
64 } else if (diff < 3600) {
65 // less 1 hour
66 return Math.ceil(diff / 60) + '分钟前'
67 } else if (diff < 3600 * 24) {
68 return Math.ceil(diff / 3600) + '小时前'
69 } else if (diff < 3600 * 24 * 2) {
70 return '1天前'
71 }
72 if (option) {
73 return parseTime(time, option)
74 } else {
75 return (
76 d.getMonth() +
77 1 +
78 '月' +
79 d.getDate() +
80 '日' +
81 d.getHours() +
82 '时' +
83 d.getMinutes() +
84 '分'
85 )
86 }
87 }
88
89 /**
90 * @param {string} url
91 * @returns {Object}
92 */
93 export function param2Obj(url) {
94 const search = url.split('?')[1]
95 if (!search) {
96 return {}
97 }
98 return JSON.parse(
99 '{"' +
100 decodeURIComponent(search)
101 .replace(/"/g, '\\"')
102 .replace(/&/g, '","')
103 .replace(/=/g, '":"')
104 .replace(/\+/g, ' ') +
105 '"}'
106 )
107 }
108
src/utils/request.js
File was created 1 import axios from 'axios'
2 import { MessageBox, Message } from 'element-ui'
3 import store from '@/store'
4 import { getToken } from '@/utils/auth'
5
6 // create an axios instance
7 const service = axios.create({
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
11 })
12
13 // request interceptor
14 service.interceptors.request.use(
15 config => {
16 // do something before request is sent
17
18 if (store.getters.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()
23 }
24 return config
25 },
26 error => {
27 // do something with request error
28 console.log(error) // for debug
29 return Promise.reject(error)
30 }
31 )
32
33 // response interceptor
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 */
45 response => {
46 const res = response.data
47
48 // if the custom code is not 20000, it is judged as an error.
49 if (res.code !== 20000) {
50 Message({
51 message: res.message || 'Error',
52 type: 'error',
53 duration: 5 * 1000
54 })
55
56 // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
57 if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
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()
66 })
67 })
68 }
69 return Promise.reject(new Error(res.message || 'Error'))
70 } else {
71 return res
72 }
73 },
74 error => {
75 console.log('err' + error) // for debug
76 Message({
77 message: error.message,
78 type: 'error',
79 duration: 5 * 1000
80 })
81 return Promise.reject(error)
82 }
83 )
84
85 export default service
86
src/utils/validate.js
File was created 1 /**
2 * Created by PanJiaChen on 16/11/18.
3 */
4
5 /**
6 * @param {string} path
7 * @returns {Boolean}
8 */
9 export function isExternal(path) {
10 return /^(https?:|mailto:|tel:)/.test(path)
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 }
21
src/views/404.vue
File was created 1 <template>
2 <div class="wscn-http404-container">
3 <div class="wscn-http404">
4 <div class="pic-404">
5 <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
6 <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
7 <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
8 <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
9 </div>
10 <div class="bullshit">
11 <div class="bullshit__oops">OOPS!</div>
12 <div class="bullshit__info">All rights reserved
13 <a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
14 </div>
15 <div class="bullshit__headline">{{ message }}</div>
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 </div>
19 </div>
20 </div>
21 </template>
22
23 <script>
24
25 export default {
26 name: 'Page404',
27 computed: {
28 message() {
29 return 'The webmaster said that you can not enter this page...'
30 }
31 }
32 }
33 </script>
34
35 <style lang="scss" scoped>
36 .wscn-http404-container{
37 transform: translate(-50%,-50%);
38 position: absolute;
39 top: 40%;
40 left: 50%;
41 }
42 .wscn-http404 {
43 position: relative;
44 width: 1200px;
45 padding: 0 50px;
46 overflow: hidden;
47 .pic-404 {
48 position: relative;
49 float: left;
50 width: 600px;
51 overflow: hidden;
52 &__parent {
53 width: 100%;
54 }
55 &__child {
56 position: absolute;
57 &.left {
58 width: 80px;
59 top: 17px;
60 left: 220px;
61 opacity: 0;
62 animation-name: cloudLeft;
63 animation-duration: 2s;
64 animation-timing-function: linear;
65 animation-fill-mode: forwards;
66 animation-delay: 1s;
67 }
68 &.mid {
69 width: 46px;
70 top: 10px;
71 left: 420px;
72 opacity: 0;
73 animation-name: cloudMid;
74 animation-duration: 2s;
75 animation-timing-function: linear;
76 animation-fill-mode: forwards;
77 animation-delay: 1.2s;
78 }
79 &.right {
80 width: 62px;
81 top: 100px;
82 left: 500px;
83 opacity: 0;
84 animation-name: cloudRight;
85 animation-duration: 2s;
86 animation-timing-function: linear;
87 animation-fill-mode: forwards;
88 animation-delay: 1s;
89 }
90 @keyframes cloudLeft {
91 0% {
92 top: 17px;
93 left: 220px;
94 opacity: 0;
95 }
96 20% {
97 top: 33px;
98 left: 188px;
99 opacity: 1;
100 }
101 80% {
102 top: 81px;
103 left: 92px;
104 opacity: 1;
105 }
106 100% {
107 top: 97px;
108 left: 60px;
109 opacity: 0;
110 }
111 }
112 @keyframes cloudMid {
113 0% {
114 top: 10px;
115 left: 420px;
116 opacity: 0;
117 }
118 20% {
119 top: 40px;
120 left: 360px;
121 opacity: 1;
122 }
123 70% {
124 top: 130px;
125 left: 180px;
126 opacity: 1;
127 }
128 100% {
129 top: 160px;
130 left: 120px;
131 opacity: 0;
132 }
133 }
134 @keyframes cloudRight {
135 0% {
136 top: 100px;
137 left: 500px;
138 opacity: 0;
139 }
140 20% {
141 top: 120px;
142 left: 460px;
143 opacity: 1;
144 }
145 80% {
146 top: 180px;
147 left: 340px;
148 opacity: 1;
149 }
150 100% {
151 top: 200px;
152 left: 300px;
153 opacity: 0;
154 }
155 }
156 }
157 }
158 .bullshit {
159 position: relative;
160 float: left;
161 width: 300px;
162 padding: 30px 0;
163 overflow: hidden;
164 &__oops {
165 font-size: 32px;
166 font-weight: bold;
167 line-height: 40px;
168 color: #1482f0;
169 opacity: 0;
170 margin-bottom: 20px;
171 animation-name: slideUp;
172 animation-duration: 0.5s;
173 animation-fill-mode: forwards;
174 }
175 &__headline {
176 font-size: 20px;
177 line-height: 24px;
178 color: #222;
179 font-weight: bold;
180 opacity: 0;
181 margin-bottom: 10px;
182 animation-name: slideUp;
183 animation-duration: 0.5s;
184 animation-delay: 0.1s;
185 animation-fill-mode: forwards;
186 }
187 &__info {
188 font-size: 13px;
189 line-height: 21px;
190 color: grey;
191 opacity: 0;
192 margin-bottom: 30px;
193 animation-name: slideUp;
194 animation-duration: 0.5s;
195 animation-delay: 0.2s;
196 animation-fill-mode: forwards;
197 }
198 &__return-home {
199 display: block;
200 float: left;
201 width: 110px;
202 height: 36px;
203 background: #1482f0;
204 border-radius: 100px;
205 text-align: center;
206 color: #ffffff;
207 opacity: 0;
208 font-size: 14px;
209 line-height: 36px;
210 cursor: pointer;
211 animation-name: slideUp;
212 animation-duration: 0.5s;
213 animation-delay: 0.3s;
214 animation-fill-mode: forwards;
215 }
216 @keyframes slideUp {
217 0% {
218 transform: translateY(60px);
219 opacity: 0;
220 }
221 100% {
222 transform: translateY(0);
223 opacity: 1;
224 }
225 }
226 }
227 }
228 </style>
229
src/views/dashboard/index.vue
File was created 1 <template>
2 <div class="dashboard-container">
3 <div class="dashboard-text">name: {{ name }}</div>
4 </div>
5 </template>
6
7 <script>
8 import { mapGetters } from 'vuex'
9
10 export default {
11 name: 'Dashboard',
12 computed: {
13 ...mapGetters([
14 'name'
15 ])
16 }
17 }
18 </script>
19
20 <style lang="scss" scoped>
21 .dashboard {
22 &-container {
23 margin: 30px;
24 }
25 &-text {
26 font-size: 30px;
27 line-height: 46px;
28 }
29 }
30 </style>
31
src/views/form/index.vue
File was created 1 <template>
2 <div class="app-container">
3 <el-form ref="form" :model="form" label-width="120px">
4 <el-form-item label="Activity name">
5 <el-input v-model="form.name" />
6 </el-form-item>
7 <el-form-item label="Activity zone">
8 <el-select v-model="form.region" placeholder="please select your zone">
9 <el-option label="Zone one" value="shanghai" />
10 <el-option label="Zone two" value="beijing" />
11 </el-select>
12 </el-form-item>
13 <el-form-item label="Activity time">
14 <el-col :span="11">
15 <el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;" />
16 </el-col>
17 <el-col :span="2" class="line">-</el-col>
18 <el-col :span="11">
19 <el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;" />
20 </el-col>
21 </el-form-item>
22 <el-form-item label="Instant delivery">
23 <el-switch v-model="form.delivery" />
24 </el-form-item>
25 <el-form-item label="Activity type">
26 <el-checkbox-group v-model="form.type">
27 <el-checkbox label="Online activities" name="type" />
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 </el-checkbox-group>
32 </el-form-item>
33 <el-form-item label="Resources">
34 <el-radio-group v-model="form.resource">
35 <el-radio label="Sponsor" />
36 <el-radio label="Venue" />
37 </el-radio-group>
38 </el-form-item>
39 <el-form-item label="Activity form">
40 <el-input v-model="form.desc" type="textarea" />
41 </el-form-item>
42 <el-form-item>
43 <el-button type="primary" @click="onSubmit">Create</el-button>
44 <el-button @click="onCancel">Cancel</el-button>
45 </el-form-item>
46 </el-form>
47 </div>
48 </template>
49
50 <script>
51 export default {
52 data() {
53 return {
54 form: {
55 name: '',
56 region: '',
57 date1: '',
58 date2: '',
59 delivery: false,
60 type: [],
61 resource: '',
62 desc: ''
63 }
64 }
65 },
66 methods: {
67 onSubmit() {
68 this.$message('submit!')
69 },
70 onCancel() {
71 this.$message({
72 message: 'cancel!',
73 type: 'warning'
74 })
75 }
76 }
77 }
78 </script>
79
80 <style scoped>
81 .line{
82 text-align: center;
83 }
84 </style>
85
86
src/views/login/index.vue
File was created 1 <template>
2 <div class="login-container">
3 <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
4
5 <div class="title-container">
6 <h3 class="title">Login Form</h3>
7 </div>
8
9 <el-form-item prop="username">
10 <span class="svg-container">
11 <svg-icon icon-class="user" />
12 </span>
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 />
22 </el-form-item>
23
24 <el-form-item prop="password">
25 <span class="svg-container">
26 <svg-icon icon-class="password" />
27 </span>
28 <el-input
29 :key="passwordType"
30 ref="password"
31 v-model="loginForm.password"
32 :type="passwordType"
33 placeholder="Password"
34 name="password"
35 tabindex="2"
36 auto-complete="on"
37 @keyup.enter.native="handleLogin"
38 />
39 <span class="show-pwd" @click="showPwd">
40 <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
41 </span>
42 </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
46 <div class="tips">
47 <span style="margin-right:20px;">username: admin</span>
48 <span> password: any</span>
49 </div>
50
51 </el-form>
52 </div>
53 </template>
54
55 <script>
56 import { validUsername } from '@/utils/validate'
57
58 export default {
59 name: 'Login',
60 data() {
61 const validateUsername = (rule, value, callback) => {
62 if (!validUsername(value)) {
63 callback(new Error('Please enter the correct user name'))
64 } else {
65 callback()
66 }
67 }
68 const validatePassword = (rule, value, callback) => {
69 if (value.length < 6) {
70 callback(new Error('The password can not be less than 6 digits'))
71 } else {
72 callback()
73 }
74 }
75 return {
76 loginForm: {
77 username: 'admin',
78 password: '111111'
79 },
80 loginRules: {
81 username: [{ required: true, trigger: 'blur', validator: validateUsername }],
82 password: [{ required: true, trigger: 'blur', validator: validatePassword }]
83 },
84 loading: false,
85 passwordType: 'password',
86 redirect: undefined
87 }
88 },
89 watch: {
90 $route: {
91 handler: function(route) {
92 this.redirect = route.query && route.query.redirect
93 },
94 immediate: true
95 }
96 },
97 methods: {
98 showPwd() {
99 if (this.passwordType === 'password') {
100 this.passwordType = ''
101 } else {
102 this.passwordType = 'password'
103 }
104 this.$nextTick(() => {
105 this.$refs.password.focus()
106 })
107 },
108 handleLogin() {
109 this.$refs.loginForm.validate(valid => {
110 if (valid) {
111 this.loading = true
112 this.$store.dispatch('user/login', this.loginForm).then(() => {
113 this.$router.push({ path: this.redirect || '/' })
114 this.loading = false
115 }).catch(() => {
116 this.loading = false
117 })
118 } else {
119 console.log('error submit!!')
120 return false
121 }
122 })
123 }
124 }
125 }
126 </script>
127
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 }
141
142 /* reset element-ui css */
143 .login-container {
144 .el-input {
145 display: inline-block;
146 height: 47px;
147 width: 85%;
148
149 input {
150 background: transparent;
151 border: 0px;
152 -webkit-appearance: none;
153 border-radius: 0px;
154 padding: 12px 5px 12px 15px;
155 color: $light_gray;
156 height: 47px;
157 caret-color: $cursor;
158
159 &:-webkit-autofill {
160 box-shadow: 0 0 0px 1000px $bg inset !important;
161 -webkit-text-fill-color: $cursor !important;
162 }
163 }
164 }
165
166 .el-form-item {
167 border: 1px solid rgba(255, 255, 255, 0.1);
168 background: rgba(0, 0, 0, 0.1);
169 border-radius: 5px;
170 color: #454545;
171 }
172 }
173 </style>
174
175 <style lang="scss" scoped>
176 $bg:#2d3a4b;
177 $dark_gray:#889aa4;
178 $light_gray:#eee;
179
180 .login-container {
181 min-height: 100%;
182 width: 100%;
183 background-color: $bg;
184 overflow: hidden;
185
186 .login-form {
187 position: relative;
188 width: 520px;
189 max-width: 100%;
190 padding: 160px 35px 0;
191 margin: 0 auto;
192 overflow: hidden;
193 }
194
195 .tips {
196 font-size: 14px;
197 color: #fff;
198 margin-bottom: 10px;
199
200 span {
201 &:first-of-type {
202 margin-right: 16px;
203 }
204 }
205 }
206
207 .svg-container {
208 padding: 6px 5px 6px 15px;
209 color: $dark_gray;
210 vertical-align: middle;
211 width: 30px;
212 display: inline-block;
213 }
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 }
225 }
226
227 .show-pwd {
228 position: absolute;
229 right: 10px;
230 top: 7px;
231 font-size: 16px;
232 color: $dark_gray;
233 cursor: pointer;
234 user-select: none;
235 }
236 }
237 </style>
238
src/views/nested/menu1/index.vue
File was created 1 <template>
2 <div style="padding:30px;">
3 <el-alert :closable="false" title="menu 1">
4 <router-view />
5 </el-alert>
6 </div>
7 </template>
8
src/views/nested/menu1/menu1-1/index.vue
File was created 1 <template>
2 <div style="padding:30px;">
3 <el-alert :closable="false" title="menu 1-1" type="success">
4 <router-view />
5 </el-alert>
6 </div>
7 </template>
8
src/views/nested/menu1/menu1-2/index.vue
File was created 1 <template>
2 <div style="padding:30px;">
3 <el-alert :closable="false" title="menu 1-2" type="success">
4 <router-view />
5 </el-alert>
6 </div>
7 </template>
8
src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
File was created 1 <template functional>
2 <div style="padding:30px;">
3 <el-alert :closable="false" title="menu 1-2-1" type="warning" />
4 </div>
5 </template>
6
src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
File was created 1 <template functional>
2 <div style="padding:30px;">
3 <el-alert :closable="false" title="menu 1-2-2" type="warning" />
4 </div>
5 </template>
6
src/views/nested/menu1/menu1-3/index.vue
File was created 1 <template functional>
2 <div style="padding:30px;">
3 <el-alert :closable="false" title="menu 1-3" type="success" />
4 </div>
5 </template>
6
src/views/nested/menu2/index.vue
File was created 1 <template>
2 <div style="padding:30px;">
3 <el-alert :closable="false" title="menu 2" />
4 </div>
5 </template>
6
src/views/table/index.vue
File was created 1 <template>
2 <div class="app-container">
3 <el-table
4 v-loading="listLoading"
5 :data="list"
6 element-loading-text="Loading"
7 border
8 fit
9 highlight-current-row
10 >
11 <el-table-column align="center" label="ID" width="95">
12 <template slot-scope="scope">
13 {{ scope.$index }}
14 </template>
15 </el-table-column>
16 <el-table-column label="Title">
17 <template slot-scope="scope">
18 {{ scope.row.title }}
19 </template>
20 </el-table-column>
21 <el-table-column label="Author" width="110" align="center">
22 <template slot-scope="scope">
23 <span>{{ scope.row.author }}</span>
24 </template>
25 </el-table-column>
26 <el-table-column label="Pageviews" width="110" align="center">
27 <template slot-scope="scope">
28 {{ scope.row.pageviews }}
29 </template>
30 </el-table-column>
31 <el-table-column class-name="status-col" label="Status" width="110" align="center">
32 <template slot-scope="scope">
33 <el-tag :type="scope.row.status | statusFilter">{{ scope.row.status }}</el-tag>
34 </template>
35 </el-table-column>
36 <el-table-column align="center" prop="created_at" label="Display_time" width="200">
37 <template slot-scope="scope">
38 <i class="el-icon-time" />
39 <span>{{ scope.row.display_time }}</span>
40 </template>
41 </el-table-column>
42 </el-table>
43 </div>
44 </template>
45
46 <script>
47 import { getList } from '@/api/table'
48
49 export default {
50 filters: {
51 statusFilter(status) {
52 const statusMap = {
53 published: 'success',
54 draft: 'gray',
55 deleted: 'danger'
56 }
57 return statusMap[status]
58 }
59 },
60 data() {
61 return {
62 list: null,
63 listLoading: true
64 }
65 },
66 created() {
67 this.fetchData()
68 },
69 methods: {
70 fetchData() {
71 this.listLoading = true
72 getList().then(response => {
73 this.list = response.data.items
74 this.listLoading = false
75 })
76 }
77 }
78 }
79 </script>
80
src/views/tree/index.vue
File was created 1 <template>
2 <div class="app-container">
3 <el-input v-model="filterText" placeholder="Filter keyword" style="margin-bottom:30px;" />
4
5 <el-tree
6 ref="tree2"
7 :data="data2"
8 :props="defaultProps"
9 :filter-node-method="filterNode"
10 class="filter-tree"
11 default-expand-all
12 />
13
14 </div>
15 </template>
16
17 <script>
18 export default {
19
20 data() {
21 return {
22 filterText: '',
23 data2: [{
24 id: 1,
25 label: 'Level one 1',
26 children: [{
27 id: 4,
28 label: 'Level two 1-1',
29 children: [{
30 id: 9,
31 label: 'Level three 1-1-1'
32 }, {
33 id: 10,
34 label: 'Level three 1-1-2'
35 }]
36 }]
37 }, {
38 id: 2,
39 label: 'Level one 2',
40 children: [{
41 id: 5,
42 label: 'Level two 2-1'
43 }, {
44 id: 6,
45 label: 'Level two 2-2'
46 }]
47 }, {
48 id: 3,
49 label: 'Level one 3',
50 children: [{
51 id: 7,
52 label: 'Level two 3-1'
53 }, {
54 id: 8,
55 label: 'Level two 3-2'
56 }]
57 }],
58 defaultProps: {
59 children: 'children',
60 label: 'label'
61 }
62 }
63 },
64 watch: {
65 filterText(val) {
66 this.$refs.tree2.filter(val)
67 }
68 },
69
70 methods: {
71 filterNode(value, data) {
72 if (!value) return true
73 return data.label.indexOf(value) !== -1
74 }
75 }
76 }
77 </script>
78
79
tests/unit/.eslintrc.js
File was created 1 module.exports = {
2 env: {
3 jest: true
4 }
5 }
6
tests/unit/components/Breadcrumb.spec.js
File was created 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 })
99
tests/unit/components/Hamburger.spec.js
File was created 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 })
19
tests/unit/components/SvgIcon.spec.js
File was created 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 })
23
tests/unit/utils/formatTime.spec.js
File was created 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 })
31
tests/unit/utils/parseTime.spec.js
File was created 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 })
29
tests/unit/utils/validate.spec.js
File was created 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 })
18
File was created 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
11 // If your port is set to 80,
12 // use administrator privileges to execute the command line.
13 // For example, Mac: sudo npm run
14 // You can change the port by the following methods:
15 // port = 9528 npm run dev OR npm run dev --port = 9528
16 const port = process.env.port || process.env.npm_config_port || 9528 // dev port
17
18 // All configuration item explanations can be find in https://cli.vuejs.org/config/
19 module.exports = {
20 /**
21 * You will need to set publicPath if you plan to deploy your site under a sub path,
22 * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
23 * then publicPath should be set to "/bar/".
24 * In most cases please use '/' !!!
25 * Detail: https://cli.vuejs.org/config/#publicpath
26 */
27 publicPath: '/',
28 outputDir: 'dist',
29 assetsDir: 'static',
30 lintOnSave: process.env.NODE_ENV === 'development',
31 productionSourceMap: false,
32 devServer: {
33 port: port,
34 open: true,
35 overlay: {
36 warnings: false,
37 errors: true
38 },
39 before: require('./mock/mock-server.js')
40 },
41 configureWebpack: {
42 // provide the app's title in webpack's name field, so that
43 // it can be accessed in index.html to inject the correct title.
44 name: name,
45 resolve: {
46 alias: {
47 '@': resolve('src')
48 }
49 }
50 },
51 chainWebpack(config) {
52 config.plugins.delete('preload') // TODO: need test
53 config.plugins.delete('prefetch') // TODO: need test
54
55 // set svg-sprite-loader
56 config.module
57 .rule('svg')
58 .exclude.add(resolve('src/icons'))
59 .end()
60 config.module
61 .rule('icons')
62 .test(/\.svg$/)
63 .include.add(resolve('src/icons'))
64 .end()
65 .use('svg-sprite-loader')
66 .loader('svg-sprite-loader')
67 .options({
68 symbolId: 'icon-[name]'
69 })
70 .end()
71
72 // set preserveWhitespace
73 config.module
74 .rule('vue')
75 .use('vue-loader')
76 .loader('vue-loader')
77 .tap(options => {
78 options.compilerOptions.preserveWhitespace = true
79 return options
80 })
81 .end()
82
83 config
84 // https://webpack.js.org/configuration/devtool/#development
85 .when(process.env.NODE_ENV === 'development',
86 config => config.devtool('cheap-source-map')
87 )
88
89 config
90 .when(process.env.NODE_ENV !== 'development',
91 config => {
92 config
93 .plugin('ScriptExtHtmlWebpackPlugin')
94 .after('html')
95 .use('script-ext-html-webpack-plugin', [{
96 // `runtime` must same as runtimeChunk name. default is `runtime`
97 inline: /runtime\..*\.js$/
98 }])
99 .end()
100 config
101 .optimization.splitChunks({
102 chunks: 'all',
103 cacheGroups: {
104 libs: {
105 name: 'chunk-libs',
106 test: /[\\/]node_modules[\\/]/,
107 priority: 10,
108 chunks: 'initial' // only package third parties that are initially dependent
109 },
110 elementUI: {
111 name: 'chunk-elementUI', // split elementUI into a single package
112 priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
113 test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
114 },
115 commons: {
116 name: 'chunk-commons',
117 test: resolve('src/components'), // can customize your rules
118 minChunks: 3, // minimum common number
119 priority: 5,
120 reuseExistingChunk: true
121 }
122 }
123 })
124 config.optimization.runtimeChunk('single')
125 }
126 )
127 }
128 }
129