@ -0,0 +1,14 @@ | 
				
			|||||||
 | 
					# http://editorconfig.org | 
				
			||||||
 | 
					root = true | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*] | 
				
			||||||
 | 
					charset = utf-8 | 
				
			||||||
 | 
					indent_style = space | 
				
			||||||
 | 
					indent_size = 2 | 
				
			||||||
 | 
					end_of_line = lf | 
				
			||||||
 | 
					insert_final_newline = true | 
				
			||||||
 | 
					trim_trailing_whitespace = true | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*.md] | 
				
			||||||
 | 
					insert_final_newline = false | 
				
			||||||
 | 
					trim_trailing_whitespace = false | 
				
			||||||
@ -0,0 +1,5 @@ | 
				
			|||||||
 | 
					# just a flag | 
				
			||||||
 | 
					ENV = 'development' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# base api | 
				
			||||||
 | 
					VUE_APP_BASE_API = 'http://localhost:7301' | 
				
			||||||
@ -0,0 +1,6 @@ | 
				
			|||||||
 | 
					# just a flag | 
				
			||||||
 | 
					ENV = 'production' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# base api | 
				
			||||||
 | 
					VUE_APP_BASE_API = '/prod-api' | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,8 @@ | 
				
			|||||||
 | 
					NODE_ENV = production | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# just a flag | 
				
			||||||
 | 
					ENV = 'staging' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# base api | 
				
			||||||
 | 
					VUE_APP_BASE_API = '/stage-api' | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,4 @@ | 
				
			|||||||
 | 
					build/*.js | 
				
			||||||
 | 
					src/assets | 
				
			||||||
 | 
					public | 
				
			||||||
 | 
					dist | 
				
			||||||
@ -0,0 +1,198 @@ | 
				
			|||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  root: true, | 
				
			||||||
 | 
					  parserOptions: { | 
				
			||||||
 | 
					    parser: 'babel-eslint', | 
				
			||||||
 | 
					    sourceType: 'module' | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  env: { | 
				
			||||||
 | 
					    browser: true, | 
				
			||||||
 | 
					    node: true, | 
				
			||||||
 | 
					    es6: true | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  extends: ['plugin:vue/recommended', 'eslint:recommended'], | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // add your custom rules here
 | 
				
			||||||
 | 
					  //it is base on https://github.com/vuejs/eslint-config-vue
 | 
				
			||||||
 | 
					  rules: { | 
				
			||||||
 | 
					    'vue/max-attributes-per-line': [2, { | 
				
			||||||
 | 
					      'singleline': 10, | 
				
			||||||
 | 
					      'multiline': { | 
				
			||||||
 | 
					        'max': 1, | 
				
			||||||
 | 
					        'allowFirstLine': false | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'vue/singleline-html-element-content-newline': 'off', | 
				
			||||||
 | 
					    'vue/multiline-html-element-content-newline': 'off', | 
				
			||||||
 | 
					    'vue/name-property-casing': ['error', 'PascalCase'], | 
				
			||||||
 | 
					    'vue/no-v-html': 'off', | 
				
			||||||
 | 
					    'accessor-pairs': 2, | 
				
			||||||
 | 
					    'arrow-spacing': [2, { | 
				
			||||||
 | 
					      'before': true, | 
				
			||||||
 | 
					      'after': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'block-spacing': [2, 'always'], | 
				
			||||||
 | 
					    'brace-style': [2, '1tbs', { | 
				
			||||||
 | 
					      'allowSingleLine': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'camelcase': [0, { | 
				
			||||||
 | 
					      'properties': 'always' | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'comma-dangle': [2, 'never'], | 
				
			||||||
 | 
					    'comma-spacing': [2, { | 
				
			||||||
 | 
					      'before': false, | 
				
			||||||
 | 
					      'after': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'comma-style': [2, 'last'], | 
				
			||||||
 | 
					    'constructor-super': 2, | 
				
			||||||
 | 
					    'curly': [2, 'multi-line'], | 
				
			||||||
 | 
					    'dot-location': [2, 'property'], | 
				
			||||||
 | 
					    'eol-last': 2, | 
				
			||||||
 | 
					    'eqeqeq': ['error', 'always', { 'null': 'ignore' }], | 
				
			||||||
 | 
					    'generator-star-spacing': [2, { | 
				
			||||||
 | 
					      'before': true, | 
				
			||||||
 | 
					      'after': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'handle-callback-err': [2, '^(err|error)$'], | 
				
			||||||
 | 
					    'indent': [2, 2, { | 
				
			||||||
 | 
					      'SwitchCase': 1 | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'jsx-quotes': [2, 'prefer-single'], | 
				
			||||||
 | 
					    'key-spacing': [2, { | 
				
			||||||
 | 
					      'beforeColon': false, | 
				
			||||||
 | 
					      'afterColon': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'keyword-spacing': [2, { | 
				
			||||||
 | 
					      'before': true, | 
				
			||||||
 | 
					      'after': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'new-cap': [2, { | 
				
			||||||
 | 
					      'newIsCap': true, | 
				
			||||||
 | 
					      'capIsNew': false | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'new-parens': 2, | 
				
			||||||
 | 
					    'no-array-constructor': 2, | 
				
			||||||
 | 
					    'no-caller': 2, | 
				
			||||||
 | 
					    'no-console': 'off', | 
				
			||||||
 | 
					    'no-class-assign': 2, | 
				
			||||||
 | 
					    'no-cond-assign': 2, | 
				
			||||||
 | 
					    'no-const-assign': 2, | 
				
			||||||
 | 
					    'no-control-regex': 0, | 
				
			||||||
 | 
					    'no-delete-var': 2, | 
				
			||||||
 | 
					    'no-dupe-args': 2, | 
				
			||||||
 | 
					    'no-dupe-class-members': 2, | 
				
			||||||
 | 
					    'no-dupe-keys': 2, | 
				
			||||||
 | 
					    'no-duplicate-case': 2, | 
				
			||||||
 | 
					    'no-empty-character-class': 2, | 
				
			||||||
 | 
					    'no-empty-pattern': 2, | 
				
			||||||
 | 
					    'no-eval': 2, | 
				
			||||||
 | 
					    'no-ex-assign': 2, | 
				
			||||||
 | 
					    'no-extend-native': 2, | 
				
			||||||
 | 
					    'no-extra-bind': 2, | 
				
			||||||
 | 
					    'no-extra-boolean-cast': 2, | 
				
			||||||
 | 
					    'no-extra-parens': [2, 'functions'], | 
				
			||||||
 | 
					    'no-fallthrough': 2, | 
				
			||||||
 | 
					    'no-floating-decimal': 2, | 
				
			||||||
 | 
					    'no-func-assign': 2, | 
				
			||||||
 | 
					    'no-implied-eval': 2, | 
				
			||||||
 | 
					    'no-inner-declarations': [2, 'functions'], | 
				
			||||||
 | 
					    'no-invalid-regexp': 2, | 
				
			||||||
 | 
					    'no-irregular-whitespace': 2, | 
				
			||||||
 | 
					    'no-iterator': 2, | 
				
			||||||
 | 
					    'no-label-var': 2, | 
				
			||||||
 | 
					    'no-labels': [2, { | 
				
			||||||
 | 
					      'allowLoop': false, | 
				
			||||||
 | 
					      'allowSwitch': false | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'no-lone-blocks': 2, | 
				
			||||||
 | 
					    'no-mixed-spaces-and-tabs': 2, | 
				
			||||||
 | 
					    'no-multi-spaces': 2, | 
				
			||||||
 | 
					    'no-multi-str': 2, | 
				
			||||||
 | 
					    'no-multiple-empty-lines': [2, { | 
				
			||||||
 | 
					      'max': 1 | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'no-native-reassign': 2, | 
				
			||||||
 | 
					    'no-negated-in-lhs': 2, | 
				
			||||||
 | 
					    'no-new-object': 2, | 
				
			||||||
 | 
					    'no-new-require': 2, | 
				
			||||||
 | 
					    'no-new-symbol': 2, | 
				
			||||||
 | 
					    'no-new-wrappers': 2, | 
				
			||||||
 | 
					    'no-obj-calls': 2, | 
				
			||||||
 | 
					    'no-octal': 2, | 
				
			||||||
 | 
					    'no-octal-escape': 2, | 
				
			||||||
 | 
					    'no-path-concat': 2, | 
				
			||||||
 | 
					    'no-proto': 2, | 
				
			||||||
 | 
					    'no-redeclare': 2, | 
				
			||||||
 | 
					    'no-regex-spaces': 2, | 
				
			||||||
 | 
					    'no-return-assign': [2, 'except-parens'], | 
				
			||||||
 | 
					    'no-self-assign': 2, | 
				
			||||||
 | 
					    'no-self-compare': 2, | 
				
			||||||
 | 
					    'no-sequences': 2, | 
				
			||||||
 | 
					    'no-shadow-restricted-names': 2, | 
				
			||||||
 | 
					    'no-spaced-func': 2, | 
				
			||||||
 | 
					    'no-sparse-arrays': 2, | 
				
			||||||
 | 
					    'no-this-before-super': 2, | 
				
			||||||
 | 
					    'no-throw-literal': 2, | 
				
			||||||
 | 
					    'no-trailing-spaces': 2, | 
				
			||||||
 | 
					    'no-undef': 2, | 
				
			||||||
 | 
					    'no-undef-init': 2, | 
				
			||||||
 | 
					    'no-unexpected-multiline': 2, | 
				
			||||||
 | 
					    'no-unmodified-loop-condition': 2, | 
				
			||||||
 | 
					    'no-unneeded-ternary': [2, { | 
				
			||||||
 | 
					      'defaultAssignment': false | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'no-unreachable': 2, | 
				
			||||||
 | 
					    'no-unsafe-finally': 2, | 
				
			||||||
 | 
					    'no-unused-vars': [2, { | 
				
			||||||
 | 
					      'vars': 'all', | 
				
			||||||
 | 
					      'args': 'none' | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'no-useless-call': 2, | 
				
			||||||
 | 
					    'no-useless-computed-key': 2, | 
				
			||||||
 | 
					    'no-useless-constructor': 2, | 
				
			||||||
 | 
					    'no-useless-escape': 0, | 
				
			||||||
 | 
					    'no-whitespace-before-property': 2, | 
				
			||||||
 | 
					    'no-with': 2, | 
				
			||||||
 | 
					    'one-var': [2, { | 
				
			||||||
 | 
					      'initialized': 'never' | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'operator-linebreak': [2, 'after', { | 
				
			||||||
 | 
					      'overrides': { | 
				
			||||||
 | 
					        '?': 'before', | 
				
			||||||
 | 
					        ':': 'before' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'padded-blocks': [2, 'never'], | 
				
			||||||
 | 
					    'quotes': [2, 'single', { | 
				
			||||||
 | 
					      'avoidEscape': true, | 
				
			||||||
 | 
					      'allowTemplateLiterals': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'semi': [2, 'never'], | 
				
			||||||
 | 
					    'semi-spacing': [2, { | 
				
			||||||
 | 
					      'before': false, | 
				
			||||||
 | 
					      'after': true | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'space-before-blocks': [2, 'always'], | 
				
			||||||
 | 
					    'space-before-function-paren': [2, 'never'], | 
				
			||||||
 | 
					    'space-in-parens': [2, 'never'], | 
				
			||||||
 | 
					    'space-infix-ops': 2, | 
				
			||||||
 | 
					    'space-unary-ops': [2, { | 
				
			||||||
 | 
					      'words': true, | 
				
			||||||
 | 
					      'nonwords': false | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'spaced-comment': [2, 'always', { | 
				
			||||||
 | 
					      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'template-curly-spacing': [2, 'never'], | 
				
			||||||
 | 
					    'use-isnan': 2, | 
				
			||||||
 | 
					    'valid-typeof': 2, | 
				
			||||||
 | 
					    'wrap-iife': [2, 'any'], | 
				
			||||||
 | 
					    'yield-star-spacing': [2, 'both'], | 
				
			||||||
 | 
					    'yoda': [2, 'never'], | 
				
			||||||
 | 
					    'prefer-const': 2, | 
				
			||||||
 | 
					    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, | 
				
			||||||
 | 
					    'object-curly-spacing': [2, 'always', { | 
				
			||||||
 | 
					      objectsInObjects: false | 
				
			||||||
 | 
					    }], | 
				
			||||||
 | 
					    'array-bracket-spacing': [2, 'never'] | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,17 @@ | 
				
			|||||||
 | 
					.DS_Store | 
				
			||||||
 | 
					node_modules/ | 
				
			||||||
 | 
					dist/ | 
				
			||||||
 | 
					npm-debug.log* | 
				
			||||||
 | 
					yarn-debug.log* | 
				
			||||||
 | 
					yarn-error.log* | 
				
			||||||
 | 
					package-lock.json | 
				
			||||||
 | 
					tests/**/coverage/ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Editor directories and files | 
				
			||||||
 | 
					.idea | 
				
			||||||
 | 
					.vscode | 
				
			||||||
 | 
					*.suo | 
				
			||||||
 | 
					*.ntvs* | 
				
			||||||
 | 
					*.njsproj | 
				
			||||||
 | 
					*.sln | 
				
			||||||
 | 
					/src/api/table.js | 
				
			||||||
@ -0,0 +1,5 @@ | 
				
			|||||||
 | 
					language: node_js | 
				
			||||||
 | 
					node_js: 10 | 
				
			||||||
 | 
					script: npm run test | 
				
			||||||
 | 
					notifications: | 
				
			||||||
 | 
					  email: false | 
				
			||||||
@ -0,0 +1,21 @@ | 
				
			|||||||
 | 
					MIT License | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (c) 2017-present PanJiaChen | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy | 
				
			||||||
 | 
					of this software and associated documentation files (the "Software"), to deal | 
				
			||||||
 | 
					in the Software without restriction, including without limitation the rights | 
				
			||||||
 | 
					to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | 
				
			||||||
 | 
					copies of the Software, and to permit persons to whom the Software is | 
				
			||||||
 | 
					furnished to do so, subject to the following conditions: | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all | 
				
			||||||
 | 
					copies or substantial portions of the Software. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | 
				
			||||||
 | 
					FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | 
				
			||||||
 | 
					AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | 
				
			||||||
 | 
					LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 
				
			||||||
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | 
				
			||||||
 | 
					SOFTWARE. | 
				
			||||||
