[release]4.1.0 (#211)
12
.babelrc
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
["env", {
|
|
||||||
"modules": false,
|
|
||||||
"targets": {
|
|
||||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"stage-2"
|
|
||||||
],
|
|
||||||
"plugins":["transform-vue-jsx", "transform-runtime"]
|
|
||||||
}
|
|
14
.env.development
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# just a flag
|
||||||
|
ENV = 'development'
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VUE_APP_BASE_API = '/dev-api'
|
||||||
|
|
||||||
|
# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
|
||||||
|
# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
|
||||||
|
# It only does one thing by converting all import() to require().
|
||||||
|
# This configuration can significantly increase the speed of hot updates,
|
||||||
|
# when you have a large number of pages.
|
||||||
|
# Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
|
||||||
|
|
||||||
|
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
6
.env.production
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# just a flag
|
||||||
|
ENV = 'production'
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VUE_APP_BASE_API = '/prod-api'
|
||||||
|
|
8
.env.staging
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
NODE_ENV = production
|
||||||
|
|
||||||
|
# just a flag
|
||||||
|
ENV = 'staging'
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VUE_APP_BASE_API = '/stage-api'
|
||||||
|
|
@ -1,3 +1,4 @@
|
|||||||
build/*.js
|
build/*.js
|
||||||
config/*.js
|
|
||||||
src/assets
|
src/assets
|
||||||
|
public
|
||||||
|
dist
|
||||||
|
@ -21,7 +21,10 @@ module.exports = {
|
|||||||
"allowFirstLine": false
|
"allowFirstLine": false
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
|
"vue/multiline-html-element-content-newline":"off",
|
||||||
"vue/name-property-casing": ["error", "PascalCase"],
|
"vue/name-property-casing": ["error", "PascalCase"],
|
||||||
|
"vue/no-v-html": "off",
|
||||||
'accessor-pairs': 2,
|
'accessor-pairs': 2,
|
||||||
'arrow-spacing': [2, {
|
'arrow-spacing': [2, {
|
||||||
'before': true,
|
'before': true,
|
||||||
@ -44,7 +47,7 @@ module.exports = {
|
|||||||
'curly': [2, 'multi-line'],
|
'curly': [2, 'multi-line'],
|
||||||
'dot-location': [2, 'property'],
|
'dot-location': [2, 'property'],
|
||||||
'eol-last': 2,
|
'eol-last': 2,
|
||||||
'eqeqeq': [2, 'allow-null'],
|
'eqeqeq': ["error", "always", {"null": "ignore"}],
|
||||||
'generator-star-spacing': [2, {
|
'generator-star-spacing': [2, {
|
||||||
'before': true,
|
'before': true,
|
||||||
'after': true
|
'after': true
|
||||||
@ -73,7 +76,7 @@ module.exports = {
|
|||||||
'no-class-assign': 2,
|
'no-class-assign': 2,
|
||||||
'no-cond-assign': 2,
|
'no-cond-assign': 2,
|
||||||
'no-const-assign': 2,
|
'no-const-assign': 2,
|
||||||
'no-control-regex': 2,
|
'no-control-regex': 0,
|
||||||
'no-delete-var': 2,
|
'no-delete-var': 2,
|
||||||
'no-dupe-args': 2,
|
'no-dupe-args': 2,
|
||||||
'no-dupe-class-members': 2,
|
'no-dupe-class-members': 2,
|
||||||
@ -193,4 +196,3 @@ module.exports = {
|
|||||||
'array-bracket-spacing': [2, 'never']
|
'array-bracket-spacing': [2, 'never']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
.gitignore
vendored
@ -5,6 +5,7 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
tests/**/coverage/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"plugins": {
|
'plugins': {
|
||||||
"postcss-import": {},
|
|
||||||
"postcss-url": {},
|
|
||||||
// to edit target browsers: use "browserslist" field in package.json
|
// to edit target browsers: use "browserslist" field in package.json
|
||||||
"autoprefixer": {}
|
'autoprefixer': {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
80
README-zh.md
@ -1,19 +1,17 @@
|
|||||||
# vue-admin-template
|
# vue-admin-template
|
||||||
|
|
||||||
> 这是一个 极简的 vue admin 管理后台 它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
|
> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
|
||||||
|
|
||||||
[线上地址](http://panjiachen.github.io/vue-admin-template)
|
[线上地址](http://panjiachen.github.io/vue-admin-template)
|
||||||
|
|
||||||
[国内访问](https://panjiachen.gitee.io/vue-admin-template)
|
[国内访问](https://panjiachen.gitee.io/vue-admin-template)
|
||||||
|
|
||||||
|
目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若发现问题,欢迎提 issue。若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
|
||||||
|
|
||||||
## Extra
|
## Extra
|
||||||
|
|
||||||
如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
|
如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
|
||||||
|
|
||||||
本项目基于`webpack4`开发,若还想使用`webpack3`开发,请使用该分支[webpack3](https://github.com/PanJiaChen/vue-admin-template/tree/webpack3)
|
|
||||||
|
|
||||||
如果你想使用基于 vue + typescript 的管理后台, 可以看看这个项目: [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (鸣谢: [@Armour](https://github.com/Armour))
|
|
||||||
|
|
||||||
## 相关项目
|
## 相关项目
|
||||||
|
|
||||||
[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||||
@ -33,54 +31,56 @@
|
|||||||
## Build Setup
|
## Build Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone project
|
# 克隆项目
|
||||||
git clone https://github.com/PanJiaChen/vue-admin-template.git
|
git clone https://github.com/PanJiaChen/vue-admin-template.git
|
||||||
|
|
||||||
# Install dependencies
|
# 进入项目目录
|
||||||
|
cd vue-admin-template
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 建议不要用cnpm 安装有各种诡异的bug 可以通过如下操作解决npm速度慢的问题
|
# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
|
||||||
npm install --registry=https://registry.npm.taobao.org
|
npm install --registry=https://registry.npm.taobao.org
|
||||||
|
|
||||||
# Serve with hot reload at localhost:9528
|
# 启动服务
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# Build for production with minification
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Build for production and view the bundle analyzer report
|
|
||||||
npm run build --report
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
浏览器访问 [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/)
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
|
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
|
||||||
|
|
||||||
### Element-Ui 使用 cdn 教程
|
|
||||||
|
|
||||||
首先找到 `index.html` ([根目录下](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/index.html))
|
|
||||||
|
|
||||||
引入 Element 的 css 和 js ,并且引入 vue 。因为 Element-Ui 是依赖 vue 的,所以必须在它之前引入 vue 。
|
|
||||||
|
|
||||||
之后找到 [webpack.base.conf.js](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/build/webpack.base.conf.js) 加入 `externals` 让 webpack 不打包 vue 和 element
|
|
||||||
|
|
||||||
```
|
|
||||||
externals: {
|
|
||||||
vue: 'Vue',
|
|
||||||
'element-ui':'ELEMENT'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
之后还有一个小细节是如果你用了全局对象方式引入 vue,就不需要 手动 `Vue.use(Vuex)` ,它会自动挂载,具体见 [issue](https://github.com/vuejs/vuex/issues/731)
|
|
||||||
|
|
||||||
最终你可以使用 `npm run build --report` 查看效果
|
|
||||||
如图:
|
|
||||||
![demo](https://panjiachen.github.io/images/element-cdn.png)
|
|
||||||
|
|
||||||
**[具体代码](https://github.com/PanJiaChen/vue-admin-template/commit/746aff560932704ae821f82f10b8b2a9681d5177)**
|
|
||||||
|
|
||||||
**[对应分支](https://github.com/PanJiaChen/vue-admin-template/tree/element-ui-cdn)**
|
|
||||||
|
|
||||||
## Browsers support
|
## Browsers support
|
||||||
|
|
||||||
Modern browsers and Internet Explorer 10+.
|
Modern browsers and Internet Explorer 10+.
|
||||||
|
85
README.md
@ -1,30 +1,61 @@
|
|||||||
# vue-admin-template
|
# vue-admin-template
|
||||||
|
|
||||||
|
English | [简体中文](./README.zh-CN.md)
|
||||||
|
|
||||||
> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
|
> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
|
||||||
|
|
||||||
**Live demo:** http://panjiachen.github.io/vue-admin-template
|
**Live demo:** http://panjiachen.github.io/vue-admin-template
|
||||||
|
|
||||||
[中文文档](https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md)
|
|
||||||
|
**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'**
|
||||||
|
|
||||||
## Build Setup
|
## Build Setup
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone project
|
|
||||||
git clone https://github.com/PanJiaChen/vue-admin-template.git
|
|
||||||
|
|
||||||
# Install dependencies
|
```bash
|
||||||
|
# clone the project
|
||||||
|
git clone https://github.com/PanJiaChen/vue-element-admin.git
|
||||||
|
|
||||||
|
# enter the project directory
|
||||||
|
cd vue-element-admin
|
||||||
|
|
||||||
|
# install dependency
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Serve with hot reload at localhost:9528
|
# develop
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# Build for production with minification
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Build for production and view the bundle analyzer report
|
|
||||||
npm run build --report
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This will automatically open http://localhost:9527
|
||||||
|
|
||||||
|
## 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
|
## Demo
|
||||||
|
|
||||||
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
|
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
|
||||||
@ -33,8 +64,6 @@ npm run build --report
|
|||||||
|
|
||||||
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)
|
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)
|
||||||
|
|
||||||
This project is based on `webpack4` development. If you want to use `webpack3` development, please use this branch [webpack3](https://github.com/PanJiaChen/vue-admin-template/tree/webpack3)
|
|
||||||
|
|
||||||
For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
|
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
|
## Related Project
|
||||||
@ -45,34 +74,6 @@ For `typescript` version, you can use [vue-typescript-admin-template](https://gi
|
|||||||
|
|
||||||
[vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
|
[vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
|
||||||
|
|
||||||
### Element-Ui using cdn tutorial
|
|
||||||
|
|
||||||
First find `index.html`([root directory](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/index.html))
|
|
||||||
|
|
||||||
Import css and js of `Element`, and then import vue. Because `Element` is vue-dependent, vue must be import before it.
|
|
||||||
|
|
||||||
Then find [webpack.base.conf.js](https://github.com/PanJiaChen/vue-admin-template/blob/element-ui-cdn/build/webpack.base.conf.js)
|
|
||||||
Add `externals` to make webpack not package vue and element.
|
|
||||||
|
|
||||||
```
|
|
||||||
externals: {
|
|
||||||
vue: 'Vue',
|
|
||||||
'element-ui':'ELEMENT'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally there is a small detail to pay attention to that if you import vue in global, you don't need to manually `Vue.use(Vuex)`, it will be automatically mounted, see
|
|
||||||
[issue](https://github.com/vuejs/vuex/issues/731)
|
|
||||||
|
|
||||||
And you can use `npm run build --report` to see the effect
|
|
||||||
|
|
||||||
Pictured:
|
|
||||||
![demo](https://panjiachen.github.io/images/element-cdn.png)
|
|
||||||
|
|
||||||
**[Detailed code](https://github.com/PanJiaChen/vue-admin-template/commit/746aff560932704ae821f82f10b8b2a9681d5177)**
|
|
||||||
|
|
||||||
**[Branch](https://github.com/PanJiaChen/vue-admin-template/tree/element-ui-cdn)**
|
|
||||||
|
|
||||||
## Browsers support
|
## Browsers support
|
||||||
|
|
||||||
Modern browsers and Internet Explorer 10+.
|
Modern browsers and Internet Explorer 10+.
|
||||||
|
5
babel.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/app'
|
||||||
|
]
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
require('./check-versions')()
|
|
||||||
|
|
||||||
process.env.NODE_ENV = 'production'
|
|
||||||
|
|
||||||
const ora = require('ora')
|
|
||||||
const rm = require('rimraf')
|
|
||||||
const path = require('path')
|
|
||||||
const chalk = require('chalk')
|
|
||||||
const webpack = require('webpack')
|
|
||||||
const config = require('../config')
|
|
||||||
const webpackConfig = require('./webpack.prod.conf')
|
|
||||||
|
|
||||||
const spinner = ora('building for production...')
|
|
||||||
spinner.start()
|
|
||||||
|
|
||||||
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
|
|
||||||
if (err) throw err
|
|
||||||
webpack(webpackConfig, (err, stats) => {
|
|
||||||
spinner.stop()
|
|
||||||
if (err) throw err
|
|
||||||
process.stdout.write(
|
|
||||||
stats.toString({
|
|
||||||
colors: true,
|
|
||||||
modules: false,
|
|
||||||
children: false,
|
|
||||||
chunks: false,
|
|
||||||
chunkModules: false
|
|
||||||
}) + '\n\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
console.log(chalk.red(' Build failed with errors.\n'))
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.cyan(' Build complete.\n'))
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
' Tip: built files are meant to be served over an HTTP server.\n' +
|
|
||||||
" Opening index.html over file:// won't work.\n"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,64 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const chalk = require('chalk')
|
|
||||||
const semver = require('semver')
|
|
||||||
const packageConfig = require('../package.json')
|
|
||||||
const shell = require('shelljs')
|
|
||||||
|
|
||||||
function exec(cmd) {
|
|
||||||
return require('child_process')
|
|
||||||
.execSync(cmd)
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionRequirements = [
|
|
||||||
{
|
|
||||||
name: 'node',
|
|
||||||
currentVersion: semver.clean(process.version),
|
|
||||||
versionRequirement: packageConfig.engines.node
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if (shell.which('npm')) {
|
|
||||||
versionRequirements.push({
|
|
||||||
name: 'npm',
|
|
||||||
currentVersion: exec('npm --version'),
|
|
||||||
versionRequirement: packageConfig.engines.npm
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function() {
|
|
||||||
const warnings = []
|
|
||||||
|
|
||||||
for (let i = 0; i < versionRequirements.length; i++) {
|
|
||||||
const mod = versionRequirements[i]
|
|
||||||
|
|
||||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
|
||||||
warnings.push(
|
|
||||||
mod.name +
|
|
||||||
': ' +
|
|
||||||
chalk.red(mod.currentVersion) +
|
|
||||||
' should be ' +
|
|
||||||
chalk.green(mod.versionRequirement)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (warnings.length) {
|
|
||||||
console.log('')
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
'To use this template, you must update following to modules:'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
for (let i = 0; i < warnings.length; i++) {
|
|
||||||
const warning = warnings[i]
|
|
||||||
console.log(' ' + warning)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log()
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
35
build/index.js
Normal file
@ -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}`)
|
||||||
|
}
|
BIN
build/logo.png
Before Width: | Height: | Size: 6.7 KiB |
108
build/utils.js
@ -1,108 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const path = require('path')
|
|
||||||
const config = require('../config')
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
|
||||||
const packageConfig = require('../package.json')
|
|
||||||
|
|
||||||
exports.assetsPath = function(_path) {
|
|
||||||
const assetsSubDirectory =
|
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? config.build.assetsSubDirectory
|
|
||||||
: config.dev.assetsSubDirectory
|
|
||||||
|
|
||||||
return path.posix.join(assetsSubDirectory, _path)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.cssLoaders = function(options) {
|
|
||||||
options = options || {}
|
|
||||||
|
|
||||||
const cssLoader = {
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
sourceMap: options.sourceMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const postcssLoader = {
|
|
||||||
loader: 'postcss-loader',
|
|
||||||
options: {
|
|
||||||
sourceMap: options.sourceMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate loader string to be used with extract text plugin
|
|
||||||
function generateLoaders(loader, loaderOptions) {
|
|
||||||
const loaders = []
|
|
||||||
|
|
||||||
// Extract CSS when that option is specified
|
|
||||||
// (which is the case during production build)
|
|
||||||
if (options.extract) {
|
|
||||||
loaders.push(MiniCssExtractPlugin.loader)
|
|
||||||
} else {
|
|
||||||
loaders.push('vue-style-loader')
|
|
||||||
}
|
|
||||||
|
|
||||||
loaders.push(cssLoader)
|
|
||||||
|
|
||||||
if (options.usePostCSS) {
|
|
||||||
loaders.push(postcssLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loader) {
|
|
||||||
loaders.push({
|
|
||||||
loader: loader + '-loader',
|
|
||||||
options: Object.assign({}, loaderOptions, {
|
|
||||||
sourceMap: options.sourceMap
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return loaders
|
|
||||||
}
|
|
||||||
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
|
||||||
return {
|
|
||||||
css: generateLoaders(),
|
|
||||||
postcss: generateLoaders(),
|
|
||||||
less: generateLoaders('less'),
|
|
||||||
sass: generateLoaders('sass', {
|
|
||||||
indentedSyntax: true
|
|
||||||
}),
|
|
||||||
scss: generateLoaders('sass'),
|
|
||||||
stylus: generateLoaders('stylus'),
|
|
||||||
styl: generateLoaders('stylus')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate loaders for standalone style files (outside of .vue)
|
|
||||||
exports.styleLoaders = function(options) {
|
|
||||||
const output = []
|
|
||||||
const loaders = exports.cssLoaders(options)
|
|
||||||
|
|
||||||
for (const extension in loaders) {
|
|
||||||
const loader = loaders[extension]
|
|
||||||
output.push({
|
|
||||||
test: new RegExp('\\.' + extension + '$'),
|
|
||||||
use: loader
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createNotifierCallback = () => {
|
|
||||||
const notifier = require('node-notifier')
|
|
||||||
|
|
||||||
return (severity, errors) => {
|
|
||||||
if (severity !== 'error') return
|
|
||||||
|
|
||||||
const error = errors[0]
|
|
||||||
const filename = error.file && error.file.split('!').pop()
|
|
||||||
|
|
||||||
notifier.notify({
|
|
||||||
title: packageConfig.name,
|
|
||||||
message: severity + ': ' + error.name,
|
|
||||||
subtitle: filename || '',
|
|
||||||
icon: path.join(__dirname, 'logo.png')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
//You can set the vue-loader configuration by yourself.
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const path = require('path')
|
|
||||||
const utils = require('./utils')
|
|
||||||
const config = require('../config')
|
|
||||||
const { VueLoaderPlugin } = require('vue-loader')
|
|
||||||
const vueLoaderConfig = require('./vue-loader.conf')
|
|
||||||
|
|
||||||
function resolve(dir) {
|
|
||||||
return path.join(__dirname, '..', dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createLintingRule = () => ({
|
|
||||||
test: /\.(js|vue)$/,
|
|
||||||
loader: 'eslint-loader',
|
|
||||||
enforce: 'pre',
|
|
||||||
include: [resolve('src'), resolve('test')],
|
|
||||||
options: {
|
|
||||||
formatter: require('eslint-friendly-formatter'),
|
|
||||||
emitWarning: !config.dev.showEslintErrorsInOverlay
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
context: path.resolve(__dirname, '../'),
|
|
||||||
entry: {
|
|
||||||
app: './src/main.js'
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: config.build.assetsRoot,
|
|
||||||
filename: '[name].js',
|
|
||||||
publicPath:
|
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? config.build.assetsPublicPath
|
|
||||||
: config.dev.assetsPublicPath
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.vue', '.json'],
|
|
||||||
alias: {
|
|
||||||
'@': resolve('src')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
...(config.dev.useEslint ? [createLintingRule()] : []),
|
|
||||||
{
|
|
||||||
test: /\.vue$/,
|
|
||||||
loader: 'vue-loader',
|
|
||||||
options: vueLoaderConfig
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
include: [
|
|
||||||
resolve('src'),
|
|
||||||
resolve('test'),
|
|
||||||
resolve('mock'),
|
|
||||||
resolve('node_modules/webpack-dev-server/client')
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
loader: 'svg-sprite-loader',
|
|
||||||
include: [resolve('src/icons')],
|
|
||||||
options: {
|
|
||||||
symbolId: 'icon-[name]'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
|
||||||
loader: 'url-loader',
|
|
||||||
exclude: [resolve('src/icons')],
|
|
||||||
options: {
|
|
||||||
limit: 10000,
|
|
||||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
|
|
||||||
loader: 'url-loader',
|
|
||||||
options: {
|
|
||||||
limit: 10000,
|
|
||||||
name: utils.assetsPath('media/[name].[hash:7].[ext]')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
|
||||||
loader: 'url-loader',
|
|
||||||
options: {
|
|
||||||
limit: 10000,
|
|
||||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [new VueLoaderPlugin()],
|
|
||||||
node: {
|
|
||||||
// prevent webpack from injecting useless setImmediate polyfill because Vue
|
|
||||||
// source contains it (although only uses it if it's native).
|
|
||||||
setImmediate: false,
|
|
||||||
// prevent webpack from injecting mocks to Node native modules
|
|
||||||
// that does not make sense for the client
|
|
||||||
dgram: 'empty',
|
|
||||||
fs: 'empty',
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty',
|
|
||||||
child_process: 'empty'
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const path = require('path')
|
|
||||||
const utils = require('./utils')
|
|
||||||
const webpack = require('webpack')
|
|
||||||
const config = require('../config')
|
|
||||||
const merge = require('webpack-merge')
|
|
||||||
const baseWebpackConfig = require('./webpack.base.conf')
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
|
||||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
|
||||||
const portfinder = require('portfinder')
|
|
||||||
|
|
||||||
function resolve(dir) {
|
|
||||||
return path.join(__dirname, '..', dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const HOST = process.env.HOST
|
|
||||||
const PORT = process.env.PORT && Number(process.env.PORT)
|
|
||||||
|
|
||||||
const devWebpackConfig = merge(baseWebpackConfig, {
|
|
||||||
mode: 'development',
|
|
||||||
module: {
|
|
||||||
rules: utils.styleLoaders({
|
|
||||||
sourceMap: config.dev.cssSourceMap,
|
|
||||||
usePostCSS: true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// cheap-module-eval-source-map is faster for development
|
|
||||||
devtool: config.dev.devtool,
|
|
||||||
|
|
||||||
// these devServer options should be customized in /config/index.js
|
|
||||||
devServer: {
|
|
||||||
clientLogLevel: 'warning',
|
|
||||||
historyApiFallback: true,
|
|
||||||
hot: true,
|
|
||||||
compress: true,
|
|
||||||
host: HOST || config.dev.host,
|
|
||||||
port: PORT || config.dev.port,
|
|
||||||
open: config.dev.autoOpenBrowser,
|
|
||||||
overlay: config.dev.errorOverlay
|
|
||||||
? { warnings: false, errors: true }
|
|
||||||
: false,
|
|
||||||
publicPath: config.dev.assetsPublicPath,
|
|
||||||
proxy: config.dev.proxyTable,
|
|
||||||
quiet: true, // necessary for FriendlyErrorsPlugin
|
|
||||||
watchOptions: {
|
|
||||||
poll: config.dev.poll
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': require('../config/dev.env')
|
|
||||||
}),
|
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
|
||||||
// https://github.com/ampedandwired/html-webpack-plugin
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: 'index.html',
|
|
||||||
template: 'index.html',
|
|
||||||
inject: true,
|
|
||||||
favicon: resolve('favicon.ico'),
|
|
||||||
title: 'vue-admin-template'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = new Promise((resolve, reject) => {
|
|
||||||
portfinder.basePort = process.env.PORT || config.dev.port
|
|
||||||
portfinder.getPort((err, port) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
// publish the new Port, necessary for e2e tests
|
|
||||||
process.env.PORT = port
|
|
||||||
// add port to devServer config
|
|
||||||
devWebpackConfig.devServer.port = port
|
|
||||||
|
|
||||||
// Add FriendlyErrorsPlugin
|
|
||||||
devWebpackConfig.plugins.push(
|
|
||||||
new FriendlyErrorsPlugin({
|
|
||||||
compilationSuccessInfo: {
|
|
||||||
messages: [
|
|
||||||
`Your application is running here: http://${
|
|
||||||
devWebpackConfig.devServer.host
|
|
||||||
}:${port}`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
onErrors: config.dev.notifyOnErrors
|
|
||||||
? utils.createNotifierCallback()
|
|
||||||
: undefined
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
resolve(devWebpackConfig)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,177 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const path = require('path')
|
|
||||||
const utils = require('./utils')
|
|
||||||
const webpack = require('webpack')
|
|
||||||
const config = require('../config')
|
|
||||||
const merge = require('webpack-merge')
|
|
||||||
const baseWebpackConfig = require('./webpack.base.conf')
|
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
|
||||||
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
|
||||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
|
|
||||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
|
||||||
|
|
||||||
function resolve(dir) {
|
|
||||||
return path.join(__dirname, '..', dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = require('../config/prod.env')
|
|
||||||
|
|
||||||
// For NamedChunksPlugin
|
|
||||||
const seen = new Set()
|
|
||||||
const nameLength = 4
|
|
||||||
|
|
||||||
const webpackConfig = merge(baseWebpackConfig, {
|
|
||||||
mode: 'production',
|
|
||||||
module: {
|
|
||||||
rules: utils.styleLoaders({
|
|
||||||
sourceMap: config.build.productionSourceMap,
|
|
||||||
extract: true,
|
|
||||||
usePostCSS: true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
devtool: config.build.productionSourceMap ? config.build.devtool : false,
|
|
||||||
output: {
|
|
||||||
path: config.build.assetsRoot,
|
|
||||||
filename: utils.assetsPath('js/[name].[chunkhash:8].js'),
|
|
||||||
chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js')
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': env
|
|
||||||
}),
|
|
||||||
// extract css into its own file
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: utils.assetsPath('css/[name].[contenthash:8].css'),
|
|
||||||
chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css')
|
|
||||||
}),
|
|
||||||
// generate dist index.html with correct asset hash for caching.
|
|
||||||
// you can customize output by editing /index.html
|
|
||||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: config.build.index,
|
|
||||||
template: 'index.html',
|
|
||||||
inject: true,
|
|
||||||
favicon: resolve('favicon.ico'),
|
|
||||||
title: 'vue-admin-template',
|
|
||||||
minify: {
|
|
||||||
removeComments: true,
|
|
||||||
collapseWhitespace: true,
|
|
||||||
removeAttributeQuotes: true
|
|
||||||
// more options:
|
|
||||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
|
||||||
}
|
|
||||||
// default sort mode uses toposort which cannot handle cyclic deps
|
|
||||||
// in certain cases, and in webpack 4, chunk order in HTML doesn't
|
|
||||||
// matter anyway
|
|
||||||
}),
|
|
||||||
new ScriptExtHtmlWebpackPlugin({
|
|
||||||
//`runtime` must same as runtimeChunk name. default is `runtime`
|
|
||||||
inline: /runtime\..*\.js$/
|
|
||||||
}),
|
|
||||||
// keep chunk.id stable when chunk has no name
|
|
||||||
new webpack.NamedChunksPlugin(chunk => {
|
|
||||||
if (chunk.name) {
|
|
||||||
return chunk.name
|
|
||||||
}
|
|
||||||
const modules = Array.from(chunk.modulesIterable)
|
|
||||||
if (modules.length > 1) {
|
|
||||||
const hash = require('hash-sum')
|
|
||||||
const joinedHash = hash(modules.map(m => m.id).join('_'))
|
|
||||||
let len = nameLength
|
|
||||||
while (seen.has(joinedHash.substr(0, len))) len++
|
|
||||||
seen.add(joinedHash.substr(0, len))
|
|
||||||
return `chunk-${joinedHash.substr(0, len)}`
|
|
||||||
} else {
|
|
||||||
return modules[0].id
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// keep module.id stable when vender modules does not change
|
|
||||||
new webpack.HashedModuleIdsPlugin(),
|
|
||||||
// copy custom static assets
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: path.resolve(__dirname, '../static'),
|
|
||||||
to: config.build.assetsSubDirectory,
|
|
||||||
ignore: ['.*']
|
|
||||||
}
|
|
||||||
])
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
splitChunks: {
|
|
||||||
chunks: 'all',
|
|
||||||
cacheGroups: {
|
|
||||||
libs: {
|
|
||||||
name: 'chunk-libs',
|
|
||||||
test: /[\\/]node_modules[\\/]/,
|
|
||||||
priority: 10,
|
|
||||||
chunks: 'initial' // 只打包初始时依赖的第三方
|
|
||||||
},
|
|
||||||
elementUI: {
|
|
||||||
name: 'chunk-elementUI', // 单独将 elementUI 拆包
|
|
||||||
priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
|
|
||||||
test: /[\\/]node_modules[\\/]element-ui[\\/]/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
runtimeChunk: 'single',
|
|
||||||
minimizer: [
|
|
||||||
new UglifyJsPlugin({
|
|
||||||
uglifyOptions: {
|
|
||||||
mangle: {
|
|
||||||
safari10: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sourceMap: config.build.productionSourceMap,
|
|
||||||
cache: true,
|
|
||||||
parallel: true
|
|
||||||
}),
|
|
||||||
// Compress extracted CSS. We are using this plugin so that possible
|
|
||||||
// duplicated CSS from different components can be deduped.
|
|
||||||
new OptimizeCSSAssetsPlugin()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (config.build.productionGzip) {
|
|
||||||
const CompressionWebpackPlugin = require('compression-webpack-plugin')
|
|
||||||
|
|
||||||
webpackConfig.plugins.push(
|
|
||||||
new CompressionWebpackPlugin({
|
|
||||||
algorithm: 'gzip',
|
|
||||||
test: new RegExp(
|
|
||||||
'\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
|
|
||||||
),
|
|
||||||
threshold: 10240,
|
|
||||||
minRatio: 0.8
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.build.generateAnalyzerReport || config.build.bundleAnalyzerReport) {
|
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
|
|
||||||
.BundleAnalyzerPlugin
|
|
||||||
|
|
||||||
if (config.build.bundleAnalyzerReport) {
|
|
||||||
webpackConfig.plugins.push(
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerPort: 8080,
|
|
||||||
generateStatsFile: false
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.build.generateAnalyzerReport) {
|
|
||||||
webpackConfig.plugins.push(
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: 'static',
|
|
||||||
reportFilename: 'bundle-report.html',
|
|
||||||
openAnalyzer: false
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = webpackConfig
|
|
@ -1,8 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
const merge = require('webpack-merge')
|
|
||||||
const prodEnv = require('./prod.env')
|
|
||||||
|
|
||||||
module.exports = merge(prodEnv, {
|
|
||||||
NODE_ENV: '"development"',
|
|
||||||
BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
|
|
||||||
})
|
|
@ -1,86 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
// Template version: 1.2.6
|
|
||||||
// see http://vuejs-templates.github.io/webpack for documentation.
|
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
dev: {
|
|
||||||
// Paths
|
|
||||||
assetsSubDirectory: 'static',
|
|
||||||
assetsPublicPath: '/',
|
|
||||||
proxyTable: {},
|
|
||||||
|
|
||||||
// Various Dev Server settings
|
|
||||||
host: 'localhost', // can be overwritten by process.env.HOST
|
|
||||||
port: 9528, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
|
|
||||||
autoOpenBrowser: true,
|
|
||||||
errorOverlay: true,
|
|
||||||
notifyOnErrors: false,
|
|
||||||
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
|
|
||||||
|
|
||||||
// Use Eslint Loader?
|
|
||||||
// If true, your code will be linted during bundling and
|
|
||||||
// linting errors and warnings will be shown in the console.
|
|
||||||
useEslint: true,
|
|
||||||
// If true, eslint errors and warnings will also be shown in the error overlay
|
|
||||||
// in the browser.
|
|
||||||
showEslintErrorsInOverlay: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source Maps
|
|
||||||
*/
|
|
||||||
|
|
||||||
// https://webpack.js.org/configuration/devtool/#development
|
|
||||||
devtool: 'cheap-source-map',
|
|
||||||
|
|
||||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
|
||||||
// with this option, according to the CSS-Loader README
|
|
||||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
|
||||||
// In our experience, they generally work as expected,
|
|
||||||
// just be aware of this issue when enabling this option.
|
|
||||||
cssSourceMap: false
|
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
|
||||||
// Template for index.html
|
|
||||||
index: path.resolve(__dirname, '../dist/index.html'),
|
|
||||||
|
|
||||||
// Paths
|
|
||||||
assetsRoot: path.resolve(__dirname, '../dist'),
|
|
||||||
assetsSubDirectory: 'static',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* You can set by youself according to actual condition
|
|
||||||
* You will need to set this 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 assetsPublicPath should be set to "/bar/".
|
|
||||||
* In most cases please use '/' !!!
|
|
||||||
*/
|
|
||||||
assetsPublicPath: '/',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source Maps
|
|
||||||
*/
|
|
||||||
|
|
||||||
productionSourceMap: false,
|
|
||||||
// https://webpack.js.org/configuration/devtool/#production
|
|
||||||
devtool: 'source-map',
|
|
||||||
|
|
||||||
// Gzip off by default as many popular static hosts such as
|
|
||||||
// Surge or Netlify already gzip all static assets for you.
|
|
||||||
// Before setting to `true`, make sure to:
|
|
||||||
// npm install --save-dev compression-webpack-plugin
|
|
||||||
productionGzip: false,
|
|
||||||
productionGzipExtensions: ['js', 'css'],
|
|
||||||
|
|
||||||
// Run the build command with an extra argument to
|
|
||||||
// View the bundle analyzer report after build finishes:
|
|
||||||
// `npm run build --report`
|
|
||||||
// Set to `true` or `false` to always turn it on or off
|
|
||||||
bundleAnalyzerReport: process.env.npm_config_report || false,
|
|
||||||
|
|
||||||
// `npm run build:prod --generate_report`
|
|
||||||
generateAnalyzerReport: process.env.npm_config_generate_report || false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
module.exports = {
|
|
||||||
NODE_ENV: '"production"',
|
|
||||||
BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
|
|
||||||
}
|
|
12
index.html
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>vue-admin-template</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
24
jest.config.js
Normal file
@ -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/'
|
||||||
|
}
|
@ -1,26 +1,66 @@
|
|||||||
import Mock from 'mockjs'
|
import Mock from 'mockjs'
|
||||||
import userAPI from './user'
|
import { param2Obj } from '../src/utils'
|
||||||
import tableAPI from './table'
|
|
||||||
|
|
||||||
// Fix an issue with setting withCredentials = true, cross-domain request lost cookies
|
import user from './user'
|
||||||
// https://github.com/nuysoft/Mock/issues/300
|
import table from './table'
|
||||||
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
|
|
||||||
Mock.XHR.prototype.send = function() {
|
const mocks = [
|
||||||
if (this.custom.xhr) {
|
...user,
|
||||||
this.custom.xhr.withCredentials = this.withCredentials || false
|
...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).
|
||||||
|
export 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))
|
||||||
}
|
}
|
||||||
this.proxy_send(...arguments)
|
|
||||||
}
|
}
|
||||||
// Mock.setup({
|
|
||||||
// timeout: '350-600'
|
|
||||||
// })
|
|
||||||
|
|
||||||
// User
|
// for mock server
|
||||||
Mock.mock(/\/user\/login/, 'post', userAPI.login)
|
const responseFake = (url, type, respond) => {
|
||||||
Mock.mock(/\/user\/info/, 'get', userAPI.getInfo)
|
return {
|
||||||
Mock.mock(/\/user\/logout/, 'post', userAPI.logout)
|
url: new RegExp(`/mock${url}`),
|
||||||
|
type: type || 'get',
|
||||||
|
response(req, res) {
|
||||||
|
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Table
|
export default mocks.map(route => {
|
||||||
Mock.mock(/\/table\/list/, 'get', tableAPI.list)
|
return responseFake(route.url, route.type, route.response)
|
||||||
|
})
|
||||||
export default Mock
|
|
||||||
|
64
mock/mock-server.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
const chokidar = require('chokidar')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
const chalk = require('chalk')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const mockDir = path.join(process.cwd(), 'mock')
|
||||||
|
|
||||||
|
function registerRoutes(app) {
|
||||||
|
let mockLastIndex
|
||||||
|
const { default: mocks } = require('./index.js')
|
||||||
|
for (const mock of mocks) {
|
||||||
|
app[mock.type](mock.url, mock.response)
|
||||||
|
mockLastIndex = app._router.stack.length
|
||||||
|
}
|
||||||
|
const mockRoutesLength = Object.keys(mocks).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)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = app => {
|
||||||
|
// es6 polyfill
|
||||||
|
require('@babel/register')
|
||||||
|
|
||||||
|
// 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') {
|
||||||
|
// 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}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,20 +1,29 @@
|
|||||||
import Mock from 'mockjs'
|
import Mock from 'mockjs'
|
||||||
|
|
||||||
export default {
|
const data = Mock.mock({
|
||||||
list: () => {
|
'items|30': [{
|
||||||
const items = Mock.mock({
|
id: '@id',
|
||||||
'items|30': [{
|
title: '@sentence(10, 20)',
|
||||||
id: '@id',
|
'status|1': ['published', 'draft', 'deleted'],
|
||||||
title: '@sentence(10, 20)',
|
author: 'name',
|
||||||
'status|1': ['published', 'draft', 'deleted'],
|
display_time: '@datetime',
|
||||||
author: 'name',
|
pageviews: '@integer(300, 5000)'
|
||||||
display_time: '@datetime',
|
}]
|
||||||
pageviews: '@integer(300, 5000)'
|
})
|
||||||
}]
|
|
||||||
})
|
export default [
|
||||||
return {
|
{
|
||||||
code: 20000,
|
url: '/table/list',
|
||||||
data: items
|
type: 'get',
|
||||||
|
response: config => {
|
||||||
|
const items = data.items
|
||||||
|
return {
|
||||||
|
code: 20000,
|
||||||
|
data: {
|
||||||
|
total: items.length,
|
||||||
|
items: items
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
68
mock/user.js
@ -1,4 +1,3 @@
|
|||||||
import { param2Obj } from './utils'
|
|
||||||
|
|
||||||
const tokens = {
|
const tokens = {
|
||||||
admin: {
|
admin: {
|
||||||
@ -24,41 +23,62 @@ const users = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default [
|
||||||
login: res => {
|
// user login
|
||||||
const { username } = JSON.parse(res.body)
|
{
|
||||||
const data = tokens[username]
|
url: '/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.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return {
|
return {
|
||||||
code: 20000,
|
code: 20000,
|
||||||
data
|
data: token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
code: 60204,
|
|
||||||
message: 'Account and password are incorrect.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getInfo: res => {
|
|
||||||
const { token } = param2Obj(res.url)
|
|
||||||
const info = users[token]
|
|
||||||
|
|
||||||
if (info) {
|
// get user info
|
||||||
|
{
|
||||||
|
url: '/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 {
|
return {
|
||||||
code: 20000,
|
code: 20000,
|
||||||
data: info
|
data: info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
code: 50008,
|
|
||||||
message: 'Login failed, unable to get user details.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
logout: () => {
|
|
||||||
return {
|
// user logout
|
||||||
code: 20000,
|
{
|
||||||
data: 'success'
|
url: '/user/logout',
|
||||||
|
type: 'post',
|
||||||
|
response: _ => {
|
||||||
|
return {
|
||||||
|
code: 20000,
|
||||||
|
data: 'success'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
export function param2Obj(url) {
|
|
||||||
const search = url.split('?')[1]
|
|
||||||
if (!search) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
return JSON.parse(
|
|
||||||
'{"' +
|
|
||||||
decodeURIComponent(search)
|
|
||||||
.replace(/"/g, '\\"')
|
|
||||||
.replace(/&/g, '","')
|
|
||||||
.replace(/=/g, '":"') +
|
|
||||||
'"}'
|
|
||||||
)
|
|
||||||
}
|
|
101
package.json
@ -1,82 +1,59 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-admin-template",
|
"name": "vue-admin-template",
|
||||||
"version": "3.9.0",
|
"version": "4.1.0",
|
||||||
"license": "MIT",
|
|
||||||
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
|
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
|
||||||
"author": "Pan <panfree23@gmail.com>",
|
"author": "Pan <panfree23@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
|
"dev": "vue-cli-service serve",
|
||||||
"start": "npm run dev",
|
"build:prod": "vue-cli-service build",
|
||||||
"build": "node build/build.js",
|
"build:stage": "vue-cli-service build --mode staging",
|
||||||
"build:report": "npm_config_report=true npm run build",
|
"preview": "node build/index.js --preview",
|
||||||
"lint": "eslint --ext .js,.vue src",
|
"lint": "eslint --ext .js,.vue src",
|
||||||
"test": "npm run lint",
|
"test:unit": "jest --clearCache && vue-cli-service test:unit",
|
||||||
|
"test:ci": "npm run lint && npm run test:unit",
|
||||||
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
|
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "0.18.0",
|
"axios": "0.18.0",
|
||||||
"element-ui": "2.4.6",
|
"element-ui": "2.7.2",
|
||||||
"js-cookie": "2.2.0",
|
"js-cookie": "2.2.0",
|
||||||
"mockjs": "1.0.1-beta3",
|
|
||||||
"normalize.css": "7.0.0",
|
"normalize.css": "7.0.0",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"vue": "2.5.17",
|
"path-to-regexp": "2.4.0",
|
||||||
"vue-router": "3.0.1",
|
"vue": "2.6.10",
|
||||||
"vuex": "3.0.1"
|
"vue-router": "3.0.6",
|
||||||
|
"vuex": "3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "8.5.0",
|
"@babel/core": "7.0.0",
|
||||||
"babel-core": "6.26.0",
|
"@babel/register": "7.0.0",
|
||||||
"babel-eslint": "8.2.6",
|
"@vue/cli-plugin-babel": "3.6.0",
|
||||||
"babel-helper-vue-jsx-merge-props": "2.0.3",
|
"@vue/cli-plugin-eslint": "3.6.0",
|
||||||
"babel-loader": "7.1.5",
|
"@vue/cli-plugin-unit-jest": "3.6.3",
|
||||||
"babel-plugin-syntax-jsx": "6.18.0",
|
"@vue/cli-service": "3.6.0",
|
||||||
"babel-plugin-transform-runtime": "6.23.0",
|
"@vue/test-utils": "1.0.0-beta.29",
|
||||||
"babel-plugin-transform-vue-jsx": "3.7.0",
|
"babel-core": "7.0.0-bridge.0",
|
||||||
"babel-preset-env": "1.7.0",
|
"babel-eslint": "10.0.1",
|
||||||
"babel-preset-stage-2": "6.24.1",
|
"babel-jest": "23.6.0",
|
||||||
"chalk": "2.4.1",
|
"chalk": "2.4.2",
|
||||||
"compression-webpack-plugin": "2.0.0",
|
"connect": "3.6.6",
|
||||||
"copy-webpack-plugin": "4.5.2",
|
"eslint": "5.15.3",
|
||||||
"css-loader": "1.0.0",
|
"eslint-plugin-vue": "5.2.2",
|
||||||
"eslint": "4.19.1",
|
"html-webpack-plugin": "3.2.0",
|
||||||
"eslint-friendly-formatter": "4.0.1",
|
"mockjs": "1.0.1-beta3",
|
||||||
"eslint-loader": "2.0.0",
|
"node-sass": "^4.9.0",
|
||||||
"eslint-plugin-vue": "4.7.1",
|
"runjs": "^4.3.2",
|
||||||
"eventsource-polyfill": "0.9.6",
|
"sass-loader": "^7.1.0",
|
||||||
"file-loader": "1.1.11",
|
"script-ext-html-webpack-plugin": "2.1.3",
|
||||||
"friendly-errors-webpack-plugin": "1.7.0",
|
"script-loader": "0.7.2",
|
||||||
"html-webpack-plugin": "4.0.0-alpha",
|
"serve-static": "^1.13.2",
|
||||||
"mini-css-extract-plugin": "0.4.1",
|
"svg-sprite-loader": "4.1.3",
|
||||||
"node-notifier": "5.2.1",
|
"svgo": "1.2.2",
|
||||||
"node-sass": "^4.7.2",
|
"vue-template-compiler": "2.6.10"
|
||||||
"optimize-css-assets-webpack-plugin": "5.0.0",
|
|
||||||
"ora": "3.0.0",
|
|
||||||
"path-to-regexp": "2.4.0",
|
|
||||||
"portfinder": "1.0.16",
|
|
||||||
"postcss-import": "12.0.0",
|
|
||||||
"postcss-loader": "2.1.6",
|
|
||||||
"postcss-url": "7.3.2",
|
|
||||||
"rimraf": "2.6.2",
|
|
||||||
"sass-loader": "7.0.3",
|
|
||||||
"script-ext-html-webpack-plugin": "2.0.1",
|
|
||||||
"semver": "5.5.0",
|
|
||||||
"shelljs": "0.8.2",
|
|
||||||
"svg-sprite-loader": "3.8.0",
|
|
||||||
"svgo": "1.0.5",
|
|
||||||
"uglifyjs-webpack-plugin": "1.2.7",
|
|
||||||
"url-loader": "1.0.1",
|
|
||||||
"vue-loader": "15.3.0",
|
|
||||||
"vue-style-loader": "4.1.2",
|
|
||||||
"vue-template-compiler": "2.5.17",
|
|
||||||
"webpack": "4.16.5",
|
|
||||||
"webpack-bundle-analyzer": "2.13.1",
|
|
||||||
"webpack-cli": "3.1.0",
|
|
||||||
"webpack-dev-server": "3.1.14",
|
|
||||||
"webpack-merge": "4.1.4"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6.0.0",
|
"node": ">=8.9",
|
||||||
"npm": ">= 3.0.0"
|
"npm": ">= 3.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
17
public/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!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>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view/>
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
export function login(username, password) {
|
export function login(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/user/login',
|
url: '/user/login',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data
|
||||||
username,
|
|
||||||
password
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
|||||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||||
<transition-group name="breadcrumb">
|
<transition-group name="breadcrumb">
|
||||||
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
|
<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>
|
<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>
|
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
@ -28,15 +28,23 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getBreadcrumb() {
|
getBreadcrumb() {
|
||||||
let matched = this.$route.matched.filter(item => item.name)
|
// only show routes with meta.title
|
||||||
|
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
|
||||||
const first = matched[0]
|
const first = matched[0]
|
||||||
if (first && first.name !== 'dashboard') {
|
|
||||||
|
if (!this.isDashboard(first)) {
|
||||||
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
|
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
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) {
|
pathCompile(path) {
|
||||||
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
|
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
|
||||||
const { params } = this.$route
|
const { params } = this.$route
|
||||||
@ -55,15 +63,16 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.app-breadcrumb.el-breadcrumb {
|
.app-breadcrumb.el-breadcrumb {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
margin-left: 10px;
|
margin-left: 8px;
|
||||||
.no-redirect {
|
|
||||||
color: #97a8be;
|
.no-redirect {
|
||||||
cursor: text;
|
color: #97a8be;
|
||||||
}
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="padding: 0 15px;" @click="toggleClick">
|
||||||
<svg
|
<svg
|
||||||
:class="{'is-active':isActive}"
|
:class="{'is-active':isActive}"
|
||||||
class="hamburger"
|
class="hamburger"
|
||||||
@ -7,7 +7,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="64"
|
width="64"
|
||||||
height="64"
|
height="64"
|
||||||
@click="toggleClick">
|
>
|
||||||
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@ -20,10 +20,11 @@ export default {
|
|||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
}
|
||||||
toggleClick: {
|
},
|
||||||
type: Function,
|
methods: {
|
||||||
default: null
|
toggleClick() {
|
||||||
|
this.$emit('toggleClick')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,10 +33,11 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.hamburger {
|
.hamburger {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
vertical-align: middle;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburger.is-active {
|
.hamburger.is-active {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
|
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
|
||||||
<use :xlink:href="iconName"/>
|
<use :xlink:href="iconName" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import SvgIcon from '@/components/SvgIcon' // svg组件
|
import SvgIcon from '@/components/SvgIcon'// svg component
|
||||||
|
|
||||||
// register globally
|
// register globally
|
||||||
Vue.component('svg-icon', SvgIcon)
|
Vue.component('svg-icon', SvgIcon)
|
||||||
|
|
||||||
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
|
||||||
const req = require.context('./svg', false, /\.svg$/)
|
const req = require.context('./svg', false, /\.svg$/)
|
||||||
|
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
||||||
requireAll(req)
|
requireAll(req)
|
||||||
|
1
src/icons/svg/dashboard.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
@ -1 +1 @@
|
|||||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@ -1 +1 @@
|
|||||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><g><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></g></svg>
|
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 285 B |
@ -1 +1 @@
|
|||||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><g><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></g></svg>
|
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 597 B |
@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="app-main">
|
<section class="app-main">
|
||||||
<transition name="fade-transform" mode="out-in">
|
<transition name="fade-transform" mode="out-in">
|
||||||
<!-- or name="fade" -->
|
<router-view :key="key" />
|
||||||
<!-- <router-view :key="key"></router-view> -->
|
|
||||||
<router-view/>
|
|
||||||
</transition>
|
</transition>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@ -12,9 +10,9 @@
|
|||||||
export default {
|
export default {
|
||||||
name: 'AppMain',
|
name: 'AppMain',
|
||||||
computed: {
|
computed: {
|
||||||
// key() {
|
key() {
|
||||||
// return this.$route.name !== undefined ? this.$route.name + +new Date() : this.$route + +new Date()
|
return this.$route.fullPath
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -23,7 +21,11 @@ export default {
|
|||||||
.app-main {
|
.app-main {
|
||||||
/*50 = navbar */
|
/*50 = navbar */
|
||||||
min-height: calc(100vh - 50px);
|
min-height: calc(100vh - 50px);
|
||||||
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.fixed-header+.app-main {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
139
src/layout/components/Navbar.vue
Normal file
@ -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>
|
||||||
|
<span style="display:block;" @click="logout">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>
|
26
src/layout/components/Sidebar/FixiOSBug.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,18 +3,17 @@ export default {
|
|||||||
name: 'MenuItem',
|
name: 'MenuItem',
|
||||||
functional: true,
|
functional: true,
|
||||||
props: {
|
props: {
|
||||||
meta: {
|
icon: {
|
||||||
type: Object,
|
type: String,
|
||||||
default: () => {
|
default: ''
|
||||||
return {
|
},
|
||||||
title: '',
|
title: {
|
||||||
icon: ''
|
type: String,
|
||||||
}
|
default: ''
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render(h, context) {
|
render(h, context) {
|
||||||
const { icon, title } = context.props.meta
|
const { icon, title } = context.props
|
||||||
const vnodes = []
|
const vnodes = []
|
||||||
|
|
||||||
if (icon) {
|
if (icon) {
|
@ -2,7 +2,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/require-component-is -->
|
<!-- eslint-disable vue/require-component-is -->
|
||||||
<component v-bind="linkProps(to)">
|
<component v-bind="linkProps(to)">
|
||||||
<slot/>
|
<slot />
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
82
src/layout/components/Sidebar/Logo.vue
Normal file
@ -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>
|
@ -1,27 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!item.hidden" class="menu-wrapper">
|
<div v-if="!item.hidden" class="menu-wrapper">
|
||||||
|
|
||||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||||
<app-link :to="resolvePath(onlyOneChild.path)">
|
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
||||||
<item :meta="Object.assign({},item.meta,onlyOneChild.meta)" />
|
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</app-link>
|
</app-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<item :meta="item.meta" />
|
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
|
||||||
</template>
|
</template>
|
||||||
<sidebar-item
|
<sidebar-item
|
||||||
v-for="child in item.children"
|
v-for="child in item.children"
|
||||||
|
:key="child.path"
|
||||||
:is-nest="true"
|
:is-nest="true"
|
||||||
:item="child"
|
:item="child"
|
||||||
:key="child.path"
|
|
||||||
:base-path="resolvePath(child.path)"
|
:base-path="resolvePath(child.path)"
|
||||||
class="nest-menu" />
|
class="nest-menu"
|
||||||
|
/>
|
||||||
</el-submenu>
|
</el-submenu>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -30,10 +29,12 @@ import path from 'path'
|
|||||||
import { isExternal } from '@/utils/validate'
|
import { isExternal } from '@/utils/validate'
|
||||||
import Item from './Item'
|
import Item from './Item'
|
||||||
import AppLink from './Link'
|
import AppLink from './Link'
|
||||||
|
import FixiOSBug from './FixiOSBug'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SidebarItem',
|
name: 'SidebarItem',
|
||||||
components: { Item, AppLink },
|
components: { Item, AppLink },
|
||||||
|
mixins: [FixiOSBug],
|
||||||
props: {
|
props: {
|
||||||
// route object
|
// route object
|
||||||
item: {
|
item: {
|
||||||
@ -84,6 +85,9 @@ export default {
|
|||||||
if (isExternal(routePath)) {
|
if (isExternal(routePath)) {
|
||||||
return routePath
|
return routePath
|
||||||
}
|
}
|
||||||
|
if (isExternal(this.basePath)) {
|
||||||
|
return this.basePath
|
||||||
|
}
|
||||||
return path.resolve(this.basePath, routePath)
|
return path.resolve(this.basePath, routePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
54
src/layout/components/Sidebar/index.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{'has-logo':showLogo}">
|
||||||
|
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||||
|
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="isCollapse"
|
||||||
|
:background-color="variables.menuBg"
|
||||||
|
:text-color="variables.menuText"
|
||||||
|
:unique-opened="false"
|
||||||
|
:active-text-color="variables.menuActiveText"
|
||||||
|
:collapse-transition="false"
|
||||||
|
mode="vertical"
|
||||||
|
>
|
||||||
|
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import Logo from './Logo'
|
||||||
|
import SidebarItem from './SidebarItem'
|
||||||
|
import variables from '@/styles/variables.scss'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { SidebarItem, Logo },
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'permission_routes',
|
||||||
|
'sidebar'
|
||||||
|
]),
|
||||||
|
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>
|
@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="classObj" class="app-wrapper">
|
<div :class="classObj" class="app-wrapper">
|
||||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||||
<sidebar class="sidebar-container"/>
|
<sidebar class="sidebar-container" />
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<navbar/>
|
<div :class="{'fixed-header':fixedHeader}">
|
||||||
<app-main/>
|
<navbar />
|
||||||
|
</div>
|
||||||
|
<app-main />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -28,6 +30,9 @@ export default {
|
|||||||
device() {
|
device() {
|
||||||
return this.$store.state.app.device
|
return this.$store.state.app.device
|
||||||
},
|
},
|
||||||
|
fixedHeader() {
|
||||||
|
return this.$store.state.settings.fixedHeader
|
||||||
|
},
|
||||||
classObj() {
|
classObj() {
|
||||||
return {
|
return {
|
||||||
hideSidebar: !this.sidebar.opened,
|
hideSidebar: !this.sidebar.opened,
|
||||||
@ -45,8 +50,10 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "src/styles/mixin.scss";
|
@import "~@/styles/mixin.scss";
|
||||||
|
@import "~@/styles/variables.scss";
|
||||||
|
|
||||||
.app-wrapper {
|
.app-wrapper {
|
||||||
@include clearfix;
|
@include clearfix;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -66,4 +73,21 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999;
|
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>
|
</style>
|
45
src/layout/mixin/ResizeHandler.js
Normal file
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/main.js
@ -16,15 +16,15 @@ import '@/icons' // icon
|
|||||||
import '@/permission' // permission control
|
import '@/permission' // permission control
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This project originally used easy-mock to simulate data,
|
* If you don't want to use mock-server
|
||||||
* but its official service is very unstable,
|
* you want to use mockjs for request interception
|
||||||
* and you can build your own service if you need it.
|
* you can execute:
|
||||||
* So here I use Mock.js for local emulation,
|
*
|
||||||
* it will intercept your request, so you won't see the request in the network.
|
* import { mockXHR } from '../mock'
|
||||||
* If you remove `../mock` it will automatically request easy-mock data.
|
* mockXHR()
|
||||||
*/
|
*/
|
||||||
import '../mock' // simulation data
|
|
||||||
|
|
||||||
|
// set ElementUI lang to EN
|
||||||
Vue.use(ElementUI, { locale })
|
Vue.use(ElementUI, { locale })
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
@ -1,43 +1,74 @@
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
import { Message } from 'element-ui'
|
||||||
import NProgress from 'nprogress' // progress bar
|
import NProgress from 'nprogress' // progress bar
|
||||||
import 'nprogress/nprogress.css' // progress bar style
|
import 'nprogress/nprogress.css' // progress bar style
|
||||||
import { Message } from 'element-ui'
|
import { getToken } from '@/utils/auth' // get token from cookie
|
||||||
import { getToken } from '@/utils/auth' // getToken from cookie
|
import getPageTitle from '@/utils/get-page-title'
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false })// NProgress configuration
|
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||||
|
|
||||||
const whiteList = ['/login'] // 不重定向白名单
|
const whiteList = ['/login'] // no redirect whitelist
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
|
router.beforeEach(async(to, from, next) => {
|
||||||
|
// start progress bar
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
if (getToken()) {
|
|
||||||
|
// 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 (to.path === '/login') {
|
||||||
|
// if is logged in, redirect to the home page
|
||||||
next({ path: '/' })
|
next({ path: '/' })
|
||||||
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
|
NProgress.done()
|
||||||
} else {
|
} else {
|
||||||
if (store.getters.roles.length === 0) {
|
// determine whether the user has obtained his permission roles through getInfo
|
||||||
store.dispatch('GetInfo').then(res => { // 拉取用户信息
|
const hasRoles = store.getters.roles && store.getters.roles.length > 0
|
||||||
next()
|
if (hasRoles) {
|
||||||
}).catch((err) => {
|
|
||||||
store.dispatch('FedLogOut').then(() => {
|
|
||||||
Message.error(err || 'Verification failed, please login again')
|
|
||||||
next({ path: '/' })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
next()
|
next()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// get user info
|
||||||
|
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
|
||||||
|
const { roles } = await store.dispatch('user/getInfo')
|
||||||
|
|
||||||
|
// generate accessible routes map based on roles
|
||||||
|
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
|
||||||
|
|
||||||
|
// dynamically add accessible routes
|
||||||
|
router.addRoutes(accessRoutes)
|
||||||
|
|
||||||
|
// hack method to ensure that addRoutes is complete
|
||||||
|
// set the replace: true, so the navigation will not leave a history record
|
||||||
|
next({ ...to, replace: true })
|
||||||
|
} 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 {
|
} else {
|
||||||
|
/* has no token*/
|
||||||
|
|
||||||
if (whiteList.indexOf(to.path) !== -1) {
|
if (whiteList.indexOf(to.path) !== -1) {
|
||||||
|
// in the free login whitelist, go directly
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
|
// other pages that do not have permission to access are redirected to the login page.
|
||||||
|
next(`/login?redirect=${to.path}`)
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.afterEach(() => {
|
router.afterEach(() => {
|
||||||
NProgress.done() // 结束Progress
|
// finish progress bar
|
||||||
|
NProgress.done()
|
||||||
})
|
})
|
||||||
|
@ -1,40 +1,57 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
|
|
||||||
// in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading;
|
|
||||||
// detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading
|
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
|
||||||
/* Layout */
|
/* Layout */
|
||||||
import Layout from '../views/layout/Layout'
|
import Layout from '@/layout'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
|
* Note: sub-menu only appear when route children.length >= 1
|
||||||
* alwaysShow: true if set true, will always show the root menu, whatever its child routes length
|
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
|
||||||
* if not set alwaysShow, only more than one route under the children
|
*
|
||||||
* it will becomes nested mode, otherwise not show the root menu
|
* hidden: true if set true, item will not show in the sidebar(default is false)
|
||||||
* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb
|
* alwaysShow: true if set true, will always show the root menu
|
||||||
* name:'router-name' the name is used by <keep-alive> (must set!!!)
|
* if not set alwaysShow, when item has more than one children route,
|
||||||
* meta : {
|
* it will becomes nested mode, otherwise not show the root menu
|
||||||
title: 'title' the name show in subMenu and breadcrumb (recommend set)
|
* 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' the icon show in the sidebar
|
icon: 'svg-name' the icon show in the sidebar
|
||||||
breadcrumb: false if false, the item will hidden in breadcrumb(default is true)
|
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
|
||||||
}
|
}
|
||||||
**/
|
*/
|
||||||
export const constantRouterMap = [
|
|
||||||
{ path: '/login', component: () => import('@/views/login/index'), hidden: true },
|
/**
|
||||||
{ path: '/404', component: () => import('@/views/404'), hidden: true },
|
* 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: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/dashboard',
|
redirect: '/dashboard',
|
||||||
name: 'Dashboard',
|
|
||||||
hidden: true,
|
|
||||||
children: [{
|
children: [{
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
component: () => import('@/views/dashboard/index')
|
name: 'Dashboard',
|
||||||
|
component: () => import('@/views/dashboard/index'),
|
||||||
|
meta: { title: 'Dashboard', icon: 'dashboard' }
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -71,8 +88,14 @@ export const constantRouterMap = [
|
|||||||
meta: { title: 'Form', icon: 'form' }
|
meta: { title: 'Form', icon: 'form' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* asyncRoutes
|
||||||
|
* the routes that need to be dynamically loaded based on user roles
|
||||||
|
*/
|
||||||
|
export const asyncRoutes = [
|
||||||
{
|
{
|
||||||
path: '/nested',
|
path: '/nested',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@ -142,11 +165,22 @@ export const constantRouterMap = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 404 page must be placed at the end !!!
|
||||||
{ path: '*', redirect: '/404', hidden: true }
|
{ path: '*', redirect: '/404', hidden: true }
|
||||||
]
|
]
|
||||||
|
|
||||||
export default new Router({
|
const createRouter = () => new Router({
|
||||||
// mode: 'history', //后端支持可开
|
// mode: 'history', // require service support
|
||||||
scrollBehavior: () => ({ y: 0 }),
|
scrollBehavior: () => ({ y: 0 }),
|
||||||
routes: constantRouterMap
|
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
|
||||||
|
16
src/settings.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
|
||||||
|
title: 'Vue Admin Template',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {boolean} true | false
|
||||||
|
* @description Whether fix the header
|
||||||
|
*/
|
||||||
|
fixedHeader: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {boolean} true | false
|
||||||
|
* @description Whether show the logo in sidebar
|
||||||
|
*/
|
||||||
|
sidebarLogo: false
|
||||||
|
}
|
@ -4,6 +4,7 @@ const getters = {
|
|||||||
token: state => state.user.token,
|
token: state => state.user.token,
|
||||||
avatar: state => state.user.avatar,
|
avatar: state => state.user.avatar,
|
||||||
name: state => state.user.name,
|
name: state => state.user.name,
|
||||||
roles: state => state.user.roles
|
roles: state => state.user.roles,
|
||||||
|
permission_routes: state => state.permission.routes
|
||||||
}
|
}
|
||||||
export default getters
|
export default getters
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import app from './modules/app'
|
|
||||||
import user from './modules/user'
|
|
||||||
import getters from './getters'
|
import getters from './getters'
|
||||||
|
import app from './modules/app'
|
||||||
|
import permission from './modules/permission'
|
||||||
|
import settings from './modules/settings'
|
||||||
|
import user from './modules/user'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
app,
|
app,
|
||||||
|
permission,
|
||||||
|
settings,
|
||||||
user
|
user
|
||||||
},
|
},
|
||||||
getters
|
getters
|
||||||
|
@ -1,43 +1,48 @@
|
|||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
const app = {
|
const state = {
|
||||||
state: {
|
sidebar: {
|
||||||
sidebar: {
|
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
|
||||||
opened: !+Cookies.get('sidebarStatus'),
|
withoutAnimation: false
|
||||||
withoutAnimation: false
|
|
||||||
},
|
|
||||||
device: 'desktop'
|
|
||||||
},
|
},
|
||||||
mutations: {
|
device: 'desktop'
|
||||||
TOGGLE_SIDEBAR: state => {
|
}
|
||||||
if (state.sidebar.opened) {
|
|
||||||
Cookies.set('sidebarStatus', 1)
|
const mutations = {
|
||||||
} else {
|
TOGGLE_SIDEBAR: state => {
|
||||||
Cookies.set('sidebarStatus', 0)
|
state.sidebar.opened = !state.sidebar.opened
|
||||||
}
|
state.sidebar.withoutAnimation = false
|
||||||
state.sidebar.opened = !state.sidebar.opened
|
if (state.sidebar.opened) {
|
||||||
state.sidebar.withoutAnimation = false
|
|
||||||
},
|
|
||||||
CLOSE_SIDEBAR: (state, withoutAnimation) => {
|
|
||||||
Cookies.set('sidebarStatus', 1)
|
Cookies.set('sidebarStatus', 1)
|
||||||
state.sidebar.opened = false
|
} else {
|
||||||
state.sidebar.withoutAnimation = withoutAnimation
|
Cookies.set('sidebarStatus', 0)
|
||||||
},
|
|
||||||
TOGGLE_DEVICE: (state, device) => {
|
|
||||||
state.device = device
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
CLOSE_SIDEBAR: (state, withoutAnimation) => {
|
||||||
ToggleSideBar: ({ commit }) => {
|
Cookies.set('sidebarStatus', 0)
|
||||||
commit('TOGGLE_SIDEBAR')
|
state.sidebar.opened = false
|
||||||
},
|
state.sidebar.withoutAnimation = withoutAnimation
|
||||||
CloseSideBar({ commit }, { withoutAnimation }) {
|
},
|
||||||
commit('CLOSE_SIDEBAR', withoutAnimation)
|
TOGGLE_DEVICE: (state, device) => {
|
||||||
},
|
state.device = device
|
||||||
ToggleDevice({ commit }, device) {
|
|
||||||
commit('TOGGLE_DEVICE', device)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default app
|
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
|
||||||
|
}
|
||||||
|
69
src/store/modules/permission.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { asyncRoutes, constantRoutes } from '@/router'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use meta.role to determine if the current user has permission
|
||||||
|
* @param roles
|
||||||
|
* @param route
|
||||||
|
*/
|
||||||
|
function hasPermission(roles, route) {
|
||||||
|
if (route.meta && route.meta.roles) {
|
||||||
|
return roles.some(role => route.meta.roles.includes(role))
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter asynchronous routing tables by recursion
|
||||||
|
* @param routes asyncRoutes
|
||||||
|
* @param roles
|
||||||
|
*/
|
||||||
|
export function filterAsyncRoutes(routes, roles) {
|
||||||
|
const res = []
|
||||||
|
|
||||||
|
routes.forEach(route => {
|
||||||
|
const tmp = { ...route }
|
||||||
|
if (hasPermission(roles, tmp)) {
|
||||||
|
if (tmp.children) {
|
||||||
|
tmp.children = filterAsyncRoutes(tmp.children, roles)
|
||||||
|
}
|
||||||
|
res.push(tmp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
routes: [],
|
||||||
|
addRoutes: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
SET_ROUTES: (state, routes) => {
|
||||||
|
state.addRoutes = routes
|
||||||
|
state.routes = constantRoutes.concat(routes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
generateRoutes({ commit }, roles) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let accessedRoutes
|
||||||
|
if (roles.includes('admin')) {
|
||||||
|
accessedRoutes = asyncRoutes || []
|
||||||
|
} else {
|
||||||
|
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
|
||||||
|
}
|
||||||
|
commit('SET_ROUTES', accessedRoutes)
|
||||||
|
resolve(accessedRoutes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
mutations,
|
||||||
|
actions
|
||||||
|
}
|
31
src/store/modules/settings.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import defaultSettings from '@/settings'
|
||||||
|
|
||||||
|
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
showSettings: showSettings,
|
||||||
|
fixedHeader: fixedHeader,
|
||||||
|
sidebarLogo: sidebarLogo
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
CHANGE_SETTING: (state, { key, value }) => {
|
||||||
|
if (state.hasOwnProperty(key)) {
|
||||||
|
state[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
changeSetting({ commit }, data) {
|
||||||
|
commit('CHANGE_SETTING', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
mutations,
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
@ -1,87 +1,102 @@
|
|||||||
import { login, logout, getInfo } from '@/api/login'
|
import { login, logout, getInfo } from '@/api/user'
|
||||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
import { getToken, setToken, removeToken } from '@/utils/auth'
|
||||||
|
import { resetRouter } from '@/router'
|
||||||
|
|
||||||
const user = {
|
const state = {
|
||||||
state: {
|
token: getToken(),
|
||||||
token: getToken(),
|
name: '',
|
||||||
name: '',
|
avatar: '',
|
||||||
avatar: '',
|
roles: []
|
||||||
roles: []
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
SET_TOKEN: (state, token) => {
|
||||||
|
state.token = token
|
||||||
},
|
},
|
||||||
|
SET_NAME: (state, name) => {
|
||||||
mutations: {
|
state.name = name
|
||||||
SET_TOKEN: (state, token) => {
|
|
||||||
state.token = token
|
|
||||||
},
|
|
||||||
SET_NAME: (state, name) => {
|
|
||||||
state.name = name
|
|
||||||
},
|
|
||||||
SET_AVATAR: (state, avatar) => {
|
|
||||||
state.avatar = avatar
|
|
||||||
},
|
|
||||||
SET_ROLES: (state, roles) => {
|
|
||||||
state.roles = roles
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
SET_AVATAR: (state, avatar) => {
|
||||||
actions: {
|
state.avatar = avatar
|
||||||
// 登录
|
},
|
||||||
Login({ commit }, userInfo) {
|
SET_ROLES: (state, roles) => {
|
||||||
const username = userInfo.username.trim()
|
state.roles = roles
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
login(username, userInfo.password).then(response => {
|
|
||||||
const data = response.data
|
|
||||||
setToken(data.token)
|
|
||||||
commit('SET_TOKEN', data.token)
|
|
||||||
resolve()
|
|
||||||
}).catch(error => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
GetInfo({ commit, state }) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
getInfo(state.token).then(response => {
|
|
||||||
const data = response.data
|
|
||||||
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
|
|
||||||
commit('SET_ROLES', data.roles)
|
|
||||||
} else {
|
|
||||||
reject('getInfo: roles must be a non-null array !')
|
|
||||||
}
|
|
||||||
commit('SET_NAME', data.name)
|
|
||||||
commit('SET_AVATAR', data.avatar)
|
|
||||||
resolve(response)
|
|
||||||
}).catch(error => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// 登出
|
|
||||||
LogOut({ commit, state }) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
logout(state.token).then(() => {
|
|
||||||
commit('SET_TOKEN', '')
|
|
||||||
commit('SET_ROLES', [])
|
|
||||||
removeToken()
|
|
||||||
resolve()
|
|
||||||
}).catch(error => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// 前端 登出
|
|
||||||
FedLogOut({ commit }) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
commit('SET_TOKEN', '')
|
|
||||||
removeToken()
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default user
|
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) {
|
||||||
|
reject('Verification failed, please Login again.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { roles, name, avatar } = data
|
||||||
|
|
||||||
|
// roles must be a non-empty array
|
||||||
|
if (!roles || roles.length <= 0) {
|
||||||
|
reject('getInfo: roles must be a non-null array!')
|
||||||
|
}
|
||||||
|
|
||||||
|
commit('SET_ROLES', roles)
|
||||||
|
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(() => {
|
||||||
|
commit('SET_TOKEN', '')
|
||||||
|
commit('SET_ROLES', [])
|
||||||
|
removeToken()
|
||||||
|
resetRouter()
|
||||||
|
resolve()
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// remove token
|
||||||
|
resetToken({ commit }) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
commit('SET_TOKEN', '')
|
||||||
|
commit('SET_ROLES', [])
|
||||||
|
removeToken()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
mutations,
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
//to reset element-ui default css
|
// cover some element-ui styles
|
||||||
|
|
||||||
|
.el-breadcrumb__inner,
|
||||||
|
.el-breadcrumb__inner a {
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.el-upload {
|
.el-upload {
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@ -9,7 +15,8 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
|
|
||||||
|
// to fixed https://github.com/ElemeFE/element/issues/2461
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
transform: none;
|
transform: none;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -17,7 +24,7 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
//element ui upload
|
// refine element ui upload
|
||||||
.upload-container {
|
.upload-container {
|
||||||
.el-upload {
|
.el-upload {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -28,3 +35,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dropdown
|
||||||
|
.el-dropdown-menu {
|
||||||
|
a {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,19 +31,6 @@ html {
|
|||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
|
||||||
a:focus,
|
|
||||||
a:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
color: inherit;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus,
|
a:focus,
|
||||||
a:active {
|
a:active {
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -57,6 +44,10 @@ a:hover {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.clearfix {
|
.clearfix {
|
||||||
&:after {
|
&:after {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@ -68,11 +59,7 @@ a:hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//main-container全局样式
|
// main-container global css
|
||||||
.app-main {
|
|
||||||
min-height: 100%
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-container {
|
.app-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
#app {
|
#app {
|
||||||
|
|
||||||
// 主体区域 Main container
|
|
||||||
.main-container {
|
.main-container {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
transition: margin-left .28s;
|
transition: margin-left .28s;
|
||||||
@ -8,10 +7,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 侧边栏 Sidebar container
|
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
transition: width 0.28s;
|
transition: width 0.28s;
|
||||||
width: $sideBarWidth !important;
|
width: $sideBarWidth !important;
|
||||||
|
background-color: $menuBg;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
@ -21,23 +20,29 @@
|
|||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
//reset element-ui css
|
// reset element-ui css
|
||||||
.horizontal-collapse-transition {
|
.horizontal-collapse-transition {
|
||||||
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
|
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-wrapper {
|
.scrollbar-wrapper {
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
|
|
||||||
.el-scrollbar__view {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-scrollbar__bar.is-vertical {
|
.el-scrollbar__bar.is-vertical {
|
||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-scrollbar {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-logo {
|
||||||
|
.el-scrollbar {
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.is-horizontal {
|
.is-horizontal {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -100,6 +105,7 @@
|
|||||||
|
|
||||||
.el-tooltip {
|
.el-tooltip {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
@ -111,6 +117,7 @@
|
|||||||
|
|
||||||
&>.el-submenu__title {
|
&>.el-submenu__title {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
@ -140,7 +147,7 @@
|
|||||||
min-width: $sideBarWidth !important;
|
min-width: $sideBarWidth !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 适配移动端, Mobile responsive
|
// mobile responsive
|
||||||
.mobile {
|
.mobile {
|
||||||
.main-container {
|
.main-container {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//globl transition css
|
// global transition css
|
||||||
|
|
||||||
/*fade*/
|
/* fade */
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.28s;
|
transition: opacity 0.28s;
|
||||||
@ -11,7 +11,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*fade-transform*/
|
/* fade-transform */
|
||||||
.fade-transform-leave-active,
|
.fade-transform-leave-active,
|
||||||
.fade-transform-enter-active {
|
.fade-transform-enter-active {
|
||||||
transition: all .5s;
|
transition: all .5s;
|
||||||
@ -27,7 +27,7 @@
|
|||||||
transform: translateX(30px);
|
transform: translateX(30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*fade*/
|
/* breadcrumb transition */
|
||||||
.breadcrumb-enter-active,
|
.breadcrumb-enter-active,
|
||||||
.breadcrumb-leave-active {
|
.breadcrumb-leave-active {
|
||||||
transition: all .5s;
|
transition: all .5s;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//sidebar
|
// sidebar
|
||||||
$menuText:#bfcbd9;
|
$menuText:#bfcbd9;
|
||||||
$menuActiveText:#409EFF;
|
$menuActiveText:#409EFF;
|
||||||
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
|
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
|
||||||
|
10
src/utils/get-page-title.js
Normal file
@ -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}`
|
||||||
|
}
|
90
src/utils/index.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Created by PanJiaChen on 16/11/18.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the time to string
|
||||||
|
* @param {(Object|string|number)} time
|
||||||
|
* @param {string} cFormat
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function parseTime(time, cFormat) {
|
||||||
|
if (arguments.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
|
||||||
|
let date
|
||||||
|
if (typeof time === 'object') {
|
||||||
|
date = time
|
||||||
|
} else {
|
||||||
|
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
|
||||||
|
time = parseInt(time)
|
||||||
|
}
|
||||||
|
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(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||||
|
let value = formatObj[key]
|
||||||
|
// Note: getDay() returns 0 on Sunday
|
||||||
|
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
|
||||||
|
if (result.length > 0 && value < 10) {
|
||||||
|
value = '0' + value
|
||||||
|
}
|
||||||
|
return value || 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() +
|
||||||
|
'分'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,62 +1,74 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Message, MessageBox } from 'element-ui'
|
import { MessageBox, Message } from 'element-ui'
|
||||||
import store from '../store'
|
import store from '@/store'
|
||||||
import { getToken } from '@/utils/auth'
|
import { getToken } from '@/utils/auth'
|
||||||
|
|
||||||
// 创建axios实例
|
// create an axios instance
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: process.env.BASE_API, // api 的 base_url
|
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
|
||||||
timeout: 5000 // 请求超时时间
|
withCredentials: true, // send cookies when cross-domain requests
|
||||||
|
timeout: 5000 // request timeout
|
||||||
})
|
})
|
||||||
|
|
||||||
// request拦截器
|
// request interceptor
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
|
// do something before request is sent
|
||||||
|
|
||||||
if (store.getters.token) {
|
if (store.getters.token) {
|
||||||
config.headers['X-Token'] = getToken() // 让每个请求携带自定义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
|
return config
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
// Do something with request error
|
// do something with request error
|
||||||
console.log(error) // for debug
|
console.log(error) // for debug
|
||||||
Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// response 拦截器
|
// response interceptor
|
||||||
service.interceptors.response.use(
|
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 => {
|
response => {
|
||||||
/**
|
|
||||||
* code为非20000是抛错 可结合自己业务进行修改
|
|
||||||
*/
|
|
||||||
const res = response.data
|
const res = response.data
|
||||||
|
|
||||||
|
// if the custom code is not 20000, it is judged as an error.
|
||||||
if (res.code !== 20000) {
|
if (res.code !== 20000) {
|
||||||
Message({
|
Message({
|
||||||
message: res.message,
|
message: res.message || 'error',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 5 * 1000
|
duration: 5 * 1000
|
||||||
})
|
})
|
||||||
|
|
||||||
// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
|
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
|
||||||
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
|
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
|
||||||
MessageBox.confirm(
|
// to re-login
|
||||||
'你已被登出,可以取消继续留在该页面,或者重新登录',
|
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
|
||||||
'确定登出',
|
confirmButtonText: 'Re-Login',
|
||||||
{
|
cancelButtonText: 'Cancel',
|
||||||
confirmButtonText: '重新登录',
|
type: 'warning'
|
||||||
cancelButtonText: '取消',
|
}).then(() => {
|
||||||
type: 'warning'
|
store.dispatch('user/resetToken').then(() => {
|
||||||
}
|
location.reload()
|
||||||
).then(() => {
|
|
||||||
store.dispatch('FedLogOut').then(() => {
|
|
||||||
location.reload() // 为了重新实例化vue-router对象 避免bug
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return Promise.reject('error')
|
return Promise.reject(res.message || 'error')
|
||||||
} else {
|
} else {
|
||||||
return response.data
|
return res
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Created by jiachenpan on 16/11/18.
|
* Created by PanJiaChen on 16/11/18.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isvalidUsername(str) {
|
/**
|
||||||
const valid_map = ['admin', 'editor']
|
* @param {string} path
|
||||||
return valid_map.indexOf(str.trim()) >= 0
|
* @returns {Boolean}
|
||||||
}
|
*/
|
||||||
|
|
||||||
export function isExternal(path) {
|
export function isExternal(path) {
|
||||||
return /^(https?:|mailto:|tel:)/.test(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
|
||||||
|
}
|
||||||
|
@ -9,12 +9,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bullshit">
|
<div class="bullshit">
|
||||||
<div class="bullshit__oops">OOPS!</div>
|
<div class="bullshit__oops">OOPS!</div>
|
||||||
<div class="bullshit__info">版权所有
|
<div class="bullshit__info">All rights reserved
|
||||||
<a class="link-type" href="https://wallstreetcn.com" target="_blank">华尔街见闻</a>
|
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="bullshit__headline">{{ message }}</div>
|
<div class="bullshit__headline">{{ message }}</div>
|
||||||
<div class="bullshit__info">请检查您输入的网址是否正确,请点击以下按钮返回主页或者发送错误报告</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">返回首页</a>
|
<a href="" class="bullshit__return-home">Back to home</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -26,13 +26,13 @@ export default {
|
|||||||
name: 'Page404',
|
name: 'Page404',
|
||||||
computed: {
|
computed: {
|
||||||
message() {
|
message() {
|
||||||
return '网管说这个页面你不能进......'
|
return 'The webmaster said that you can not enter this page...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.wscn-http404-container{
|
.wscn-http404-container{
|
||||||
transform: translate(-50%,-50%);
|
transform: translate(-50%,-50%);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container">
|
||||||
<div class="dashboard-text">name:{{ name }}</div>
|
<div class="dashboard-text">name: {{ name }}</div>
|
||||||
<div class="dashboard-text">roles:<span v-for="role in roles" :key="role">{{ role }}</span></div>
|
<div class="dashboard-text">roles: <span v-for="role in roles" :key="role">{{ role }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dashboard {
|
.dashboard {
|
||||||
&-container {
|
&-container {
|
||||||
margin: 30px;
|
margin: 30px;
|
||||||
|
@ -2,42 +2,42 @@
|
|||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-form ref="form" :model="form" label-width="120px">
|
<el-form ref="form" :model="form" label-width="120px">
|
||||||
<el-form-item label="Activity name">
|
<el-form-item label="Activity name">
|
||||||
<el-input v-model="form.name"/>
|
<el-input v-model="form.name" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Activity zone">
|
<el-form-item label="Activity zone">
|
||||||
<el-select v-model="form.region" placeholder="please select your zone">
|
<el-select v-model="form.region" placeholder="please select your zone">
|
||||||
<el-option label="Zone one" value="shanghai"/>
|
<el-option label="Zone one" value="shanghai" />
|
||||||
<el-option label="Zone two" value="beijing"/>
|
<el-option label="Zone two" value="beijing" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Activity time">
|
<el-form-item label="Activity time">
|
||||||
<el-col :span="11">
|
<el-col :span="11">
|
||||||
<el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;"/>
|
<el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;" />
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2" class="line">-</el-col>
|
<el-col :span="2" class="line">-</el-col>
|
||||||
<el-col :span="11">
|
<el-col :span="11">
|
||||||
<el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;"/>
|
<el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;" />
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Instant delivery">
|
<el-form-item label="Instant delivery">
|
||||||
<el-switch v-model="form.delivery"/>
|
<el-switch v-model="form.delivery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Activity type">
|
<el-form-item label="Activity type">
|
||||||
<el-checkbox-group v-model="form.type">
|
<el-checkbox-group v-model="form.type">
|
||||||
<el-checkbox label="Online activities" name="type"/>
|
<el-checkbox label="Online activities" name="type" />
|
||||||
<el-checkbox label="Promotion activities" name="type"/>
|
<el-checkbox label="Promotion activities" name="type" />
|
||||||
<el-checkbox label="Offline activities" name="type"/>
|
<el-checkbox label="Offline activities" name="type" />
|
||||||
<el-checkbox label="Simple brand exposure" name="type"/>
|
<el-checkbox label="Simple brand exposure" name="type" />
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Resources">
|
<el-form-item label="Resources">
|
||||||
<el-radio-group v-model="form.resource">
|
<el-radio-group v-model="form.resource">
|
||||||
<el-radio label="Sponsor"/>
|
<el-radio label="Sponsor" />
|
||||||
<el-radio label="Venue"/>
|
<el-radio label="Venue" />
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Activity form">
|
<el-form-item label="Activity form">
|
||||||
<el-input v-model="form.desc" type="textarea"/>
|
<el-input v-model="form.desc" type="textarea" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="onSubmit">Create</el-button>
|
<el-button type="primary" @click="onSubmit">Create</el-button>
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="navbar">
|
|
||||||
<hamburger :toggle-click="toggleSideBar" :is-active="sidebar.opened" class="hamburger-container"/>
|
|
||||||
<breadcrumb />
|
|
||||||
<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 class="inlineBlock" to="/">
|
|
||||||
<el-dropdown-item>
|
|
||||||
Home
|
|
||||||
</el-dropdown-item>
|
|
||||||
</router-link>
|
|
||||||
<el-dropdown-item divided>
|
|
||||||
<span style="display:block;" @click="logout">LogOut</span>
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</el-dropdown>
|
|
||||||
</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('ToggleSideBar')
|
|
||||||
},
|
|
||||||
logout() {
|
|
||||||
this.$store.dispatch('LogOut').then(() => {
|
|
||||||
location.reload() // 为了重新实例化vue-router对象 避免bug
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
|
||||||
.navbar {
|
|
||||||
height: 50px;
|
|
||||||
line-height: 50px;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,.12), 0 0 3px 0 rgba(0,0,0,.04);
|
|
||||||
.hamburger-container {
|
|
||||||
line-height: 58px;
|
|
||||||
height: 50px;
|
|
||||||
float: left;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
.screenfull {
|
|
||||||
position: absolute;
|
|
||||||
right: 90px;
|
|
||||||
top: 16px;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
.avatar-container {
|
|
||||||
height: 50px;
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
right: 35px;
|
|
||||||
.avatar-wrapper {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 5px;
|
|
||||||
position: relative;
|
|
||||||
line-height: initial;
|
|
||||||
.user-avatar {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.el-icon-caret-bottom {
|
|
||||||
position: absolute;
|
|
||||||
right: -20px;
|
|
||||||
top: 25px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
|
||||||
<el-menu
|
|
||||||
:default-active="$route.path"
|
|
||||||
:collapse="isCollapse"
|
|
||||||
:background-color="variables.menuBg"
|
|
||||||
:text-color="variables.menuText"
|
|
||||||
: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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import variables from '@/styles/variables.scss'
|
|
||||||
import SidebarItem from './SidebarItem'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { SidebarItem },
|
|
||||||
computed: {
|
|
||||||
...mapGetters([
|
|
||||||
'sidebar'
|
|
||||||
]),
|
|
||||||
routes() {
|
|
||||||
return this.$router.options.routes
|
|
||||||
},
|
|
||||||
variables() {
|
|
||||||
return variables
|
|
||||||
},
|
|
||||||
isCollapse() {
|
|
||||||
return !this.sidebar.opened
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,40 +0,0 @@
|
|||||||
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('CloseSideBar', { withoutAnimation: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeMount() {
|
|
||||||
window.addEventListener('resize', this.resizeHandler)
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const isMobile = this.isMobile()
|
|
||||||
if (isMobile) {
|
|
||||||
store.dispatch('ToggleDevice', 'mobile')
|
|
||||||
store.dispatch('CloseSideBar', { withoutAnimation: true })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isMobile() {
|
|
||||||
const rect = body.getBoundingClientRect()
|
|
||||||
return rect.width - 1 < WIDTH
|
|
||||||
},
|
|
||||||
resizeHandler() {
|
|
||||||
if (!document.hidden) {
|
|
||||||
const isMobile = this.isMobile()
|
|
||||||
store.dispatch('ToggleDevice', isMobile ? 'mobile' : 'desktop')
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
store.dispatch('CloseSideBar', { withoutAnimation: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
|
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
|
||||||
<h3 class="title">vue-admin-template</h3>
|
|
||||||
|
<div class="title-container">
|
||||||
|
<h3 class="title">Login Form</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<span class="svg-container">
|
<span class="svg-container">
|
||||||
<svg-icon icon-class="user" />
|
<svg-icon icon-class="user" />
|
||||||
</span>
|
</span>
|
||||||
<el-input v-model="loginForm.username" name="username" type="text" auto-complete="on" placeholder="username" />
|
<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>
|
||||||
|
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<span class="svg-container">
|
<span class="svg-container">
|
||||||
<svg-icon icon-class="password" />
|
<svg-icon icon-class="password" />
|
||||||
</span>
|
</span>
|
||||||
<el-input
|
<el-input
|
||||||
:type="pwdType"
|
:key="passwordType"
|
||||||
|
ref="password"
|
||||||
v-model="loginForm.password"
|
v-model="loginForm.password"
|
||||||
|
:type="passwordType"
|
||||||
|
placeholder="Password"
|
||||||
name="password"
|
name="password"
|
||||||
|
tabindex="2"
|
||||||
auto-complete="on"
|
auto-complete="on"
|
||||||
placeholder="password"
|
@keyup.enter.native="handleLogin"
|
||||||
@keyup.enter.native="handleLogin" />
|
/>
|
||||||
<span class="show-pwd" @click="showPwd">
|
<span class="show-pwd" @click="showPwd">
|
||||||
<svg-icon :icon-class="pwdType === 'password' ? 'eye' : 'eye-open'" />
|
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
|
||||||
</span>
|
</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
|
||||||
<el-button :loading="loading" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
|
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
|
||||||
Sign in
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
<div class="tips">
|
<div class="tips">
|
||||||
<span style="margin-right:20px;">username: admin</span>
|
<span style="margin-right:20px;">username: admin</span>
|
||||||
<span> password: admin</span>
|
<span> password: any</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { isvalidUsername } from '@/utils/validate'
|
import { validUsername } from '@/utils/validate'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
data() {
|
data() {
|
||||||
const validateUsername = (rule, value, callback) => {
|
const validateUsername = (rule, value, callback) => {
|
||||||
if (!isvalidUsername(value)) {
|
if (!validUsername(value)) {
|
||||||
callback(new Error('请输入正确的用户名'))
|
callback(new Error('Please enter the correct user name'))
|
||||||
} else {
|
} else {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const validatePass = (rule, value, callback) => {
|
const validatePassword = (rule, value, callback) => {
|
||||||
if (value.length < 5) {
|
if (value.length < 6) {
|
||||||
callback(new Error('密码不能小于5位'))
|
callback(new Error('The password can not be less than 6 digits'))
|
||||||
} else {
|
} else {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
@ -59,14 +75,14 @@ export default {
|
|||||||
return {
|
return {
|
||||||
loginForm: {
|
loginForm: {
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
password: 'admin'
|
password: '111111'
|
||||||
},
|
},
|
||||||
loginRules: {
|
loginRules: {
|
||||||
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
|
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
|
||||||
password: [{ required: true, trigger: 'blur', validator: validatePass }]
|
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
pwdType: 'password',
|
passwordType: 'password',
|
||||||
redirect: undefined
|
redirect: undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -80,19 +96,22 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showPwd() {
|
showPwd() {
|
||||||
if (this.pwdType === 'password') {
|
if (this.passwordType === 'password') {
|
||||||
this.pwdType = ''
|
this.passwordType = ''
|
||||||
} else {
|
} else {
|
||||||
this.pwdType = 'password'
|
this.passwordType = 'password'
|
||||||
}
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.password.focus()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handleLogin() {
|
handleLogin() {
|
||||||
this.$refs.loginForm.validate(valid => {
|
this.$refs.loginForm.validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.$store.dispatch('Login', this.loginForm).then(() => {
|
this.$store.dispatch('user/login', this.loginForm).then(() => {
|
||||||
this.loading = false
|
|
||||||
this.$router.push({ path: this.redirect || '/' })
|
this.$router.push({ path: this.redirect || '/' })
|
||||||
|
this.loading = false
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
@ -106,9 +125,19 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss">
|
<style lang="scss">
|
||||||
$bg:#2d3a4b;
|
/* 修复input 背景不协调 和光标变色 */
|
||||||
$light_gray:#eee;
|
/* 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 */
|
/* reset element-ui css */
|
||||||
.login-container {
|
.login-container {
|
||||||
@ -116,6 +145,7 @@ $light_gray:#eee;
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 47px;
|
height: 47px;
|
||||||
width: 85%;
|
width: 85%;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
@ -124,12 +154,15 @@ $light_gray:#eee;
|
|||||||
padding: 12px 5px 12px 15px;
|
padding: 12px 5px 12px 15px;
|
||||||
color: $light_gray;
|
color: $light_gray;
|
||||||
height: 47px;
|
height: 47px;
|
||||||
|
caret-color: $cursor;
|
||||||
|
|
||||||
&:-webkit-autofill {
|
&:-webkit-autofill {
|
||||||
-webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
|
box-shadow: 0 0 0px 1000px $bg inset !important;
|
||||||
-webkit-text-fill-color: #fff !important;
|
-webkit-text-fill-color: $cursor !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
@ -137,37 +170,40 @@ $light_gray:#eee;
|
|||||||
color: #454545;
|
color: #454545;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$bg:#2d3a4b;
|
$bg:#2d3a4b;
|
||||||
$dark_gray:#889aa4;
|
$dark_gray:#889aa4;
|
||||||
$light_gray:#eee;
|
$light_gray:#eee;
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
position: fixed;
|
min-height: 100%;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: $bg;
|
background-color: $bg;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
position: absolute;
|
position: relative;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 520px;
|
width: 520px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 35px 35px 15px 35px;
|
padding: 160px 35px 0;
|
||||||
margin: 120px auto;
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tips {
|
.tips {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-container {
|
.svg-container {
|
||||||
padding: 6px 5px 6px 15px;
|
padding: 6px 5px 6px 15px;
|
||||||
color: $dark_gray;
|
color: $dark_gray;
|
||||||
@ -175,14 +211,19 @@ $light_gray:#eee;
|
|||||||
width: 30px;
|
width: 30px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.title {
|
|
||||||
font-size: 26px;
|
.title-container {
|
||||||
font-weight: 400;
|
position: relative;
|
||||||
color: $light_gray;
|
|
||||||
margin: 0px auto 40px auto;
|
.title {
|
||||||
text-align: center;
|
font-size: 26px;
|
||||||
font-weight: bold;
|
color: $light_gray;
|
||||||
|
margin: 0px auto 40px auto;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-pwd {
|
.show-pwd {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<template >
|
<template>
|
||||||
<div style="padding:30px;">
|
<div style="padding:30px;">
|
||||||
<el-alert :closable="false" title="menu 1">
|
<el-alert :closable="false" title="menu 1">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<template >
|
<template>
|
||||||
<div style="padding:30px;">
|
<div style="padding:30px;">
|
||||||
<el-alert :closable="false" title="menu 1-1" type="success">
|
<el-alert :closable="false" title="menu 1-1" type="success">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
element-loading-text="Loading"
|
element-loading-text="Loading"
|
||||||
border
|
border
|
||||||
fit
|
fit
|
||||||
highlight-current-row>
|
highlight-current-row
|
||||||
|
>
|
||||||
<el-table-column align="center" label="ID" width="95">
|
<el-table-column align="center" label="ID" width="95">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ scope.$index }}
|
{{ scope.$index }}
|
||||||
@ -34,7 +35,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column align="center" prop="created_at" label="Display_time" width="200">
|
<el-table-column align="center" prop="created_at" label="Display_time" width="200">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<i class="el-icon-time"/>
|
<i class="el-icon-time" />
|
||||||
<span>{{ scope.row.display_time }}</span>
|
<span>{{ scope.row.display_time }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -68,7 +69,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
fetchData() {
|
fetchData() {
|
||||||
this.listLoading = true
|
this.listLoading = true
|
||||||
getList(this.listQuery).then(response => {
|
getList().then(response => {
|
||||||
this.list = response.data.items
|
this.list = response.data.items
|
||||||
this.listLoading = false
|
this.listLoading = false
|
||||||
})
|
})
|
||||||
|
5
tests/unit/.eslintrc.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
jest: true
|
||||||
|
}
|
||||||
|
}
|
98
tests/unit/components/Breadcrumb.spec.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { mount, createLocalVue } 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)
|
||||||
|
})
|
||||||
|
})
|
18
tests/unit/components/Hamburger.spec.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
22
tests/unit/components/SvgIcon.spec.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
30
tests/unit/utils/formatTime.spec.js
Normal file
@ -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')
|
||||||
|
})
|
||||||
|
})
|
28
tests/unit/utils/parseTime.spec.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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('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()
|
||||||
|
})
|
||||||
|
})
|
17
tests/unit/utils/validate.spec.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { validUsername, isExternal } 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)
|
||||||
|
})
|
||||||
|
})
|
133
vue.config.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
'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
|
||||||
|
const port = 9528 // 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
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
// change xxx-api/login => mock/login
|
||||||
|
// detail: https://cli.vuejs.org/config/#devserver-proxy
|
||||||
|
[process.env.VUE_APP_BASE_API]: {
|
||||||
|
target: `http://localhost:${port}/mock`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: {
|
||||||
|
['^' + process.env.VUE_APP_BASE_API]: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
after: 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) {
|
||||||
|
config.plugins.delete('preload') // TODO: need test
|
||||||
|
config.plugins.delete('prefetch') // TODO: need test
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// set preserveWhitespace
|
||||||
|
config.module
|
||||||
|
.rule('vue')
|
||||||
|
.use('vue-loader')
|
||||||
|
.loader('vue-loader')
|
||||||
|
.tap(options => {
|
||||||
|
options.compilerOptions.preserveWhitespace = true
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
.end()
|
||||||
|
|
||||||
|
config
|
||||||
|
// https://webpack.js.org/configuration/devtool/#development
|
||||||
|
.when(process.env.NODE_ENV === 'development',
|
||||||
|
config => config.devtool('cheap-source-map')
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
config.optimization.runtimeChunk('single')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|