diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index a2a380f..0000000
--- a/.babelrc
+++ /dev/null
@@ -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"]
-}
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..8f5856d
--- /dev/null
+++ b/.env.development
@@ -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
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..80c8103
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,6 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = '/prod-api'
+
diff --git a/.env.staging b/.env.staging
new file mode 100644
index 0000000..a8793a0
--- /dev/null
+++ b/.env.staging
@@ -0,0 +1,8 @@
+NODE_ENV = production
+
+# just a flag
+ENV = 'staging'
+
+# base api
+VUE_APP_BASE_API = '/stage-api'
+
diff --git a/.eslintignore b/.eslintignore
index e3a4037..e6529fc 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,4 @@
build/*.js
-config/*.js
src/assets
+public
+dist
diff --git a/.eslintrc.js b/.eslintrc.js
index 0e5c28a..c977505 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -21,7 +21,10 @@ module.exports = {
"allowFirstLine": false
}
}],
+ "vue/singleline-html-element-content-newline": "off",
+ "vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
+ "vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
@@ -44,7 +47,7 @@ module.exports = {
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
- 'eqeqeq': [2, 'allow-null'],
+ 'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
@@ -73,7 +76,7 @@ module.exports = {
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
- 'no-control-regex': 2,
+ 'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
@@ -193,4 +196,3 @@ module.exports = {
'array-bracket-spacing': [2, 'never']
}
}
-
diff --git a/.gitignore b/.gitignore
index 78a0ead..9ad28d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
+tests/**/coverage/
# Editor directories and files
.idea
diff --git a/.postcssrc.js b/.postcssrc.js
index eee3e92..10473ef 100644
--- a/.postcssrc.js
+++ b/.postcssrc.js
@@ -1,10 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
- "plugins": {
- "postcss-import": {},
- "postcss-url": {},
+ 'plugins': {
// to edit target browsers: use "browserslist" field in package.json
- "autoprefixer": {}
+ 'autoprefixer': {}
}
}
diff --git a/README-zh.md b/README-zh.md
index d3c3690..573deca 100644
--- a/README-zh.md
+++ b/README-zh.md
@@ -1,19 +1,17 @@
# 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)
[国内访问](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
如果你想要根据用户角色来动态生成侧边栏和 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)
@@ -33,54 +31,56 @@
## Build Setup
```bash
-# Clone project
+# 克隆项目
git clone https://github.com/PanJiaChen/vue-admin-template.git
-# Install dependencies
+# 进入项目目录
+cd vue-admin-template
+
+# 安装依赖
npm install
-# 建议不要用cnpm 安装有各种诡异的bug 可以通过如下操作解决npm速度慢的问题
+# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
-# Serve with hot reload at localhost:9528
+# 启动服务
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](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
Modern browsers and Internet Explorer 10+.
diff --git a/README.md b/README.md
index e65db28..7ceba5b 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,61 @@
# vue-admin-template
+English | [简体中文](./README.zh-CN.md)
+
> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
**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
-```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
-# Serve with hot reload at localhost:9528
+# develop
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](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)
-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))
## 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)
-### 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
Modern browsers and Internet Explorer 10+.
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..ba17966
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ presets: [
+ '@vue/app'
+ ]
+}
diff --git a/build/build.js b/build/build.js
deleted file mode 100644
index 8c6cebd..0000000
--- a/build/build.js
+++ /dev/null
@@ -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"
- )
- )
- })
-})
diff --git a/build/check-versions.js b/build/check-versions.js
deleted file mode 100644
index c5c29e9..0000000
--- a/build/check-versions.js
+++ /dev/null
@@ -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)
- }
-}
diff --git a/build/index.js b/build/index.js
new file mode 100644
index 0000000..0c57de2
--- /dev/null
+++ b/build/index.js
@@ -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}`)
+}
diff --git a/build/logo.png b/build/logo.png
deleted file mode 100644
index f3d2503..0000000
Binary files a/build/logo.png and /dev/null differ
diff --git a/build/utils.js b/build/utils.js
deleted file mode 100644
index c96d093..0000000
--- a/build/utils.js
+++ /dev/null
@@ -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')
- })
- }
-}
diff --git a/build/vue-loader.conf.js b/build/vue-loader.conf.js
deleted file mode 100644
index 5496c93..0000000
--- a/build/vue-loader.conf.js
+++ /dev/null
@@ -1,5 +0,0 @@
-'use strict'
-
-module.exports = {
- //You can set the vue-loader configuration by yourself.
-}
diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js
deleted file mode 100644
index c9529f7..0000000
--- a/build/webpack.base.conf.js
+++ /dev/null
@@ -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'
- }
-}
diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js
deleted file mode 100644
index d92516f..0000000
--- a/build/webpack.dev.conf.js
+++ /dev/null
@@ -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)
- }
- })
-})
diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js
deleted file mode 100644
index 9009b9d..0000000
--- a/build/webpack.prod.conf.js
+++ /dev/null
@@ -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
diff --git a/config/dev.env.js b/config/dev.env.js
deleted file mode 100644
index e3c8ffc..0000000
--- a/config/dev.env.js
+++ /dev/null
@@ -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"',
-})
diff --git a/config/index.js b/config/index.js
deleted file mode 100644
index a42218c..0000000
--- a/config/index.js
+++ /dev/null
@@ -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
- }
-}
diff --git a/config/prod.env.js b/config/prod.env.js
deleted file mode 100644
index 836ec67..0000000
--- a/config/prod.env.js
+++ /dev/null
@@ -1,5 +0,0 @@
-'use strict'
-module.exports = {
- NODE_ENV: '"production"',
- BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
-}
diff --git a/index.html b/index.html
deleted file mode 100644
index 8be7f4b..0000000
--- a/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- vue-admin-template
-
-
-
-
-
-
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..143cdc8
--- /dev/null
+++ b/jest.config.js
@@ -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: {
+ '^@/(.*)$': '/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: '/tests/unit/coverage',
+ // 'collectCoverage': true,
+ 'coverageReporters': [
+ 'lcov',
+ 'text-summary'
+ ],
+ testURL: 'http://localhost/'
+}
diff --git a/mock/index.js b/mock/index.js
index 9eb5a47..e14f94e 100644
--- a/mock/index.js
+++ b/mock/index.js
@@ -1,26 +1,66 @@
import Mock from 'mockjs'
-import userAPI from './user'
-import tableAPI from './table'
+import { param2Obj } from '../src/utils'
-// Fix an issue with setting withCredentials = true, cross-domain request lost cookies
-// 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
+import user from './user'
+import table from './table'
+
+const mocks = [
+ ...user,
+ ...table
+]
+
+// for front mock
+// please use it cautiously, it will redefine XMLHttpRequest,
+// which will cause many of your third-party libraries to be invalidated(like progress event).
+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
-Mock.mock(/\/user\/login/, 'post', userAPI.login)
-Mock.mock(/\/user\/info/, 'get', userAPI.getInfo)
-Mock.mock(/\/user\/logout/, 'post', userAPI.logout)
+// for mock server
+const responseFake = (url, type, respond) => {
+ return {
+ url: new RegExp(`/mock${url}`),
+ type: type || 'get',
+ response(req, res) {
+ res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+ }
+ }
+}
-// Table
-Mock.mock(/\/table\/list/, 'get', tableAPI.list)
-
-export default Mock
+export default mocks.map(route => {
+ return responseFake(route.url, route.type, route.response)
+})
diff --git a/mock/mock-server.js b/mock/mock-server.js
new file mode 100644
index 0000000..ccce04d
--- /dev/null
+++ b/mock/mock-server.js
@@ -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}`))
+ }
+ })
+}
diff --git a/mock/table.js b/mock/table.js
index 810b3c1..a072a21 100644
--- a/mock/table.js
+++ b/mock/table.js
@@ -1,20 +1,29 @@
import Mock from 'mockjs'
-export default {
- list: () => {
- const items = Mock.mock({
- 'items|30': [{
- id: '@id',
- title: '@sentence(10, 20)',
- 'status|1': ['published', 'draft', 'deleted'],
- author: 'name',
- display_time: '@datetime',
- pageviews: '@integer(300, 5000)'
- }]
- })
- return {
- code: 20000,
- data: items
+const data = Mock.mock({
+ 'items|30': [{
+ id: '@id',
+ title: '@sentence(10, 20)',
+ 'status|1': ['published', 'draft', 'deleted'],
+ author: 'name',
+ display_time: '@datetime',
+ pageviews: '@integer(300, 5000)'
+ }]
+})
+
+export default [
+ {
+ url: '/table/list',
+ type: 'get',
+ response: config => {
+ const items = data.items
+ return {
+ code: 20000,
+ data: {
+ total: items.length,
+ items: items
+ }
+ }
}
}
-}
+]
diff --git a/mock/user.js b/mock/user.js
index 81e82e0..43f93a0 100644
--- a/mock/user.js
+++ b/mock/user.js
@@ -1,4 +1,3 @@
-import { param2Obj } from './utils'
const tokens = {
admin: {
@@ -24,41 +23,62 @@ const users = {
}
}
-export default {
- login: res => {
- const { username } = JSON.parse(res.body)
- const data = tokens[username]
+export default [
+ // user login
+ {
+ 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 {
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 {
code: 20000,
data: info
}
}
- return {
- code: 50008,
- message: 'Login failed, unable to get user details.'
- }
},
- logout: () => {
- return {
- code: 20000,
- data: 'success'
+
+ // user logout
+ {
+ url: '/user/logout',
+ type: 'post',
+ response: _ => {
+ return {
+ code: 20000,
+ data: 'success'
+ }
}
}
-}
+]
diff --git a/mock/utils.js b/mock/utils.js
deleted file mode 100644
index 7d5f7cb..0000000
--- a/mock/utils.js
+++ /dev/null
@@ -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, '":"') +
- '"}'
- )
-}
diff --git a/package.json b/package.json
index 12294f9..bd8e864 100644
--- a/package.json
+++ b/package.json
@@ -1,82 +1,59 @@
{
"name": "vue-admin-template",
- "version": "3.9.0",
- "license": "MIT",
+ "version": "4.1.0",
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
"author": "Pan ",
+ "license": "MIT",
"scripts": {
- "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
- "start": "npm run dev",
- "build": "node build/build.js",
- "build:report": "npm_config_report=true npm run build",
+ "dev": "vue-cli-service serve",
+ "build:prod": "vue-cli-service build",
+ "build:stage": "vue-cli-service build --mode staging",
+ "preview": "node build/index.js --preview",
"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"
},
"dependencies": {
"axios": "0.18.0",
- "element-ui": "2.4.6",
+ "element-ui": "2.7.2",
"js-cookie": "2.2.0",
- "mockjs": "1.0.1-beta3",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
- "vue": "2.5.17",
- "vue-router": "3.0.1",
- "vuex": "3.0.1"
+ "path-to-regexp": "2.4.0",
+ "vue": "2.6.10",
+ "vue-router": "3.0.6",
+ "vuex": "3.1.0"
},
"devDependencies": {
- "autoprefixer": "8.5.0",
- "babel-core": "6.26.0",
- "babel-eslint": "8.2.6",
- "babel-helper-vue-jsx-merge-props": "2.0.3",
- "babel-loader": "7.1.5",
- "babel-plugin-syntax-jsx": "6.18.0",
- "babel-plugin-transform-runtime": "6.23.0",
- "babel-plugin-transform-vue-jsx": "3.7.0",
- "babel-preset-env": "1.7.0",
- "babel-preset-stage-2": "6.24.1",
- "chalk": "2.4.1",
- "compression-webpack-plugin": "2.0.0",
- "copy-webpack-plugin": "4.5.2",
- "css-loader": "1.0.0",
- "eslint": "4.19.1",
- "eslint-friendly-formatter": "4.0.1",
- "eslint-loader": "2.0.0",
- "eslint-plugin-vue": "4.7.1",
- "eventsource-polyfill": "0.9.6",
- "file-loader": "1.1.11",
- "friendly-errors-webpack-plugin": "1.7.0",
- "html-webpack-plugin": "4.0.0-alpha",
- "mini-css-extract-plugin": "0.4.1",
- "node-notifier": "5.2.1",
- "node-sass": "^4.7.2",
- "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"
+ "@babel/core": "7.0.0",
+ "@babel/register": "7.0.0",
+ "@vue/cli-plugin-babel": "3.6.0",
+ "@vue/cli-plugin-eslint": "3.6.0",
+ "@vue/cli-plugin-unit-jest": "3.6.3",
+ "@vue/cli-service": "3.6.0",
+ "@vue/test-utils": "1.0.0-beta.29",
+ "babel-core": "7.0.0-bridge.0",
+ "babel-eslint": "10.0.1",
+ "babel-jest": "23.6.0",
+ "chalk": "2.4.2",
+ "connect": "3.6.6",
+ "eslint": "5.15.3",
+ "eslint-plugin-vue": "5.2.2",
+ "html-webpack-plugin": "3.2.0",
+ "mockjs": "1.0.1-beta3",
+ "node-sass": "^4.9.0",
+ "runjs": "^4.3.2",
+ "sass-loader": "^7.1.0",
+ "script-ext-html-webpack-plugin": "2.1.3",
+ "script-loader": "0.7.2",
+ "serve-static": "^1.13.2",
+ "svg-sprite-loader": "4.1.3",
+ "svgo": "1.2.2",
+ "vue-template-compiler": "2.6.10"
},
"engines": {
- "node": ">= 6.0.0",
+ "node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
diff --git a/favicon.ico b/public/favicon.ico
similarity index 100%
rename from favicon.ico
rename to public/favicon.ico
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..fa2be91
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ <%= webpackConfig.name %>
+
+
+
+
+
+
+
diff --git a/src/App.vue b/src/App.vue
index 721d3a3..ec9032c 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/src/api/login.js b/src/api/user.js
similarity index 78%
rename from src/api/login.js
rename to src/api/user.js
index 3758dec..de69f70 100644
--- a/src/api/login.js
+++ b/src/api/user.js
@@ -1,13 +1,10 @@
import request from '@/utils/request'
-export function login(username, password) {
+export function login(data) {
return request({
url: '/user/login',
method: 'post',
- data: {
- username,
- password
- }
+ data
})
}
diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue
index 0c8a7fb..29f9a04 100644
--- a/src/components/Breadcrumb/index.vue
+++ b/src/components/Breadcrumb/index.vue
@@ -2,7 +2,7 @@
- {{ item.meta.title }}
+ {{ item.meta.title }}
{{ item.meta.title }}
@@ -28,15 +28,23 @@ export default {
},
methods: {
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]
- if (first && first.name !== 'dashboard') {
+
+ if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
+ isDashboard(route) {
+ const name = route && route.name
+ if (!name) {
+ return false
+ }
+ return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
+ },
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
@@ -55,15 +63,16 @@ export default {
}
-
diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue
index 220d67e..368b002 100644
--- a/src/components/Hamburger/index.vue
+++ b/src/components/Hamburger/index.vue
@@ -1,5 +1,5 @@
-
+
@@ -20,10 +20,11 @@ export default {
isActive: {
type: Boolean,
default: false
- },
- toggleClick: {
- type: Function,
- default: null
+ }
+ },
+ methods: {
+ toggleClick() {
+ this.$emit('toggleClick')
}
}
}
@@ -32,10 +33,11 @@ export default {
diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue
new file mode 100644
index 0000000..4953f36
--- /dev/null
+++ b/src/layout/components/Navbar.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/FixiOSBug.js b/src/layout/components/Sidebar/FixiOSBug.js
new file mode 100644
index 0000000..bc14856
--- /dev/null
+++ b/src/layout/components/Sidebar/FixiOSBug.js
@@ -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)
+ }
+ }
+ }
+ }
+}
diff --git a/src/views/layout/components/Sidebar/Item.vue b/src/layout/components/Sidebar/Item.vue
similarity index 64%
rename from src/views/layout/components/Sidebar/Item.vue
rename to src/layout/components/Sidebar/Item.vue
index 7dd341d..b515f61 100644
--- a/src/views/layout/components/Sidebar/Item.vue
+++ b/src/layout/components/Sidebar/Item.vue
@@ -3,18 +3,17 @@ export default {
name: 'MenuItem',
functional: true,
props: {
- meta: {
- type: Object,
- default: () => {
- return {
- title: '',
- icon: ''
- }
- }
+ icon: {
+ type: String,
+ default: ''
+ },
+ title: {
+ type: String,
+ default: ''
}
},
render(h, context) {
- const { icon, title } = context.props.meta
+ const { icon, title } = context.props
const vnodes = []
if (icon) {
diff --git a/src/views/layout/components/Sidebar/Link.vue b/src/layout/components/Sidebar/Link.vue
similarity index 97%
rename from src/views/layout/components/Sidebar/Link.vue
rename to src/layout/components/Sidebar/Link.vue
index bff45cd..eb4dd10 100644
--- a/src/views/layout/components/Sidebar/Link.vue
+++ b/src/layout/components/Sidebar/Link.vue
@@ -2,7 +2,7 @@
-
+
diff --git a/src/layout/components/Sidebar/Logo.vue b/src/layout/components/Sidebar/Logo.vue
new file mode 100644
index 0000000..040fab6
--- /dev/null
+++ b/src/layout/components/Sidebar/Logo.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/src/views/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue
similarity index 82%
rename from src/views/layout/components/Sidebar/SidebarItem.vue
rename to src/layout/components/Sidebar/SidebarItem.vue
index b9d3819..2d49dd8 100644
--- a/src/views/layout/components/Sidebar/SidebarItem.vue
+++ b/src/layout/components/Sidebar/SidebarItem.vue
@@ -1,27 +1,26 @@
@@ -30,10 +29,12 @@ import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
+import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
+ mixins: [FixiOSBug],
props: {
// route object
item: {
@@ -84,6 +85,9 @@ export default {
if (isExternal(routePath)) {
return routePath
}
+ if (isExternal(this.basePath)) {
+ return this.basePath
+ }
return path.resolve(this.basePath, routePath)
}
}
diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue
new file mode 100644
index 0000000..fb014a2
--- /dev/null
+++ b/src/layout/components/Sidebar/index.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/layout/components/index.js b/src/layout/components/index.js
similarity index 100%
rename from src/views/layout/components/index.js
rename to src/layout/components/index.js
diff --git a/src/views/layout/Layout.vue b/src/layout/index.vue
similarity index 66%
rename from src/views/layout/Layout.vue
rename to src/layout/index.vue
index d306586..aa67907 100644
--- a/src/views/layout/Layout.vue
+++ b/src/layout/index.vue
@@ -1,10 +1,12 @@
@@ -28,6 +30,9 @@ export default {
device() {
return this.$store.state.app.device
},
+ fixedHeader() {
+ return this.$store.state.settings.fixedHeader
+ },
classObj() {
return {
hideSidebar: !this.sidebar.opened,
@@ -45,8 +50,10 @@ export default {
}
-
diff --git a/src/layout/mixin/ResizeHandler.js b/src/layout/mixin/ResizeHandler.js
new file mode 100644
index 0000000..e8d0df8
--- /dev/null
+++ b/src/layout/mixin/ResizeHandler.js
@@ -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 })
+ }
+ }
+ }
+ }
+}
diff --git a/src/main.js b/src/main.js
index ed4e355..4ba95a7 100644
--- a/src/main.js
+++ b/src/main.js
@@ -16,15 +16,15 @@ import '@/icons' // icon
import '@/permission' // permission control
/**
- * This project originally used easy-mock to simulate data,
- * but its official service is very unstable,
- * and you can build your own service if you need it.
- * So here I use Mock.js for local emulation,
- * it will intercept your request, so you won't see the request in the network.
- * If you remove `../mock` it will automatically request easy-mock data.
+ * If you don't want to use mock-server
+ * you want to use mockjs for request interception
+ * you can execute:
+ *
+ * import { mockXHR } from '../mock'
+ * mockXHR()
*/
-import '../mock' // simulation data
+// set ElementUI lang to EN
Vue.use(ElementUI, { locale })
Vue.config.productionTip = false
diff --git a/src/permission.js b/src/permission.js
index 18abdb6..3d08d6b 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -1,43 +1,74 @@
import router from './router'
import store from './store'
+import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
-import { Message } from 'element-ui'
-import { getToken } from '@/utils/auth' // getToken from cookie
+import { getToken } from '@/utils/auth' // get token from cookie
+import getPageTitle from '@/utils/get-page-title'
-NProgress.configure({ showSpinner: false })// NProgress configuration
+NProgress.configure({ showSpinner: false }) // NProgress Configuration
-const whiteList = ['/login'] // 不重定向白名单
-router.beforeEach((to, from, next) => {
+const whiteList = ['/login'] // no redirect whitelist
+
+router.beforeEach(async(to, from, next) => {
+ // start progress bar
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 is logged in, redirect to the home page
next({ path: '/' })
- NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
+ NProgress.done()
} else {
- if (store.getters.roles.length === 0) {
- store.dispatch('GetInfo').then(res => { // 拉取用户信息
- next()
- }).catch((err) => {
- store.dispatch('FedLogOut').then(() => {
- Message.error(err || 'Verification failed, please login again')
- next({ path: '/' })
- })
- })
- } else {
+ // determine whether the user has obtained his permission roles through getInfo
+ const hasRoles = store.getters.roles && store.getters.roles.length > 0
+ if (hasRoles) {
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 {
+ /* has no token*/
+
if (whiteList.indexOf(to.path) !== -1) {
+ // in the free login whitelist, go directly
next()
} 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()
}
}
})
router.afterEach(() => {
- NProgress.done() // 结束Progress
+ // finish progress bar
+ NProgress.done()
})
diff --git a/src/router/index.js b/src/router/index.js
index 3e74077..b572aac 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,40 +1,57 @@
import Vue from 'vue'
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)
/* 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)
-* alwaysShow: true if set true, will always show the root menu, whatever its child routes length
-* if not set alwaysShow, only more than one route under the children
-* it will becomes nested mode, otherwise not show the root menu
-* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb
-* name:'router-name' the name is used by
(must set!!!)
-* meta : {
- title: 'title' the name show in subMenu and breadcrumb (recommend set)
+ * Note: sub-menu only appear when route children.length >= 1
+ * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
+ *
+ * hidden: true if set true, item will not show in the sidebar(default is false)
+ * alwaysShow: true if set true, will always show the root menu
+ * if not set alwaysShow, when item has more than one children route,
+ * it will becomes nested mode, otherwise not show the root menu
+ * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
+ * name:'router-name' the name is used by (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
- 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: '/',
component: Layout,
redirect: '/dashboard',
- name: 'Dashboard',
- hidden: true,
children: [{
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' }
}
]
- },
+ }
+]
+/**
+ * asyncRoutes
+ * the routes that need to be dynamically loaded based on user roles
+ */
+export const asyncRoutes = [
{
path: '/nested',
component: Layout,
@@ -142,11 +165,22 @@ export const constantRouterMap = [
]
},
+ // 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
-export default new Router({
- // mode: 'history', //后端支持可开
+const createRouter = () => new Router({
+ // mode: 'history', // require service support
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
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..ae3c494
--- /dev/null
+++ b/src/settings.js
@@ -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
+}
diff --git a/src/store/getters.js b/src/store/getters.js
index 7fbf1f4..a108508 100644
--- a/src/store/getters.js
+++ b/src/store/getters.js
@@ -4,6 +4,7 @@ const getters = {
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
- roles: state => state.user.roles
+ roles: state => state.user.roles,
+ permission_routes: state => state.permission.routes
}
export default getters
diff --git a/src/store/index.js b/src/store/index.js
index 6b6be08..6ae5dad 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -1,14 +1,18 @@
import Vue from 'vue'
import Vuex from 'vuex'
-import app from './modules/app'
-import user from './modules/user'
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)
const store = new Vuex.Store({
modules: {
app,
+ permission,
+ settings,
user
},
getters
diff --git a/src/store/modules/app.js b/src/store/modules/app.js
index f487241..7ea7e33 100644
--- a/src/store/modules/app.js
+++ b/src/store/modules/app.js
@@ -1,43 +1,48 @@
import Cookies from 'js-cookie'
-const app = {
- state: {
- sidebar: {
- opened: !+Cookies.get('sidebarStatus'),
- withoutAnimation: false
- },
- device: 'desktop'
+const state = {
+ sidebar: {
+ opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+ withoutAnimation: false
},
- mutations: {
- TOGGLE_SIDEBAR: state => {
- if (state.sidebar.opened) {
- Cookies.set('sidebarStatus', 1)
- } else {
- Cookies.set('sidebarStatus', 0)
- }
- state.sidebar.opened = !state.sidebar.opened
- state.sidebar.withoutAnimation = false
- },
- CLOSE_SIDEBAR: (state, withoutAnimation) => {
+ device: 'desktop'
+}
+
+const mutations = {
+ TOGGLE_SIDEBAR: state => {
+ state.sidebar.opened = !state.sidebar.opened
+ state.sidebar.withoutAnimation = false
+ if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
- state.sidebar.opened = false
- state.sidebar.withoutAnimation = withoutAnimation
- },
- TOGGLE_DEVICE: (state, device) => {
- state.device = device
+ } else {
+ Cookies.set('sidebarStatus', 0)
}
},
- actions: {
- ToggleSideBar: ({ commit }) => {
- commit('TOGGLE_SIDEBAR')
- },
- CloseSideBar({ commit }, { withoutAnimation }) {
- commit('CLOSE_SIDEBAR', withoutAnimation)
- },
- ToggleDevice({ commit }, device) {
- commit('TOGGLE_DEVICE', device)
- }
+ CLOSE_SIDEBAR: (state, withoutAnimation) => {
+ Cookies.set('sidebarStatus', 0)
+ state.sidebar.opened = false
+ state.sidebar.withoutAnimation = withoutAnimation
+ },
+ TOGGLE_DEVICE: (state, device) => {
+ state.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
+}
diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js
new file mode 100644
index 0000000..aeb5ee5
--- /dev/null
+++ b/src/store/modules/permission.js
@@ -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
+}
diff --git a/src/store/modules/settings.js b/src/store/modules/settings.js
new file mode 100644
index 0000000..aab31a2
--- /dev/null
+++ b/src/store/modules/settings.js
@@ -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
+}
+
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
index b504067..702f39a 100644
--- a/src/store/modules/user.js
+++ b/src/store/modules/user.js
@@ -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 { resetRouter } from '@/router'
-const user = {
- state: {
- token: getToken(),
- name: '',
- avatar: '',
- roles: []
+const state = {
+ token: getToken(),
+ name: '',
+ avatar: '',
+ roles: []
+}
+
+const mutations = {
+ SET_TOKEN: (state, token) => {
+ state.token = token
},
-
- mutations: {
- 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_NAME: (state, name) => {
+ state.name = name
},
-
- actions: {
- // 登录
- Login({ commit }, userInfo) {
- const username = userInfo.username.trim()
- 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()
- })
- }
+ SET_AVATAR: (state, avatar) => {
+ state.avatar = avatar
+ },
+ SET_ROLES: (state, roles) => {
+ state.roles = roles
}
}
-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
+}
+
diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss
index e60a687..6af3bfd 100644
--- a/src/styles/element-ui.scss
+++ b/src/styles/element-ui.scss
@@ -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 {
input[type="file"] {
display: none !important;
@@ -9,7 +15,8 @@
display: none;
}
-//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
@@ -17,7 +24,7 @@
margin: 0 auto;
}
-//element ui upload
+// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
@@ -28,3 +35,10 @@
}
}
}
+
+// dropdown
+.el-dropdown-menu {
+ a {
+ display: block
+ }
+}
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 9af325d..3b4da51 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -31,19 +31,6 @@ html {
box-sizing: inherit;
}
-a,
-a:focus,
-a:hover {
- cursor: pointer;
- color: inherit;
- outline: none;
- text-decoration: none;
-}
-
-div:focus {
- outline: none;
-}
-
a:focus,
a:active {
outline: none;
@@ -57,6 +44,10 @@ a:hover {
text-decoration: none;
}
+div:focus {
+ outline: none;
+}
+
.clearfix {
&:after {
visibility: hidden;
@@ -68,11 +59,7 @@ a:hover {
}
}
-//main-container全局样式
-.app-main {
- min-height: 100%
-}
-
+// main-container global css
.app-container {
padding: 20px;
}
diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss
index 0344970..e55f865 100644
--- a/src/styles/sidebar.scss
+++ b/src/styles/sidebar.scss
@@ -1,6 +1,5 @@
#app {
- // 主体区域 Main container
.main-container {
min-height: 100%;
transition: margin-left .28s;
@@ -8,10 +7,10 @@
position: relative;
}
- // 侧边栏 Sidebar container
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
+ background-color: $menuBg;
height: 100%;
position: fixed;
font-size: 0px;
@@ -21,23 +20,29 @@
z-index: 1001;
overflow: hidden;
- //reset element-ui css
+ // reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
-
- .el-scrollbar__view {
- height: 100%;
- }
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
+ .el-scrollbar {
+ height: 100%;
+ }
+
+ &.has-logo {
+ .el-scrollbar {
+ height: calc(100% - 50px);
+ }
+ }
+
.is-horizontal {
display: none;
}
@@ -100,6 +105,7 @@
.el-tooltip {
padding: 0 !important;
+
.svg-icon {
margin-left: 20px;
}
@@ -111,6 +117,7 @@
&>.el-submenu__title {
padding: 0 !important;
+
.svg-icon {
margin-left: 20px;
}
@@ -140,7 +147,7 @@
min-width: $sideBarWidth !important;
}
- // 适配移动端, Mobile responsive
+ // mobile responsive
.mobile {
.main-container {
margin-left: 0px;
diff --git a/src/styles/transition.scss b/src/styles/transition.scss
index 1c4a4c5..4cb27cc 100644
--- a/src/styles/transition.scss
+++ b/src/styles/transition.scss
@@ -1,6 +1,6 @@
-//globl transition css
+// global transition css
-/*fade*/
+/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
@@ -11,7 +11,7 @@
opacity: 0;
}
-/*fade-transform*/
+/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
@@ -27,7 +27,7 @@
transform: translateX(30px);
}
-/*fade*/
+/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index fb19d5d..be55772 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -1,4 +1,4 @@
-//sidebar
+// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
diff --git a/src/utils/get-page-title.js b/src/utils/get-page-title.js
new file mode 100644
index 0000000..a6de99d
--- /dev/null
+++ b/src/utils/get-page-title.js
@@ -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}`
+}
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 0000000..f18fccb
--- /dev/null
+++ b/src/utils/index.js
@@ -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() +
+ '分'
+ )
+ }
+}
diff --git a/src/utils/request.js b/src/utils/request.js
index 7744512..bc346a8 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -1,62 +1,74 @@
import axios from 'axios'
-import { Message, MessageBox } from 'element-ui'
-import store from '../store'
+import { MessageBox, Message } from 'element-ui'
+import store from '@/store'
import { getToken } from '@/utils/auth'
-// 创建axios实例
+// create an axios instance
const service = axios.create({
- baseURL: process.env.BASE_API, // api 的 base_url
- timeout: 5000 // 请求超时时间
+ baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+ withCredentials: true, // send cookies when cross-domain requests
+ timeout: 5000 // request timeout
})
-// request拦截器
+// request interceptor
service.interceptors.request.use(
config => {
+ // do something before request is sent
+
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
},
error => {
- // Do something with request error
+ // do something with request error
console.log(error) // for debug
- Promise.reject(error)
+ return Promise.reject(error)
}
)
-// response 拦截器
+// response interceptor
service.interceptors.response.use(
+ /**
+ * If you want to get http information such as headers or status
+ * Please return response => response
+ */
+
+ /**
+ * Determine the request status by custom code
+ * Here is just an example
+ * You can also judge the status by HTTP Status Code
+ */
response => {
- /**
- * code为非20000是抛错 可结合自己业务进行修改
- */
const res = response.data
+
+ // if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
Message({
- message: res.message,
+ message: res.message || 'error',
type: 'error',
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) {
- MessageBox.confirm(
- '你已被登出,可以取消继续留在该页面,或者重新登录',
- '确定登出',
- {
- confirmButtonText: '重新登录',
- cancelButtonText: '取消',
- type: 'warning'
- }
- ).then(() => {
- store.dispatch('FedLogOut').then(() => {
- location.reload() // 为了重新实例化vue-router对象 避免bug
+ // 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',
+ type: 'warning'
+ }).then(() => {
+ store.dispatch('user/resetToken').then(() => {
+ location.reload()
})
})
}
- return Promise.reject('error')
+ return Promise.reject(res.message || 'error')
} else {
- return response.data
+ return res
}
},
error => {
diff --git a/src/utils/validate.js b/src/utils/validate.js
index 122603a..8d962ad 100644
--- a/src/utils/validate.js
+++ b/src/utils/validate.js
@@ -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']
- return valid_map.indexOf(str.trim()) >= 0
-}
-
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+ const valid_map = ['admin', 'editor']
+ return valid_map.indexOf(str.trim()) >= 0
+}
diff --git a/src/views/404.vue b/src/views/404.vue
index c3eafea..1791f55 100644
--- a/src/views/404.vue
+++ b/src/views/404.vue
@@ -9,12 +9,12 @@
OOPS!
-
版权所有
-
华尔街见闻
+
{{ message }}
-
请检查您输入的网址是否正确,请点击以下按钮返回主页或者发送错误报告
-
返回首页
+
Please check that the URL you entered is correct, or click the button below to return to the homepage.
+
Back to home
@@ -26,13 +26,13 @@ export default {
name: 'Page404',
computed: {
message() {
- return '网管说这个页面你不能进......'
+ return 'The webmaster said that you can not enter this page...'
}
}
}
-
-
diff --git a/src/views/layout/components/Sidebar/index.vue b/src/views/layout/components/Sidebar/index.vue
deleted file mode 100644
index 225ee25..0000000
--- a/src/views/layout/components/Sidebar/index.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/views/layout/mixin/ResizeHandler.js b/src/views/layout/mixin/ResizeHandler.js
deleted file mode 100644
index 604fe3d..0000000
--- a/src/views/layout/mixin/ResizeHandler.js
+++ /dev/null
@@ -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 })
- }
- }
- }
- }
-}
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
index 0edbff1..1db2464 100644
--- a/src/views/login/index.vue
+++ b/src/views/login/index.vue
@@ -1,57 +1,73 @@
- vue-admin-template
+
+
+
Login Form
+
+
-
+
+
+ @keyup.enter.native="handleLogin"
+ />
-
+
-
-
- Sign in
-
-
+
+ Login
+
username: admin
- password: admin
+ password: any
+
-
-