@ -0,0 +1,113 @@ | 
				
			|||||||
 | 
					# vue-admin-template | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[线上地址](http://panjiachen.github.io/vue-admin-template) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[国内访问](https://panjiachen.gitee.io/vue-admin-template) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					目前版本为 `v4.0+` 基于 `vue-cli` | 
				
			||||||
 | 
					进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p align="center"> | 
				
			||||||
 | 
					  <b>SPONSORED BY</b> | 
				
			||||||
 | 
					</p> | 
				
			||||||
 | 
					<p align="center"> | 
				
			||||||
 | 
					   <a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank"> | 
				
			||||||
 | 
					      <img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip"> | 
				
			||||||
 | 
					   </a> | 
				
			||||||
 | 
					</p> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Extra | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如果你想要根据用户角色来动态生成侧边栏和 | 
				
			||||||
 | 
					router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 相关项目 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) | 
				
			||||||
 | 
					- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) | 
				
			||||||
 | 
					- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) | 
				
			||||||
 | 
					- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) | 
				
			||||||
 | 
					- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Build Setup | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash | 
				
			||||||
 | 
					# 克隆项目 | 
				
			||||||
 | 
					git clone https://github.com/PanJiaChen/vue-admin-template.git | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 进入项目目录 | 
				
			||||||
 | 
					cd vue-admin-template | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 安装依赖 | 
				
			||||||
 | 
					npm install | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 | 
				
			||||||
 | 
					npm install --registry=https://registry.npm.taobao.org | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 启动服务 | 
				
			||||||
 | 
					npm run dev | 
				
			||||||
 | 
					``` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					浏览器访问 [http://localhost:9528](http://localhost:9528) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 发布 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash | 
				
			||||||
 | 
					# 构建测试环境 | 
				
			||||||
 | 
					npm run build:stage | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 构建生产环境 | 
				
			||||||
 | 
					npm run build:prod | 
				
			||||||
 | 
					``` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 其它 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash | 
				
			||||||
 | 
					# 预览发布环境效果 | 
				
			||||||
 | 
					npm run preview | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 预览发布环境效果 + 静态资源分析 | 
				
			||||||
 | 
					npm run preview -- --report | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 代码格式检查 | 
				
			||||||
 | 
					npm run lint | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 代码格式检查并自动修复 | 
				
			||||||
 | 
					npm run lint -- --fix | 
				
			||||||
 | 
					``` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 购买贴纸 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Demo | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Browsers support | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Modern browsers and Internet Explorer 10+. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | | 
				
			||||||
 | 
					| --------- | --------- | --------- | --------- | | 
				
			||||||
 | 
					| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## License | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (c) 2017-present PanJiaChen | 
				
			||||||
@ -1,3 +1,106 @@ | 
				
			|||||||
# soul2-web-left-menu-demo-v1 | 
					# soul2-web-left-menu-demo-v1 | 
				
			||||||
 | 
					
 | 
				
			||||||
试验性的vue左侧菜单demo,一般常用于后台管理。 | 
					试验性的vue左侧菜单demo,一般常用于后台管理。 | 
				
			||||||
 | 
					# vue-admin-template | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					English | [简体中文](./README-zh.md) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Live demo:** http://panjiachen.github.io/vue-admin-template | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch | 
				
			||||||
 | 
					to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`** | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p align="center"> | 
				
			||||||
 | 
					  <b>SPONSORED BY</b> | 
				
			||||||
 | 
					</p> | 
				
			||||||
 | 
					<p align="center"> | 
				
			||||||
 | 
					   <a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank"> | 
				
			||||||
 | 
					      <img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip"> | 
				
			||||||
 | 
					   </a> | 
				
			||||||
 | 
					</p> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Build Setup | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash | 
				
			||||||
 | 
					# clone the project | 
				
			||||||
 | 
					git clone https://github.com/PanJiaChen/vue-admin-template.git | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# enter the project directory | 
				
			||||||
 | 
					cd vue-admin-template | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# install dependency | 
				
			||||||
 | 
					npm install | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# develop | 
				
			||||||
 | 
					npm run dev | 
				
			||||||
 | 
					``` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This will automatically open http://localhost:9528 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Build | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash | 
				
			||||||
 | 
					# build for test environment | 
				
			||||||
 | 
					npm run build:stage | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# build for production environment | 
				
			||||||
 | 
					npm run build:prod | 
				
			||||||
 | 
					``` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Advanced | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash | 
				
			||||||
 | 
					# preview the release environment effect | 
				
			||||||
 | 
					npm run preview | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# preview the release environment effect + static resource analysis | 
				
			||||||
 | 
					npm run preview -- --report | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# code format check | 
				
			||||||
 | 
					npm run lint | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# code format check and auto fix | 
				
			||||||
 | 
					npm run lint -- --fix | 
				
			||||||
 | 
					``` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more | 
				
			||||||
 | 
					information | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Demo | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Extra | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you want router permission && generate menu by user roles , you can use this | 
				
			||||||
 | 
					branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For `typescript` version, you can | 
				
			||||||
 | 
					use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) ( | 
				
			||||||
 | 
					Credits: [@Armour](https://github.com/Armour)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Related Project | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Browsers support | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Modern browsers and Internet Explorer 10+. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | | 
				
			||||||
 | 
					| --------- | --------- | --------- | --------- | | 
				
			||||||
 | 
					| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## License | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (c) 2017-present PanJiaChen | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,14 @@ | 
				
			|||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  presets: [ | 
				
			||||||
 | 
					    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
 | 
				
			||||||
 | 
					    '@vue/cli-plugin-babel/preset' | 
				
			||||||
 | 
					  ], | 
				
			||||||
 | 
					  'env': { | 
				
			||||||
 | 
					    'development': { | 
				
			||||||
 | 
					      // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
 | 
				
			||||||
 | 
					      // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
 | 
				
			||||||
 | 
					      // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
 | 
				
			||||||
 | 
					      'plugins': ['dynamic-import-node'] | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,35 @@ | 
				
			|||||||
 | 
					const { run } = require('runjs') | 
				
			||||||
 | 
					const chalk = require('chalk') | 
				
			||||||
 | 
					const config = require('../vue.config.js') | 
				
			||||||
 | 
					const rawArgv = process.argv.slice(2) | 
				
			||||||
 | 
					const args = rawArgv.join(' ') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (process.env.npm_config_preview || rawArgv.includes('--preview')) { | 
				
			||||||
 | 
					  const report = rawArgv.includes('--report') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  run(`vue-cli-service build ${args}`) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const port = 9526 | 
				
			||||||
 | 
					  const publicPath = config.publicPath | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var connect = require('connect') | 
				
			||||||
 | 
					  var serveStatic = require('serve-static') | 
				
			||||||
 | 
					  const app = connect() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  app.use( | 
				
			||||||
 | 
					    publicPath, | 
				
			||||||
 | 
					    serveStatic('./dist', { | 
				
			||||||
 | 
					      index: ['index.html', '/'] | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					  ) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  app.listen(port, function() { | 
				
			||||||
 | 
					    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`)) | 
				
			||||||
 | 
					    if (report) { | 
				
			||||||
 | 
					      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`)) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} else { | 
				
			||||||
 | 
					  run(`vue-cli-service build ${args}`) | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,24 @@ | 
				
			|||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], | 
				
			||||||
 | 
					  transform: { | 
				
			||||||
 | 
					    '^.+\\.vue$': 'vue-jest', | 
				
			||||||
 | 
					    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': | 
				
			||||||
 | 
					      'jest-transform-stub', | 
				
			||||||
 | 
					    '^.+\\.jsx?$': 'babel-jest' | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  moduleNameMapper: { | 
				
			||||||
 | 
					    '^@/(.*)$': '<rootDir>/src/$1' | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  snapshotSerializers: ['jest-serializer-vue'], | 
				
			||||||
 | 
					  testMatch: [ | 
				
			||||||
 | 
					    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' | 
				
			||||||
 | 
					  ], | 
				
			||||||
 | 
					  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], | 
				
			||||||
 | 
					  coverageDirectory: '<rootDir>/tests/unit/coverage', | 
				
			||||||
 | 
					  // 'collectCoverage': true,
 | 
				
			||||||
 | 
					  'coverageReporters': [ | 
				
			||||||
 | 
					    'lcov', | 
				
			||||||
 | 
					    'text-summary' | 
				
			||||||
 | 
					  ], | 
				
			||||||
 | 
					  testURL: 'http://localhost/' | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,14 @@ | 
				
			|||||||
 | 
					{ | 
				
			||||||
 | 
					  "compilerOptions": { | 
				
			||||||
 | 
					    "baseUrl": "./", | 
				
			||||||
 | 
					    "paths": { | 
				
			||||||
 | 
					      "@/*": [ | 
				
			||||||
 | 
					        "src/*" | 
				
			||||||
 | 
					      ] | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  "exclude": [ | 
				
			||||||
 | 
					    "node_modules", | 
				
			||||||
 | 
					    "dist" | 
				
			||||||
 | 
					  ] | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,57 @@ | 
				
			|||||||
 | 
					const Mock = require('mockjs') | 
				
			||||||
 | 
					const { param2Obj } = require('./utils') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const user = require('./user') | 
				
			||||||
 | 
					const table = require('./table') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mocks = [ | 
				
			||||||
 | 
					  ...user, | 
				
			||||||
 | 
					  ...table | 
				
			||||||
 | 
					] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// for front mock
 | 
				
			||||||
 | 
					// please use it cautiously, it will redefine XMLHttpRequest,
 | 
				
			||||||
 | 
					// which will cause many of your third-party libraries to be invalidated(like progress event).
 | 
				
			||||||
 | 
					function mockXHR() { | 
				
			||||||
 | 
					  // mock patch
 | 
				
			||||||
 | 
					  // https://github.com/nuysoft/Mock/issues/300
 | 
				
			||||||
 | 
					  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send | 
				
			||||||
 | 
					  Mock.XHR.prototype.send = function() { | 
				
			||||||
 | 
					    if (this.custom.xhr) { | 
				
			||||||
 | 
					      this.custom.xhr.withCredentials = this.withCredentials || false | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.responseType) { | 
				
			||||||
 | 
					        this.custom.xhr.responseType = this.responseType | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    this.proxy_send(...arguments) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function XHR2ExpressReqWrap(respond) { | 
				
			||||||
 | 
					    return function(options) { | 
				
			||||||
 | 
					      let result = null | 
				
			||||||
 | 
					      if (respond instanceof Function) { | 
				
			||||||
 | 
					        const { body, type, url } = options | 
				
			||||||
 | 
					        // https://expressjs.com/en/4x/api.html#req
 | 
				
			||||||
 | 
					        result = respond({ | 
				
			||||||
 | 
					          method: type, | 
				
			||||||
 | 
					          body: JSON.parse(body), | 
				
			||||||
 | 
					          query: param2Obj(url) | 
				
			||||||
 | 
					        }) | 
				
			||||||
 | 
					      } else { | 
				
			||||||
 | 
					        result = respond | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      return Mock.mock(result) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const i of mocks) { | 
				
			||||||
 | 
					    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  mocks, | 
				
			||||||
 | 
					  mockXHR | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,81 @@ | 
				
			|||||||
 | 
					const chokidar = require('chokidar') | 
				
			||||||
 | 
					const bodyParser = require('body-parser') | 
				
			||||||
 | 
					const chalk = require('chalk') | 
				
			||||||
 | 
					const path = require('path') | 
				
			||||||
 | 
					const Mock = require('mockjs') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mockDir = path.join(process.cwd(), 'mock') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function registerRoutes(app) { | 
				
			||||||
 | 
					  let mockLastIndex | 
				
			||||||
 | 
					  const { mocks } = require('./index.js') | 
				
			||||||
 | 
					  const mocksForServer = mocks.map(route => { | 
				
			||||||
 | 
					    return responseFake(route.url, route.type, route.response) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  for (const mock of mocksForServer) { | 
				
			||||||
 | 
					    app[mock.type](mock.url, mock.response) | 
				
			||||||
 | 
					    mockLastIndex = app._router.stack.length | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  const mockRoutesLength = Object.keys(mocksForServer).length | 
				
			||||||
 | 
					  return { | 
				
			||||||
 | 
					    mockRoutesLength: mockRoutesLength, | 
				
			||||||
 | 
					    mockStartIndex: mockLastIndex - mockRoutesLength | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function unregisterRoutes() { | 
				
			||||||
 | 
					  Object.keys(require.cache).forEach(i => { | 
				
			||||||
 | 
					    if (i.includes(mockDir)) { | 
				
			||||||
 | 
					      delete require.cache[require.resolve(i)] | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// for mock server
 | 
				
			||||||
 | 
					const responseFake = (url, type, respond) => { | 
				
			||||||
 | 
					  return { | 
				
			||||||
 | 
					    url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), | 
				
			||||||
 | 
					    type: type || 'get', | 
				
			||||||
 | 
					    response(req, res) { | 
				
			||||||
 | 
					      console.log('request invoke:' + req.path) | 
				
			||||||
 | 
					      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = app => { | 
				
			||||||
 | 
					  // parse app.body
 | 
				
			||||||
 | 
					  // https://expressjs.com/en/4x/api.html#req.body
 | 
				
			||||||
 | 
					  app.use(bodyParser.json()) | 
				
			||||||
 | 
					  app.use(bodyParser.urlencoded({ | 
				
			||||||
 | 
					    extended: true | 
				
			||||||
 | 
					  })) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mockRoutes = registerRoutes(app) | 
				
			||||||
 | 
					  var mockRoutesLength = mockRoutes.mockRoutesLength | 
				
			||||||
 | 
					  var mockStartIndex = mockRoutes.mockStartIndex | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // watch files, hot reload mock server
 | 
				
			||||||
 | 
					  chokidar.watch(mockDir, { | 
				
			||||||
 | 
					    ignored: /mock-server/, | 
				
			||||||
 | 
					    ignoreInitial: true | 
				
			||||||
 | 
					  }).on('all', (event, path) => { | 
				
			||||||
 | 
					    if (event === 'change' || event === 'add') { | 
				
			||||||
 | 
					      try { | 
				
			||||||
 | 
					        // remove mock routes stack
 | 
				
			||||||
 | 
					        app._router.stack.splice(mockStartIndex, mockRoutesLength) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // clear routes cache
 | 
				
			||||||
 | 
					        unregisterRoutes() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const mockRoutes = registerRoutes(app) | 
				
			||||||
 | 
					        mockRoutesLength = mockRoutes.mockRoutesLength | 
				
			||||||
 | 
					        mockStartIndex = mockRoutes.mockStartIndex | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`)) | 
				
			||||||
 | 
					      } catch (error) { | 
				
			||||||
 | 
					        console.log(chalk.redBright(error)) | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,29 @@ | 
				
			|||||||
 | 
					const Mock = require('mockjs') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const data = Mock.mock({ | 
				
			||||||
 | 
					  'items|30': [{ | 
				
			||||||
 | 
					    id: '@id', | 
				
			||||||
 | 
					    title: '@sentence(10, 20)', | 
				
			||||||
 | 
					    'status|1': ['published', 'draft', 'deleted'], | 
				
			||||||
 | 
					    author: 'name', | 
				
			||||||
 | 
					    display_time: '@datetime', | 
				
			||||||
 | 
					    pageviews: '@integer(300, 5000)' | 
				
			||||||
 | 
					  }] | 
				
			||||||
 | 
					}) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = [ | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    url: '/vue-admin-template/table/list', | 
				
			||||||
 | 
					    type: 'get', | 
				
			||||||
 | 
					    response: config => { | 
				
			||||||
 | 
					      const items = data.items | 
				
			||||||
 | 
					      return { | 
				
			||||||
 | 
					        code: 20000, | 
				
			||||||
 | 
					        data: { | 
				
			||||||
 | 
					          total: items.length, | 
				
			||||||
 | 
					          items: items | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					] | 
				
			||||||
@ -0,0 +1,83 @@ | 
				
			|||||||
 | 
					const tokens = { | 
				
			||||||
 | 
					  admin: { | 
				
			||||||
 | 
					    token: 'admin-token' | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  editor: { | 
				
			||||||
 | 
					    token: 'editor-token' | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const users = { | 
				
			||||||
 | 
					  'admin-token': { | 
				
			||||||
 | 
					    roles: ['admin'], | 
				
			||||||
 | 
					    introduction: 'I am a super administrator', | 
				
			||||||
 | 
					    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', | 
				
			||||||
 | 
					    name: 'Super Admin' | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  'editor-token': { | 
				
			||||||
 | 
					    roles: ['editor'], | 
				
			||||||
 | 
					    introduction: 'I am an editor', | 
				
			||||||
 | 
					    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', | 
				
			||||||
 | 
					    name: 'Normal Editor' | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = [ | 
				
			||||||
 | 
					  // user login
 | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    url: '/vue-admin-template/user/login', | 
				
			||||||
 | 
					    type: 'post', | 
				
			||||||
 | 
					    response: config => { | 
				
			||||||
 | 
					      const { username } = config.body | 
				
			||||||
 | 
					      const token = tokens[username] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // mock error
 | 
				
			||||||
 | 
					      if (!token) { | 
				
			||||||
 | 
					        return { | 
				
			||||||
 | 
					          code: 60204, | 
				
			||||||
 | 
					          message: 'Account and password are incorrect.' | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return { | 
				
			||||||
 | 
					        code: 20000, | 
				
			||||||
 | 
					        data: token | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // get user info
 | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    url: '/vue-admin-template/user/info\.*', | 
				
			||||||
 | 
					    type: 'get', | 
				
			||||||
 | 
					    response: config => { | 
				
			||||||
 | 
					      const { token } = config.query | 
				
			||||||
 | 
					      const info = users[token] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // mock error
 | 
				
			||||||
 | 
					      if (!info) { | 
				
			||||||
 | 
					        return { | 
				
			||||||
 | 
					          code: 50008, | 
				
			||||||
 | 
					          message: 'Login failed, unable to get user details.' | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return { | 
				
			||||||
 | 
					        code: 20000, | 
				
			||||||
 | 
					        data: info | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // user logout
 | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    url: '/vue-admin-template/user/logout', | 
				
			||||||
 | 
					    type: 'post', | 
				
			||||||
 | 
					    response: _ => { | 
				
			||||||
 | 
					      return { | 
				
			||||||
 | 
					        code: 20000, | 
				
			||||||
 | 
					        data: 'success' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					] | 
				
			||||||
@ -0,0 +1,25 @@ | 
				
			|||||||
 | 
					/** | 
				
			||||||
 | 
					 * @param {string} url | 
				
			||||||
 | 
					 * @returns {Object} | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					function param2Obj(url) { | 
				
			||||||
 | 
					  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') | 
				
			||||||
 | 
					  if (!search) { | 
				
			||||||
 | 
					    return {} | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  const obj = {} | 
				
			||||||
 | 
					  const searchArr = search.split('&') | 
				
			||||||
 | 
					  searchArr.forEach(v => { | 
				
			||||||
 | 
					    const index = v.indexOf('=') | 
				
			||||||
 | 
					    if (index !== -1) { | 
				
			||||||
 | 
					      const name = v.substring(0, index) | 
				
			||||||
 | 
					      const val = v.substring(index + 1, v.length) | 
				
			||||||
 | 
					      obj[name] = val | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  return obj | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  param2Obj | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,62 @@ | 
				
			|||||||
 | 
					{ | 
				
			||||||
 | 
					  "name": "vue-admin-template", | 
				
			||||||
 | 
					  "version": "0.0.1", | 
				
			||||||
 | 
					  "description": "Soul2's vue-demo to left-menu", | 
				
			||||||
 | 
					  "author": "Pan <panfree23@gmail.com>", | 
				
			||||||
 | 
					  "scripts": { | 
				
			||||||
 | 
					    "dev": "vue-cli-service serve", | 
				
			||||||
 | 
					    "build:prod": "vue-cli-service build", | 
				
			||||||
 | 
					    "build:stage": "vue-cli-service build --mode staging", | 
				
			||||||
 | 
					    "preview": "node build/index.js --preview", | 
				
			||||||
 | 
					    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", | 
				
			||||||
 | 
					    "lint": "eslint --ext .js,.vue src", | 
				
			||||||
 | 
					    "test:unit": "jest --clearCache && vue-cli-service test:unit", | 
				
			||||||
 | 
					    "test:ci": "npm run lint && npm run test:unit" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  "dependencies": { | 
				
			||||||
 | 
					    "axios": "0.18.1", | 
				
			||||||
 | 
					    "core-js": "3.6.5", | 
				
			||||||
 | 
					    "element-ui": "2.15.6", | 
				
			||||||
 | 
					    "js-cookie": "2.2.0", | 
				
			||||||
 | 
					    "normalize.css": "7.0.0", | 
				
			||||||
 | 
					    "nprogress": "0.2.0", | 
				
			||||||
 | 
					    "path-to-regexp": "2.4.0", | 
				
			||||||
 | 
					    "vue": "2.6.10", | 
				
			||||||
 | 
					    "vue-router": "3.0.6", | 
				
			||||||
 | 
					    "vuex": "3.1.0" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  "devDependencies": { | 
				
			||||||
 | 
					    "@vue/cli-plugin-babel": "4.4.4", | 
				
			||||||
 | 
					    "@vue/cli-plugin-eslint": "4.4.4", | 
				
			||||||
 | 
					    "@vue/cli-plugin-unit-jest": "4.4.4", | 
				
			||||||
 | 
					    "@vue/cli-service": "4.4.4", | 
				
			||||||
 | 
					    "@vue/test-utils": "1.0.0-beta.29", | 
				
			||||||
 | 
					    "autoprefixer": "9.5.1", | 
				
			||||||
 | 
					    "babel-eslint": "10.1.0", | 
				
			||||||
 | 
					    "babel-jest": "23.6.0", | 
				
			||||||
 | 
					    "babel-plugin-dynamic-import-node": "2.3.3", | 
				
			||||||
 | 
					    "chalk": "2.4.2", | 
				
			||||||
 | 
					    "connect": "3.6.6", | 
				
			||||||
 | 
					    "eslint": "6.7.2", | 
				
			||||||
 | 
					    "eslint-plugin-vue": "6.2.2", | 
				
			||||||
 | 
					    "html-webpack-plugin": "3.2.0", | 
				
			||||||
 | 
					    "mockjs": "1.0.1-beta3", | 
				
			||||||
 | 
					    "runjs": "4.3.2", | 
				
			||||||
 | 
					    "sass": "1.26.8", | 
				
			||||||
 | 
					    "sass-loader": "8.0.2", | 
				
			||||||
 | 
					    "script-ext-html-webpack-plugin": "2.1.3", | 
				
			||||||
 | 
					    "serve-static": "1.13.2", | 
				
			||||||
 | 
					    "svg-sprite-loader": "4.1.3", | 
				
			||||||
 | 
					    "svgo": "1.2.2", | 
				
			||||||
 | 
					    "vue-template-compiler": "2.6.10" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  "browserslist": [ | 
				
			||||||
 | 
					    "> 1%", | 
				
			||||||
 | 
					    "last 2 versions" | 
				
			||||||
 | 
					  ], | 
				
			||||||
 | 
					  "engines": { | 
				
			||||||
 | 
					    "node": ">=8.9", | 
				
			||||||
 | 
					    "npm": ">= 3.0.0" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  "license": "MIT" | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,8 @@ | 
				
			|||||||
 | 
					// https://github.com/michael-ciniawsky/postcss-load-config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  'plugins': { | 
				
			||||||
 | 
					    // to edit target browsers: use "browserslist" field in package.json
 | 
				
			||||||
 | 
					    'autoprefixer': {} | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
| 
		 After Width: | Height: | Size: 66 KiB  | 
@ -0,0 +1,18 @@ | 
				
			|||||||
 | 
					<!DOCTYPE html> | 
				
			||||||
 | 
					<html> | 
				
			||||||
 | 
					<head> | 
				
			||||||
 | 
					  <meta charset="utf-8"> | 
				
			||||||
 | 
					  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | 
				
			||||||
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | 
				
			||||||
 | 
					  <link rel="icon" href="<%= BASE_URL %>favicon.ico"> | 
				
			||||||
 | 
					  <title><%= webpackConfig.name %></title> | 
				
			||||||
 | 
					</head> | 
				
			||||||
 | 
					<body> | 
				
			||||||
 | 
					<noscript> | 
				
			||||||
 | 
					  <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it | 
				
			||||||
 | 
					    to continue.</strong> | 
				
			||||||
 | 
					</noscript> | 
				
			||||||
 | 
					<div id="app"></div> | 
				
			||||||
 | 
					<!-- built files will be auto injected --> | 
				
			||||||
 | 
					</body> | 
				
			||||||
 | 
					</html> | 
				
			||||||
@ -0,0 +1,11 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div id="app"> | 
				
			||||||
 | 
					    <router-view/> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'App' | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
@ -0,0 +1,22 @@ | 
				
			|||||||
 | 
					import request from '@/utils/request' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function page(data) { | 
				
			||||||
 | 
					  return request({ | 
				
			||||||
 | 
					    url: '/demo/page', | 
				
			||||||
 | 
					    data | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function save(data) { | 
				
			||||||
 | 
					  return request({ | 
				
			||||||
 | 
					    url: '/demo/save', | 
				
			||||||
 | 
					    data | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function remove(data) { | 
				
			||||||
 | 
					  return request({ | 
				
			||||||
 | 
					    url: '/demo/remove', | 
				
			||||||
 | 
					    data | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,24 @@ | 
				
			|||||||
 | 
					import request from '@/utils/request' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function login(data) { | 
				
			||||||
 | 
					  return request({ | 
				
			||||||
 | 
					    url: '/vue-admin-template/user/login', | 
				
			||||||
 | 
					    method: 'post', | 
				
			||||||
 | 
					    data | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getInfo(token) { | 
				
			||||||
 | 
					  return request({ | 
				
			||||||
 | 
					    url: '/vue-admin-template/user/info', | 
				
			||||||
 | 
					    method: 'get', | 
				
			||||||
 | 
					    params: { token } | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function logout() { | 
				
			||||||
 | 
					  return request({ | 
				
			||||||
 | 
					    url: '/vue-admin-template/user/logout', | 
				
			||||||
 | 
					    method: 'post' | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					} | 
				
			||||||
| 
		 After Width: | Height: | Size: 96 KiB  | 
| 
		 After Width: | Height: | Size: 4.7 KiB  | 
@ -0,0 +1,80 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <el-breadcrumb class="app-breadcrumb" separator="/"> | 
				
			||||||
 | 
					    <transition-group name="breadcrumb"> | 
				
			||||||
 | 
					      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path"> | 
				
			||||||
 | 
					        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ | 
				
			||||||
 | 
					            item.meta.title | 
				
			||||||
 | 
					          }}</span> | 
				
			||||||
 | 
					        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a> | 
				
			||||||
 | 
					      </el-breadcrumb-item> | 
				
			||||||
 | 
					    </transition-group> | 
				
			||||||
 | 
					  </el-breadcrumb> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import * as pathToRegexp from 'path-to-regexp' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  data() { | 
				
			||||||
 | 
					    return { | 
				
			||||||
 | 
					      levelList: null | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  watch: { | 
				
			||||||
 | 
					    $route() { | 
				
			||||||
 | 
					      this.getBreadcrumb() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  created() { | 
				
			||||||
 | 
					    this.getBreadcrumb() | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    getBreadcrumb() { | 
				
			||||||
 | 
					      // only show routes with meta.title | 
				
			||||||
 | 
					      const matched = this.$route.matched.filter(item => item.meta && item.meta.title) | 
				
			||||||
 | 
					      // const first = matched[0] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // if (!this.isDashboard(first)) { | 
				
			||||||
 | 
					      // matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched) | 
				
			||||||
 | 
					      // } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    isDashboard(route) { | 
				
			||||||
 | 
					      const name = route && route.name | 
				
			||||||
 | 
					      if (!name) { | 
				
			||||||
 | 
					        return false | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase() | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    pathCompile(path) { | 
				
			||||||
 | 
					      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561 | 
				
			||||||
 | 
					      const { params } = this.$route | 
				
			||||||
 | 
					      var toPath = pathToRegexp.compile(path) | 
				
			||||||
 | 
					      return toPath(params) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    handleLink(item) { | 
				
			||||||
 | 
					      const { redirect, path } = item | 
				
			||||||
 | 
					      if (redirect) { | 
				
			||||||
 | 
					        this.$router.push(redirect) | 
				
			||||||
 | 
					        return | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      this.$router.push(this.pathCompile(path)) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped> | 
				
			||||||
 | 
					.app-breadcrumb.el-breadcrumb { | 
				
			||||||
 | 
					  display: inline-block; | 
				
			||||||
 | 
					  font-size: 14px; | 
				
			||||||
 | 
					  line-height: 50px; | 
				
			||||||
 | 
					  margin-left: 8px; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .no-redirect { | 
				
			||||||
 | 
					    color: #97a8be; | 
				
			||||||
 | 
					    cursor: text; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,46 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div style="padding: 0 15px;" @click="toggleClick"> | 
				
			||||||
 | 
					    <svg | 
				
			||||||
 | 
					      :class="{'is-active':isActive}" | 
				
			||||||
 | 
					      class="hamburger" | 
				
			||||||
 | 
					      viewBox="0 0 1024 1024" | 
				
			||||||
 | 
					      xmlns="http://www.w3.org/2000/svg" | 
				
			||||||
 | 
					      width="64" | 
				
			||||||
 | 
					      height="64" | 
				
			||||||
 | 
					    > | 
				
			||||||
 | 
					      <path | 
				
			||||||
 | 
					        d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" | 
				
			||||||
 | 
					      /> | 
				
			||||||
 | 
					    </svg> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'Hamburger', | 
				
			||||||
 | 
					  props: { | 
				
			||||||
 | 
					    isActive: { | 
				
			||||||
 | 
					      type: Boolean, | 
				
			||||||
 | 
					      default: false | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    toggleClick() { | 
				
			||||||
 | 
					      this.$emit('toggleClick') | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped> | 
				
			||||||
 | 
					.hamburger { | 
				
			||||||
 | 
					  display: inline-block; | 
				
			||||||
 | 
					  vertical-align: middle; | 
				
			||||||
 | 
					  width: 20px; | 
				
			||||||
 | 
					  height: 20px; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hamburger.is-active { | 
				
			||||||
 | 
					  transform: rotate(180deg); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,62 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners"/> | 
				
			||||||
 | 
					  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners"> | 
				
			||||||
 | 
					    <use :xlink:href="iconName"/> | 
				
			||||||
 | 
					  </svg> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage | 
				
			||||||
 | 
					import { isExternal } from '@/utils/validate' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'SvgIcon', | 
				
			||||||
 | 
					  props: { | 
				
			||||||
 | 
					    iconClass: { | 
				
			||||||
 | 
					      type: String, | 
				
			||||||
 | 
					      required: true | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    className: { | 
				
			||||||
 | 
					      type: String, | 
				
			||||||
 | 
					      default: '' | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    isExternal() { | 
				
			||||||
 | 
					      return isExternal(this.iconClass) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    iconName() { | 
				
			||||||
 | 
					      return `#icon-${this.iconClass}` | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    svgClass() { | 
				
			||||||
 | 
					      if (this.className) { | 
				
			||||||
 | 
					        return 'svg-icon ' + this.className | 
				
			||||||
 | 
					      } else { | 
				
			||||||
 | 
					        return 'svg-icon' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    styleExternalIcon() { | 
				
			||||||
 | 
					      return { | 
				
			||||||
 | 
					        mask: `url(${this.iconClass}) no-repeat 50% 50%`, | 
				
			||||||
 | 
					        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%` | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped> | 
				
			||||||
 | 
					.svg-icon { | 
				
			||||||
 | 
					  width: 1em; | 
				
			||||||
 | 
					  height: 1em; | 
				
			||||||
 | 
					  vertical-align: -0.15em; | 
				
			||||||
 | 
					  fill: currentColor; | 
				
			||||||
 | 
					  overflow: hidden; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.svg-external-icon { | 
				
			||||||
 | 
					  background-color: currentColor; | 
				
			||||||
 | 
					  mask-size: cover !important; | 
				
			||||||
 | 
					  display: inline-block; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,5 @@ | 
				
			|||||||
 | 
					// 背景颜色
 | 
				
			||||||
 | 
					const loadingBackground = 'rgba(0, 0, 0, 0.2)' | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  loadingBackground | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,9 @@ | 
				
			|||||||
 | 
					import Vue from 'vue' | 
				
			||||||
 | 
					import SvgIcon from '@/components/SvgIcon' // svg component
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// register globally
 | 
				
			||||||
 | 
					Vue.component('svg-icon', SvgIcon) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const req = require.context('./svg', false, /\.svg$/) | 
				
			||||||
 | 
					const requireAll = requireContext => requireContext.keys().map(requireContext) | 
				
			||||||
 | 
					requireAll(req) | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.3 KiB  | 
| 
		 After Width: | Height: | Size: 506 B  | 
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
		 After Width: | Height: | Size: 953 B  | 
| 
		 After Width: | Height: | Size: 2.4 KiB  | 
| 
		 After Width: | Height: | Size: 296 B  | 
| 
		 After Width: | Height: | Size: 830 B  | 
| 
		 After Width: | Height: | Size: 632 B  | 
| 
		 After Width: | Height: | Size: 613 B  | 
| 
		 After Width: | Height: | Size: 1.8 KiB  | 
| 
		 After Width: | Height: | Size: 453 B  | 
@ -0,0 +1,22 @@ | 
				
			|||||||
 | 
					# replace default config | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# multipass: true | 
				
			||||||
 | 
					# full: true | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					plugins: | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # - name | 
				
			||||||
 | 
					  # | 
				
			||||||
 | 
					  # or: | 
				
			||||||
 | 
					  # - name: false | 
				
			||||||
 | 
					  # - name: true | 
				
			||||||
 | 
					  # | 
				
			||||||
 | 
					  # or: | 
				
			||||||
 | 
					  # - name: | 
				
			||||||
 | 
					  #     param1: 1 | 
				
			||||||
 | 
					  #     param2: 2 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - removeAttrs: | 
				
			||||||
 | 
					      attrs: | 
				
			||||||
 | 
					        - 'fill' | 
				
			||||||
 | 
					        - 'fill-rule' | 
				
			||||||
@ -0,0 +1,41 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <section class="app-main"> | 
				
			||||||
 | 
					    <transition name="fade-transform" mode="out-in"> | 
				
			||||||
 | 
					      <router-view :key="key"/> | 
				
			||||||
 | 
					    </transition> | 
				
			||||||
 | 
					  </section> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'AppMain', | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    key() { | 
				
			||||||
 | 
					      return this.$route.path | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped> | 
				
			||||||
 | 
					.app-main { | 
				
			||||||
 | 
					  /*50 = navbar  */ | 
				
			||||||
 | 
					  min-height: calc(100vh - 50px); | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  overflow: hidden; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fixed-header + .app-main { | 
				
			||||||
 | 
					  padding-top: 50px; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"> | 
				
			||||||
 | 
					// fix css style bug in open el-dialog | 
				
			||||||
 | 
					.el-popup-parent--hidden { | 
				
			||||||
 | 
					  .fixed-header { | 
				
			||||||
 | 
					    padding-right: 15px; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,139 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div class="navbar"> | 
				
			||||||
 | 
					    <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar"/> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <breadcrumb class="breadcrumb-container"/> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="right-menu"> | 
				
			||||||
 | 
					      <el-dropdown class="avatar-container" trigger="click"> | 
				
			||||||
 | 
					        <div class="avatar-wrapper"> | 
				
			||||||
 | 
					          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar"> | 
				
			||||||
 | 
					          <i class="el-icon-caret-bottom"/> | 
				
			||||||
 | 
					        </div> | 
				
			||||||
 | 
					        <el-dropdown-menu slot="dropdown" class="user-dropdown"> | 
				
			||||||
 | 
					          <router-link to="/"> | 
				
			||||||
 | 
					            <el-dropdown-item> | 
				
			||||||
 | 
					              Home | 
				
			||||||
 | 
					            </el-dropdown-item> | 
				
			||||||
 | 
					          </router-link> | 
				
			||||||
 | 
					          <a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/"> | 
				
			||||||
 | 
					            <el-dropdown-item>Github</el-dropdown-item> | 
				
			||||||
 | 
					          </a> | 
				
			||||||
 | 
					          <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/"> | 
				
			||||||
 | 
					            <el-dropdown-item>Docs</el-dropdown-item> | 
				
			||||||
 | 
					          </a> | 
				
			||||||
 | 
					          <el-dropdown-item divided @click.native="logout"> | 
				
			||||||
 | 
					            <span style="display:block;">Log Out</span> | 
				
			||||||
 | 
					          </el-dropdown-item> | 
				
			||||||
 | 
					        </el-dropdown-menu> | 
				
			||||||
 | 
					      </el-dropdown> | 
				
			||||||
 | 
					    </div> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { mapGetters } from 'vuex' | 
				
			||||||
 | 
					import Breadcrumb from '@/components/Breadcrumb' | 
				
			||||||
 | 
					import Hamburger from '@/components/Hamburger' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  components: { | 
				
			||||||
 | 
					    Breadcrumb, | 
				
			||||||
 | 
					    Hamburger | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    ...mapGetters([ | 
				
			||||||
 | 
					      'sidebar', | 
				
			||||||
 | 
					      'avatar' | 
				
			||||||
 | 
					    ]) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    toggleSideBar() { | 
				
			||||||
 | 
					      this.$store.dispatch('app/toggleSideBar') | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    async logout() { | 
				
			||||||
 | 
					      await this.$store.dispatch('user/logout') | 
				
			||||||
 | 
					      this.$router.push(`/login?redirect=${this.$route.fullPath}`) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped> | 
				
			||||||
 | 
					.navbar { | 
				
			||||||
 | 
					  height: 50px; | 
				
			||||||
 | 
					  overflow: hidden; | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  background: #fff; | 
				
			||||||
 | 
					  box-shadow: 0 1px 4px rgba(0, 21, 41, .08); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .hamburger-container { | 
				
			||||||
 | 
					    line-height: 46px; | 
				
			||||||
 | 
					    height: 100%; | 
				
			||||||
 | 
					    float: left; | 
				
			||||||
 | 
					    cursor: pointer; | 
				
			||||||
 | 
					    transition: background .3s; | 
				
			||||||
 | 
					    -webkit-tap-highlight-color: transparent; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover { | 
				
			||||||
 | 
					      background: rgba(0, 0, 0, .025) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .breadcrumb-container { | 
				
			||||||
 | 
					    float: left; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .right-menu { | 
				
			||||||
 | 
					    float: right; | 
				
			||||||
 | 
					    height: 100%; | 
				
			||||||
 | 
					    line-height: 50px; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus { | 
				
			||||||
 | 
					      outline: none; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .right-menu-item { | 
				
			||||||
 | 
					      display: inline-block; | 
				
			||||||
 | 
					      padding: 0 8px; | 
				
			||||||
 | 
					      height: 100%; | 
				
			||||||
 | 
					      font-size: 18px; | 
				
			||||||
 | 
					      color: #5a5e66; | 
				
			||||||
 | 
					      vertical-align: text-bottom; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.hover-effect { | 
				
			||||||
 | 
					        cursor: pointer; | 
				
			||||||
 | 
					        transition: background .3s; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover { | 
				
			||||||
 | 
					          background: rgba(0, 0, 0, .025) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .avatar-container { | 
				
			||||||
 | 
					      margin-right: 30px; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .avatar-wrapper { | 
				
			||||||
 | 
					        margin-top: 5px; | 
				
			||||||
 | 
					        position: relative; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .user-avatar { | 
				
			||||||
 | 
					          cursor: pointer; | 
				
			||||||
 | 
					          width: 40px; | 
				
			||||||
 | 
					          height: 40px; | 
				
			||||||
 | 
					          border-radius: 10px; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .el-icon-caret-bottom { | 
				
			||||||
 | 
					          cursor: pointer; | 
				
			||||||
 | 
					          position: absolute; | 
				
			||||||
 | 
					          right: -20px; | 
				
			||||||
 | 
					          top: 25px; | 
				
			||||||
 | 
					          font-size: 12px; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,26 @@ | 
				
			|||||||
 | 
					export default { | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    device() { | 
				
			||||||
 | 
					      return this.$store.state.app.device | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  mounted() { | 
				
			||||||
 | 
					    // In order to fix the click on menu on the ios device will trigger the mouseleave bug
 | 
				
			||||||
 | 
					    // https://github.com/PanJiaChen/vue-element-admin/issues/1135
 | 
				
			||||||
 | 
					    this.fixBugIniOS() | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    fixBugIniOS() { | 
				
			||||||
 | 
					      const $subMenu = this.$refs.subMenu | 
				
			||||||
 | 
					      if ($subMenu) { | 
				
			||||||
 | 
					        const handleMouseleave = $subMenu.handleMouseleave | 
				
			||||||
 | 
					        $subMenu.handleMouseleave = (e) => { | 
				
			||||||
 | 
					          if (this.device === 'mobile') { | 
				
			||||||
 | 
					            return | 
				
			||||||
 | 
					          } | 
				
			||||||
 | 
					          handleMouseleave(e) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,41 @@ | 
				
			|||||||
 | 
					<script> | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'MenuItem', | 
				
			||||||
 | 
					  functional: true, | 
				
			||||||
 | 
					  props: { | 
				
			||||||
 | 
					    icon: { | 
				
			||||||
 | 
					      type: String, | 
				
			||||||
 | 
					      default: '' | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    title: { | 
				
			||||||
 | 
					      type: String, | 
				
			||||||
 | 
					      default: '' | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  render(h, context) { | 
				
			||||||
 | 
					    const { icon, title } = context.props | 
				
			||||||
 | 
					    const vnodes = [] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (icon) { | 
				
			||||||
 | 
					      if (icon.includes('el-icon')) { | 
				
			||||||
 | 
					        vnodes.push(<i class={[icon, 'sub-el-icon']}/>) | 
				
			||||||
 | 
					      } else { | 
				
			||||||
 | 
					        vnodes.push(<svg-icon icon-class={icon}/>) | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (title) { | 
				
			||||||
 | 
					      vnodes.push(<span slot="title">{(title)}</span>) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    return vnodes | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped> | 
				
			||||||
 | 
					.sub-el-icon { | 
				
			||||||
 | 
					  color: currentColor; | 
				
			||||||
 | 
					  width: 1em; | 
				
			||||||
 | 
					  height: 1em; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,43 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <component :is="type" v-bind="linkProps(to)"> | 
				
			||||||
 | 
					    <slot/> | 
				
			||||||
 | 
					  </component> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { isExternal } from '@/utils/validate' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  props: { | 
				
			||||||
 | 
					    to: { | 
				
			||||||
 | 
					      type: String, | 
				
			||||||
 | 
					      required: true | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    isExternal() { | 
				
			||||||
 | 
					      return isExternal(this.to) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    type() { | 
				
			||||||
 | 
					      if (this.isExternal) { | 
				
			||||||
 | 
					        return 'a' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      return 'router-link' | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    linkProps(to) { | 
				
			||||||
 | 
					      if (this.isExternal) { | 
				
			||||||
 | 
					        return { | 
				
			||||||
 | 
					          href: to, | 
				
			||||||
 | 
					          target: '_blank', | 
				
			||||||
 | 
					          rel: 'noopener' | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      return { | 
				
			||||||
 | 
					        to: to | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
@ -0,0 +1,82 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div class="sidebar-logo-container" :class="{'collapse':collapse}"> | 
				
			||||||
 | 
					    <transition name="sidebarLogoFade"> | 
				
			||||||
 | 
					      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> | 
				
			||||||
 | 
					        <img v-if="logo" :src="logo" class="sidebar-logo"> | 
				
			||||||
 | 
					        <h1 v-else class="sidebar-title">{{ title }} </h1> | 
				
			||||||
 | 
					      </router-link> | 
				
			||||||
 | 
					      <router-link v-else key="expand" class="sidebar-logo-link" to="/"> | 
				
			||||||
 | 
					        <img v-if="logo" :src="logo" class="sidebar-logo"> | 
				
			||||||
 | 
					        <h1 class="sidebar-title">{{ title }} </h1> | 
				
			||||||
 | 
					      </router-link> | 
				
			||||||
 | 
					    </transition> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'SidebarLogo', | 
				
			||||||
 | 
					  props: { | 
				
			||||||
 | 
					    collapse: { | 
				
			||||||
 | 
					      type: Boolean, | 
				
			||||||
 | 
					      required: true | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  data() { | 
				
			||||||
 | 
					    return { | 
				
			||||||
 | 
					      title: 'Vue Admin Template', | 
				
			||||||
 | 
					      logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png' | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped> | 
				
			||||||
 | 
					.sidebarLogoFade-enter-active { | 
				
			||||||
 | 
					  transition: opacity 1.5s; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sidebarLogoFade-enter, | 
				
			||||||
 | 
					.sidebarLogoFade-leave-to { | 
				
			||||||
 | 
					  opacity: 0; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sidebar-logo-container { | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					  height: 50px; | 
				
			||||||
 | 
					  line-height: 50px; | 
				
			||||||
 | 
					  background: #2b2f3a; | 
				
			||||||
 | 
					  text-align: center; | 
				
			||||||
 | 
					  overflow: hidden; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  & .sidebar-logo-link { | 
				
			||||||
 | 
					    height: 100%; | 
				
			||||||
 | 
					    width: 100%; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    & .sidebar-logo { | 
				
			||||||
 | 
					      width: 32px; | 
				
			||||||
 | 
					      height: 32px; | 
				
			||||||
 | 
					      vertical-align: middle; | 
				
			||||||
 | 
					      margin-right: 12px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    & .sidebar-title { | 
				
			||||||
 | 
					      display: inline-block; | 
				
			||||||
 | 
					      margin: 0; | 
				
			||||||
 | 
					      color: #fff; | 
				
			||||||
 | 
					      font-weight: 600; | 
				
			||||||
 | 
					      line-height: 50px; | 
				
			||||||
 | 
					      font-size: 14px; | 
				
			||||||
 | 
					      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; | 
				
			||||||
 | 
					      vertical-align: middle; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.collapse { | 
				
			||||||
 | 
					    .sidebar-logo { | 
				
			||||||
 | 
					      margin-right: 0px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,97 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div v-if="!item.hidden"> | 
				
			||||||
 | 
					    <template | 
				
			||||||
 | 
					      v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow" | 
				
			||||||
 | 
					    > | 
				
			||||||
 | 
					      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> | 
				
			||||||
 | 
					        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> | 
				
			||||||
 | 
					          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title"/> | 
				
			||||||
 | 
					        </el-menu-item> | 
				
			||||||
 | 
					      </app-link> | 
				
			||||||
 | 
					    </template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> | 
				
			||||||
 | 
					      <template slot="title"> | 
				
			||||||
 | 
					        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title"/> | 
				
			||||||
 | 
					      </template> | 
				
			||||||
 | 
					      <sidebar-item | 
				
			||||||
 | 
					        v-for="child in item.children" | 
				
			||||||
 | 
					        :key="child.path" | 
				
			||||||
 | 
					        :is-nest="true" | 
				
			||||||
 | 
					        :item="child" | 
				
			||||||
 | 
					        :base-path="resolvePath(child.path)" | 
				
			||||||
 | 
					        class="nest-menu" | 
				
			||||||
 | 
					      /> | 
				
			||||||
 | 
					    </el-submenu> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import path from 'path' | 
				
			||||||
 | 
					import { isExternal } from '@/utils/validate' | 
				
			||||||
 | 
					import Item from './Item' | 
				
			||||||
 | 
					import AppLink from './Link' | 
				
			||||||
 | 
					import FixiOSBug from './FixiOSBug' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'SidebarItem', | 
				
			||||||
 | 
					  components: { Item, AppLink }, | 
				
			||||||
 | 
					  mixins: [FixiOSBug], | 
				
			||||||
 | 
					  props: { | 
				
			||||||
 | 
					    // route object | 
				
			||||||
 | 
					    item: { | 
				
			||||||
 | 
					      type: Object, | 
				
			||||||
 | 
					      required: true | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    isNest: { | 
				
			||||||
 | 
					      type: Boolean, | 
				
			||||||
 | 
					      default: false | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    basePath: { | 
				
			||||||
 | 
					      type: String, | 
				
			||||||
 | 
					      default: '' | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  data() { | 
				
			||||||
 | 
					    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237 | 
				
			||||||
 | 
					    // TODO: refactor with render function | 
				
			||||||
 | 
					    this.onlyOneChild = null | 
				
			||||||
 | 
					    return {} | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    hasOneShowingChild(children = [], parent) { | 
				
			||||||
 | 
					      const showingChildren = children.filter(item => { | 
				
			||||||
 | 
					        if (item.hidden) { | 
				
			||||||
 | 
					          return false | 
				
			||||||
 | 
					        } else { | 
				
			||||||
 | 
					          // Temp set(will be used if only has one showing child) | 
				
			||||||
 | 
					          this.onlyOneChild = item | 
				
			||||||
 | 
					          return true | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // When there is only one child router, the child router is displayed by default | 
				
			||||||
 | 
					      if (showingChildren.length === 1) { | 
				
			||||||
 | 
					        return true | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Show parent if there are no child router to display | 
				
			||||||
 | 
					      if (showingChildren.length === 0) { | 
				
			||||||
 | 
					        this.onlyOneChild = { ...parent, path: '', noShowingChildren: true } | 
				
			||||||
 | 
					        return true | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return false | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    resolvePath(routePath) { | 
				
			||||||
 | 
					      if (isExternal(routePath)) { | 
				
			||||||
 | 
					        return routePath | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      if (isExternal(this.basePath)) { | 
				
			||||||
 | 
					        return this.basePath | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      return path.resolve(this.basePath, routePath) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
@ -0,0 +1,56 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div :class="{'has-logo':showLogo}"> | 
				
			||||||
 | 
					    <logo v-if="showLogo" :collapse="isCollapse"/> | 
				
			||||||
 | 
					    <el-scrollbar wrap-class="scrollbar-wrapper"> | 
				
			||||||
 | 
					      <el-menu | 
				
			||||||
 | 
					        :default-active="activeMenu" | 
				
			||||||
 | 
					        :collapse="isCollapse" | 
				
			||||||
 | 
					        :background-color="variables.menuBg" | 
				
			||||||
 | 
					        :text-color="variables.menuText" | 
				
			||||||
 | 
					        :unique-opened="false" | 
				
			||||||
 | 
					        :active-text-color="variables.menuActiveText" | 
				
			||||||
 | 
					        :collapse-transition="false" | 
				
			||||||
 | 
					        mode="vertical" | 
				
			||||||
 | 
					      > | 
				
			||||||
 | 
					        <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path"/> | 
				
			||||||
 | 
					      </el-menu> | 
				
			||||||
 | 
					    </el-scrollbar> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { mapGetters } from 'vuex' | 
				
			||||||
 | 
					import Logo from './Logo' | 
				
			||||||
 | 
					import SidebarItem from './SidebarItem' | 
				
			||||||
 | 
					import variables from '@/styles/variables.scss' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  components: { SidebarItem, Logo }, | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    ...mapGetters([ | 
				
			||||||
 | 
					      'sidebar' | 
				
			||||||
 | 
					    ]), | 
				
			||||||
 | 
					    routes() { | 
				
			||||||
 | 
					      return this.$router.options.routes | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    activeMenu() { | 
				
			||||||
 | 
					      const route = this.$route | 
				
			||||||
 | 
					      const { meta, path } = route | 
				
			||||||
 | 
					      // if set path, the sidebar will highlight the path you set | 
				
			||||||
 | 
					      if (meta.activeMenu) { | 
				
			||||||
 | 
					        return meta.activeMenu | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      return path | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    showLogo() { | 
				
			||||||
 | 
					      return this.$store.state.settings.sidebarLogo | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    variables() { | 
				
			||||||
 | 
					      return variables | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    isCollapse() { | 
				
			||||||
 | 
					      return !this.sidebar.opened | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
@ -0,0 +1,3 @@ | 
				
			|||||||
 | 
					export { default as Navbar } from './Navbar' | 
				
			||||||
 | 
					export { default as Sidebar } from './Sidebar' | 
				
			||||||
 | 
					export { default as AppMain } from './AppMain' | 
				
			||||||
@ -0,0 +1,95 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div :class="classObj" class="app-wrapper"> | 
				
			||||||
 | 
					    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> | 
				
			||||||
 | 
					    <sidebar class="sidebar-container"/> | 
				
			||||||
 | 
					    <div class="main-container"> | 
				
			||||||
 | 
					      <div :class="{'fixed-header':fixedHeader}"> | 
				
			||||||
 | 
					        <navbar/> | 
				
			||||||
 | 
					      </div> | 
				
			||||||
 | 
					      <app-main/> | 
				
			||||||
 | 
					    </div> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { AppMain, Navbar, Sidebar } from './components' | 
				
			||||||
 | 
					import ResizeMixin from './mixin/ResizeHandler' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'Layout', | 
				
			||||||
 | 
					  components: { | 
				
			||||||
 | 
					    Navbar, | 
				
			||||||
 | 
					    Sidebar, | 
				
			||||||
 | 
					    AppMain | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  mixins: [ResizeMixin], | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    sidebar() { | 
				
			||||||
 | 
					      return this.$store.state.app.sidebar | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    device() { | 
				
			||||||
 | 
					      return this.$store.state.app.device | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    fixedHeader() { | 
				
			||||||
 | 
					      return this.$store.state.settings.fixedHeader | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    classObj() { | 
				
			||||||
 | 
					      return { | 
				
			||||||
 | 
					        hideSidebar: !this.sidebar.opened, | 
				
			||||||
 | 
					        openSidebar: this.sidebar.opened, | 
				
			||||||
 | 
					        withoutAnimation: this.sidebar.withoutAnimation, | 
				
			||||||
 | 
					        mobile: this.device === 'mobile' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    handleClickOutside() { | 
				
			||||||
 | 
					      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped> | 
				
			||||||
 | 
					@import "~@/styles/mixin.scss"; | 
				
			||||||
 | 
					@import "~@/styles/variables.scss"; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.app-wrapper { | 
				
			||||||
 | 
					  @include clearfix; | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  height: 100%; | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.mobile.openSidebar { | 
				
			||||||
 | 
					    position: fixed; | 
				
			||||||
 | 
					    top: 0; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.drawer-bg { | 
				
			||||||
 | 
					  background: #000; | 
				
			||||||
 | 
					  opacity: 0.3; | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					  top: 0; | 
				
			||||||
 | 
					  height: 100%; | 
				
			||||||
 | 
					  position: absolute; | 
				
			||||||
 | 
					  z-index: 999; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fixed-header { | 
				
			||||||
 | 
					  position: fixed; | 
				
			||||||
 | 
					  top: 0; | 
				
			||||||
 | 
					  right: 0; | 
				
			||||||
 | 
					  z-index: 9; | 
				
			||||||
 | 
					  width: calc(100% - #{$sideBarWidth}); | 
				
			||||||
 | 
					  transition: width 0.28s; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hideSidebar .fixed-header { | 
				
			||||||
 | 
					  width: calc(100% - 54px) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mobile .fixed-header { | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,45 @@ | 
				
			|||||||
 | 
					import store from '@/store' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { body } = document | 
				
			||||||
 | 
					const WIDTH = 992 // refer to Bootstrap's responsive design
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  watch: { | 
				
			||||||
 | 
					    $route(route) { | 
				
			||||||
 | 
					      if (this.device === 'mobile' && this.sidebar.opened) { | 
				
			||||||
 | 
					        store.dispatch('app/closeSideBar', { withoutAnimation: false }) | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  beforeMount() { | 
				
			||||||
 | 
					    window.addEventListener('resize', this.$_resizeHandler) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  beforeDestroy() { | 
				
			||||||
 | 
					    window.removeEventListener('resize', this.$_resizeHandler) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  mounted() { | 
				
			||||||
 | 
					    const isMobile = this.$_isMobile() | 
				
			||||||
 | 
					    if (isMobile) { | 
				
			||||||
 | 
					      store.dispatch('app/toggleDevice', 'mobile') | 
				
			||||||
 | 
					      store.dispatch('app/closeSideBar', { withoutAnimation: true }) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    // use $_ for mixins properties
 | 
				
			||||||
 | 
					    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
 | 
				
			||||||
 | 
					    $_isMobile() { | 
				
			||||||
 | 
					      const rect = body.getBoundingClientRect() | 
				
			||||||
 | 
					      return rect.width - 1 < WIDTH | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    $_resizeHandler() { | 
				
			||||||
 | 
					      if (!document.hidden) { | 
				
			||||||
 | 
					        const isMobile = this.$_isMobile() | 
				
			||||||
 | 
					        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isMobile) { | 
				
			||||||
 | 
					          store.dispatch('app/closeSideBar', { withoutAnimation: true }) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,42 @@ | 
				
			|||||||
 | 
					import Vue from 'vue' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'normalize.css/normalize.css' // A modern alternative to CSS resets
 | 
				
			||||||
 | 
					import ElementUI from 'element-ui' | 
				
			||||||
 | 
					import 'element-ui/lib/theme-chalk/index.css' | 
				
			||||||
 | 
					import '@/styles/index.scss' // global css
 | 
				
			||||||
 | 
					import App from './App' | 
				
			||||||
 | 
					import store from './store' | 
				
			||||||
 | 
					import router from './router' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '@/icons' // icon
 | 
				
			||||||
 | 
					import '@/permission' // permission control
 | 
				
			||||||
 | 
					import elementGlobal from './element-ui-global' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * If you don't want to use mock-server | 
				
			||||||
 | 
					 * you want to use MockJs for mock api | 
				
			||||||
 | 
					 * you can execute: mockXHR() | 
				
			||||||
 | 
					 * | 
				
			||||||
 | 
					 * Currently MockJs will be used in the production environment, | 
				
			||||||
 | 
					 * please remove it before going online ! ! ! | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					if (process.env.NODE_ENV === 'production') { | 
				
			||||||
 | 
					  const { mockXHR } = require('../mock') | 
				
			||||||
 | 
					  mockXHR() | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// set ElementUI lang to EN
 | 
				
			||||||
 | 
					// Vue.use(ElementUI, { locale })
 | 
				
			||||||
 | 
					// 如果想要中文版 element-ui,按如下方式声明
 | 
				
			||||||
 | 
					Vue.use(ElementUI) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vue.config.productionTip = false | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vue.prototype.$elementGlobal = elementGlobal | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					new Vue({ | 
				
			||||||
 | 
					  el: '#app', | 
				
			||||||
 | 
					  router, | 
				
			||||||
 | 
					  store, | 
				
			||||||
 | 
					  render: h => h(App) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,64 @@ | 
				
			|||||||
 | 
					// import router from './router'
 | 
				
			||||||
 | 
					// import store from './store'
 | 
				
			||||||
 | 
					// import { Message } from 'element-ui'
 | 
				
			||||||
 | 
					// import NProgress from 'nprogress' // progress bar
 | 
				
			||||||
 | 
					// import 'nprogress/nprogress.css' // progress bar style
 | 
				
			||||||
 | 
					// import { getToken } from '@/utils/auth' // get token from cookie
 | 
				
			||||||
 | 
					// import getPageTitle from '@/utils/get-page-title'
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// NProgress.configure({ showSpinner: false }) // NProgress Configuration
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// const whiteList = ['/login'] // no redirect whitelist
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// router.beforeEach(async(to, from, next) => {
 | 
				
			||||||
 | 
					//   // start progress bar
 | 
				
			||||||
 | 
					//   NProgress.start()
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//   // set page title
 | 
				
			||||||
 | 
					//   document.title = getPageTitle(to.meta.title)
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//   // determine whether the user has logged in
 | 
				
			||||||
 | 
					//   const hasToken = getToken()
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//   if (hasToken) {
 | 
				
			||||||
 | 
					//     if (to.path === '/login') {
 | 
				
			||||||
 | 
					//       // if is logged in, redirect to the home page
 | 
				
			||||||
 | 
					//       next({ path: '/' })
 | 
				
			||||||
 | 
					//       NProgress.done()
 | 
				
			||||||
 | 
					//     } else {
 | 
				
			||||||
 | 
					//       const hasGetUserInfo = store.getters.name
 | 
				
			||||||
 | 
					//       if (hasGetUserInfo) {
 | 
				
			||||||
 | 
					//         next()
 | 
				
			||||||
 | 
					//       } else {
 | 
				
			||||||
 | 
					//         try {
 | 
				
			||||||
 | 
					//           // get user info
 | 
				
			||||||
 | 
					//           await store.dispatch('user/getInfo')
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//           next()
 | 
				
			||||||
 | 
					//         } catch (error) {
 | 
				
			||||||
 | 
					//           // remove token and go to login page to re-login
 | 
				
			||||||
 | 
					//           await store.dispatch('user/resetToken')
 | 
				
			||||||
 | 
					//           Message.error(error || 'Has Error')
 | 
				
			||||||
 | 
					//           next(`/login?redirect=${to.path}`)
 | 
				
			||||||
 | 
					//           NProgress.done()
 | 
				
			||||||
 | 
					//         }
 | 
				
			||||||
 | 
					//       }
 | 
				
			||||||
 | 
					//     }
 | 
				
			||||||
 | 
					//   } else {
 | 
				
			||||||
 | 
					//     /* has no token*/
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     if (whiteList.indexOf(to.path) !== -1) {
 | 
				
			||||||
 | 
					//       // in the free login whitelist, go directly
 | 
				
			||||||
 | 
					//       next()
 | 
				
			||||||
 | 
					//     } else {
 | 
				
			||||||
 | 
					//       // other pages that do not have permission to access are redirected to the login page.
 | 
				
			||||||
 | 
					//       next(`/login?redirect=${to.path}`)
 | 
				
			||||||
 | 
					//       NProgress.done()
 | 
				
			||||||
 | 
					//     }
 | 
				
			||||||
 | 
					//   }
 | 
				
			||||||
 | 
					// })
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// router.afterEach(() => {
 | 
				
			||||||
 | 
					//   // finish progress bar
 | 
				
			||||||
 | 
					//   NProgress.done()
 | 
				
			||||||
 | 
					// })
 | 
				
			||||||
@ -0,0 +1,88 @@ | 
				
			|||||||
 | 
					import Vue from 'vue' | 
				
			||||||
 | 
					import Router from 'vue-router' | 
				
			||||||
 | 
					/* Layout */ | 
				
			||||||
 | 
					import Layout from '@/layout' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vue.use(Router) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * Note: sub-menu only appear when route children.length >= 1 | 
				
			||||||
 | 
					 * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
 | 
				
			||||||
 | 
					 * | 
				
			||||||
 | 
					 * hidden: true                   if set true, item will not show in the sidebar(default is false) | 
				
			||||||
 | 
					 * alwaysShow: true               if set true, will always show the root menu | 
				
			||||||
 | 
					 *                                if not set alwaysShow, when item has more than one children route, | 
				
			||||||
 | 
					 *                                it will becomes nested mode, otherwise not show the root menu | 
				
			||||||
 | 
					 * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb | 
				
			||||||
 | 
					 * name:'router-name'             the name is used by <keep-alive> (must set!!!) | 
				
			||||||
 | 
					 * meta : { | 
				
			||||||
 | 
					    roles: ['admin','editor']    control the page roles (you can set multiple roles) | 
				
			||||||
 | 
					    title: 'title'               the name show in sidebar and breadcrumb (recommend set) | 
				
			||||||
 | 
					    icon: 'svg-name'/'el-icon-x' the icon show in the sidebar | 
				
			||||||
 | 
					    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true) | 
				
			||||||
 | 
					    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * constantRoutes | 
				
			||||||
 | 
					 * a base page that does not have permission requirements | 
				
			||||||
 | 
					 * all roles can be accessed | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					export const constantRoutes = [ | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    path: '/login', | 
				
			||||||
 | 
					    component: () => import('@/views/login/index'), | 
				
			||||||
 | 
					    hidden: true | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    path: '/404', | 
				
			||||||
 | 
					    component: () => import('@/views/404'), | 
				
			||||||
 | 
					    hidden: true | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    path: '/', | 
				
			||||||
 | 
					    component: Layout, | 
				
			||||||
 | 
					    redirect: '/dashboard', | 
				
			||||||
 | 
					    children: [{ | 
				
			||||||
 | 
					      path: 'dashboard', | 
				
			||||||
 | 
					      name: 'Dashboard', | 
				
			||||||
 | 
					      component: () => import('@/views/dashboard/index'), | 
				
			||||||
 | 
					      meta: { title: '欢迎', icon: 'dashboard' } | 
				
			||||||
 | 
					    }] | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    path: '/demo', | 
				
			||||||
 | 
					    component: Layout, | 
				
			||||||
 | 
					    children: [ | 
				
			||||||
 | 
					      { | 
				
			||||||
 | 
					        path: 'index', | 
				
			||||||
 | 
					        name: 'Demo', | 
				
			||||||
 | 
					        component: () => import('@/views/demo/index'), | 
				
			||||||
 | 
					        meta: { title: '表格示例', icon: 'table' } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    ] | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 404 page must be placed at the end !!!
 | 
				
			||||||
 | 
					  { path: '*', redirect: '/404', hidden: true } | 
				
			||||||
 | 
					] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createRouter = () => new Router({ | 
				
			||||||
 | 
					  // mode: 'history', // require service support
 | 
				
			||||||
 | 
					  scrollBehavior: () => ({ y: 0 }), | 
				
			||||||
 | 
					  routes: constantRoutes | 
				
			||||||
 | 
					}) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = createRouter() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
 | 
				
			||||||
 | 
					export function resetRouter() { | 
				
			||||||
 | 
					  const newRouter = createRouter() | 
				
			||||||
 | 
					  router.matcher = newRouter.matcher // reset router
 | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default router | 
				
			||||||
@ -0,0 +1,16 @@ | 
				
			|||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  title: 'Vue Demo For Soul2', | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** | 
				
			||||||
 | 
					   * @type {boolean} true | false | 
				
			||||||
 | 
					   * @description Whether fix the header | 
				
			||||||
 | 
					   */ | 
				
			||||||
 | 
					  fixedHeader: false, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** | 
				
			||||||
 | 
					   * @type {boolean} true | false | 
				
			||||||
 | 
					   * @description Whether show the logo in sidebar | 
				
			||||||
 | 
					   */ | 
				
			||||||
 | 
					  sidebarLogo: false | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,8 @@ | 
				
			|||||||
 | 
					const getters = { | 
				
			||||||
 | 
					  sidebar: state => state.app.sidebar, | 
				
			||||||
 | 
					  device: state => state.app.device, | 
				
			||||||
 | 
					  token: state => state.user.token, | 
				
			||||||
 | 
					  avatar: state => state.user.avatar, | 
				
			||||||
 | 
					  name: state => state.user.name | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					export default getters | 
				
			||||||
@ -0,0 +1,19 @@ | 
				
			|||||||
 | 
					import Vue from 'vue' | 
				
			||||||
 | 
					import Vuex from 'vuex' | 
				
			||||||
 | 
					import getters from './getters' | 
				
			||||||
 | 
					import app from './modules/app' | 
				
			||||||
 | 
					import settings from './modules/settings' | 
				
			||||||
 | 
					import user from './modules/user' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vue.use(Vuex) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const store = new Vuex.Store({ | 
				
			||||||
 | 
					  modules: { | 
				
			||||||
 | 
					    app, | 
				
			||||||
 | 
					    settings, | 
				
			||||||
 | 
					    user | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  getters | 
				
			||||||
 | 
					}) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default store | 
				
			||||||
@ -0,0 +1,48 @@ | 
				
			|||||||
 | 
					import Cookies from 'js-cookie' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = { | 
				
			||||||
 | 
					  sidebar: { | 
				
			||||||
 | 
					    opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, | 
				
			||||||
 | 
					    withoutAnimation: false | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  device: 'desktop' | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mutations = { | 
				
			||||||
 | 
					  TOGGLE_SIDEBAR: state => { | 
				
			||||||
 | 
					    state.sidebar.opened = !state.sidebar.opened | 
				
			||||||
 | 
					    state.sidebar.withoutAnimation = false | 
				
			||||||
 | 
					    if (state.sidebar.opened) { | 
				
			||||||
 | 
					      Cookies.set('sidebarStatus', 1) | 
				
			||||||
 | 
					    } else { | 
				
			||||||
 | 
					      Cookies.set('sidebarStatus', 0) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  CLOSE_SIDEBAR: (state, withoutAnimation) => { | 
				
			||||||
 | 
					    Cookies.set('sidebarStatus', 0) | 
				
			||||||
 | 
					    state.sidebar.opened = false | 
				
			||||||
 | 
					    state.sidebar.withoutAnimation = withoutAnimation | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  TOGGLE_DEVICE: (state, device) => { | 
				
			||||||
 | 
					    state.device = device | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const actions = { | 
				
			||||||
 | 
					  toggleSideBar({ commit }) { | 
				
			||||||
 | 
					    commit('TOGGLE_SIDEBAR') | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  closeSideBar({ commit }, { withoutAnimation }) { | 
				
			||||||
 | 
					    commit('CLOSE_SIDEBAR', withoutAnimation) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  toggleDevice({ commit }, device) { | 
				
			||||||
 | 
					    commit('TOGGLE_DEVICE', device) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  namespaced: true, | 
				
			||||||
 | 
					  state, | 
				
			||||||
 | 
					  mutations, | 
				
			||||||
 | 
					  actions | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,32 @@ | 
				
			|||||||
 | 
					import defaultSettings from '@/settings' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { showSettings, fixedHeader, sidebarLogo } = defaultSettings | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = { | 
				
			||||||
 | 
					  showSettings: showSettings, | 
				
			||||||
 | 
					  fixedHeader: fixedHeader, | 
				
			||||||
 | 
					  sidebarLogo: sidebarLogo | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mutations = { | 
				
			||||||
 | 
					  CHANGE_SETTING: (state, { key, value }) => { | 
				
			||||||
 | 
					    // eslint-disable-next-line no-prototype-builtins
 | 
				
			||||||
 | 
					    if (state.hasOwnProperty(key)) { | 
				
			||||||
 | 
					      state[key] = value | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const actions = { | 
				
			||||||
 | 
					  changeSetting({ commit }, data) { | 
				
			||||||
 | 
					    commit('CHANGE_SETTING', data) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  namespaced: true, | 
				
			||||||
 | 
					  state, | 
				
			||||||
 | 
					  mutations, | 
				
			||||||
 | 
					  actions | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,97 @@ | 
				
			|||||||
 | 
					import { getInfo, login, logout } from '@/api/user' | 
				
			||||||
 | 
					import { getToken, removeToken, setToken } from '@/utils/auth' | 
				
			||||||
 | 
					import { resetRouter } from '@/router' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getDefaultState = () => { | 
				
			||||||
 | 
					  return { | 
				
			||||||
 | 
					    token: getToken(), | 
				
			||||||
 | 
					    name: '', | 
				
			||||||
 | 
					    avatar: '' | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = getDefaultState() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mutations = { | 
				
			||||||
 | 
					  RESET_STATE: (state) => { | 
				
			||||||
 | 
					    Object.assign(state, getDefaultState()) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  SET_TOKEN: (state, token) => { | 
				
			||||||
 | 
					    state.token = token | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  SET_NAME: (state, name) => { | 
				
			||||||
 | 
					    state.name = name | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  SET_AVATAR: (state, avatar) => { | 
				
			||||||
 | 
					    state.avatar = avatar | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const actions = { | 
				
			||||||
 | 
					  // user login
 | 
				
			||||||
 | 
					  login({ commit }, userInfo) { | 
				
			||||||
 | 
					    const { username, password } = userInfo | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => { | 
				
			||||||
 | 
					      login({ username: username.trim(), password: password }).then(response => { | 
				
			||||||
 | 
					        const { data } = response | 
				
			||||||
 | 
					        commit('SET_TOKEN', data.token) | 
				
			||||||
 | 
					        setToken(data.token) | 
				
			||||||
 | 
					        resolve() | 
				
			||||||
 | 
					      }).catch(error => { | 
				
			||||||
 | 
					        reject(error) | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // get user info
 | 
				
			||||||
 | 
					  getInfo({ commit, state }) { | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => { | 
				
			||||||
 | 
					      getInfo(state.token).then(response => { | 
				
			||||||
 | 
					        const { data } = response | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!data) { | 
				
			||||||
 | 
					          return reject('Verification failed, please Login again.') | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const { name, avatar } = data | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        commit('SET_NAME', name) | 
				
			||||||
 | 
					        commit('SET_AVATAR', avatar) | 
				
			||||||
 | 
					        resolve(data) | 
				
			||||||
 | 
					      }).catch(error => { | 
				
			||||||
 | 
					        reject(error) | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // user logout
 | 
				
			||||||
 | 
					  logout({ commit, state }) { | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => { | 
				
			||||||
 | 
					      logout(state.token).then(() => { | 
				
			||||||
 | 
					        removeToken() // must remove  token  first
 | 
				
			||||||
 | 
					        resetRouter() | 
				
			||||||
 | 
					        commit('RESET_STATE') | 
				
			||||||
 | 
					        resolve() | 
				
			||||||
 | 
					      }).catch(error => { | 
				
			||||||
 | 
					        reject(error) | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // remove token
 | 
				
			||||||
 | 
					  resetToken({ commit }) { | 
				
			||||||
 | 
					    return new Promise(resolve => { | 
				
			||||||
 | 
					      removeToken() // must remove  token  first
 | 
				
			||||||
 | 
					      commit('RESET_STATE') | 
				
			||||||
 | 
					      resolve() | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  namespaced: true, | 
				
			||||||
 | 
					  state, | 
				
			||||||
 | 
					  mutations, | 
				
			||||||
 | 
					  actions | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,49 @@ | 
				
			|||||||
 | 
					// cover some element-ui styles | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.el-breadcrumb__inner, | 
				
			||||||
 | 
					.el-breadcrumb__inner a { | 
				
			||||||
 | 
					  font-weight: 400 !important; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.el-upload { | 
				
			||||||
 | 
					  input[type="file"] { | 
				
			||||||
 | 
					    display: none !important; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.el-upload__input { | 
				
			||||||
 | 
					  display: none; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// to fixed https://github.com/ElemeFE/element/issues/2461 | 
				
			||||||
 | 
					.el-dialog { | 
				
			||||||
 | 
					  transform: none; | 
				
			||||||
 | 
					  left: 0; | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  margin: 0 auto; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// refine element ui upload | 
				
			||||||
 | 
					.upload-container { | 
				
			||||||
 | 
					  .el-upload { | 
				
			||||||
 | 
					    width: 100%; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-upload-dragger { | 
				
			||||||
 | 
					      width: 100%; | 
				
			||||||
 | 
					      height: 200px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dropdown | 
				
			||||||
 | 
					.el-dropdown-menu { | 
				
			||||||
 | 
					  a { | 
				
			||||||
 | 
					    display: block | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// to fix el-date-picker css style | 
				
			||||||
 | 
					.el-range-separator { | 
				
			||||||
 | 
					  box-sizing: content-box; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,122 @@ | 
				
			|||||||
 | 
					@import "./variables.scss"; | 
				
			||||||
 | 
					@import "./mixin.scss"; | 
				
			||||||
 | 
					@import "./transition.scss"; | 
				
			||||||
 | 
					@import "./element-ui.scss"; | 
				
			||||||
 | 
					@import "./sidebar.scss"; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body { | 
				
			||||||
 | 
					  height: 100%; | 
				
			||||||
 | 
					  -moz-osx-font-smoothing: grayscale; | 
				
			||||||
 | 
					  -webkit-font-smoothing: antialiased; | 
				
			||||||
 | 
					  text-rendering: optimizeLegibility; | 
				
			||||||
 | 
					  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, | 
				
			||||||
 | 
					  Microsoft YaHei, Arial, sans-serif; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					label { | 
				
			||||||
 | 
					  font-weight: 700; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html { | 
				
			||||||
 | 
					  height: 100%; | 
				
			||||||
 | 
					  box-sizing: border-box; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#app { | 
				
			||||||
 | 
					  height: 100%; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*, | 
				
			||||||
 | 
					*:before, | 
				
			||||||
 | 
					*:after { | 
				
			||||||
 | 
					  box-sizing: inherit; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a:focus, | 
				
			||||||
 | 
					a:active { | 
				
			||||||
 | 
					  outline: none; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a, | 
				
			||||||
 | 
					a:focus, | 
				
			||||||
 | 
					a:hover { | 
				
			||||||
 | 
					  cursor: pointer; | 
				
			||||||
 | 
					  color: inherit; | 
				
			||||||
 | 
					  text-decoration: none; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div:focus { | 
				
			||||||
 | 
					  outline: none; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.clearfix { | 
				
			||||||
 | 
					  &:after { | 
				
			||||||
 | 
					    visibility: hidden; | 
				
			||||||
 | 
					    display: block; | 
				
			||||||
 | 
					    font-size: 0; | 
				
			||||||
 | 
					    content: " "; | 
				
			||||||
 | 
					    clear: both; | 
				
			||||||
 | 
					    height: 0; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// main-container global css | 
				
			||||||
 | 
					.app-container { | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  padding: 20px; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// table-operate-container global css | 
				
			||||||
 | 
					.table-operate-container { | 
				
			||||||
 | 
					  margin-top: 20px; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// table-container global css | 
				
			||||||
 | 
					.table-container { | 
				
			||||||
 | 
					  margin-top: 20px; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 分页栏样式 | 
				
			||||||
 | 
					.pagination-container { | 
				
			||||||
 | 
					  margin-top: 10px; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// element-ui message 位置往上提 | 
				
			||||||
 | 
					.el-message-box__wrapper { | 
				
			||||||
 | 
					  height: 70%; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.full-width { | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.full-height { | 
				
			||||||
 | 
					  height: 100%; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// webkit内核滚动条优化(Chrome 和 Safari 浏览器) | 
				
			||||||
 | 
					*:hover::-webkit-scrollbar { | 
				
			||||||
 | 
					  display: block; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::-webkit-scrollbar { | 
				
			||||||
 | 
					  display: none; | 
				
			||||||
 | 
					  /*滚动条整体样式*/ | 
				
			||||||
 | 
					  width: 9px; /*高宽分别对应横竖滚动条的尺寸*/ | 
				
			||||||
 | 
					  height: 9px; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::-webkit-scrollbar-thumb { | 
				
			||||||
 | 
					  /*滚动条里面小方块*/ | 
				
			||||||
 | 
					  border-radius: 7px; | 
				
			||||||
 | 
					  background-color: rgba(144, 147, 153, 0.3); | 
				
			||||||
 | 
					  -webkit-transition: 0.3s background-color; | 
				
			||||||
 | 
					  transition: 0.3s background-color; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::-webkit-scrollbar-track { | 
				
			||||||
 | 
					  /*滚动条里面轨道*/ | 
				
			||||||
 | 
					  /* -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); | 
				
			||||||
 | 
					  border-radius: 6px; | 
				
			||||||
 | 
					  background-color: rgba(144,147,153,.3); */ | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,28 @@ | 
				
			|||||||
 | 
					@mixin clearfix { | 
				
			||||||
 | 
					  &:after { | 
				
			||||||
 | 
					    content: ""; | 
				
			||||||
 | 
					    display: table; | 
				
			||||||
 | 
					    clear: both; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin scrollBar { | 
				
			||||||
 | 
					  &::-webkit-scrollbar-track-piece { | 
				
			||||||
 | 
					    background: #d3dce6; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::-webkit-scrollbar { | 
				
			||||||
 | 
					    width: 6px; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::-webkit-scrollbar-thumb { | 
				
			||||||
 | 
					    background: #99a9bf; | 
				
			||||||
 | 
					    border-radius: 20px; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin relative { | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					  height: 100%; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,228 @@ | 
				
			|||||||
 | 
					#app { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .main-container { | 
				
			||||||
 | 
					    height: 100%; | 
				
			||||||
 | 
					    transition: margin-left .28s; | 
				
			||||||
 | 
					    margin-left: $sideBarWidth; | 
				
			||||||
 | 
					    position: relative; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .sidebar-container { | 
				
			||||||
 | 
					    transition: width 0.28s; | 
				
			||||||
 | 
					    width: $sideBarWidth !important; | 
				
			||||||
 | 
					    background-color: $menuBg; | 
				
			||||||
 | 
					    height: 100%; | 
				
			||||||
 | 
					    position: fixed; | 
				
			||||||
 | 
					    border-right: 1px solid #EBEEF5; | 
				
			||||||
 | 
					    font-size: 0px; | 
				
			||||||
 | 
					    top: 0; | 
				
			||||||
 | 
					    bottom: 0; | 
				
			||||||
 | 
					    left: 0; | 
				
			||||||
 | 
					    z-index: 1001; | 
				
			||||||
 | 
					    overflow: hidden; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // reset element-ui css | 
				
			||||||
 | 
					    .horizontal-collapse-transition { | 
				
			||||||
 | 
					      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .scrollbar-wrapper { | 
				
			||||||
 | 
					      overflow-x: hidden !important; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-scrollbar__bar.is-vertical { | 
				
			||||||
 | 
					      right: 0px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-scrollbar { | 
				
			||||||
 | 
					      height: 100%; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.has-logo { | 
				
			||||||
 | 
					      .el-scrollbar { | 
				
			||||||
 | 
					        height: calc(100% - 50px); | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .is-horizontal { | 
				
			||||||
 | 
					      display: none; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a { | 
				
			||||||
 | 
					      display: inline-block; | 
				
			||||||
 | 
					      width: 100%; | 
				
			||||||
 | 
					      overflow: hidden; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .svg-icon { | 
				
			||||||
 | 
					      margin-right: 16px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .sub-el-icon { | 
				
			||||||
 | 
					      margin-right: 12px; | 
				
			||||||
 | 
					      margin-left: -2px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-menu { | 
				
			||||||
 | 
					      border: none; | 
				
			||||||
 | 
					      height: 100%; | 
				
			||||||
 | 
					      width: 100% !important; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // menu hover | 
				
			||||||
 | 
					    .submenu-title-noDropdown, | 
				
			||||||
 | 
					    .el-submenu__title { | 
				
			||||||
 | 
					      &:hover { | 
				
			||||||
 | 
					        background-color: $menuHover !important; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .is-active > .el-submenu__title { | 
				
			||||||
 | 
					      color: $subMenuActiveText !important; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    & .nest-menu .el-submenu > .el-submenu__title, | 
				
			||||||
 | 
					    & .el-submenu .el-menu-item { | 
				
			||||||
 | 
					      min-width: $sideBarWidth !important; | 
				
			||||||
 | 
					      background-color: $subMenuBg !important; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover { | 
				
			||||||
 | 
					        background-color: $subMenuHover !important; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .hideSidebar { | 
				
			||||||
 | 
					    .sidebar-container { | 
				
			||||||
 | 
					      width: 54px !important; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .main-container { | 
				
			||||||
 | 
					      margin-left: 54px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .submenu-title-noDropdown { | 
				
			||||||
 | 
					      padding: 0 !important; | 
				
			||||||
 | 
					      position: relative; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .el-tooltip { | 
				
			||||||
 | 
					        padding: 0 !important; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .svg-icon { | 
				
			||||||
 | 
					          margin-left: 20px; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .sub-el-icon { | 
				
			||||||
 | 
					          margin-left: 19px; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-submenu { | 
				
			||||||
 | 
					      overflow: hidden; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      & > .el-submenu__title { | 
				
			||||||
 | 
					        padding: 0 !important; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .svg-icon { | 
				
			||||||
 | 
					          margin-left: 20px; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .sub-el-icon { | 
				
			||||||
 | 
					          margin-left: 19px; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .el-submenu__icon-arrow { | 
				
			||||||
 | 
					          display: none; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-menu--collapse { | 
				
			||||||
 | 
					      .el-submenu { | 
				
			||||||
 | 
					        & > .el-submenu__title { | 
				
			||||||
 | 
					          & > span { | 
				
			||||||
 | 
					            height: 0; | 
				
			||||||
 | 
					            width: 0; | 
				
			||||||
 | 
					            overflow: hidden; | 
				
			||||||
 | 
					            visibility: hidden; | 
				
			||||||
 | 
					            display: inline-block; | 
				
			||||||
 | 
					          } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .el-menu--collapse .el-menu .el-submenu { | 
				
			||||||
 | 
					    min-width: $sideBarWidth !important; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // mobile responsive | 
				
			||||||
 | 
					  .mobile { | 
				
			||||||
 | 
					    .main-container { | 
				
			||||||
 | 
					      margin-left: 0px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .sidebar-container { | 
				
			||||||
 | 
					      transition: transform .28s; | 
				
			||||||
 | 
					      width: $sideBarWidth !important; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.hideSidebar { | 
				
			||||||
 | 
					      .sidebar-container { | 
				
			||||||
 | 
					        pointer-events: none; | 
				
			||||||
 | 
					        transition-duration: 0.3s; | 
				
			||||||
 | 
					        transform: translate3d(-$sideBarWidth, 0, 0); | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .withoutAnimation { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .main-container, | 
				
			||||||
 | 
					    .sidebar-container { | 
				
			||||||
 | 
					      transition: none; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// when menu collapsed | 
				
			||||||
 | 
					.el-menu--vertical { | 
				
			||||||
 | 
					  & > .el-menu { | 
				
			||||||
 | 
					    .svg-icon { | 
				
			||||||
 | 
					      margin-right: 16px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .sub-el-icon { | 
				
			||||||
 | 
					      margin-right: 12px; | 
				
			||||||
 | 
					      margin-left: -2px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .nest-menu .el-submenu > .el-submenu__title, | 
				
			||||||
 | 
					  .el-menu-item { | 
				
			||||||
 | 
					    &:hover { | 
				
			||||||
 | 
					      // you can use $subMenuHover | 
				
			||||||
 | 
					      background-color: $menuHover !important; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // the scroll bar appears when the subMenu is too long | 
				
			||||||
 | 
					  > .el-menu--popup { | 
				
			||||||
 | 
					    max-height: 100vh; | 
				
			||||||
 | 
					    overflow-y: auto; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::-webkit-scrollbar-track-piece { | 
				
			||||||
 | 
					      background: #d3dce6; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::-webkit-scrollbar { | 
				
			||||||
 | 
					      width: 20px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::-webkit-scrollbar-thumb { | 
				
			||||||
 | 
					      background: #99a9bf; | 
				
			||||||
 | 
					      border-radius: 20px; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,48 @@ | 
				
			|||||||
 | 
					// global transition css | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* fade */ | 
				
			||||||
 | 
					.fade-enter-active, | 
				
			||||||
 | 
					.fade-leave-active { | 
				
			||||||
 | 
					  transition: opacity 0.28s; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fade-enter, | 
				
			||||||
 | 
					.fade-leave-active { | 
				
			||||||
 | 
					  opacity: 0; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* fade-transform */ | 
				
			||||||
 | 
					.fade-transform-leave-active, | 
				
			||||||
 | 
					.fade-transform-enter-active { | 
				
			||||||
 | 
					  transition: all .5s; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fade-transform-enter { | 
				
			||||||
 | 
					  opacity: 0; | 
				
			||||||
 | 
					  transform: translateX(-30px); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fade-transform-leave-to { | 
				
			||||||
 | 
					  opacity: 0; | 
				
			||||||
 | 
					  transform: translateX(30px); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* breadcrumb transition */ | 
				
			||||||
 | 
					.breadcrumb-enter-active, | 
				
			||||||
 | 
					.breadcrumb-leave-active { | 
				
			||||||
 | 
					  transition: all .5s; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-enter, | 
				
			||||||
 | 
					.breadcrumb-leave-active { | 
				
			||||||
 | 
					  opacity: 0; | 
				
			||||||
 | 
					  transform: translateX(20px); | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-move { | 
				
			||||||
 | 
					  transition: all .5s; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.breadcrumb-leave-active { | 
				
			||||||
 | 
					  position: absolute; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,36 @@ | 
				
			|||||||
 | 
					// sidebar | 
				
			||||||
 | 
					//$menuText:#000; | 
				
			||||||
 | 
					//$menuActiveText:#409EFF; | 
				
			||||||
 | 
					//$subMenuActiveText:#409EFF; //https://github.com/ElemeFE/element/issues/12951 | 
				
			||||||
 | 
					// | 
				
			||||||
 | 
					//$menuBg:#fff; | 
				
			||||||
 | 
					//$menuHover:#ecf5ff; | 
				
			||||||
 | 
					// | 
				
			||||||
 | 
					//$subMenuBg:#fff; | 
				
			||||||
 | 
					//$subMenuHover:#ecf5ff; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//// sidebar 暗色主题 | 
				
			||||||
 | 
					$menuText: #bfcbd9; | 
				
			||||||
 | 
					$menuActiveText: #409EFF; | 
				
			||||||
 | 
					$subMenuActiveText: #f4f4f5; //https://github.com/ElemeFE/element/issues/12951 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$menuBg: #304156; | 
				
			||||||
 | 
					$menuHover: #263445; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$subMenuBg: #1f2d3d; | 
				
			||||||
 | 
					$subMenuHover: #001528; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$sideBarWidth: 210px; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// the :export directive is the magic sauce for webpack | 
				
			||||||
 | 
					// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass | 
				
			||||||
 | 
					:export { | 
				
			||||||
 | 
					  menuText: $menuText; | 
				
			||||||
 | 
					  menuActiveText: $menuActiveText; | 
				
			||||||
 | 
					  subMenuActiveText: $subMenuActiveText; | 
				
			||||||
 | 
					  menuBg: $menuBg; | 
				
			||||||
 | 
					  menuHover: $menuHover; | 
				
			||||||
 | 
					  subMenuBg: $subMenuBg; | 
				
			||||||
 | 
					  subMenuHover: $subMenuHover; | 
				
			||||||
 | 
					  sideBarWidth: $sideBarWidth; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,15 @@ | 
				
			|||||||
 | 
					import Cookies from 'js-cookie' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TokenKey = 'vue_admin_template_token' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getToken() { | 
				
			||||||
 | 
					  return Cookies.get(TokenKey) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function setToken(token) { | 
				
			||||||
 | 
					  return Cookies.set(TokenKey, token) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function removeToken() { | 
				
			||||||
 | 
					  return Cookies.remove(TokenKey) | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,10 @@ | 
				
			|||||||
 | 
					import defaultSettings from '@/settings' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const title = defaultSettings.title || 'Vue Admin Template' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function getPageTitle(pageTitle) { | 
				
			||||||
 | 
					  if (pageTitle) { | 
				
			||||||
 | 
					    return `${pageTitle} - ${title}` | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  return `${title}` | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,119 @@ | 
				
			|||||||
 | 
					/** | 
				
			||||||
 | 
					 * Created by PanJiaChen on 16/11/18. | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * Parse the time to string | 
				
			||||||
 | 
					 * @param {(Object|string|number)} time | 
				
			||||||
 | 
					 * @param {string} cFormat | 
				
			||||||
 | 
					 * @returns {string | null} | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					export function parseTime(time, cFormat) { | 
				
			||||||
 | 
					  if (arguments.length === 0 || !time) { | 
				
			||||||
 | 
					    return null | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' | 
				
			||||||
 | 
					  let date | 
				
			||||||
 | 
					  if (typeof time === 'object') { | 
				
			||||||
 | 
					    date = time | 
				
			||||||
 | 
					  } else { | 
				
			||||||
 | 
					    if ((typeof time === 'string')) { | 
				
			||||||
 | 
					      if ((/^[0-9]+$/.test(time))) { | 
				
			||||||
 | 
					        // support "1548221490638"
 | 
				
			||||||
 | 
					        time = parseInt(time) | 
				
			||||||
 | 
					      } else { | 
				
			||||||
 | 
					        // support safari
 | 
				
			||||||
 | 
					        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
 | 
				
			||||||
 | 
					        time = time.replace(new RegExp(/-/gm), '/') | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ((typeof time === 'number') && (time.toString().length === 10)) { | 
				
			||||||
 | 
					      time = time * 1000 | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    date = new Date(time) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  const formatObj = { | 
				
			||||||
 | 
					    y: date.getFullYear(), | 
				
			||||||
 | 
					    m: date.getMonth() + 1, | 
				
			||||||
 | 
					    d: date.getDate(), | 
				
			||||||
 | 
					    h: date.getHours(), | 
				
			||||||
 | 
					    i: date.getMinutes(), | 
				
			||||||
 | 
					    s: date.getSeconds(), | 
				
			||||||
 | 
					    a: date.getDay() | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { | 
				
			||||||
 | 
					    const value = formatObj[key] | 
				
			||||||
 | 
					    // Note: getDay() returns 0 on Sunday
 | 
				
			||||||
 | 
					    if (key === 'a') { | 
				
			||||||
 | 
					      return ['日', '一', '二', '三', '四', '五', '六'][value] | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    return value.toString().padStart(2, '0') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  return time_str | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * @param {number} time | 
				
			||||||
 | 
					 * @param {string} option | 
				
			||||||
 | 
					 * @returns {string} | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					export function formatTime(time, option) { | 
				
			||||||
 | 
					  if (('' + time).length === 10) { | 
				
			||||||
 | 
					    time = parseInt(time) * 1000 | 
				
			||||||
 | 
					  } else { | 
				
			||||||
 | 
					    time = +time | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  const d = new Date(time) | 
				
			||||||
 | 
					  const now = Date.now() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const diff = (now - d) / 1000 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (diff < 30) { | 
				
			||||||
 | 
					    return '刚刚' | 
				
			||||||
 | 
					  } else if (diff < 3600) { | 
				
			||||||
 | 
					    // less 1 hour
 | 
				
			||||||
 | 
					    return Math.ceil(diff / 60) + '分钟前' | 
				
			||||||
 | 
					  } else if (diff < 3600 * 24) { | 
				
			||||||
 | 
					    return Math.ceil(diff / 3600) + '小时前' | 
				
			||||||
 | 
					  } else if (diff < 3600 * 24 * 2) { | 
				
			||||||
 | 
					    return '1天前' | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  if (option) { | 
				
			||||||
 | 
					    return parseTime(time, option) | 
				
			||||||
 | 
					  } else { | 
				
			||||||
 | 
					    return ( | 
				
			||||||
 | 
					      d.getMonth() + | 
				
			||||||
 | 
					      1 + | 
				
			||||||
 | 
					      '月' + | 
				
			||||||
 | 
					      d.getDate() + | 
				
			||||||
 | 
					      '日' + | 
				
			||||||
 | 
					      d.getHours() + | 
				
			||||||
 | 
					      '时' + | 
				
			||||||
 | 
					      d.getMinutes() + | 
				
			||||||
 | 
					      '分' | 
				
			||||||
 | 
					    ) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * @param {string} url | 
				
			||||||
 | 
					 * @returns {Object} | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					export function param2Obj(url) { | 
				
			||||||
 | 
					  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') | 
				
			||||||
 | 
					  if (!search) { | 
				
			||||||
 | 
					    return {} | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					  const obj = {} | 
				
			||||||
 | 
					  const searchArr = search.split('&') | 
				
			||||||
 | 
					  searchArr.forEach(v => { | 
				
			||||||
 | 
					    const index = v.indexOf('=') | 
				
			||||||
 | 
					    if (index !== -1) { | 
				
			||||||
 | 
					      const name = v.substring(0, index) | 
				
			||||||
 | 
					      const val = v.substring(index + 1, v.length) | 
				
			||||||
 | 
					      obj[name] = val | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  return obj | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,88 @@ | 
				
			|||||||
 | 
					import axios from 'axios' | 
				
			||||||
 | 
					import { Message, MessageBox } from 'element-ui' | 
				
			||||||
 | 
					import store from '@/store' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// create an axios instance
 | 
				
			||||||
 | 
					const service = axios.create({ | 
				
			||||||
 | 
					  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
 | 
				
			||||||
 | 
					  // withCredentials: true, // send cookies when cross-domain requests
 | 
				
			||||||
 | 
					  timeout: 20000, // request timeout
 | 
				
			||||||
 | 
					  method: 'POST', | 
				
			||||||
 | 
					  headers: { | 
				
			||||||
 | 
					    'Content-Type': 'application/json' | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					}) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// request interceptor
 | 
				
			||||||
 | 
					service.interceptors.request.use( | 
				
			||||||
 | 
					  config => { | 
				
			||||||
 | 
					    // do something before request is sent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if (store.getters.token) {
 | 
				
			||||||
 | 
					    //   // let each request carry token
 | 
				
			||||||
 | 
					    //   // ['X-Token'] is a custom headers key
 | 
				
			||||||
 | 
					    //   // please modify it according to the actual situation
 | 
				
			||||||
 | 
					    //   config.headers['X-Token'] = getToken()
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					    return config | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  error => { | 
				
			||||||
 | 
					    // do something with request error
 | 
				
			||||||
 | 
					    console.log(error) // for debug
 | 
				
			||||||
 | 
					    return Promise.reject(error) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// response interceptor
 | 
				
			||||||
 | 
					service.interceptors.response.use( | 
				
			||||||
 | 
					  /** | 
				
			||||||
 | 
					   * If you want to get http information such as headers or status | 
				
			||||||
 | 
					   * Please return  response => response | 
				
			||||||
 | 
					   */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** | 
				
			||||||
 | 
					   * Determine the request status by custom code | 
				
			||||||
 | 
					   * Here is just an example | 
				
			||||||
 | 
					   * You can also judge the status by HTTP Status Code | 
				
			||||||
 | 
					   */ | 
				
			||||||
 | 
					  response => { | 
				
			||||||
 | 
					    const res = response.data | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if the custom code is not 0, it is judged as an error.
 | 
				
			||||||
 | 
					    if (res.code !== 0) { | 
				
			||||||
 | 
					      Message({ | 
				
			||||||
 | 
					        message: res.message || 'Error', | 
				
			||||||
 | 
					        type: 'error', | 
				
			||||||
 | 
					        duration: 5 * 1000 | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 40401:非法令牌;40402:其他客户登录;40403:令牌过期;
 | 
				
			||||||
 | 
					      if (res.code === 40401 || res.code === 40402 || res.code === 40403) { | 
				
			||||||
 | 
					        // to re-login
 | 
				
			||||||
 | 
					        MessageBox.confirm('当前账户的登录信息已过期,点击“取消”按钮停留在当前页面,或重新登录', '走丢了', { | 
				
			||||||
 | 
					          confirmButtonText: '重新登录', | 
				
			||||||
 | 
					          cancelButtonText: '取消', | 
				
			||||||
 | 
					          type: 'warning' | 
				
			||||||
 | 
					        }).then(() => { | 
				
			||||||
 | 
					          store.dispatch('user/resetToken').then(() => { | 
				
			||||||
 | 
					            location.reload() | 
				
			||||||
 | 
					          }) | 
				
			||||||
 | 
					        }) | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      return Promise.reject(new Error(res.message || 'Error')) | 
				
			||||||
 | 
					    } else { | 
				
			||||||
 | 
					      return res | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  error => { | 
				
			||||||
 | 
					    console.log('err' + error) // for debug
 | 
				
			||||||
 | 
					    Message({ | 
				
			||||||
 | 
					      message: error.message, | 
				
			||||||
 | 
					      type: 'error', | 
				
			||||||
 | 
					      duration: 5 * 1000 | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					    return Promise.reject(error) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default service | 
				
			||||||
@ -0,0 +1,20 @@ | 
				
			|||||||
 | 
					/** | 
				
			||||||
 | 
					 * Created by PanJiaChen on 16/11/18. | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * @param {string} path | 
				
			||||||
 | 
					 * @returns {Boolean} | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					export function isExternal(path) { | 
				
			||||||
 | 
					  return /^(https?:|mailto:|tel:)/.test(path) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * @param {string} str | 
				
			||||||
 | 
					 * @returns {Boolean} | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					export function validUsername(str) { | 
				
			||||||
 | 
					  const valid_map = ['admin', 'editor'] | 
				
			||||||
 | 
					  return valid_map.indexOf(str.trim()) >= 0 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,244 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div class="wscn-http404-container"> | 
				
			||||||
 | 
					    <div class="wscn-http404"> | 
				
			||||||
 | 
					      <div class="pic-404"> | 
				
			||||||
 | 
					        <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404"> | 
				
			||||||
 | 
					        <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404"> | 
				
			||||||
 | 
					        <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404"> | 
				
			||||||
 | 
					        <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404"> | 
				
			||||||
 | 
					      </div> | 
				
			||||||
 | 
					      <div class="bullshit"> | 
				
			||||||
 | 
					        <div class="bullshit__oops">OOPS!</div> | 
				
			||||||
 | 
					        <div class="bullshit__info">All rights reserved | 
				
			||||||
 | 
					          <a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a> | 
				
			||||||
 | 
					        </div> | 
				
			||||||
 | 
					        <div class="bullshit__headline">{{ message }}</div> | 
				
			||||||
 | 
					        <div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to | 
				
			||||||
 | 
					          return to the homepage. | 
				
			||||||
 | 
					        </div> | 
				
			||||||
 | 
					        <a href="" class="bullshit__return-home">Back to home</a> | 
				
			||||||
 | 
					      </div> | 
				
			||||||
 | 
					    </div> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'Page404', | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    message() { | 
				
			||||||
 | 
					      return 'The webmaster said that you can not enter this page...' | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped> | 
				
			||||||
 | 
					.wscn-http404-container { | 
				
			||||||
 | 
					  transform: translate(-50%, -50%); | 
				
			||||||
 | 
					  position: absolute; | 
				
			||||||
 | 
					  top: 40%; | 
				
			||||||
 | 
					  left: 50%; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.wscn-http404 { | 
				
			||||||
 | 
					  position: relative; | 
				
			||||||
 | 
					  width: 1200px; | 
				
			||||||
 | 
					  padding: 0 50px; | 
				
			||||||
 | 
					  overflow: hidden; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .pic-404 { | 
				
			||||||
 | 
					    position: relative; | 
				
			||||||
 | 
					    float: left; | 
				
			||||||
 | 
					    width: 600px; | 
				
			||||||
 | 
					    overflow: hidden; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__parent { | 
				
			||||||
 | 
					      width: 100%; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__child { | 
				
			||||||
 | 
					      position: absolute; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.left { | 
				
			||||||
 | 
					        width: 80px; | 
				
			||||||
 | 
					        top: 17px; | 
				
			||||||
 | 
					        left: 220px; | 
				
			||||||
 | 
					        opacity: 0; | 
				
			||||||
 | 
					        animation-name: cloudLeft; | 
				
			||||||
 | 
					        animation-duration: 2s; | 
				
			||||||
 | 
					        animation-timing-function: linear; | 
				
			||||||
 | 
					        animation-fill-mode: forwards; | 
				
			||||||
 | 
					        animation-delay: 1s; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.mid { | 
				
			||||||
 | 
					        width: 46px; | 
				
			||||||
 | 
					        top: 10px; | 
				
			||||||
 | 
					        left: 420px; | 
				
			||||||
 | 
					        opacity: 0; | 
				
			||||||
 | 
					        animation-name: cloudMid; | 
				
			||||||
 | 
					        animation-duration: 2s; | 
				
			||||||
 | 
					        animation-timing-function: linear; | 
				
			||||||
 | 
					        animation-fill-mode: forwards; | 
				
			||||||
 | 
					        animation-delay: 1.2s; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.right { | 
				
			||||||
 | 
					        width: 62px; | 
				
			||||||
 | 
					        top: 100px; | 
				
			||||||
 | 
					        left: 500px; | 
				
			||||||
 | 
					        opacity: 0; | 
				
			||||||
 | 
					        animation-name: cloudRight; | 
				
			||||||
 | 
					        animation-duration: 2s; | 
				
			||||||
 | 
					        animation-timing-function: linear; | 
				
			||||||
 | 
					        animation-fill-mode: forwards; | 
				
			||||||
 | 
					        animation-delay: 1s; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @keyframes cloudLeft { | 
				
			||||||
 | 
					        0% { | 
				
			||||||
 | 
					          top: 17px; | 
				
			||||||
 | 
					          left: 220px; | 
				
			||||||
 | 
					          opacity: 0; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        20% { | 
				
			||||||
 | 
					          top: 33px; | 
				
			||||||
 | 
					          left: 188px; | 
				
			||||||
 | 
					          opacity: 1; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        80% { | 
				
			||||||
 | 
					          top: 81px; | 
				
			||||||
 | 
					          left: 92px; | 
				
			||||||
 | 
					          opacity: 1; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        100% { | 
				
			||||||
 | 
					          top: 97px; | 
				
			||||||
 | 
					          left: 60px; | 
				
			||||||
 | 
					          opacity: 0; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      @keyframes cloudMid { | 
				
			||||||
 | 
					        0% { | 
				
			||||||
 | 
					          top: 10px; | 
				
			||||||
 | 
					          left: 420px; | 
				
			||||||
 | 
					          opacity: 0; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        20% { | 
				
			||||||
 | 
					          top: 40px; | 
				
			||||||
 | 
					          left: 360px; | 
				
			||||||
 | 
					          opacity: 1; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        70% { | 
				
			||||||
 | 
					          top: 130px; | 
				
			||||||
 | 
					          left: 180px; | 
				
			||||||
 | 
					          opacity: 1; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        100% { | 
				
			||||||
 | 
					          top: 160px; | 
				
			||||||
 | 
					          left: 120px; | 
				
			||||||
 | 
					          opacity: 0; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      @keyframes cloudRight { | 
				
			||||||
 | 
					        0% { | 
				
			||||||
 | 
					          top: 100px; | 
				
			||||||
 | 
					          left: 500px; | 
				
			||||||
 | 
					          opacity: 0; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        20% { | 
				
			||||||
 | 
					          top: 120px; | 
				
			||||||
 | 
					          left: 460px; | 
				
			||||||
 | 
					          opacity: 1; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        80% { | 
				
			||||||
 | 
					          top: 180px; | 
				
			||||||
 | 
					          left: 340px; | 
				
			||||||
 | 
					          opacity: 1; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        100% { | 
				
			||||||
 | 
					          top: 200px; | 
				
			||||||
 | 
					          left: 300px; | 
				
			||||||
 | 
					          opacity: 0; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .bullshit { | 
				
			||||||
 | 
					    position: relative; | 
				
			||||||
 | 
					    float: left; | 
				
			||||||
 | 
					    width: 300px; | 
				
			||||||
 | 
					    padding: 30px 0; | 
				
			||||||
 | 
					    overflow: hidden; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__oops { | 
				
			||||||
 | 
					      font-size: 32px; | 
				
			||||||
 | 
					      font-weight: bold; | 
				
			||||||
 | 
					      line-height: 40px; | 
				
			||||||
 | 
					      color: #1482f0; | 
				
			||||||
 | 
					      opacity: 0; | 
				
			||||||
 | 
					      margin-bottom: 20px; | 
				
			||||||
 | 
					      animation-name: slideUp; | 
				
			||||||
 | 
					      animation-duration: 0.5s; | 
				
			||||||
 | 
					      animation-fill-mode: forwards; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__headline { | 
				
			||||||
 | 
					      font-size: 20px; | 
				
			||||||
 | 
					      line-height: 24px; | 
				
			||||||
 | 
					      color: #222; | 
				
			||||||
 | 
					      font-weight: bold; | 
				
			||||||
 | 
					      opacity: 0; | 
				
			||||||
 | 
					      margin-bottom: 10px; | 
				
			||||||
 | 
					      animation-name: slideUp; | 
				
			||||||
 | 
					      animation-duration: 0.5s; | 
				
			||||||
 | 
					      animation-delay: 0.1s; | 
				
			||||||
 | 
					      animation-fill-mode: forwards; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__info { | 
				
			||||||
 | 
					      font-size: 13px; | 
				
			||||||
 | 
					      line-height: 21px; | 
				
			||||||
 | 
					      color: grey; | 
				
			||||||
 | 
					      opacity: 0; | 
				
			||||||
 | 
					      margin-bottom: 30px; | 
				
			||||||
 | 
					      animation-name: slideUp; | 
				
			||||||
 | 
					      animation-duration: 0.5s; | 
				
			||||||
 | 
					      animation-delay: 0.2s; | 
				
			||||||
 | 
					      animation-fill-mode: forwards; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__return-home { | 
				
			||||||
 | 
					      display: block; | 
				
			||||||
 | 
					      float: left; | 
				
			||||||
 | 
					      width: 110px; | 
				
			||||||
 | 
					      height: 36px; | 
				
			||||||
 | 
					      background: #1482f0; | 
				
			||||||
 | 
					      border-radius: 100px; | 
				
			||||||
 | 
					      text-align: center; | 
				
			||||||
 | 
					      color: #ffffff; | 
				
			||||||
 | 
					      opacity: 0; | 
				
			||||||
 | 
					      font-size: 14px; | 
				
			||||||
 | 
					      line-height: 36px; | 
				
			||||||
 | 
					      cursor: pointer; | 
				
			||||||
 | 
					      animation-name: slideUp; | 
				
			||||||
 | 
					      animation-duration: 0.5s; | 
				
			||||||
 | 
					      animation-delay: 0.3s; | 
				
			||||||
 | 
					      animation-fill-mode: forwards; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @keyframes slideUp { | 
				
			||||||
 | 
					      0% { | 
				
			||||||
 | 
					        transform: translateY(60px); | 
				
			||||||
 | 
					        opacity: 0; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      100% { | 
				
			||||||
 | 
					        transform: translateY(0); | 
				
			||||||
 | 
					        opacity: 1; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,31 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div class="dashboard-container"> | 
				
			||||||
 | 
					    <div class="dashboard-text">name: {{ name }}</div> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { mapGetters } from 'vuex' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'Dashboard', | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    ...mapGetters([ | 
				
			||||||
 | 
					      'name' | 
				
			||||||
 | 
					    ]) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped> | 
				
			||||||
 | 
					.dashboard { | 
				
			||||||
 | 
					  &-container { | 
				
			||||||
 | 
					    margin: 30px; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-text { | 
				
			||||||
 | 
					    font-size: 30px; | 
				
			||||||
 | 
					    line-height: 46px; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,107 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <el-dialog ref="dialogForm" width="45%" :title="title" :visible.sync="visible" @close="closeDialog"> | 
				
			||||||
 | 
					    <el-form label-width="25%" ref="edit" :rules="rules" :model="edit"> | 
				
			||||||
 | 
					      <el-input v-show="false" v-model="edit.id"/> | 
				
			||||||
 | 
					      <el-form-item label="姓名" prop="name"> | 
				
			||||||
 | 
					        <el-col :span="18"> | 
				
			||||||
 | 
					          <el-input v-model="edit.name"/> | 
				
			||||||
 | 
					        </el-col> | 
				
			||||||
 | 
					      </el-form-item> | 
				
			||||||
 | 
					      <el-form-item label="属性1" prop="attr1"> | 
				
			||||||
 | 
					        <el-col :span="18"> | 
				
			||||||
 | 
					          <el-input v-model="edit.attr1"/> | 
				
			||||||
 | 
					        </el-col> | 
				
			||||||
 | 
					      </el-form-item> | 
				
			||||||
 | 
					      <el-form-item label="属性2" prop="attr2"> | 
				
			||||||
 | 
					        <el-col :span="18"> | 
				
			||||||
 | 
					          <el-input v-model="edit.attr2"/> | 
				
			||||||
 | 
					        </el-col> | 
				
			||||||
 | 
					      </el-form-item> | 
				
			||||||
 | 
					    </el-form> | 
				
			||||||
 | 
					    <div slot="footer" class="dialog-footer"> | 
				
			||||||
 | 
					      <el-button @click="visible = false">取消</el-button> | 
				
			||||||
 | 
					      <el-button type="primary" @click="handleSave">保存</el-button> | 
				
			||||||
 | 
					    </div> | 
				
			||||||
 | 
					  </el-dialog> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { save } from '@/api/demo' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'EditDialog', | 
				
			||||||
 | 
					  props: { | 
				
			||||||
 | 
					    row: { | 
				
			||||||
 | 
					      type: Object, | 
				
			||||||
 | 
					      require: true | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    show: { | 
				
			||||||
 | 
					      type: Boolean, | 
				
			||||||
 | 
					      default: () => false | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  components: {}, | 
				
			||||||
 | 
					  data() { | 
				
			||||||
 | 
					    return { | 
				
			||||||
 | 
					      edit: { id: null }, | 
				
			||||||
 | 
					      visible: this.show, | 
				
			||||||
 | 
					      rules: { | 
				
			||||||
 | 
					        name: [{ required: true, message: '请输入', trigger: 'blur' }] | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    title() { | 
				
			||||||
 | 
					      return (this.edit.id === null ? '新增' : '编辑') | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  watch: { | 
				
			||||||
 | 
					    visible() { | 
				
			||||||
 | 
					      this.$emit('update:show', false) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  created() { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  mounted() { | 
				
			||||||
 | 
					    if (this.row !== null) { | 
				
			||||||
 | 
					      this.edit = JSON.parse(JSON.stringify(this.row)) | 
				
			||||||
 | 
					      // console.log(this.edit) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    closeDialog() { | 
				
			||||||
 | 
					      // 关闭 | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    handleSave() { | 
				
			||||||
 | 
					      this.$refs.edit.validate(valid => { | 
				
			||||||
 | 
					        if (valid) { | 
				
			||||||
 | 
					          const loading = this.$loading({ | 
				
			||||||
 | 
					            background: this.$elementGlobal.loadingBackground | 
				
			||||||
 | 
					          }) | 
				
			||||||
 | 
					          save(this.edit) | 
				
			||||||
 | 
					            .then(r => { | 
				
			||||||
 | 
					              loading.close() | 
				
			||||||
 | 
					              if (r.data === true) { | 
				
			||||||
 | 
					                this.visible = false | 
				
			||||||
 | 
					                this.$message({ type: 'success', message: '保存成功!' }) | 
				
			||||||
 | 
					                this.$emit('saved') | 
				
			||||||
 | 
					              } else { | 
				
			||||||
 | 
					                this.$message({ type: 'error', message: '保存失败!' }) | 
				
			||||||
 | 
					              } | 
				
			||||||
 | 
					            }) | 
				
			||||||
 | 
					            .catch(() => { | 
				
			||||||
 | 
					              loading.close() | 
				
			||||||
 | 
					            }) | 
				
			||||||
 | 
					        } else { | 
				
			||||||
 | 
					          return false | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,236 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div class="app-container full-height"> | 
				
			||||||
 | 
					    <!-- 查询条件 --> | 
				
			||||||
 | 
					    <el-card class="filter-container" shadow="never"> | 
				
			||||||
 | 
					      <el-form label-width="110px" :inline="true" :model="condition" @submit.native.prevent | 
				
			||||||
 | 
					               @keyup.enter.native="conditionQuery" | 
				
			||||||
 | 
					      > | 
				
			||||||
 | 
					        <!--查询条件--> | 
				
			||||||
 | 
					        <el-form-item label="姓名" prop="name"> | 
				
			||||||
 | 
					          <el-input v-model="condition.name" placeholder="请输入" clearable/> | 
				
			||||||
 | 
					        </el-form-item> | 
				
			||||||
 | 
					        <!-- End 查询条件 --> | 
				
			||||||
 | 
					        <el-form-item> | 
				
			||||||
 | 
					          <el-button type="primary" @click="conditionQuery">查询</el-button> | 
				
			||||||
 | 
					        </el-form-item> | 
				
			||||||
 | 
					        <el-form-item> | 
				
			||||||
 | 
					          <el-button @click="handleResetCondition">重置</el-button> | 
				
			||||||
 | 
					        </el-form-item> | 
				
			||||||
 | 
					      </el-form> | 
				
			||||||
 | 
					    </el-card> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 表格操作 --> | 
				
			||||||
 | 
					    <el-card class="table-operate-container" shadow="never"> | 
				
			||||||
 | 
					      <el-button size="mini" icon="el-icon-plus" type="primary" plain @click="addRow">添加</el-button> | 
				
			||||||
 | 
					      <el-button :disabled="!hasSelection" type="danger" size="mini" @click="removeBatchRows">批量删除 | 
				
			||||||
 | 
					      </el-button> | 
				
			||||||
 | 
					    </el-card> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 表格 --> | 
				
			||||||
 | 
					    <div ref="tableContainer" class="table-container"> | 
				
			||||||
 | 
					      <!-- 表格主体 --> | 
				
			||||||
 | 
					      <el-table ref="listTable" v-loading="selectLoading" :max-height="tableMaxHeight" :data="tableData" border | 
				
			||||||
 | 
					                style="width: 100%" @row-click="handleRowClick" @selection-change="handleSelectionChange" | 
				
			||||||
 | 
					      > | 
				
			||||||
 | 
					        <el-table-column type="selection" width="40" align="center"/> | 
				
			||||||
 | 
					        <el-table-column type="index" label="序号" width="60" align="center"/> | 
				
			||||||
 | 
					        <el-table-column property="name" label="姓名" min-width="25%" align="center"/> | 
				
			||||||
 | 
					        <el-table-column property="attr1" label="属性1" min-width="25%" align="center"/> | 
				
			||||||
 | 
					        <el-table-column property="attr2" label="属性2" min-width="25%" align="center"/> | 
				
			||||||
 | 
					        <el-table-column property="updatedTime" label="更新时间" min-width="25%" align="center"/> | 
				
			||||||
 | 
					        <el-table-column property="createdTime" label="创建时间" min-width="25%" align="center"/> | 
				
			||||||
 | 
					        <el-table-column label="操作" width="180" align="center"> | 
				
			||||||
 | 
					          <template slot-scope="scope"> | 
				
			||||||
 | 
					            <el-button size="mini" @click.stop="handleEdit(scope.row)">编辑</el-button> | 
				
			||||||
 | 
					            <el-button type="danger" plain size="mini" @click.stop="handleRemove(scope.row)">删除</el-button> | 
				
			||||||
 | 
					          </template> | 
				
			||||||
 | 
					        </el-table-column> | 
				
			||||||
 | 
					      </el-table> | 
				
			||||||
 | 
					      <!-- End 表格主体 --> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 表格页码控件 --> | 
				
			||||||
 | 
					      <div class="pagination-container"> | 
				
			||||||
 | 
					        <el-pagination background layout="total, sizes, prev, pager, next,jumper" | 
				
			||||||
 | 
					                       :current-page.sync="condition.pageNumber" :page-size.sync="condition.pageSize" | 
				
			||||||
 | 
					                       :page-sizes="[10, 20, 30, 50, 100]" :total="total" @size-change="conditionQuery" | 
				
			||||||
 | 
					                       @current-change="conditionQuery" | 
				
			||||||
 | 
					        /> | 
				
			||||||
 | 
					      </div> | 
				
			||||||
 | 
					      <!-- End 表格页码控件 --> | 
				
			||||||
 | 
					    </div> | 
				
			||||||
 | 
					    <!-- End 表格 --> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 编辑弹窗 --> | 
				
			||||||
 | 
					    <edit-dialog v-if="showDialog" :row="editRowData" :show.sync="showDialog" :is-add="isAddDialog" | 
				
			||||||
 | 
					                 @saved="conditionQuery" | 
				
			||||||
 | 
					    /> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { page, remove } from '@/api/demo' | 
				
			||||||
 | 
					import editDialog from './edit' | 
				
			||||||
 | 
					/** | 
				
			||||||
 | 
					 * @version 2.0 | 
				
			||||||
 | 
					 */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Base */ | 
				
			||||||
 | 
					const default_condition = { | 
				
			||||||
 | 
					  name: '', | 
				
			||||||
 | 
					  pageNumber: 1, | 
				
			||||||
 | 
					  pageSize: 10 | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					/* ↑ Base */ | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'Demo', | 
				
			||||||
 | 
					  components: { editDialog }, | 
				
			||||||
 | 
					  data() { | 
				
			||||||
 | 
					    return { | 
				
			||||||
 | 
					      /* Edit Dialog */ | 
				
			||||||
 | 
					      editRowData: null, | 
				
			||||||
 | 
					      showDialog: false, | 
				
			||||||
 | 
					      isAddDialog: null, | 
				
			||||||
 | 
					      /* ↑ Edit Dialog */ | 
				
			||||||
 | 
					      /* Base */ | 
				
			||||||
 | 
					      condition: JSON.parse(JSON.stringify(default_condition)), | 
				
			||||||
 | 
					      tableData: [], | 
				
			||||||
 | 
					      total: null, | 
				
			||||||
 | 
					      selectLoading: false, | 
				
			||||||
 | 
					      tableMaxHeight: null, | 
				
			||||||
 | 
					      multipleSelection: [] | 
				
			||||||
 | 
					      /* ↑ Base */ | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  computed: { | 
				
			||||||
 | 
					    hasSelection() { | 
				
			||||||
 | 
					      return this.multipleSelection.length > 1 | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  watch: {}, | 
				
			||||||
 | 
					  created() { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  mounted() { | 
				
			||||||
 | 
					    /* Base */ | 
				
			||||||
 | 
					    this.resizeTable() | 
				
			||||||
 | 
					    window.onresize = () => { | 
				
			||||||
 | 
					      return (() => { | 
				
			||||||
 | 
					        this.resizeTable() | 
				
			||||||
 | 
					      })() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    /* ↑ Base */ | 
				
			||||||
 | 
					    this.conditionQuery() | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    /* Base */ | 
				
			||||||
 | 
					    addRow() { | 
				
			||||||
 | 
					      // 表格操作 - 添加 | 
				
			||||||
 | 
					      this.isAddDialog = true | 
				
			||||||
 | 
					      this.editRowData = null | 
				
			||||||
 | 
					      this.showDialog = true | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    handleRemove(row) { | 
				
			||||||
 | 
					      // 表格 - 操作列 - 删除 | 
				
			||||||
 | 
					      let title = '确定要删除吗? 该操作可能无法恢复!' | 
				
			||||||
 | 
					      this.$confirm(title, '提示', { | 
				
			||||||
 | 
					        type: 'warning' | 
				
			||||||
 | 
					      }).then(() => { | 
				
			||||||
 | 
					        const loading = this.$loading({ | 
				
			||||||
 | 
					          background: this.$elementGlobal.loadingBackground | 
				
			||||||
 | 
					        }) | 
				
			||||||
 | 
					        remove([row.id]).then(r => { | 
				
			||||||
 | 
					          loading.close() | 
				
			||||||
 | 
					          if (r.data) { | 
				
			||||||
 | 
					            this.$message({ | 
				
			||||||
 | 
					              type: 'success', | 
				
			||||||
 | 
					              message: '操作成功!' | 
				
			||||||
 | 
					            }) | 
				
			||||||
 | 
					          } else { | 
				
			||||||
 | 
					            this.$message({ | 
				
			||||||
 | 
					              type: 'error', | 
				
			||||||
 | 
					              message: '操作失败!' | 
				
			||||||
 | 
					            }) | 
				
			||||||
 | 
					          } | 
				
			||||||
 | 
					          this.conditionQuery() | 
				
			||||||
 | 
					        }).catch(e => { | 
				
			||||||
 | 
					          loading.close() | 
				
			||||||
 | 
					          console.log(e) | 
				
			||||||
 | 
					        }) | 
				
			||||||
 | 
					      }).catch(() => { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    removeBatchRows() { | 
				
			||||||
 | 
					      // 表格操作 - 批量删除 | 
				
			||||||
 | 
					      const h = this.$createElement | 
				
			||||||
 | 
					      this.$confirm(h('p', null, [ | 
				
			||||||
 | 
					        h('p', null, '确定要删除 ' + this.multipleSelection.length + ' 个吗?'), | 
				
			||||||
 | 
					        h('p', { style: 'color: #f80' }, ' 该操作可能无法恢复!') | 
				
			||||||
 | 
					      ]), '提示', { | 
				
			||||||
 | 
					        type: 'warning' | 
				
			||||||
 | 
					      }).then(() => { | 
				
			||||||
 | 
					        const loading = this.$loading({ | 
				
			||||||
 | 
					          background: this.$elementGlobal.loadingBackground | 
				
			||||||
 | 
					        }) | 
				
			||||||
 | 
					        remove(this.multipleSelection.map(e => e.id)).then(r => { | 
				
			||||||
 | 
					          loading.close() | 
				
			||||||
 | 
					          if (r.data) { | 
				
			||||||
 | 
					            this.$message({ | 
				
			||||||
 | 
					              type: 'success', | 
				
			||||||
 | 
					              message: '操作成功!' | 
				
			||||||
 | 
					            }) | 
				
			||||||
 | 
					          } else { | 
				
			||||||
 | 
					            this.$message({ | 
				
			||||||
 | 
					              type: 'error', | 
				
			||||||
 | 
					              message: '操作失败!' | 
				
			||||||
 | 
					            }) | 
				
			||||||
 | 
					          } | 
				
			||||||
 | 
					          this.conditionQuery() | 
				
			||||||
 | 
					        }).catch(e => { | 
				
			||||||
 | 
					          loading.close() | 
				
			||||||
 | 
					          console.log(e) | 
				
			||||||
 | 
					        }) | 
				
			||||||
 | 
					      }).catch(() => { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    // 表格 - 编辑行 | 
				
			||||||
 | 
					    handleEdit(row) { | 
				
			||||||
 | 
					      this.isAddDialog = false | 
				
			||||||
 | 
					      this.showDialog = true | 
				
			||||||
 | 
					      this.editRowData = row | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    // 表格 - 查询表数据 | 
				
			||||||
 | 
					    conditionQuery() { | 
				
			||||||
 | 
					      this.selectLoading = true | 
				
			||||||
 | 
					      page(this.condition).then(r => { | 
				
			||||||
 | 
					        this.selectLoading = false | 
				
			||||||
 | 
					        this.tableData = r.data.rows | 
				
			||||||
 | 
					        this.total = r.data.total | 
				
			||||||
 | 
					      }).catch(e => { | 
				
			||||||
 | 
					        this.selectLoading = false | 
				
			||||||
 | 
					        console.log('query error -> ', e) | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    resizeTable() { | 
				
			||||||
 | 
					      this.tableMaxHeight = window.innerHeight - (this.$refs.tableContainer.offsetTop + 100) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    // 重置表格查询条件 | 
				
			||||||
 | 
					    handleResetCondition() { | 
				
			||||||
 | 
					      this.condition = Object.assign({}, default_condition) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    // [事件]表格 - 点击行 | 
				
			||||||
 | 
					    handleRowClick(row) { | 
				
			||||||
 | 
					      this.$refs.listTable.toggleRowSelection(row) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    handleSelectionChange(val) { | 
				
			||||||
 | 
					      this.multipleSelection = val | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    /* ↑ Base */ | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,242 @@ | 
				
			|||||||
 | 
					<template> | 
				
			||||||
 | 
					  <div class="login-container"> | 
				
			||||||
 | 
					    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" | 
				
			||||||
 | 
					             label-position="left" | 
				
			||||||
 | 
					    > | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="title-container"> | 
				
			||||||
 | 
					        <h3 class="title">Login Form</h3> | 
				
			||||||
 | 
					      </div> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <el-form-item prop="username"> | 
				
			||||||
 | 
					        <span class="svg-container"> | 
				
			||||||
 | 
					          <svg-icon icon-class="user"/> | 
				
			||||||
 | 
					        </span> | 
				
			||||||
 | 
					        <el-input | 
				
			||||||
 | 
					          ref="username" | 
				
			||||||
 | 
					          v-model="loginForm.username" | 
				
			||||||
 | 
					          placeholder="Username" | 
				
			||||||
 | 
					          name="username" | 
				
			||||||
 | 
					          type="text" | 
				
			||||||
 | 
					          tabindex="1" | 
				
			||||||
 | 
					          auto-complete="on" | 
				
			||||||
 | 
					        /> | 
				
			||||||
 | 
					      </el-form-item> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <el-form-item prop="password"> | 
				
			||||||
 | 
					        <span class="svg-container"> | 
				
			||||||
 | 
					          <svg-icon icon-class="password"/> | 
				
			||||||
 | 
					        </span> | 
				
			||||||
 | 
					        <el-input | 
				
			||||||
 | 
					          :key="passwordType" | 
				
			||||||
 | 
					          ref="password" | 
				
			||||||
 | 
					          v-model="loginForm.password" | 
				
			||||||
 | 
					          :type="passwordType" | 
				
			||||||
 | 
					          placeholder="Password" | 
				
			||||||
 | 
					          name="password" | 
				
			||||||
 | 
					          tabindex="2" | 
				
			||||||
 | 
					          auto-complete="on" | 
				
			||||||
 | 
					          @keyup.enter.native="handleLogin" | 
				
			||||||
 | 
					        /> | 
				
			||||||
 | 
					        <span class="show-pwd" @click="showPwd"> | 
				
			||||||
 | 
					          <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'"/> | 
				
			||||||
 | 
					        </span> | 
				
			||||||
 | 
					      </el-form-item> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" | 
				
			||||||
 | 
					                 @click.native.prevent="handleLogin" | 
				
			||||||
 | 
					      >Login | 
				
			||||||
 | 
					      </el-button> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="tips"> | 
				
			||||||
 | 
					        <span style="margin-right:20px;">username: admin</span> | 
				
			||||||
 | 
					        <span> password: any</span> | 
				
			||||||
 | 
					      </div> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </el-form> | 
				
			||||||
 | 
					  </div> | 
				
			||||||
 | 
					</template> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script> | 
				
			||||||
 | 
					import { validUsername } from '@/utils/validate' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default { | 
				
			||||||
 | 
					  name: 'Login', | 
				
			||||||
 | 
					  data() { | 
				
			||||||
 | 
					    const validateUsername = (rule, value, callback) => { | 
				
			||||||
 | 
					      if (!validUsername(value)) { | 
				
			||||||
 | 
					        callback(new Error('Please enter the correct user name')) | 
				
			||||||
 | 
					      } else { | 
				
			||||||
 | 
					        callback() | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    const validatePassword = (rule, value, callback) => { | 
				
			||||||
 | 
					      if (value.length < 6) { | 
				
			||||||
 | 
					        callback(new Error('The password can not be less than 6 digits')) | 
				
			||||||
 | 
					      } else { | 
				
			||||||
 | 
					        callback() | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    return { | 
				
			||||||
 | 
					      loginForm: { | 
				
			||||||
 | 
					        username: 'admin', | 
				
			||||||
 | 
					        password: '111111' | 
				
			||||||
 | 
					      }, | 
				
			||||||
 | 
					      loginRules: { | 
				
			||||||
 | 
					        username: [{ required: true, trigger: 'blur', validator: validateUsername }], | 
				
			||||||
 | 
					        password: [{ required: true, trigger: 'blur', validator: validatePassword }] | 
				
			||||||
 | 
					      }, | 
				
			||||||
 | 
					      loading: false, | 
				
			||||||
 | 
					      passwordType: 'password', | 
				
			||||||
 | 
					      redirect: undefined | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  watch: { | 
				
			||||||
 | 
					    $route: { | 
				
			||||||
 | 
					      handler: function(route) { | 
				
			||||||
 | 
					        this.redirect = route.query && route.query.redirect | 
				
			||||||
 | 
					      }, | 
				
			||||||
 | 
					      immediate: true | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  methods: { | 
				
			||||||
 | 
					    showPwd() { | 
				
			||||||
 | 
					      if (this.passwordType === 'password') { | 
				
			||||||
 | 
					        this.passwordType = '' | 
				
			||||||
 | 
					      } else { | 
				
			||||||
 | 
					        this.passwordType = 'password' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      this.$nextTick(() => { | 
				
			||||||
 | 
					        this.$refs.password.focus() | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    handleLogin() { | 
				
			||||||
 | 
					      this.$refs.loginForm.validate(valid => { | 
				
			||||||
 | 
					        if (valid) { | 
				
			||||||
 | 
					          this.loading = true | 
				
			||||||
 | 
					          this.$store.dispatch('user/login', this.loginForm).then(() => { | 
				
			||||||
 | 
					            this.$router.push({ path: this.redirect || '/' }) | 
				
			||||||
 | 
					            this.loading = false | 
				
			||||||
 | 
					          }).catch(() => { | 
				
			||||||
 | 
					            this.loading = false | 
				
			||||||
 | 
					          }) | 
				
			||||||
 | 
					        } else { | 
				
			||||||
 | 
					          console.log('error submit!!') | 
				
			||||||
 | 
					          return false | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</script> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"> | 
				
			||||||
 | 
					/* 修复input 背景不协调 和光标变色 */ | 
				
			||||||
 | 
					/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$bg: #283443; | 
				
			||||||
 | 
					$light_gray: #fff; | 
				
			||||||
 | 
					$cursor: #fff; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@supports (-webkit-mask: none) and (not (cater-color: $cursor)) { | 
				
			||||||
 | 
					  .login-container .el-input input { | 
				
			||||||
 | 
					    color: $cursor; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* reset element-ui css */ | 
				
			||||||
 | 
					.login-container { | 
				
			||||||
 | 
					  .el-input { | 
				
			||||||
 | 
					    display: inline-block; | 
				
			||||||
 | 
					    height: 47px; | 
				
			||||||
 | 
					    width: 85%; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    input { | 
				
			||||||
 | 
					      background: transparent; | 
				
			||||||
 | 
					      border: 0px; | 
				
			||||||
 | 
					      -webkit-appearance: none; | 
				
			||||||
 | 
					      border-radius: 0px; | 
				
			||||||
 | 
					      padding: 12px 5px 12px 15px; | 
				
			||||||
 | 
					      color: $light_gray; | 
				
			||||||
 | 
					      height: 47px; | 
				
			||||||
 | 
					      caret-color: $cursor; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:-webkit-autofill { | 
				
			||||||
 | 
					        box-shadow: 0 0 0px 1000px $bg inset !important; | 
				
			||||||
 | 
					        -webkit-text-fill-color: $cursor !important; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .el-form-item { | 
				
			||||||
 | 
					    border: 1px solid rgba(255, 255, 255, 0.1); | 
				
			||||||
 | 
					    background: rgba(0, 0, 0, 0.1); | 
				
			||||||
 | 
					    border-radius: 5px; | 
				
			||||||
 | 
					    color: #454545; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped> | 
				
			||||||
 | 
					$bg: #2d3a4b; | 
				
			||||||
 | 
					$dark_gray: #889aa4; | 
				
			||||||
 | 
					$light_gray: #eee; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-container { | 
				
			||||||
 | 
					  min-height: 100%; | 
				
			||||||
 | 
					  width: 100%; | 
				
			||||||
 | 
					  background-color: $bg; | 
				
			||||||
 | 
					  overflow: hidden; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .login-form { | 
				
			||||||
 | 
					    position: relative; | 
				
			||||||
 | 
					    width: 520px; | 
				
			||||||
 | 
					    max-width: 100%; | 
				
			||||||
 | 
					    padding: 160px 35px 0; | 
				
			||||||
 | 
					    margin: 0 auto; | 
				
			||||||
 | 
					    overflow: hidden; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .tips { | 
				
			||||||
 | 
					    font-size: 14px; | 
				
			||||||
 | 
					    color: #fff; | 
				
			||||||
 | 
					    margin-bottom: 10px; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    span { | 
				
			||||||
 | 
					      &:first-of-type { | 
				
			||||||
 | 
					        margin-right: 16px; | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .svg-container { | 
				
			||||||
 | 
					    padding: 6px 5px 6px 15px; | 
				
			||||||
 | 
					    color: $dark_gray; | 
				
			||||||
 | 
					    vertical-align: middle; | 
				
			||||||
 | 
					    width: 30px; | 
				
			||||||
 | 
					    display: inline-block; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .title-container { | 
				
			||||||
 | 
					    position: relative; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .title { | 
				
			||||||
 | 
					      font-size: 26px; | 
				
			||||||
 | 
					      color: $light_gray; | 
				
			||||||
 | 
					      margin: 0px auto 40px auto; | 
				
			||||||
 | 
					      text-align: center; | 
				
			||||||
 | 
					      font-weight: bold; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .show-pwd { | 
				
			||||||
 | 
					    position: absolute; | 
				
			||||||
 | 
					    right: 10px; | 
				
			||||||
 | 
					    top: 7px; | 
				
			||||||
 | 
					    font-size: 16px; | 
				
			||||||
 | 
					    color: $dark_gray; | 
				
			||||||
 | 
					    cursor: pointer; | 
				
			||||||
 | 
					    user-select: none; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					</style> | 
				
			||||||
@ -0,0 +1,5 @@ | 
				
			|||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  env: { | 
				
			||||||
 | 
					    jest: true | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,98 @@ | 
				
			|||||||
 | 
					import { createLocalVue, mount } from '@vue/test-utils' | 
				
			||||||
 | 
					import VueRouter from 'vue-router' | 
				
			||||||
 | 
					import ElementUI from 'element-ui' | 
				
			||||||
 | 
					import Breadcrumb from '@/components/Breadcrumb/index.vue' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const localVue = createLocalVue() | 
				
			||||||
 | 
					localVue.use(VueRouter) | 
				
			||||||
 | 
					localVue.use(ElementUI) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const routes = [ | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    path: '/', | 
				
			||||||
 | 
					    name: 'home', | 
				
			||||||
 | 
					    children: [{ | 
				
			||||||
 | 
					      path: 'dashboard', | 
				
			||||||
 | 
					      name: 'dashboard' | 
				
			||||||
 | 
					    }] | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    path: '/menu', | 
				
			||||||
 | 
					    name: 'menu', | 
				
			||||||
 | 
					    children: [{ | 
				
			||||||
 | 
					      path: 'menu1', | 
				
			||||||
 | 
					      name: 'menu1', | 
				
			||||||
 | 
					      meta: { title: 'menu1' }, | 
				
			||||||
 | 
					      children: [{ | 
				
			||||||
 | 
					        path: 'menu1-1', | 
				
			||||||
 | 
					        name: 'menu1-1', | 
				
			||||||
 | 
					        meta: { title: 'menu1-1' } | 
				
			||||||
 | 
					      }, | 
				
			||||||
 | 
					        { | 
				
			||||||
 | 
					          path: 'menu1-2', | 
				
			||||||
 | 
					          name: 'menu1-2', | 
				
			||||||
 | 
					          redirect: 'noredirect', | 
				
			||||||
 | 
					          meta: { title: 'menu1-2' }, | 
				
			||||||
 | 
					          children: [{ | 
				
			||||||
 | 
					            path: 'menu1-2-1', | 
				
			||||||
 | 
					            name: 'menu1-2-1', | 
				
			||||||
 | 
					            meta: { title: 'menu1-2-1' } | 
				
			||||||
 | 
					          }, | 
				
			||||||
 | 
					            { | 
				
			||||||
 | 
					              path: 'menu1-2-2', | 
				
			||||||
 | 
					              name: 'menu1-2-2' | 
				
			||||||
 | 
					            }] | 
				
			||||||
 | 
					        }] | 
				
			||||||
 | 
					    }] | 
				
			||||||
 | 
					  }] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = new VueRouter({ | 
				
			||||||
 | 
					  routes | 
				
			||||||
 | 
					}) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Breadcrumb.vue', () => { | 
				
			||||||
 | 
					  const wrapper = mount(Breadcrumb, { | 
				
			||||||
 | 
					    localVue, | 
				
			||||||
 | 
					    router | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('dashboard', () => { | 
				
			||||||
 | 
					    router.push('/dashboard') | 
				
			||||||
 | 
					    const len = wrapper.findAll('.el-breadcrumb__inner').length | 
				
			||||||
 | 
					    expect(len).toBe(1) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('normal route', () => { | 
				
			||||||
 | 
					    router.push('/menu/menu1') | 
				
			||||||
 | 
					    const len = wrapper.findAll('.el-breadcrumb__inner').length | 
				
			||||||
 | 
					    expect(len).toBe(2) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('nested route', () => { | 
				
			||||||
 | 
					    router.push('/menu/menu1/menu1-2/menu1-2-1') | 
				
			||||||
 | 
					    const len = wrapper.findAll('.el-breadcrumb__inner').length | 
				
			||||||
 | 
					    expect(len).toBe(4) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('no meta.title', () => { | 
				
			||||||
 | 
					    router.push('/menu/menu1/menu1-2/menu1-2-2') | 
				
			||||||
 | 
					    const len = wrapper.findAll('.el-breadcrumb__inner').length | 
				
			||||||
 | 
					    expect(len).toBe(3) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  // it('click link', () => {
 | 
				
			||||||
 | 
					  //   router.push('/menu/menu1/menu1-2/menu1-2-2')
 | 
				
			||||||
 | 
					  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
 | 
				
			||||||
 | 
					  //   const second = breadcrumbArray.at(1)
 | 
				
			||||||
 | 
					  //   console.log(breadcrumbArray)
 | 
				
			||||||
 | 
					  //   const href = second.find('a').attributes().href
 | 
				
			||||||
 | 
					  //   expect(href).toBe('#/menu/menu1')
 | 
				
			||||||
 | 
					  // })
 | 
				
			||||||
 | 
					  // it('noRedirect', () => {
 | 
				
			||||||
 | 
					  //   router.push('/menu/menu1/menu1-2/menu1-2-1')
 | 
				
			||||||
 | 
					  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
 | 
				
			||||||
 | 
					  //   const redirectBreadcrumb = breadcrumbArray.at(2)
 | 
				
			||||||
 | 
					  //   expect(redirectBreadcrumb.contains('a')).toBe(false)
 | 
				
			||||||
 | 
					  // })
 | 
				
			||||||
 | 
					  it('last breadcrumb', () => { | 
				
			||||||
 | 
					    router.push('/menu/menu1/menu1-2/menu1-2-1') | 
				
			||||||
 | 
					    const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') | 
				
			||||||
 | 
					    const redirectBreadcrumb = breadcrumbArray.at(3) | 
				
			||||||
 | 
					    expect(redirectBreadcrumb.contains('a')).toBe(false) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,19 @@ | 
				
			|||||||
 | 
					import { shallowMount } from '@vue/test-utils' | 
				
			||||||
 | 
					import Hamburger from '@/components/Hamburger/index.vue' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Hamburger.vue', () => { | 
				
			||||||
 | 
					  it('toggle click', () => { | 
				
			||||||
 | 
					    const wrapper = shallowMount(Hamburger) | 
				
			||||||
 | 
					    const mockFn = jest.fn() | 
				
			||||||
 | 
					    wrapper.vm.$on('toggleClick', mockFn) | 
				
			||||||
 | 
					    wrapper.find('.hamburger').trigger('click') | 
				
			||||||
 | 
					    expect(mockFn).toBeCalled() | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('prop isActive', () => { | 
				
			||||||
 | 
					    const wrapper = shallowMount(Hamburger) | 
				
			||||||
 | 
					    wrapper.setProps({ isActive: true }) | 
				
			||||||
 | 
					    expect(wrapper.contains('.is-active')).toBe(true) | 
				
			||||||
 | 
					    wrapper.setProps({ isActive: false }) | 
				
			||||||
 | 
					    expect(wrapper.contains('.is-active')).toBe(false) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,23 @@ | 
				
			|||||||
 | 
					import { shallowMount } from '@vue/test-utils' | 
				
			||||||
 | 
					import SvgIcon from '@/components/SvgIcon/index.vue' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('SvgIcon.vue', () => { | 
				
			||||||
 | 
					  it('iconClass', () => { | 
				
			||||||
 | 
					    const wrapper = shallowMount(SvgIcon, { | 
				
			||||||
 | 
					      propsData: { | 
				
			||||||
 | 
					        iconClass: 'test' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					    expect(wrapper.find('use').attributes().href).toBe('#icon-test') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('className', () => { | 
				
			||||||
 | 
					    const wrapper = shallowMount(SvgIcon, { | 
				
			||||||
 | 
					      propsData: { | 
				
			||||||
 | 
					        iconClass: 'test' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					    expect(wrapper.classes().length).toBe(1) | 
				
			||||||
 | 
					    wrapper.setProps({ className: 'test' }) | 
				
			||||||
 | 
					    expect(wrapper.classes().includes('test')).toBe(true) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,30 @@ | 
				
			|||||||
 | 
					import { formatTime } from '@/utils/index.js' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Utils:formatTime', () => { | 
				
			||||||
 | 
					  const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
 | 
				
			||||||
 | 
					  const retrofit = 5 * 1000 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('ten digits timestamp', () => { | 
				
			||||||
 | 
					    expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('test now', () => { | 
				
			||||||
 | 
					    expect(formatTime(+new Date() - 1)).toBe('刚刚') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('less two minute', () => { | 
				
			||||||
 | 
					    expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('less two hour', () => { | 
				
			||||||
 | 
					    expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('less one day', () => { | 
				
			||||||
 | 
					    expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('more than one day', () => { | 
				
			||||||
 | 
					    expect(formatTime(d)).toBe('7月13日17时54分') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('format', () => { | 
				
			||||||
 | 
					    expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') | 
				
			||||||
 | 
					    expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') | 
				
			||||||
 | 
					    expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,15 @@ | 
				
			|||||||
 | 
					import { param2Obj } from '@/utils/index.js' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Utils:param2Obj', () => { | 
				
			||||||
 | 
					  const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('param2Obj test', () => { | 
				
			||||||
 | 
					    expect(param2Obj(url)).toEqual({ | 
				
			||||||
 | 
					      name: 'bill', | 
				
			||||||
 | 
					      age: '29', | 
				
			||||||
 | 
					      sex: '1', | 
				
			||||||
 | 
					      field: window.btoa('test'), | 
				
			||||||
 | 
					      key: '测试' | 
				
			||||||
 | 
					    }) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,35 @@ | 
				
			|||||||
 | 
					import { parseTime } from '@/utils/index.js' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Utils:parseTime', () => { | 
				
			||||||
 | 
					  const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
 | 
				
			||||||
 | 
					  it('timestamp', () => { | 
				
			||||||
 | 
					    expect(parseTime(d)).toBe('2018-07-13 17:54:01') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('timestamp string', () => { | 
				
			||||||
 | 
					    expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('ten digits timestamp', () => { | 
				
			||||||
 | 
					    expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('new Date', () => { | 
				
			||||||
 | 
					    expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('format', () => { | 
				
			||||||
 | 
					    expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') | 
				
			||||||
 | 
					    expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') | 
				
			||||||
 | 
					    expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('get the day of the week', () => { | 
				
			||||||
 | 
					    expect(parseTime(d, '{a}')).toBe('五') // 星期五
 | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('get the day of the week', () => { | 
				
			||||||
 | 
					    expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
 | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('empty argument', () => { | 
				
			||||||
 | 
					    expect(parseTime()).toBeNull() | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('null', () => { | 
				
			||||||
 | 
					    expect(parseTime(null)).toBeNull() | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,17 @@ | 
				
			|||||||
 | 
					import { isExternal, validUsername } from '@/utils/validate.js' | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Utils:validate', () => { | 
				
			||||||
 | 
					  it('validUsername', () => { | 
				
			||||||
 | 
					    expect(validUsername('admin')).toBe(true) | 
				
			||||||
 | 
					    expect(validUsername('editor')).toBe(true) | 
				
			||||||
 | 
					    expect(validUsername('xxxx')).toBe(false) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					  it('isExternal', () => { | 
				
			||||||
 | 
					    expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) | 
				
			||||||
 | 
					    expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) | 
				
			||||||
 | 
					    expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) | 
				
			||||||
 | 
					    expect(isExternal('/dashboard')).toBe(false) | 
				
			||||||
 | 
					    expect(isExternal('./dashboard')).toBe(false) | 
				
			||||||
 | 
					    expect(isExternal('dashboard')).toBe(false) | 
				
			||||||
 | 
					  }) | 
				
			||||||
 | 
					}) | 
				
			||||||
@ -0,0 +1,123 @@ | 
				
			|||||||
 | 
					'use strict' | 
				
			||||||
 | 
					const path = require('path') | 
				
			||||||
 | 
					const defaultSettings = require('./src/settings.js') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function resolve(dir) { | 
				
			||||||
 | 
					  return path.join(__dirname, dir) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const name = defaultSettings.title || 'vue Admin Template' // page title
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// If your port is set to 80,
 | 
				
			||||||
 | 
					// use administrator privileges to execute the command line.
 | 
				
			||||||
 | 
					// For example, Mac: sudo npm run
 | 
				
			||||||
 | 
					// You can change the port by the following methods:
 | 
				
			||||||
 | 
					// port = 9528 npm run dev OR npm run dev --port = 9528
 | 
				
			||||||
 | 
					const port = process.env.port || process.env.npm_config_port || 6901 // dev port
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// All configuration item explanations can be find in https://cli.vuejs.org/config/
 | 
				
			||||||
 | 
					module.exports = { | 
				
			||||||
 | 
					  /** | 
				
			||||||
 | 
					   * You will need to set publicPath if you plan to deploy your site under a sub path, | 
				
			||||||
 | 
					   * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
 | 
				
			||||||
 | 
					   * then publicPath should be set to "/bar/". | 
				
			||||||
 | 
					   * In most cases please use '/' !!! | 
				
			||||||
 | 
					   * Detail: https://cli.vuejs.org/config/#publicpath
 | 
				
			||||||
 | 
					   */ | 
				
			||||||
 | 
					  publicPath: '/', | 
				
			||||||
 | 
					  outputDir: 'dist', | 
				
			||||||
 | 
					  assetsDir: 'static', | 
				
			||||||
 | 
					  lintOnSave: process.env.NODE_ENV === 'development', | 
				
			||||||
 | 
					  productionSourceMap: false, | 
				
			||||||
 | 
					  devServer: { | 
				
			||||||
 | 
					    port: port, | 
				
			||||||
 | 
					    open: true, | 
				
			||||||
 | 
					    overlay: { | 
				
			||||||
 | 
					      warnings: false, | 
				
			||||||
 | 
					      errors: true | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    before: require('./mock/mock-server.js') | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  configureWebpack: { | 
				
			||||||
 | 
					    // provide the app's title in webpack's name field, so that
 | 
				
			||||||
 | 
					    // it can be accessed in index.html to inject the correct title.
 | 
				
			||||||
 | 
					    name: name, | 
				
			||||||
 | 
					    resolve: { | 
				
			||||||
 | 
					      alias: { | 
				
			||||||
 | 
					        '@': resolve('src') | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  chainWebpack(config) { | 
				
			||||||
 | 
					    // it can improve the speed of the first screen, it is recommended to turn on preload
 | 
				
			||||||
 | 
					    config.plugin('preload').tap(() => [ | 
				
			||||||
 | 
					      { | 
				
			||||||
 | 
					        rel: 'preload', | 
				
			||||||
 | 
					        // to ignore runtime.js
 | 
				
			||||||
 | 
					        // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
 | 
				
			||||||
 | 
					        fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], | 
				
			||||||
 | 
					        include: 'initial' | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					    ]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // when there are many pages, it will cause too many meaningless requests
 | 
				
			||||||
 | 
					    config.plugins.delete('prefetch') | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // set svg-sprite-loader
 | 
				
			||||||
 | 
					    config.module | 
				
			||||||
 | 
					      .rule('svg') | 
				
			||||||
 | 
					      .exclude.add(resolve('src/icons')) | 
				
			||||||
 | 
					      .end() | 
				
			||||||
 | 
					    config.module | 
				
			||||||
 | 
					      .rule('icons') | 
				
			||||||
 | 
					      .test(/\.svg$/) | 
				
			||||||
 | 
					      .include.add(resolve('src/icons')) | 
				
			||||||
 | 
					      .end() | 
				
			||||||
 | 
					      .use('svg-sprite-loader') | 
				
			||||||
 | 
					      .loader('svg-sprite-loader') | 
				
			||||||
 | 
					      .options({ | 
				
			||||||
 | 
					        symbolId: 'icon-[name]' | 
				
			||||||
 | 
					      }) | 
				
			||||||
 | 
					      .end() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    config | 
				
			||||||
 | 
					      .when(process.env.NODE_ENV !== 'development', | 
				
			||||||
 | 
					        config => { | 
				
			||||||
 | 
					          config | 
				
			||||||
 | 
					            .plugin('ScriptExtHtmlWebpackPlugin') | 
				
			||||||
 | 
					            .after('html') | 
				
			||||||
 | 
					            .use('script-ext-html-webpack-plugin', [{ | 
				
			||||||
 | 
					              // `runtime` must same as runtimeChunk name. default is `runtime`
 | 
				
			||||||
 | 
					              inline: /runtime\..*\.js$/ | 
				
			||||||
 | 
					            }]) | 
				
			||||||
 | 
					            .end() | 
				
			||||||
 | 
					          config | 
				
			||||||
 | 
					            .optimization.splitChunks({ | 
				
			||||||
 | 
					            chunks: 'all', | 
				
			||||||
 | 
					            cacheGroups: { | 
				
			||||||
 | 
					              libs: { | 
				
			||||||
 | 
					                name: 'chunk-libs', | 
				
			||||||
 | 
					                test: /[\\/]node_modules[\\/]/, | 
				
			||||||
 | 
					                priority: 10, | 
				
			||||||
 | 
					                chunks: 'initial' // only package third parties that are initially dependent
 | 
				
			||||||
 | 
					              }, | 
				
			||||||
 | 
					              elementUI: { | 
				
			||||||
 | 
					                name: 'chunk-elementUI', // split elementUI into a single package
 | 
				
			||||||
 | 
					                priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
 | 
				
			||||||
 | 
					                test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
 | 
				
			||||||
 | 
					              }, | 
				
			||||||
 | 
					              commons: { | 
				
			||||||
 | 
					                name: 'chunk-commons', | 
				
			||||||
 | 
					                test: resolve('src/components'), // can customize your rules
 | 
				
			||||||
 | 
					                minChunks: 3, //  minimum common number
 | 
				
			||||||
 | 
					                priority: 5, | 
				
			||||||
 | 
					                reuseExistingChunk: true | 
				
			||||||
 | 
					              } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					          }) | 
				
			||||||
 | 
					          // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
 | 
				
			||||||
 | 
					          config.optimization.runtimeChunk('single') | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					      ) | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||