diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 6c0b7f27..00000000 --- a/.babelrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "presets": [ - ["env", { - "modules": false, - "targets": { - "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] - } - }], - "stage-2" - ], - "plugins": ["transform-vue-jsx", "transform-runtime"], - "env": { - "development":{ - "plugins": ["dynamic-import-node"] - } - } -} diff --git a/.editorconfig b/.editorconfig index ea6e20f5..3454886e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# http://editorconfig.org +# https://editorconfig.org root = true [*] diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..8f5856db --- /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 00000000..80c81030 --- /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 00000000..a8793a09 --- /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 e3a4037e..e6529fc0 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 6f55c5a1..c9775054 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 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100755 index 00000000..1a114bc0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report(报告问题) +about: Create a report to help us improve +--- + + + +## Bug report(问题描述) + +#### Steps to reproduce(问题复现步骤) + + +#### Screenshot or Gif(截图或动态图) + + +#### Link to minimal reproduction(最小可在线还原demo) + + + +#### Other relevant information(格外信息) +- Your OS: +- Node.js version: +- vue-element-admin version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100755 index 00000000..c33d10d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,7 @@ +--- +name: Feature Request(新功能建议) +about: Suggest an idea for this project +--- + +## Feature request(新功能建议) + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100755 index 00000000..96be4532 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,14 @@ +--- +name: Question(提问) +about: Asking questions about use +--- + +## Question(提问) + + diff --git a/.gitignore b/.gitignore index 9322b8a6..78a752d8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ yarn-debug.log* yarn-error.log* **/*.log -test/unit/coverage -test/e2e/reports +tests/**/coverage/ +tests/e2e/reports selenium-debug.log # Editor directories and files @@ -17,5 +17,7 @@ selenium-debug.log *.ntvs* *.njsproj *.sln +*.local package-lock.json +yarn.lock diff --git a/.postcssrc.js b/.postcssrc.js index eee3e92d..09948d63 100644 --- a/.postcssrc.js +++ b/.postcssrc.js @@ -2,8 +2,6 @@ module.exports = { "plugins": { - "postcss-import": {}, - "postcss-url": {}, // to edit target browsers: use "browserslist" field in package.json "autoprefixer": {} } diff --git a/README.md b/README.md index bf0e08be..01034b51 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@

- vue + vue - element-ui + element-ui Build Status @@ -30,46 +30,41 @@ English | [简体中文](./README.zh-CN.md) ## Introduction -[vue-element-admin](http://panjiachen.github.io/vue-element-admin) is a front-end management background integration solution. It based on [vue](https://github.com/vuejs/vue) and use the UI Toolkit [element](https://github.com/ElemeFE/element). +[vue-element-admin](https://panjiachen.github.io/vue-element-admin) is a production-ready front-end solution for admin interfaces. It based on [vue](https://github.com/vuejs/vue) and use the UI Toolkit [element-ui](https://github.com/ElemeFE/element). It is a magical vue admin based on the newest development stack of vue, built-in i18n solution, typical templates for enterprise applications, lots of awesome features. It helps you build a large complex Single-Page Applications. I believe whatever your needs are, this project will help you. -- [Preview](http://panjiachen.github.io/vue-element-admin) +- [Preview](https://panjiachen.github.io/vue-element-admin) - [Documentation](https://panjiachen.github.io/vue-element-admin-site/) - [Gitter](https://gitter.im/vue-element-admin/discuss) -- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki) - - [Donate](https://panjiachen.github.io/vue-element-admin-site/donate/) -- [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 国内用户可访问该地址在线预览 +- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki) -**This project is positioned as a background integration solution and is not suitable for secondary development as a basic template.** +- [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 国内用户可访问该地址在线预览 - Base template recommends using: [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template) - Desktop: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) - Typescript: [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) -**This project does not support low version browsers (e.g. IE). Please add polyfill yourself if you need them.** +**The current version is `v4.0+` build on `vue-cli`. If you find a problem, please put [issue](https://github.com/PanJiaChen/vue-element-admin/issues/new). If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-element-admin/tree/tag/3.11.0), it does not rely on `vue-cli'** -**Note: This project uses element-ui@2.3.0+ version, so the minimum compatible vue@2.5.0+** - -**Start using `webpack4` from `v3.8.0`. If you still want to continue using `webpack3`, please use this branch [webpack3](https://github.com/PanJiaChen/vue-element-admin/tree/webpack3)** +**This project does not support low version browsers (e.g. IE). Please add polyfill by yourself.** ## Preparation -You need to install [node](http://nodejs.org/) and [git](https://git-scm.com/) locally. The project is based on [ES2015+](http://es6.ruanyifeng.com/), [vue](https://cn.vuejs.org/index.html), [vuex](https://vuex.vuejs.org/zh-cn/), [vue-router](https://router.vuejs.org/zh-cn/), [axios](https://github.com/axios/axios) and [element-ui](https://github.com/ElemeFE/element), all request data is simulated using [Mock.js](https://github.com/nuysoft/Mock). +You need to install [node](https://nodejs.org/) and [git](https://git-scm.com/) locally. The project is based on [ES2015+](https://es6.ruanyifeng.com/), [vue](https://cn.vuejs.org/index.html), [vuex](https://vuex.vuejs.org/zh-cn/), [vue-router](https://router.vuejs.org/zh-cn/), [vue-cli](https://github.com/vuejs/vue-cli) , [axios](https://github.com/axios/axios) and [element-ui](https://github.com/ElemeFE/element), all request data is simulated using [Mock.js](https://github.com/nuysoft/Mock). Understanding and learning this knowledge in advance will greatly help the use of this project. ---- -

## Sponsors + Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor]](https://www.patreon.com/panjiachen)

Admin Dashboard Templates made with Vue, React and Angular.

@@ -82,6 +77,7 @@ Become a sponsor and get your logo on our README on GitHub with a link to your s - Permission Authentication - Page permission - Directive permission + - Permission configuration page - Two-step login - Multi-environment build @@ -105,14 +101,13 @@ Become a sponsor and get your logo on our README on GitHub with a link to your s - Excel - Export Excel - - Export zip - Upload Excel - Visualization Excel + - Export zip - Table - Dynamic Table - Drag And Drop Table - - Tree Table - Inline Edit Table - Error Page @@ -146,6 +141,9 @@ Become a sponsor and get your logo on our README on GitHub with a link to your s # 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 @@ -153,13 +151,13 @@ npm install npm run dev ``` -This will automatically open http://localhost:9527. +This will automatically open http://localhost:9527 ## Build ```bash # build for test environment -npm run build:sit +npm run build:stage # build for production environment npm run build:prod @@ -168,19 +166,16 @@ npm run build:prod ## Advanced ```bash -# --report to build with bundle size analytics -npm run build:prod --report +# preview the release environment effect +npm run preview -# --generate a bundle size analytics. default: bundle-report.html -npm run build:prod --generate_report +# preview the release environment effect + static resource analysis +npm run preview -- --report -# --preview to start a server in local to preview -npm run build:prod --preview - -# lint code +# code format check npm run lint -# auto fix +# code format check and auto fix npm run lint -- --fix ``` @@ -192,7 +187,7 @@ Detailed changes for each release are documented in the [release notes](https:// ## Online Demo -[Preview](http://panjiachen.github.io/vue-element-admin) +[Preview](https://panjiachen.github.io/vue-element-admin) ## Donate @@ -208,7 +203,7 @@ If you find this project useful, you can buy author a glass of juice :tropical_d Modern browsers and Internet Explorer 10+. -| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| [IE / Edge](https://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](https://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](https://godban.github.io/browsers-support-badges/)
Chrome | [Safari](https://godban.github.io/browsers-support-badges/)
Safari | | --------- | --------- | --------- | --------- | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions diff --git a/README.zh-CN.md b/README.zh-CN.md index 5dc9f1d7..0e15d8a4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,10 +4,10 @@

- vue + vue - element-ui + element-ui Build Status @@ -30,41 +30,37 @@ ## 简介 -[vue-element-admin](http://panjiachen.github.io/vue-element-admin) 是一个后台集成解决方案,它基于 [vue](https://github.com/vuejs/vue) 和 [element](https://github.com/ElemeFE/element)。它使用了最新的前端技术栈,内置了 i18 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。相信不管你的需求是什么,本项目都能帮助到你。 +[vue-element-admin](https://panjiachen.github.io/vue-element-admin) 是一个后台前端解决方案,它基于 [vue](https://github.com/vuejs/vue) 和 [element-ui](https://github.com/ElemeFE/element)实现。它使用了最新的前端技术栈,内置了 i18n 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。相信不管你的需求是什么,本项目都能帮助到你。 -- [在线访问](http://panjiachen.github.io/vue-element-admin) +- [在线预览](https://panjiachen.github.io/vue-element-admin) - [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) - [Gitter 讨论组](https://gitter.im/vue-element-admin/discuss) -- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki) - - [Donate](https://panjiachen.gitee.io/vue-element-admin-site/zh/donate) -- [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 国内用户可访问该地址在线预览 +- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki) -- [国内访问文档](https://panjiachen.gitee.io/vue-element-admin-site/zh/) 方便没翻墙的用户查看文档 +- [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 在线预览(国内用户可访问该地址) -**本项目的定位是后台集成方案,不适合当基础模板来开发。** +- [国内访问文档](https://panjiachen.gitee.io/vue-element-admin-site/zh/) 文档(方便没翻墙的用户查看) -- 模板建议使用: [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template) +- 基础模板建议使用: [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template) - 桌面端: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) -- Typescript版: [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (鸣谢: [@Armour](https://github.com/Armour)) +- Typescript 版: [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (鸣谢: [@Armour](https://github.com/Armour)) -群主 **[圈子](https://jianshiapp.com/circles/1209)** 楼主会经常分享一些技术相关的东西,或者加入[qq 群](https://github.com/PanJiaChen/vue-element-admin/issues/602) - -**注意:该项目使用 element-ui@2.3.0+ 版本,所以最低兼容 vue@2.5.0+** - -**从`v3.8.0`开始使用`webpack4`。所以若还想使用`webpack3`开发,请使用该分支[webpack3](https://github.com/PanJiaChen/vue-element-admin/tree/webpack3)** +**目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若发现问题,欢迎提[issue](https://github.com/PanJiaChen/vue-element-admin/issues/new)。若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-element-admin/tree/tag/3.11.0),它不依赖 `vue-cli`** **该项目不支持低版本浏览器(如 ie),有需求请自行添加 polyfill [详情](https://github.com/PanJiaChen/vue-element-admin/wiki#babel-polyfill)** +群主 **[圈子](https://jianshiapp.com/circles/1209)** 群主会经常分享一些技术相关的东西,或者加入 [qq 群](https://github.com/PanJiaChen/vue-element-admin/issues/602) 或者关注 [微博](https://weibo.com/u/3423485724?is_all=1) + ## 前序准备 -你需要在本地安装 [node](http://nodejs.org/) 和 [git](https://git-scm.com/)。本项目技术栈基于 [ES2015+](http://es6.ruanyifeng.com/)、[vue](https://cn.vuejs.org/index.html)、[vuex](https://vuex.vuejs.org/zh-cn/)、[vue-router](https://router.vuejs.org/zh-cn/) 、[axios](https://github.com/axios/axios) 和 [element-ui](https://github.com/ElemeFE/element),所有的请求数据都使用[Mock.js](https://github.com/nuysoft/Mock)模拟,提前了解和学习这些知识会对使用本项目有很大的帮助。 +你需要在本地安装 [node](http://nodejs.org/) 和 [git](https://git-scm.com/)。本项目技术栈基于 [ES2015+](http://es6.ruanyifeng.com/)、[vue](https://cn.vuejs.org/index.html)、[vuex](https://vuex.vuejs.org/zh-cn/)、[vue-router](https://router.vuejs.org/zh-cn/) 、[vue-cli](https://github.com/vuejs/vue-cli) 、[axios](https://github.com/axios/axios) 和 [element-ui](https://github.com/ElemeFE/element),所有的请求数据都使用[Mock.js](https://github.com/nuysoft/Mock)进行模拟,提前了解和学习这些知识会对使用本项目有很大的帮助。 -同时配套一个系列的教程文章,如何从零构建后一个完整的后台项目,建议大家先看完这些文章再来实践本项目 +同时配套了系列教程文章,如何从零构建后一个完整的后台项目,建议大家先看完这些文章再来实践本项目 - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) @@ -82,6 +78,7 @@

## Sponsors + Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor]](https://www.patreon.com/panjiachen)

Admin Dashboard Templates made with Vue, React and Angular.

@@ -94,6 +91,7 @@ Become a sponsor and get your logo on our README on GitHub with a link to your s - 权限验证 - 页面权限 - 指令权限 + - 权限配置 - 二步登录 - 多环境发布 @@ -106,7 +104,7 @@ Become a sponsor and get your logo on our README on GitHub with a link to your s - 动态面包屑 - 快捷导航(标签页) - Svg Sprite 图标 - - 本地mock数据 + - 本地/后端 mock 数据 - Screenfull全屏 - 自适应收缩侧边栏 @@ -117,14 +115,13 @@ Become a sponsor and get your logo on our README on GitHub with a link to your s - Excel - 导出excel - - 导出zip - 导入excel - 前端可视化excel + - 导出zip - 表格 - 动态表格 - 拖拽表格 - - 树形表格 - 内联编辑 - 错误页面 @@ -158,6 +155,9 @@ Become a sponsor and get your logo on our README on GitHub with a link to your s # 克隆项目 git clone https://github.com/PanJiaChen/vue-element-admin.git +# 进入项目目录 +cd vue-element-admin + # 安装依赖 npm install @@ -174,7 +174,7 @@ npm run dev ```bash # 构建测试环境 -npm run build:sit +npm run build:stage # 构建生产环境 npm run build:prod @@ -183,19 +183,16 @@ npm run build:prod ## 其它 ```bash -# --report to build with bundle size analytics -npm run build:prod +# 预览发布环境效果 +npm run preview -# --generate a bundle size analytics. default: bundle-report.html -npm run build:prod --generate_report +# 预览发布环境效果 + 静态资源分析 +npm run preview -- --report -# --preview to start a server in local to preview -npm run build:prod --preview - -# lint code +# 代码格式检查 npm run lint -# auto fix +# 代码格式检查并自动修复 npm run lint -- --fix ``` @@ -207,7 +204,7 @@ Detailed changes for each release are documented in the [release notes](https:// ## Online Demo -[在线 Demo](http://panjiachen.github.io/vue-element-admin) +[在线 Demo](https://panjiachen.github.io/vue-element-admin) ## Donate @@ -218,11 +215,13 @@ Detailed changes for each release are documented in the [release notes](https:// [Paypal Me](https://www.paypal.me/panfree23) +[Buy me a coffee](https://www.buymeacoffee.com/Pan) + ## Browsers support Modern browsers and Internet Explorer 10+. -| [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | +| [IE / Edge](https://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](https://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](https://godban.github.io/browsers-support-badges/)
Chrome | [Safari](https://godban.github.io/browsers-support-badges/)
Safari | | --------- | --------- | --------- | --------- | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..ba179669 --- /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 34c71a55..00000000 --- a/build/build.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict' -require('./check-versions')() - -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') -var connect = require('connect') -var serveStatic = require('serve-static') - -const spinner = ora( - 'building for ' + process.env.env_config + ' environment...' -) -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" - ) - ) - - if (process.env.npm_config_preview) { - const port = 9526 - const host = 'http://localhost:' + port - const basePath = config.build.assetsPublicPath - const app = connect() - - app.use( - basePath, - serveStatic('./dist', { - index: ['index.html', '/'] - }) - ) - - app.listen(port, function() { - console.log( - chalk.green(`> Listening at http://localhost:${port}${basePath}`) - ) - }) - } - }) -}) diff --git a/build/check-versions.js b/build/check-versions.js deleted file mode 100644 index c5c29e90..00000000 --- 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 00000000..0c57de2a --- /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 f3d2503f..00000000 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 c96d0936..00000000 --- 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 5496c931..00000000 --- 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 3b946b4b..00000000 --- a/build/webpack.base.conf.js +++ /dev/null @@ -1,107 +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?cacheDirectory', - include: [ - resolve('src'), - resolve('test'), - 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 26a5584a..00000000 --- a/build/webpack.dev.conf.js +++ /dev/null @@ -1,98 +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-element-admin', - templateParameters: { - BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory, - }, - }), - ] -}) - -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 4f84e0c6..00000000 --- a/build/webpack.prod.conf.js +++ /dev/null @@ -1,187 +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/' + process.env.env_config + '.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-element-admin', - templateParameters: { - BASE_URL: config.build.assetsPublicPath + config.build.assetsSubDirectory, - }, - 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[\\/]/ - }, - commons: { - name: 'chunk-commons', - test: resolve('src/components'), // 可自定义拓展你的规则 - minChunks: 3, // 最小公用次数 - priority: 5, - reuseExistingChunk: true - } - } - }, - 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 68ddea56..00000000 --- a/config/dev.env.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - NODE_ENV: '"development"', - ENV_CONFIG: '"dev"', - BASE_API: '"https://api-dev"' -} diff --git a/config/index.js b/config/index.js deleted file mode 100644 index 599e4a63..00000000 --- a/config/index.js +++ /dev/null @@ -1,88 +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 - - // can be overwritten by process.env.HOST - // if you want dev by ip, please set host: '0.0.0.0' - host: 'localhost', - port: 9527, // 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:prod --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 bfcd6d27..00000000 --- a/config/prod.env.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - NODE_ENV: '"production"', - ENV_CONFIG: '"prod"', - BASE_API: '"https://api-prod"' -} diff --git a/config/sit.env.js b/config/sit.env.js deleted file mode 100644 index 93178e80..00000000 --- a/config/sit.env.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - NODE_ENV: '"production"', - ENV_CONFIG: '"sit"', - BASE_API: '"https://api-sit"' -} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..1ce813e1 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,27 @@ +module.exports = { + verbose: true, + moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], + transformIgnorePatterns: [ + 'node_modules/(?!(babel-jest|jest-vue-preprocessor)/)' + ], + 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/article.js b/mock/article.js new file mode 100644 index 00000000..bc236eb9 --- /dev/null +++ b/mock/article.js @@ -0,0 +1,116 @@ +import Mock from 'mockjs' + +const List = [] +const count = 100 + +const baseContent = '

I am testing data, I am testing data.

' +const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3' + +for (let i = 0; i < count; i++) { + List.push(Mock.mock({ + id: '@increment', + timestamp: +Mock.Random.date('T'), + author: '@first', + reviewer: '@first', + title: '@title(5, 10)', + content_short: 'mock data', + content: baseContent, + forecast: '@float(0, 100, 2, 2)', + importance: '@integer(1, 3)', + 'type|1': ['CN', 'US', 'JP', 'EU'], + 'status|1': ['published', 'draft', 'deleted'], + display_time: '@datetime', + comment_disabled: true, + pageviews: '@integer(300, 5000)', + image_uri, + platforms: ['a-platform'] + })) +} + +export default [ + { + url: '/article/list', + type: 'get', + response: config => { + const { importance, type, title, page = 1, limit = 20, sort } = config.query + + let mockList = List.filter(item => { + if (importance && item.importance !== +importance) return false + if (type && item.type !== type) return false + if (title && item.title.indexOf(title) < 0) return false + return true + }) + + if (sort === '-id') { + mockList = mockList.reverse() + } + + const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1)) + + return { + code: 20000, + data: { + total: mockList.length, + items: pageList + } + } + } + }, + + { + url: '/article/detail', + type: 'get', + response: config => { + const { id } = config.query + for (const article of List) { + if (article.id === +id) { + return { + code: 20000, + data: article + } + } + } + } + }, + + { + url: '/article/pv', + type: 'get', + response: _ => { + return { + code: 20000, + data: { + pvData: [ + { key: 'PC', pv: 1024 }, + { key: 'mobile', pv: 1024 }, + { key: 'ios', pv: 1024 }, + { key: 'android', pv: 1024 } + ] + } + } + } + }, + + { + url: '/article/create', + type: 'post', + response: _ => { + return { + code: 20000, + data: 'success' + } + } + }, + + { + url: '/article/update', + type: 'post', + response: _ => { + return { + code: 20000, + data: 'success' + } + } + } +] + diff --git a/mock/index.js b/mock/index.js new file mode 100644 index 00000000..6907e861 --- /dev/null +++ b/mock/index.js @@ -0,0 +1,70 @@ +import Mock from 'mockjs' +import { param2Obj } from '../src/utils' + +import user from './user' +import role from './role' +import article from './article' +import search from './remote-search' + +const mocks = [ + ...user, + ...role, + ...article, + ...search +] + +// 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)) + } +} + +// 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)) + } + } +} + +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 00000000..b29b7c3f --- /dev/null +++ b/mock/mock-server.js @@ -0,0 +1,62 @@ +const chokidar = require('chokidar') +const bodyParser = require('body-parser') +const chalk = require('chalk') + +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('/mock')) { + 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(('./mock'), { + ignored: 'mock/mock-server.js', + persistent: true, + 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/remote-search.js b/mock/remote-search.js new file mode 100644 index 00000000..bb33c2f4 --- /dev/null +++ b/mock/remote-search.js @@ -0,0 +1,51 @@ +import Mock from 'mockjs' + +const NameList = [] +const count = 100 + +for (let i = 0; i < count; i++) { + NameList.push(Mock.mock({ + name: '@first' + })) +} +NameList.push({ name: 'mock-Pan' }) + +export default [ + // username search + { + url: '/search/user', + type: 'get', + response: config => { + const { name } = config.query + const mockNameList = NameList.filter(item => { + const lowerCaseName = item.name.toLowerCase() + return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0) + }) + return { + code: 20000, + data: { items: mockNameList } + } + } + }, + + // transaction list + { + url: '/transaction/list', + type: 'get', + response: _ => { + return { + code: 20000, + data: { + total: 20, + 'items|20': [{ + order_no: '@guid()', + timestamp: +Mock.Random.date('T'), + username: '@name()', + price: '@float(1000, 15000, 0, 2)', + 'status|1': ['success', 'pending'] + }] + } + } + } + } +] diff --git a/mock/role/index.js b/mock/role/index.js new file mode 100644 index 00000000..39148076 --- /dev/null +++ b/mock/role/index.js @@ -0,0 +1,98 @@ +import Mock from 'mockjs' +import { deepClone } from '../../src/utils/index.js' +import { asyncRoutes, constantRoutes } from './routes.js' + +const routes = deepClone([...constantRoutes, ...asyncRoutes]) + +const roles = [ + { + key: 'admin', + name: 'admin', + description: 'Super Administrator. Have access to view all pages.', + routes: routes + }, + { + key: 'editor', + name: 'editor', + description: 'Normal Editor. Can see all pages except permission page', + routes: routes.filter(i => i.path !== '/permission')// just a mock + }, + { + key: 'visitor', + name: 'visitor', + description: 'Just a visitor. Can only see the home page and the document page', + routes: [{ + path: '', + redirect: 'dashboard', + children: [ + { + path: 'dashboard', + name: 'Dashboard', + meta: { title: 'dashboard', icon: 'dashboard' } + } + ] + }] + } +] + +export default [ + // mock get all routes form server + { + url: '/routes', + type: 'get', + response: _ => { + return { + code: 20000, + data: routes + } + } + }, + + // mock get all roles form server + { + url: '/roles', + type: 'get', + response: _ => { + return { + code: 20000, + data: roles + } + } + }, + + // add role + { + url: '/role', + type: 'post', + response: { + code: 20000, + data: { + key: Mock.mock('@integer(300, 5000)') + } + } + }, + + // update role + { + url: '/role/[A-Za-z0-9]', + type: 'put', + response: { + code: 20000, + data: { + status: 'success' + } + } + }, + + // delete role + { + url: '/role/[A-Za-z0-9]', + type: 'delete', + response: { + code: 20000, + data: { + status: 'success' + } + } + } +] diff --git a/mock/role/routes.js b/mock/role/routes.js new file mode 100644 index 00000000..62a09d01 --- /dev/null +++ b/mock/role/routes.js @@ -0,0 +1,525 @@ +// Just a mock data + +export const constantRoutes = [ + { + path: '/redirect', + component: 'layout/Layout', + hidden: true, + children: [ + { + path: '/redirect/:path*', + component: 'views/redirect/index' + } + ] + }, + { + path: '/login', + component: 'views/login/index', + hidden: true + }, + { + path: '/auth-redirect', + component: 'views/login/auth-redirect', + hidden: true + }, + { + path: '/404', + component: 'views/error-page/404', + hidden: true + }, + { + path: '/401', + component: 'views/error-page/401', + hidden: true + }, + { + path: '', + component: 'layout/Layout', + redirect: 'dashboard', + children: [ + { + path: 'dashboard', + component: 'views/dashboard/index', + name: 'Dashboard', + meta: { title: 'dashboard', icon: 'dashboard', noCache: true, affix: true } + } + ] + }, + { + path: '/documentation', + component: 'layout/Layout', + children: [ + { + path: 'index', + component: 'views/documentation/index', + name: 'Documentation', + meta: { title: 'documentation', icon: 'documentation', affix: true } + } + ] + }, + { + path: '/guide', + component: 'layout/Layout', + redirect: '/guide/index', + children: [ + { + path: 'index', + component: 'views/guide/index', + name: 'Guide', + meta: { title: 'guide', icon: 'guide', noCache: true } + } + ] + } +] + +export const asyncRoutes = [ + { + path: '/permission', + component: 'layout/Layout', + redirect: '/permission/index', + alwaysShow: true, + meta: { + title: 'permission', + icon: 'lock', + roles: ['admin', 'editor'] + }, + children: [ + { + path: 'page', + component: 'views/permission/page', + name: 'PagePermission', + meta: { + title: 'pagePermission', + roles: ['admin'] + } + }, + { + path: 'directive', + component: 'views/permission/directive', + name: 'DirectivePermission', + meta: { + title: 'directivePermission' + } + }, + { + path: 'role', + component: 'views/permission/role', + name: 'RolePermission', + meta: { + title: 'rolePermission', + roles: ['admin'] + } + } + ] + }, + + { + path: '/icon', + component: 'layout/Layout', + children: [ + { + path: 'index', + component: 'views/svg-icons/index', + name: 'Icons', + meta: { title: 'icons', icon: 'icon', noCache: true } + } + ] + }, + + { + path: '/components', + component: 'layout/Layout', + redirect: 'noredirect', + name: 'ComponentDemo', + meta: { + title: 'components', + icon: 'component' + }, + children: [ + { + path: 'tinymce', + component: 'views/components-demo/tinymce', + name: 'TinymceDemo', + meta: { title: 'tinymce' } + }, + { + path: 'markdown', + component: 'views/components-demo/markdown', + name: 'MarkdownDemo', + meta: { title: 'markdown' } + }, + { + path: 'json-editor', + component: 'views/components-demo/json-editor', + name: 'JsonEditorDemo', + meta: { title: 'jsonEditor' } + }, + { + path: 'split-pane', + component: 'views/components-demo/split-pane', + name: 'SplitpaneDemo', + meta: { title: 'splitPane' } + }, + { + path: 'avatar-upload', + component: 'views/components-demo/avatar-upload', + name: 'AvatarUploadDemo', + meta: { title: 'avatarUpload' } + }, + { + path: 'dropzone', + component: 'views/components-demo/dropzone', + name: 'DropzoneDemo', + meta: { title: 'dropzone' } + }, + { + path: 'sticky', + component: 'views/components-demo/sticky', + name: 'StickyDemo', + meta: { title: 'sticky' } + }, + { + path: 'count-to', + component: 'views/components-demo/count-to', + name: 'CountToDemo', + meta: { title: 'countTo' } + }, + { + path: 'mixin', + component: 'views/components-demo/mixin', + name: 'ComponentMixinDemo', + meta: { title: 'componentMixin' } + }, + { + path: 'back-to-top', + component: 'views/components-demo/back-to-top', + name: 'BackToTopDemo', + meta: { title: 'backToTop' } + }, + { + path: 'drag-dialog', + component: 'views/components-demo/drag-dialog', + name: 'DragDialogDemo', + meta: { title: 'dragDialog' } + }, + { + path: 'drag-select', + component: 'views/components-demo/drag-select', + name: 'DragSelectDemo', + meta: { title: 'dragSelect' } + }, + { + path: 'dnd-list', + component: 'views/components-demo/dnd-list', + name: 'DndListDemo', + meta: { title: 'dndList' } + }, + { + path: 'drag-kanban', + component: 'views/components-demo/drag-kanban', + name: 'DragKanbanDemo', + meta: { title: 'dragKanban' } + } + ] + }, + { + path: '/charts', + component: 'layout/Layout', + redirect: 'noredirect', + name: 'Charts', + meta: { + title: 'charts', + icon: 'chart' + }, + children: [ + { + path: 'keyboard', + component: 'views/charts/keyboard', + name: 'KeyboardChart', + meta: { title: 'keyboardChart', noCache: true } + }, + { + path: 'line', + component: 'views/charts/line', + name: 'LineChart', + meta: { title: 'lineChart', noCache: true } + }, + { + path: 'mixchart', + component: 'views/charts/mixChart', + name: 'MixChart', + meta: { title: 'mixChart', noCache: true } + } + ] + }, + { + path: '/nested', + component: 'layout/Layout', + redirect: '/nested/menu1/menu1-1', + name: 'Nested', + meta: { + title: 'nested', + icon: 'nested' + }, + children: [ + { + path: 'menu1', + component: 'views/nested/menu1/index', + name: 'Menu1', + meta: { title: 'menu1' }, + redirect: '/nested/menu1/menu1-1', + children: [ + { + path: 'menu1-1', + component: 'views/nested/menu1/menu1-1', + name: 'Menu1-1', + meta: { title: 'menu1-1' } + }, + { + path: 'menu1-2', + component: 'views/nested/menu1/menu1-2', + name: 'Menu1-2', + redirect: '/nested/menu1/menu1-2/menu1-2-1', + meta: { title: 'menu1-2' }, + children: [ + { + path: 'menu1-2-1', + component: 'views/nested/menu1/menu1-2/menu1-2-1', + name: 'Menu1-2-1', + meta: { title: 'menu1-2-1' } + }, + { + path: 'menu1-2-2', + component: 'views/nested/menu1/menu1-2/menu1-2-2', + name: 'Menu1-2-2', + meta: { title: 'menu1-2-2' } + } + ] + }, + { + path: 'menu1-3', + component: 'views/nested/menu1/menu1-3', + name: 'Menu1-3', + meta: { title: 'menu1-3' } + } + ] + }, + { + path: 'menu2', + name: 'Menu2', + component: 'views/nested/menu2/index', + meta: { title: 'menu2' } + } + ] + }, + + { + path: '/example', + component: 'layout/Layout', + redirect: '/example/list', + name: 'Example', + meta: { + title: 'example', + icon: 'example' + }, + children: [ + { + path: 'create', + component: 'views/example/create', + name: 'CreateArticle', + meta: { title: 'createArticle', icon: 'edit' } + }, + { + path: 'edit/:id(\\d+)', + component: 'views/example/edit', + name: 'EditArticle', + meta: { title: 'editArticle', noCache: true }, + hidden: true + }, + { + path: 'list', + component: 'views/example/list', + name: 'ArticleList', + meta: { title: 'articleList', icon: 'list' } + } + ] + }, + + { + path: '/tab', + component: 'layout/Layout', + children: [ + { + path: 'index', + component: 'views/tab/index', + name: 'Tab', + meta: { title: 'tab', icon: 'tab' } + } + ] + }, + + { + path: '/error', + component: 'layout/Layout', + redirect: 'noredirect', + name: 'ErrorPages', + meta: { + title: 'errorPages', + icon: '404' + }, + children: [ + { + path: '401', + component: 'views/error-page/401', + name: 'Page401', + meta: { title: 'page401', noCache: true } + }, + { + path: '404', + component: 'views/error-page/404', + name: 'Page404', + meta: { title: 'page404', noCache: true } + } + ] + }, + + { + path: '/error-log', + component: 'layout/Layout', + redirect: 'noredirect', + children: [ + { + path: 'log', + component: 'views/error-log/index', + name: 'ErrorLog', + meta: { title: 'errorLog', icon: 'bug' } + } + ] + }, + + { + path: '/excel', + component: 'layout/Layout', + redirect: '/excel/export-excel', + name: 'Excel', + meta: { + title: 'excel', + icon: 'excel' + }, + children: [ + { + path: 'export-excel', + component: 'views/excel/export-excel', + name: 'ExportExcel', + meta: { title: 'exportExcel' } + }, + { + path: 'export-selected-excel', + component: 'views/excel/select-excel', + name: 'SelectExcel', + meta: { title: 'selectExcel' } + }, + { + path: 'export-merge-header', + component: 'views/excel/merge-header', + name: 'MergeHeader', + meta: { title: 'mergeHeader' } + }, + { + path: 'upload-excel', + component: 'views/excel/upload-excel', + name: 'UploadExcel', + meta: { title: 'uploadExcel' } + } + ] + }, + + { + path: '/zip', + component: 'layout/Layout', + redirect: '/zip/download', + alwaysShow: true, + meta: { title: 'zip', icon: 'zip' }, + children: [ + { + path: 'download', + component: 'views/zip/index', + name: 'ExportZip', + meta: { title: 'exportZip' } + } + ] + }, + + { + path: '/pdf', + component: 'layout/Layout', + redirect: '/pdf/index', + children: [ + { + path: 'index', + component: 'views/pdf/index', + name: 'PDF', + meta: { title: 'pdf', icon: 'pdf' } + } + ] + }, + { + path: '/pdf/download', + component: 'views/pdf/download', + hidden: true + }, + + { + path: '/theme', + component: 'layout/Layout', + redirect: 'noredirect', + children: [ + { + path: 'index', + component: 'views/theme/index', + name: 'Theme', + meta: { title: 'theme', icon: 'theme' } + } + ] + }, + + { + path: '/clipboard', + component: 'layout/Layout', + redirect: 'noredirect', + children: [ + { + path: 'index', + component: 'views/clipboard/index', + name: 'ClipboardDemo', + meta: { title: 'clipboardDemo', icon: 'clipboard' } + } + ] + }, + + { + path: '/i18n', + component: 'layout/Layout', + children: [ + { + path: 'index', + component: 'views/i18n-demo/index', + name: 'I18n', + meta: { title: 'i18n', icon: 'international' } + } + ] + }, + + { + path: 'external-link', + component: 'layout/Layout', + children: [ + { + path: 'https://github.com/PanJiaChen/vue-element-admin', + meta: { title: 'externalLink', icon: 'link' } + } + ] + }, + + { path: '*', redirect: '/404', hidden: true } +] diff --git a/mock/user.js b/mock/user.js new file mode 100644 index 00000000..43f93a04 --- /dev/null +++ b/mock/user.js @@ -0,0 +1,84 @@ + +const tokens = { + admin: { + token: 'admin-token' + }, + editor: { + token: 'editor-token' + } +} + +const users = { + 'admin-token': { + roles: ['admin'], + introduction: 'I am a super administrator', + avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', + name: 'Super Admin' + }, + 'editor-token': { + roles: ['editor'], + introduction: 'I am an editor', + avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', + name: 'Normal Editor' + } +} + +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.' + } + } + + return { + code: 20000, + data: token + } + } + }, + + // 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 + } + } + }, + + // user logout + { + url: '/user/logout', + type: 'post', + response: _ => { + return { + code: 20000, + data: 'success' + } + } + } +] diff --git a/package.json b/package.json index aef30525..72e92927 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,24 @@ { "name": "vue-element-admin", - "version": "3.10.0", - "description": "A magical vue admin. Typical templates for enterprise applications. Newest development stack of vue. Lots of awesome features", + "version": "4.0.1", + "description": "A magical vue admin. An out-of-box UI solution for enterprise applications. Newest development stack of vue. Lots of awesome features", "author": "Pan ", "license": "MIT", "scripts": { - "dev": "cross-env BABEL_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", - "build:prod": "cross-env NODE_ENV=production env_config=prod node build/build.js", - "build:sit": "cross-env NODE_ENV=production env_config=sit node build/build.js", + "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", - "precommit": "lint-staged", - "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" + "test:unit": "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", + "new": "plop" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } }, "lint-staged": { "src/**/*.{js,vue}": [ @@ -21,10 +28,12 @@ }, "keywords": [ "vue", - "element-ui", "admin", - "management-system", - "admin-template" + "dashboard", + "element-ui", + "boilerplate", + "admin-template", + "management-system" ], "repository": { "type": "git", @@ -35,93 +44,66 @@ }, "dependencies": { "axios": "0.18.0", - "clipboard": "1.7.1", - "codemirror": "5.39.2", - "driver.js": "0.8.1", - "dropzone": "5.2.0", - "echarts": "4.1.0", - "element-ui": "2.4.11", - "file-saver": "1.3.8", - "fuse.js": "3.4.2", + "clipboard": "2.0.4", + "codemirror": "5.45.0", + "driver.js": "0.9.5", + "dropzone": "5.5.1", + "echarts": "4.2.1", + "element-ui": "2.7.0", + "file-saver": "2.0.1", + "fuse.js": "3.4.4", "js-cookie": "2.2.0", "jsonlint": "1.6.3", - "jszip": "3.1.5", - "mockjs": "1.0.1-beta3", + "jszip": "3.2.1", "normalize.css": "7.0.0", "nprogress": "0.2.0", - "screenfull": "4.0.0", - "showdown": "1.8.6", - "sortablejs": "1.7.0", - "tui-editor": "1.2.7", - "vue": "2.5.17", + "path-to-regexp": "2.4.0", + "screenfull": "4.2.0", + "showdown": "1.9.0", + "sortablejs": "1.8.4", + "tui-editor": "1.3.3", + "vue": "2.6.10", "vue-count-to": "1.0.13", "vue-i18n": "7.3.2", "vue-router": "3.0.2", - "vue-splitpane": "1.0.2", - "vuedraggable": "^2.16.0", - "vuex": "3.0.1", - "xlsx": "^0.11.16" + "vue-splitpane": "1.0.4", + "vuedraggable": "2.20.0", + "vuex": "3.1.0", + "xlsx": "0.14.1" }, "devDependencies": { - "autoprefixer": "8.5.0", - "babel-core": "6.26.3", - "babel-eslint": "8.2.6", - "babel-helper-vue-jsx-merge-props": "2.0.3", - "babel-loader": "7.1.5", - "babel-plugin-dynamic-import-node": "2.0.0", - "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", + "@babel/core": "7.0.0", + "@babel/register": "7.0.0", + "@vue/cli-plugin-babel": "3.5.3", + "@vue/cli-plugin-eslint": "3.5.1", + "@vue/cli-plugin-unit-jest": "3.5.3", + "@vue/cli-service": "3.5.3", + "@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", + "chokidar": "2.1.5", "connect": "3.6.6", - "copy-webpack-plugin": "4.5.2", - "cross-env": "5.2.0", - "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", - "file-loader": "1.1.11", - "friendly-errors-webpack-plugin": "1.7.0", - "hash-sum": "1.0.2", - "html-webpack-plugin": "4.0.0-alpha", - "husky": "0.14.3", - "lint-staged": "7.2.2", - "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.13", - "postcss-import": "11.1.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", + "eslint": "5.15.3", + "eslint-plugin-vue": "5.2.2", + "html-webpack-plugin": "3.2.0", + "husky": "1.3.1", + "lint-staged": "8.1.5", + "mockjs": "1.0.1-beta3", + "node-sass": "^4.9.0", + "plop": "2.3.0", + "runjs": "^4.3.2", + "sass-loader": "^7.1.0", + "script-ext-html-webpack-plugin": "2.1.3", "script-loader": "0.7.2", - "semver": "5.5.0", - "serve-static": "1.13.2", - "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" + "serve-static": "^1.13.2", + "svg-sprite-loader": "4.1.3", + "svgo": "1.2.0", + "vue-template-compiler": "2.6.10" }, "engines": { - "node": ">= 6.0.0", + "node": ">=8.9", "npm": ">= 3.0.0" }, "browserslist": [ diff --git a/plop-templates/component/index.hbs b/plop-templates/component/index.hbs new file mode 100644 index 00000000..76610552 --- /dev/null +++ b/plop-templates/component/index.hbs @@ -0,0 +1,26 @@ +{{#if template}} + +{{/if}} + +{{#if script}} + +{{/if}} + +{{#if style}} + +{{/if}} diff --git a/plop-templates/component/prompt.js b/plop-templates/component/prompt.js new file mode 100644 index 00000000..3723e8e1 --- /dev/null +++ b/plop-templates/component/prompt.js @@ -0,0 +1,55 @@ +const { notEmpty } = require('../utils.js') + +module.exports = { + description: 'generate vue component', + prompts: [{ + type: 'input', + name: 'name', + message: 'component name please', + validate: notEmpty('name') + }, + { + type: 'checkbox', + name: 'blocks', + message: 'Blocks:', + choices: [{ + name: ' @@ -21,7 +27,7 @@ export default { methods: { handleSetLanguage(lang) { this.$i18n.locale = lang - this.$store.dispatch('setLanguage', lang) + this.$store.dispatch('app/setLanguage', lang) this.$message({ message: 'Switch Language Success', type: 'success' diff --git a/src/components/MDinput/index.vue b/src/components/MDinput/index.vue index 7ede73db..013462fa 100644 --- a/src/components/MDinput/index.vue +++ b/src/components/MDinput/index.vue @@ -1,12 +1,12 @@ - diff --git a/src/views/layout/components/Navbar.vue b/src/layout/components/Navbar.vue similarity index 77% rename from src/views/layout/components/Navbar.vue rename to src/layout/components/Navbar.vue index 5c3bcda4..d544f079 100644 --- a/src/views/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -1,32 +1,29 @@ @@ -87,6 +86,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 00000000..773cd5c8 --- /dev/null +++ b/src/layout/components/Sidebar/index.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/views/layout/components/TagsView/ScrollPane.vue b/src/layout/components/TagsView/ScrollPane.vue similarity index 97% rename from src/views/layout/components/TagsView/ScrollPane.vue rename to src/layout/components/TagsView/ScrollPane.vue index 820a536e..0ec76e72 100644 --- a/src/views/layout/components/TagsView/ScrollPane.vue +++ b/src/layout/components/TagsView/ScrollPane.vue @@ -1,6 +1,6 @@ @@ -67,7 +67,7 @@ export default { } - - diff --git a/src/layout/mixin/ResizeHandler.js b/src/layout/mixin/ResizeHandler.js new file mode 100644 index 00000000..e8d0df8c --- /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 7739639d..41298d0c 100644 --- a/src/main.js +++ b/src/main.js @@ -2,10 +2,10 @@ import Vue from 'vue' import Cookies from 'js-cookie' -import 'normalize.css/normalize.css' // A modern alternative to CSS resets +import 'normalize.css/normalize.css' // a modern alternative to CSS resets import Element from 'element-ui' -import 'element-ui/lib/theme-chalk/index.css' +import './styles/element-variables.scss' import '@/styles/index.scss' // global css @@ -13,20 +13,28 @@ import App from './App' import store from './store' import router from './router' -import i18n from './lang' // Internationalization +import i18n from './lang' // internationalization import './icons' // icon -import './errorLog' // error log import './permission' // permission control -import './mock' // simulation data +import './utils/error-log' // error log import * as filters from './filters' // global filters +/** + * 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() + */ + Vue.use(Element, { size: Cookies.get('size') || 'medium', // set element-ui default size i18n: (key, value) => i18n.t(key, value) }) -// register global utility filters. +// register global utility filters Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]) }) diff --git a/src/mock/article.js b/src/mock/article.js deleted file mode 100644 index 45923ddd..00000000 --- a/src/mock/article.js +++ /dev/null @@ -1,70 +0,0 @@ -import Mock from 'mockjs' -import { param2Obj } from '@/utils' - -const List = [] -const count = 100 - -const baseContent = '

我是测试数据我是测试数据

' -const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3' - -for (let i = 0; i < count; i++) { - List.push(Mock.mock({ - id: '@increment', - timestamp: +Mock.Random.date('T'), - author: '@first', - reviewer: '@first', - title: '@title(5, 10)', - content_short: '我是测试数据', - content: baseContent, - forecast: '@float(0, 100, 2, 2)', - importance: '@integer(1, 3)', - 'type|1': ['CN', 'US', 'JP', 'EU'], - 'status|1': ['published', 'draft', 'deleted'], - display_time: '@datetime', - comment_disabled: true, - pageviews: '@integer(300, 5000)', - image_uri, - platforms: ['a-platform'] - })) -} - -export default { - getList: config => { - const { importance, type, title, page = 1, limit = 20, sort } = param2Obj(config.url) - - let mockList = List.filter(item => { - if (importance && item.importance !== +importance) return false - if (type && item.type !== type) return false - if (title && item.title.indexOf(title) < 0) return false - return true - }) - - if (sort === '-id') { - mockList = mockList.reverse() - } - - const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1)) - - return { - total: mockList.length, - items: pageList - } - }, - getPv: () => ({ - pvData: [{ key: 'PC', pv: 1024 }, { key: 'mobile', pv: 1024 }, { key: 'ios', pv: 1024 }, { key: 'android', pv: 1024 }] - }), - getArticle: (config) => { - const { id } = param2Obj(config.url) - for (const article of List) { - if (article.id === +id) { - return article - } - } - }, - createArticle: () => ({ - data: 'success' - }), - updateArticle: () => ({ - data: 'success' - }) -} diff --git a/src/mock/index.js b/src/mock/index.js deleted file mode 100644 index 3e00e918..00000000 --- a/src/mock/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import Mock from 'mockjs' -import loginAPI from './login' -import articleAPI from './article' -import remoteSearchAPI from './remoteSearch' -import transactionAPI from './transaction' - -// 修复在使用 MockJS 情况下,设置 withCredentials = true,且未被拦截的跨域请求丢失 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 - } - this.proxy_send(...arguments) -} - -// Mock.setup({ -// timeout: '350-600' -// }) - -// 登录相关 -Mock.mock(/\/login\/login/, 'post', loginAPI.loginByUsername) -Mock.mock(/\/login\/logout/, 'post', loginAPI.logout) -Mock.mock(/\/user\/info\.*/, 'get', loginAPI.getUserInfo) - -// 文章相关 -Mock.mock(/\/article\/list/, 'get', articleAPI.getList) -Mock.mock(/\/article\/detail/, 'get', articleAPI.getArticle) -Mock.mock(/\/article\/pv/, 'get', articleAPI.getPv) -Mock.mock(/\/article\/create/, 'post', articleAPI.createArticle) -Mock.mock(/\/article\/update/, 'post', articleAPI.updateArticle) - -// 搜索相关 -Mock.mock(/\/search\/user/, 'get', remoteSearchAPI.searchUser) - -// 账单相关 -Mock.mock(/\/transaction\/list/, 'get', transactionAPI.getList) - -export default Mock diff --git a/src/mock/login.js b/src/mock/login.js deleted file mode 100644 index b9694f09..00000000 --- a/src/mock/login.js +++ /dev/null @@ -1,34 +0,0 @@ -import { param2Obj } from '@/utils' - -const userMap = { - admin: { - roles: ['admin'], - token: 'admin', - introduction: '我是超级管理员', - avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', - name: 'Super Admin' - }, - editor: { - roles: ['editor'], - token: 'editor', - introduction: '我是编辑', - avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', - name: 'Normal Editor' - } -} - -export default { - loginByUsername: config => { - const { username } = JSON.parse(config.body) - return userMap[username] - }, - getUserInfo: config => { - const { token } = param2Obj(config.url) - if (userMap[token]) { - return userMap[token] - } else { - return false - } - }, - logout: () => 'success' -} diff --git a/src/mock/remoteSearch.js b/src/mock/remoteSearch.js deleted file mode 100644 index b70f6f7d..00000000 --- a/src/mock/remoteSearch.js +++ /dev/null @@ -1,24 +0,0 @@ -import Mock from 'mockjs' -import { param2Obj } from '@/utils' - -const NameList = [] -const count = 100 - -for (let i = 0; i < count; i++) { - NameList.push(Mock.mock({ - name: '@first' - })) -} -NameList.push({ name: 'mockPan' }) - -export default { - searchUser: config => { - const { name } = param2Obj(config.url) - const mockNameList = NameList.filter(item => { - const lowerCaseName = item.name.toLowerCase() - if (name && lowerCaseName.indexOf(name.toLowerCase()) < 0) return false - return true - }) - return { items: mockNameList } - } -} diff --git a/src/mock/transaction.js b/src/mock/transaction.js deleted file mode 100644 index a17517e4..00000000 --- a/src/mock/transaction.js +++ /dev/null @@ -1,23 +0,0 @@ -import Mock from 'mockjs' - -const List = [] -const count = 20 - -for (let i = 0; i < count; i++) { - List.push(Mock.mock({ - order_no: '@guid()', - timestamp: +Mock.Random.date('T'), - username: '@name()', - price: '@float(1000, 15000, 0, 2)', - 'status|1': ['success', 'pending'] - })) -} - -export default { - getList: () => { - return { - total: List.length, - items: List - } - } -} diff --git a/src/permission.js b/src/permission.js index e556cb00..750aeab6 100644 --- a/src/permission.js +++ b/src/permission.js @@ -2,62 +2,73 @@ import router from './router' import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' // progress bar -import 'nprogress/nprogress.css'// progress bar style -import { getToken } from '@/utils/auth' // getToken from cookie +import 'nprogress/nprogress.css' // progress bar style +import { getToken } from '@/utils/auth' // get token from cookie +import getPageTitle from '@/utils/get-page-title' -NProgress.configure({ showSpinner: false })// NProgress Configuration +NProgress.configure({ showSpinner: false }) // NProgress Configuration -// permission judge function -function hasPermission(roles, permissionRoles) { - if (roles.indexOf('admin') >= 0) return true // admin permission passed directly - if (!permissionRoles) return true - return roles.some(role => permissionRoles.indexOf(role) >= 0) -} +const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist -const whiteList = ['/login', '/auth-redirect']// no redirect whitelist +router.beforeEach(async(to, from, next) => { + // start progress bar + NProgress.start() -router.beforeEach((to, from, next) => { - NProgress.start() // start progress bar - if (getToken()) { // determine if there has token - /* has token*/ + // 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) { // 判断当前用户是否已拉取完user_info信息 - store.dispatch('GetUserInfo').then(res => { // 拉取user_info - const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop'] - store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表 - router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 - next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record - }) - }).catch((err) => { - store.dispatch('FedLogOut').then(() => { - Message.error(err) - next({ path: '/' }) - }) - }) + // 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 { - // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ - if (hasPermission(store.getters.roles, to.meta.roles)) { - next() - } else { - next({ path: '/401', replace: true, query: { noGoBack: true }}) + 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) { // 在免登录白名单,直接进入 + + if (whiteList.indexOf(to.path) !== -1) { + // in the free login whitelist, go directly next() } else { - next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 - NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it + // 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() // finish progress bar + // finish progress bar + NProgress.done() }) diff --git a/src/router/index.js b/src/router/index.js index a4a71e46..d04a64be 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -4,36 +4,41 @@ import Router from 'vue-router' Vue.use(Router) /* Layout */ -import Layout from '@/views/layout/Layout' +import Layout from '@/layout' /* Router Modules */ import componentsRouter from './modules/components' import chartsRouter from './modules/charts' import tableRouter from './modules/table' -import treeTableRouter from './modules/tree-table' import nestedRouter from './modules/nested' -/** note: sub-menu only appear when children.length>=1 - * detail see https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html - **/ +/** + * 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 `redirect: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 + noCache: true if set true, the page will no be cached(default is false) + affix: true if set true, the tag will affix in the tags-view + 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 + } + */ /** -* 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 : { - roles: ['admin','editor'] will control the page roles (you can set multiple roles) - title: 'title' the name show in sub-menu and breadcrumb (recommend set) - icon: 'svg-name' the icon show in the sidebar - noCache: true if true, the page will no be cached(default is false) - breadcrumb: false if false, the item will hidden in breadcrumb(default is true) - affix: true if true, the tag will affix in the tags-view - } -**/ -export const constantRouterMap = [ + * constantRoutes + * a base page that does not have permission requirements + * all roles can be accessed + */ +export const constantRoutes = [ { path: '/redirect', component: Layout, @@ -52,17 +57,17 @@ export const constantRouterMap = [ }, { path: '/auth-redirect', - component: () => import('@/views/login/authredirect'), + component: () => import('@/views/login/auth-redirect'), hidden: true }, { path: '/404', - component: () => import('@/views/errorPage/404'), + component: () => import('@/views/error-page/404'), hidden: true }, { path: '/401', - component: () => import('@/views/errorPage/401'), + component: () => import('@/views/error-page/401'), hidden: true }, { @@ -81,7 +86,6 @@ export const constantRouterMap = [ { path: '/documentation', component: Layout, - redirect: '/documentation/index', children: [ { path: 'index', @@ -106,18 +110,17 @@ export const constantRouterMap = [ } ] -export default new Router({ - // mode: 'history', // require service support - scrollBehavior: () => ({ y: 0 }), - routes: constantRouterMap -}) - -export const asyncRouterMap = [ +/** + * asyncRoutes + * the routes that need to be dynamically loaded based on user roles + */ +export const asyncRoutes = [ { path: '/permission', component: Layout, - redirect: '/permission/index', + redirect: '/permission/page', alwaysShow: true, // will always show the root menu + name: 'Permission', meta: { title: 'permission', icon: 'lock', @@ -141,6 +144,15 @@ export const asyncRouterMap = [ title: 'directivePermission' // if do not set roles, means: this page does not require permission } + }, + { + path: 'role', + component: () => import('@/views/permission/role'), + name: 'RolePermission', + meta: { + title: 'rolePermission', + roles: ['admin'] + } } ] }, @@ -158,12 +170,11 @@ export const asyncRouterMap = [ ] }, - /** When your routing table is too long, you can split it into small modules**/ + /** when your routing map is too long, you can split it into small modules **/ componentsRouter, chartsRouter, nestedRouter, tableRouter, - treeTableRouter, { path: '/example', @@ -185,7 +196,7 @@ export const asyncRouterMap = [ path: 'edit/:id(\\d+)', component: () => import('@/views/example/edit'), name: 'EditArticle', - meta: { title: 'editArticle', noCache: true }, + meta: { title: 'editArticle', noCache: true, activeMenu: '/example/list' }, hidden: true }, { @@ -222,13 +233,13 @@ export const asyncRouterMap = [ children: [ { path: '401', - component: () => import('@/views/errorPage/401'), + component: () => import('@/views/error-page/401'), name: 'Page401', meta: { title: 'page401', noCache: true } }, { path: '404', - component: () => import('@/views/errorPage/404'), + component: () => import('@/views/error-page/404'), name: 'Page404', meta: { title: 'page404', noCache: true } } @@ -242,7 +253,7 @@ export const asyncRouterMap = [ children: [ { path: 'log', - component: () => import('@/views/errorLog/index'), + component: () => import('@/views/error-log/index'), name: 'ErrorLog', meta: { title: 'errorLog', icon: 'bug' } } @@ -261,19 +272,25 @@ export const asyncRouterMap = [ children: [ { path: 'export-excel', - component: () => import('@/views/excel/exportExcel'), + component: () => import('@/views/excel/export-excel'), name: 'ExportExcel', meta: { title: 'exportExcel' } }, { path: 'export-selected-excel', - component: () => import('@/views/excel/selectExcel'), + component: () => import('@/views/excel/select-excel'), name: 'SelectExcel', meta: { title: 'selectExcel' } }, + { + path: 'export-merge-header', + component: () => import('@/views/excel/merge-header'), + name: 'MergeHeader', + meta: { title: 'mergeHeader' } + }, { path: 'upload-excel', - component: () => import('@/views/excel/uploadExcel'), + component: () => import('@/views/excel/upload-excel'), name: 'UploadExcel', meta: { title: 'uploadExcel' } } @@ -285,6 +302,7 @@ export const asyncRouterMap = [ component: Layout, redirect: '/zip/download', alwaysShow: true, + name: 'Zip', meta: { title: 'zip', icon: 'zip' }, children: [ { @@ -369,3 +387,19 @@ export const asyncRouterMap = [ { path: '*', redirect: '/404', hidden: true } ] + +const createRouter = () => new Router({ + // mode: 'history', // require service support + scrollBehavior: () => ({ y: 0 }), + routes: constantRoutes +}) + +const router = createRouter() + +// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 +export function resetRouter() { + const newRouter = createRouter() + router.matcher = newRouter.matcher // reset router +} + +export default router diff --git a/src/router/modules/charts.js b/src/router/modules/charts.js index d11f6efd..5797ba04 100644 --- a/src/router/modules/charts.js +++ b/src/router/modules/charts.js @@ -1,6 +1,6 @@ /** When your routing table is too long, you can split it into small modules**/ -import Layout from '@/views/layout/Layout' +import Layout from '@/layout' const chartsRouter = { path: '/charts', @@ -25,8 +25,8 @@ const chartsRouter = { meta: { title: 'lineChart', noCache: true } }, { - path: 'mixchart', - component: () => import('@/views/charts/mixChart'), + path: 'mix-chart', + component: () => import('@/views/charts/mix-chart'), name: 'MixChart', meta: { title: 'mixChart', noCache: true } } diff --git a/src/router/modules/components.js b/src/router/modules/components.js index 5fd9bd29..0d8055bd 100644 --- a/src/router/modules/components.js +++ b/src/router/modules/components.js @@ -1,6 +1,6 @@ -/** When your routing table is too long, you can split it into small modules**/ +/** When your routing table is too long, you can split it into small modules **/ -import Layout from '@/views/layout/Layout' +import Layout from '@/layout' const componentsRouter = { path: '/components', @@ -26,19 +26,19 @@ const componentsRouter = { }, { path: 'json-editor', - component: () => import('@/views/components-demo/jsonEditor'), + component: () => import('@/views/components-demo/json-editor'), name: 'JsonEditorDemo', meta: { title: 'jsonEditor' } }, { - path: 'splitpane', - component: () => import('@/views/components-demo/splitpane'), + path: 'split-pane', + component: () => import('@/views/components-demo/split-pane'), name: 'SplitpaneDemo', meta: { title: 'splitPane' } }, { path: 'avatar-upload', - component: () => import('@/views/components-demo/avatarUpload'), + component: () => import('@/views/components-demo/avatar-upload'), name: 'AvatarUploadDemo', meta: { title: 'avatarUpload' } }, @@ -56,7 +56,7 @@ const componentsRouter = { }, { path: 'count-to', - component: () => import('@/views/components-demo/countTo'), + component: () => import('@/views/components-demo/count-to'), name: 'CountToDemo', meta: { title: 'countTo' } }, @@ -68,31 +68,31 @@ const componentsRouter = { }, { path: 'back-to-top', - component: () => import('@/views/components-demo/backToTop'), + component: () => import('@/views/components-demo/back-to-top'), name: 'BackToTopDemo', meta: { title: 'backToTop' } }, { path: 'drag-dialog', - component: () => import('@/views/components-demo/dragDialog'), + component: () => import('@/views/components-demo/drag-dialog'), name: 'DragDialogDemo', meta: { title: 'dragDialog' } }, { path: 'drag-select', - component: () => import('@/views/components-demo/dragSelect'), + component: () => import('@/views/components-demo/drag-select'), name: 'DragSelectDemo', meta: { title: 'dragSelect' } }, { path: 'dnd-list', - component: () => import('@/views/components-demo/dndList'), + component: () => import('@/views/components-demo/dnd-list'), name: 'DndListDemo', meta: { title: 'dndList' } }, { path: 'drag-kanban', - component: () => import('@/views/components-demo/dragKanban'), + component: () => import('@/views/components-demo/drag-kanban'), name: 'DragKanbanDemo', meta: { title: 'dragKanban' } } diff --git a/src/router/modules/nested.js b/src/router/modules/nested.js index ad8e31f9..c52664c9 100644 --- a/src/router/modules/nested.js +++ b/src/router/modules/nested.js @@ -1,6 +1,6 @@ -/** When your routing table is too long, you can split it into small modules**/ +/** When your routing table is too long, you can split it into small modules **/ -import Layout from '@/views/layout/Layout' +import Layout from '@/layout' const nestedRouter = { path: '/nested', diff --git a/src/router/modules/table.js b/src/router/modules/table.js index 4d7f55ef..d8e50302 100644 --- a/src/router/modules/table.js +++ b/src/router/modules/table.js @@ -1,6 +1,6 @@ -/** When your routing table is too long, you can split it into small modules**/ +/** When your routing table is too long, you can split it into small modules **/ -import Layout from '@/views/layout/Layout' +import Layout from '@/layout' const tableRouter = { path: '/table', @@ -14,25 +14,25 @@ const tableRouter = { children: [ { path: 'dynamic-table', - component: () => import('@/views/table/dynamicTable/index'), + component: () => import('@/views/table/dynamic-table/index'), name: 'DynamicTable', meta: { title: 'dynamicTable' } }, { path: 'drag-table', - component: () => import('@/views/table/dragTable'), + component: () => import('@/views/table/drag-table'), name: 'DragTable', meta: { title: 'dragTable' } }, { path: 'inline-edit-table', - component: () => import('@/views/table/inlineEditTable'), + component: () => import('@/views/table/inline-edit-table'), name: 'InlineEditTable', meta: { title: 'inlineEditTable' } }, { path: 'complex-table', - component: () => import('@/views/table/complexTable'), + component: () => import('@/views/table/complex-table'), name: 'ComplexTable', meta: { title: 'complexTable' } } diff --git a/src/router/modules/tree-table.js b/src/router/modules/tree-table.js deleted file mode 100644 index 5ee26828..00000000 --- a/src/router/modules/tree-table.js +++ /dev/null @@ -1,29 +0,0 @@ -/** When your routing table is too long, you can split it into small modules**/ - -import Layout from '@/views/layout/Layout' - -const treeTableRouter = { - path: '/tree-table', - component: Layout, - redirect: '/table/complex-table', - name: 'TreeTable', - meta: { - title: 'treeTable', - icon: 'tree-table' - }, - children: [ - { - path: 'index', - component: () => import('@/views/tree-table/index'), - name: 'TreeTableDemo', - meta: { title: 'treeTable' } - }, - { - path: 'custom', - component: () => import('@/views/tree-table/custom'), - name: 'CustomTreeTableDemo', - meta: { title: 'customTreeTable' } - } - ] -} -export default treeTableRouter diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 00000000..1ebc7f29 --- /dev/null +++ b/src/settings.js @@ -0,0 +1,35 @@ +module.exports = { + title: 'Vue Element Admin', + + /** + * @type {boolean} true | false + * @description Whether show the settings right-panel + */ + showSettings: true, + + /** + * @type {boolean} true | false + * @description Whether need tagsView + */ + tagsView: true, + + /** + * @type {boolean} true | false + * @description Whether fix the header + */ + fixedHeader: false, + + /** + * @type {boolean} true | false + * @description Whether show the logo in sidebar + */ + sidebarLogo: false, + + /** + * @type {string | array} 'production' | ['production', 'development'] + * @description Need show err logs component. + * The default is only used in the production env + * If you want to also use it in dev, you can pass ['production', 'development'] + */ + errorLog: 'production' +} diff --git a/src/store/getters.js b/src/store/getters.js index cf314f5c..3fb5b068 100644 --- a/src/store/getters.js +++ b/src/store/getters.js @@ -9,11 +9,9 @@ const getters = { avatar: state => state.user.avatar, name: state => state.user.name, introduction: state => state.user.introduction, - status: state => state.user.status, roles: state => state.user.roles, - setting: state => state.user.setting, - permission_routers: state => state.permission.routers, - addRouters: state => state.permission.addRouters, + permission_routes: state => state.permission.routes, + addRoutes: state => state.permission.addRoutes, errorLogs: state => state.errorLog.logs } export default getters diff --git a/src/store/index.js b/src/store/index.js index 24778fad..70736d3a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,22 +1,24 @@ import Vue from 'vue' import Vuex from 'vuex' -import app from './modules/app' -import errorLog from './modules/errorLog' -import permission from './modules/permission' -import tagsView from './modules/tagsView' -import user from './modules/user' import getters from './getters' Vue.use(Vuex) +// https://webpack.js.org/guides/dependency-management/#requirecontext +const modulesFiles = require.context('./modules', false, /\.js$/) + +// you do not need `import app from './modules/app'` +// it will auto require all vuex module from modules file +const modules = modulesFiles.keys().reduce((modules, modulePath) => { + // set './app.js' => 'app' + const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') + const value = modulesFiles(modulePath) + modules[moduleName] = value.default + return modules +}, {}) + const store = new Vuex.Store({ - modules: { - app, - errorLog, - permission, - tagsView, - user - }, + modules, getters }) diff --git a/src/store/modules/app.js b/src/store/modules/app.js index fba4b05c..73616469 100644 --- a/src/store/modules/app.js +++ b/src/store/modules/app.js @@ -1,59 +1,65 @@ import Cookies from 'js-cookie' +import { getLanguage } from '@/lang/index' -const app = { - state: { - sidebar: { - opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, - withoutAnimation: false - }, - device: 'desktop', - language: Cookies.get('language') || 'en', - size: Cookies.get('size') || 'medium' +const state = { + sidebar: { + opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, + withoutAnimation: false }, - mutations: { - TOGGLE_SIDEBAR: state => { - state.sidebar.opened = !state.sidebar.opened - state.sidebar.withoutAnimation = false - if (state.sidebar.opened) { - Cookies.set('sidebarStatus', 1) - } else { - Cookies.set('sidebarStatus', 0) - } - }, - CLOSE_SIDEBAR: (state, withoutAnimation) => { + device: 'desktop', + language: getLanguage(), + size: Cookies.get('size') || 'medium' +} + +const mutations = { + TOGGLE_SIDEBAR: state => { + state.sidebar.opened = !state.sidebar.opened + state.sidebar.withoutAnimation = false + if (state.sidebar.opened) { + Cookies.set('sidebarStatus', 1) + } else { Cookies.set('sidebarStatus', 0) - state.sidebar.opened = false - state.sidebar.withoutAnimation = withoutAnimation - }, - TOGGLE_DEVICE: (state, device) => { - state.device = device - }, - SET_LANGUAGE: (state, language) => { - state.language = language - Cookies.set('language', language) - }, - SET_SIZE: (state, size) => { - state.size = size - Cookies.set('size', size) } }, - actions: { - toggleSideBar({ commit }) { - commit('TOGGLE_SIDEBAR') - }, - closeSideBar({ commit }, { withoutAnimation }) { - commit('CLOSE_SIDEBAR', withoutAnimation) - }, - toggleDevice({ commit }, device) { - commit('TOGGLE_DEVICE', device) - }, - setLanguage({ commit }, language) { - commit('SET_LANGUAGE', language) - }, - setSize({ commit }, size) { - commit('SET_SIZE', size) - } + CLOSE_SIDEBAR: (state, withoutAnimation) => { + Cookies.set('sidebarStatus', 0) + state.sidebar.opened = false + state.sidebar.withoutAnimation = withoutAnimation + }, + TOGGLE_DEVICE: (state, device) => { + state.device = device + }, + SET_LANGUAGE: (state, language) => { + state.language = language + Cookies.set('language', language) + }, + SET_SIZE: (state, size) => { + state.size = size + Cookies.set('size', size) } } -export default app +const actions = { + toggleSideBar({ commit }) { + commit('TOGGLE_SIDEBAR') + }, + closeSideBar({ commit }, { withoutAnimation }) { + commit('CLOSE_SIDEBAR', withoutAnimation) + }, + toggleDevice({ commit }, device) { + commit('TOGGLE_DEVICE', device) + }, + setLanguage({ commit }, language) { + commit('SET_LANGUAGE', language) + }, + setSize({ commit }, size) { + commit('SET_SIZE', size) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/src/store/modules/errorLog.js b/src/store/modules/errorLog.js index 50fc1b1a..8e3db060 100644 --- a/src/store/modules/errorLog.js +++ b/src/store/modules/errorLog.js @@ -1,17 +1,22 @@ -const errorLog = { - state: { - logs: [] - }, - mutations: { - ADD_ERROR_LOG: (state, log) => { - state.logs.push(log) - } - }, - actions: { - addErrorLog({ commit }, log) { - commit('ADD_ERROR_LOG', log) - } +const state = { + logs: [] +} + +const mutations = { + ADD_ERROR_LOG: (state, log) => { + state.logs.push(log) } } -export default errorLog +const actions = { + addErrorLog({ commit }, log) { + commit('ADD_ERROR_LOG', log) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js index 13f60efb..bd35252c 100644 --- a/src/store/modules/permission.js +++ b/src/store/modules/permission.js @@ -1,7 +1,7 @@ -import { asyncRouterMap, constantRouterMap } from '@/router' +import { asyncRoutes, constantRoutes } from '@/router' /** - * 通过meta.role判断是否与当前用户权限匹配 + * Use meta.role to determine if the current user has permission * @param roles * @param route */ @@ -14,18 +14,18 @@ function hasPermission(roles, route) { } /** - * 递归过滤异步路由表,返回符合用户角色权限的路由表 - * @param routes asyncRouterMap + * Filter asynchronous routing tables by recursion + * @param routes asyncRoutes * @param roles */ -function filterAsyncRouter(routes, roles) { +export function filterAsyncRoutes(routes, roles) { const res = [] routes.forEach(route => { const tmp = { ...route } if (hasPermission(roles, tmp)) { if (tmp.children) { - tmp.children = filterAsyncRouter(tmp.children, roles) + tmp.children = filterAsyncRoutes(tmp.children, roles) } res.push(tmp) } @@ -34,32 +34,36 @@ function filterAsyncRouter(routes, roles) { return res } -const permission = { - state: { - routers: [], - addRouters: [] - }, - mutations: { - SET_ROUTERS: (state, routers) => { - state.addRouters = routers - state.routers = constantRouterMap.concat(routers) - } - }, - actions: { - GenerateRoutes({ commit }, data) { - return new Promise(resolve => { - const { roles } = data - let accessedRouters - if (roles.includes('admin')) { - accessedRouters = asyncRouterMap - } else { - accessedRouters = filterAsyncRouter(asyncRouterMap, roles) - } - commit('SET_ROUTERS', accessedRouters) - resolve() - }) - } +const state = { + routes: [], + addRoutes: [] +} + +const mutations = { + SET_ROUTES: (state, routes) => { + state.addRoutes = routes + state.routes = constantRoutes.concat(routes) } } -export default permission +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 00000000..e389af79 --- /dev/null +++ b/src/store/modules/settings.js @@ -0,0 +1,34 @@ +import variables from '@/styles/element-variables.scss' +import defaultSettings from '@/settings' + +const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings + +const state = { + theme: variables.theme, + showSettings: showSettings, + tagsView: tagsView, + 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/tagsView.js b/src/store/modules/tagsView.js index 378cbcd3..3e2c1703 100644 --- a/src/store/modules/tagsView.js +++ b/src/store/modules/tagsView.js @@ -1,161 +1,165 @@ -const tagsView = { - state: { - visitedViews: [], - cachedViews: [] - }, - mutations: { - ADD_VISITED_VIEW: (state, view) => { - if (state.visitedViews.some(v => v.path === view.path)) return - state.visitedViews.push( - Object.assign({}, view, { - title: view.meta.title || 'no-name' - }) - ) - }, - ADD_CACHED_VIEW: (state, view) => { - if (state.cachedViews.includes(view.name)) return - if (!view.meta.noCache) { - state.cachedViews.push(view.name) - } - }, +const state = { + visitedViews: [], + cachedViews: [] +} - DEL_VISITED_VIEW: (state, view) => { - for (const [i, v] of state.visitedViews.entries()) { - if (v.path === view.path) { - state.visitedViews.splice(i, 1) - break - } - } - }, - DEL_CACHED_VIEW: (state, view) => { - for (const i of state.cachedViews) { - if (i === view.name) { - const index = state.cachedViews.indexOf(i) - state.cachedViews.splice(index, 1) - break - } - } - }, - - DEL_OTHERS_VISITED_VIEWS: (state, view) => { - state.visitedViews = state.visitedViews.filter(v => { - return v.meta.affix || v.path === view.path +const mutations = { + ADD_VISITED_VIEW: (state, view) => { + if (state.visitedViews.some(v => v.path === view.path)) return + state.visitedViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' }) - }, - DEL_OTHERS_CACHED_VIEWS: (state, view) => { - for (const i of state.cachedViews) { - if (i === view.name) { - const index = state.cachedViews.indexOf(i) - state.cachedViews = state.cachedViews.slice(index, index + 1) - break - } - } - }, + ) + }, + ADD_CACHED_VIEW: (state, view) => { + if (state.cachedViews.includes(view.name)) return + if (!view.meta.noCache) { + state.cachedViews.push(view.name) + } + }, - DEL_ALL_VISITED_VIEWS: state => { - // keep affix tags - const affixTags = state.visitedViews.filter(tag => tag.meta.affix) - state.visitedViews = affixTags - }, - DEL_ALL_CACHED_VIEWS: state => { - state.cachedViews = [] - }, - - UPDATE_VISITED_VIEW: (state, view) => { - for (let v of state.visitedViews) { - if (v.path === view.path) { - v = Object.assign(v, view) - break - } + DEL_VISITED_VIEW: (state, view) => { + for (const [i, v] of state.visitedViews.entries()) { + if (v.path === view.path) { + state.visitedViews.splice(i, 1) + break + } + } + }, + DEL_CACHED_VIEW: (state, view) => { + for (const i of state.cachedViews) { + if (i === view.name) { + const index = state.cachedViews.indexOf(i) + state.cachedViews.splice(index, 1) + break } } - }, - actions: { - addView({ dispatch }, view) { - dispatch('addVisitedView', view) - dispatch('addCachedView', view) - }, - addVisitedView({ commit }, view) { - commit('ADD_VISITED_VIEW', view) - }, - addCachedView({ commit }, view) { - commit('ADD_CACHED_VIEW', view) - }, - delView({ dispatch, state }, view) { - return new Promise(resolve => { - dispatch('delVisitedView', view) - dispatch('delCachedView', view) - resolve({ - visitedViews: [...state.visitedViews], - cachedViews: [...state.cachedViews] - }) - }) - }, - delVisitedView({ commit, state }, view) { - return new Promise(resolve => { - commit('DEL_VISITED_VIEW', view) - resolve([...state.visitedViews]) - }) - }, - delCachedView({ commit, state }, view) { - return new Promise(resolve => { - commit('DEL_CACHED_VIEW', view) - resolve([...state.cachedViews]) - }) - }, + DEL_OTHERS_VISITED_VIEWS: (state, view) => { + state.visitedViews = state.visitedViews.filter(v => { + return v.meta.affix || v.path === view.path + }) + }, + DEL_OTHERS_CACHED_VIEWS: (state, view) => { + for (const i of state.cachedViews) { + if (i === view.name) { + const index = state.cachedViews.indexOf(i) + state.cachedViews = state.cachedViews.slice(index, index + 1) + break + } + } + }, - delOthersViews({ dispatch, state }, view) { - return new Promise(resolve => { - dispatch('delOthersVisitedViews', view) - dispatch('delOthersCachedViews', view) - resolve({ - visitedViews: [...state.visitedViews], - cachedViews: [...state.cachedViews] - }) - }) - }, - delOthersVisitedViews({ commit, state }, view) { - return new Promise(resolve => { - commit('DEL_OTHERS_VISITED_VIEWS', view) - resolve([...state.visitedViews]) - }) - }, - delOthersCachedViews({ commit, state }, view) { - return new Promise(resolve => { - commit('DEL_OTHERS_CACHED_VIEWS', view) - resolve([...state.cachedViews]) - }) - }, + DEL_ALL_VISITED_VIEWS: state => { + // keep affix tags + const affixTags = state.visitedViews.filter(tag => tag.meta.affix) + state.visitedViews = affixTags + }, + DEL_ALL_CACHED_VIEWS: state => { + state.cachedViews = [] + }, - delAllViews({ dispatch, state }, view) { - return new Promise(resolve => { - dispatch('delAllVisitedViews', view) - dispatch('delAllCachedViews', view) - resolve({ - visitedViews: [...state.visitedViews], - cachedViews: [...state.cachedViews] - }) - }) - }, - delAllVisitedViews({ commit, state }) { - return new Promise(resolve => { - commit('DEL_ALL_VISITED_VIEWS') - resolve([...state.visitedViews]) - }) - }, - delAllCachedViews({ commit, state }) { - return new Promise(resolve => { - commit('DEL_ALL_CACHED_VIEWS') - resolve([...state.cachedViews]) - }) - }, - - updateVisitedView({ commit }, view) { - commit('UPDATE_VISITED_VIEW', view) + UPDATE_VISITED_VIEW: (state, view) => { + for (let v of state.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } } } } -export default tagsView +const actions = { + addView({ dispatch }, view) { + dispatch('addVisitedView', view) + dispatch('addCachedView', view) + }, + addVisitedView({ commit }, view) { + commit('ADD_VISITED_VIEW', view) + }, + addCachedView({ commit }, view) { + commit('ADD_CACHED_VIEW', view) + }, + + delView({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delVisitedView', view) + dispatch('delCachedView', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delVisitedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_VISITED_VIEW', view) + resolve([...state.visitedViews]) + }) + }, + delCachedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_CACHED_VIEW', view) + resolve([...state.cachedViews]) + }) + }, + + delOthersViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delOthersVisitedViews', view) + dispatch('delOthersCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delOthersVisitedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_VISITED_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, + delOthersCachedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_CACHED_VIEWS', view) + resolve([...state.cachedViews]) + }) + }, + + delAllViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delAllVisitedViews', view) + dispatch('delAllCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delAllVisitedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_VISITED_VIEWS') + resolve([...state.visitedViews]) + }) + }, + delAllCachedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_CACHED_VIEWS') + resolve([...state.cachedViews]) + }) + }, + + updateVisitedView({ commit }, view) { + commit('UPDATE_VISITED_VIEW', view) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/src/store/modules/user.js b/src/store/modules/user.js index 38e81a36..f27615b0 100644 --- a/src/store/modules/user.js +++ b/src/store/modules/user.js @@ -1,144 +1,128 @@ -import { loginByUsername, logout, getUserInfo } from '@/api/login' +import { login, logout, getInfo } from '@/api/user' import { getToken, setToken, removeToken } from '@/utils/auth' +import router, { resetRouter } from '@/router' -const user = { - state: { - user: '', - status: '', - code: '', - token: getToken(), - name: '', - avatar: '', - introduction: '', - roles: [], - setting: { - articlePlatform: [] - } +const state = { + token: getToken(), + name: '', + avatar: '', + introduction: '', + roles: [] +} + +const mutations = { + SET_TOKEN: (state, token) => { + state.token = token }, - - mutations: { - SET_CODE: (state, code) => { - state.code = code - }, - SET_TOKEN: (state, token) => { - state.token = token - }, - SET_INTRODUCTION: (state, introduction) => { - state.introduction = introduction - }, - SET_SETTING: (state, setting) => { - state.setting = setting - }, - SET_STATUS: (state, status) => { - state.status = status - }, - SET_NAME: (state, name) => { - state.name = name - }, - SET_AVATAR: (state, avatar) => { - state.avatar = avatar - }, - SET_ROLES: (state, roles) => { - state.roles = roles - } + SET_INTRODUCTION: (state, introduction) => { + state.introduction = introduction }, - - actions: { - // 用户名登录 - LoginByUsername({ commit }, userInfo) { - const username = userInfo.username.trim() - return new Promise((resolve, reject) => { - loginByUsername(username, userInfo.password).then(response => { - const data = response.data - commit('SET_TOKEN', data.token) - setToken(response.data.token) - resolve() - }).catch(error => { - reject(error) - }) - }) - }, - - // 获取用户信息 - GetUserInfo({ commit, state }) { - return new Promise((resolve, reject) => { - getUserInfo(state.token).then(response => { - // 由于mockjs 不支持自定义状态码只能这样hack - if (!response.data) { - reject('Verification failed, please login again.') - } - 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) - commit('SET_INTRODUCTION', data.introduction) - resolve(response) - }).catch(error => { - reject(error) - }) - }) - }, - - // 第三方验证登录 - // LoginByThirdparty({ commit, state }, code) { - // return new Promise((resolve, reject) => { - // commit('SET_CODE', code) - // loginByThirdparty(state.status, state.email, state.code).then(response => { - // commit('SET_TOKEN', response.data.token) - // setToken(response.data.token) - // resolve() - // }).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() - }) - }, - - // 动态修改权限 - ChangeRoles({ commit, dispatch }, role) { - return new Promise(resolve => { - commit('SET_TOKEN', role) - setToken(role) - getUserInfo(role).then(response => { - const data = response.data - commit('SET_ROLES', data.roles) - commit('SET_NAME', data.name) - commit('SET_AVATAR', data.avatar) - commit('SET_INTRODUCTION', data.introduction) - dispatch('GenerateRoutes', data) // 动态修改权限后 重绘侧边菜单 - resolve() - }) - }) - } + SET_NAME: (state, name) => { + state.name = name + }, + 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, introduction } = 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) + commit('SET_INTRODUCTION', introduction) + 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() + }) + }, + + // Dynamically modify permissions + changeRoles({ commit, dispatch }, role) { + return new Promise(async resolve => { + const token = role + '-token' + + commit('SET_TOKEN', token) + setToken(token) + + const { roles } = await dispatch('getInfo') + + resetRouter() + + // generate accessible routes map based on roles + const accessRoutes = await dispatch('permission/generateRoutes', roles, { root: true }) + + // dynamically add accessible routes + router.addRoutes(accessRoutes) + + resolve() + }) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss index 494dcfa4..9422dd18 100644 --- a/src/styles/element-ui.scss +++ b/src/styles/element-ui.scss @@ -1,4 +1,4 @@ -//覆盖一些element-ui样式 +// cover some element-ui styles .el-breadcrumb__inner, .el-breadcrumb__inner a { diff --git a/src/styles/element-variables.scss b/src/styles/element-variables.scss new file mode 100644 index 00000000..30a0e6bc --- /dev/null +++ b/src/styles/element-variables.scss @@ -0,0 +1,31 @@ +/** +* I think element-ui's default theme color is too light for long-term use. +* So I modified the default color and you can modify it to your liking. +**/ + +/* theme color */ +$--color-primary: #1890ff; +$--color-success: #13ce66; +$--color-warning: #FFBA00; +$--color-danger: #ff4949; +// $--color-info: #1E1E1E; + +$--button-font-weight: 400; + +// $--color-text-regular: #1f2d3d; + +$--border-color-light: #dfe4ed; +$--border-color-lighter: #e6ebf5; + +$--table-border:1px solid#dfe6ec; + +/* icon font path, required */ +$--font-path: '~element-ui/lib/theme-chalk/fonts'; + +@import "~element-ui/packages/theme-chalk/src/index"; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + theme: $--color-primary; +} diff --git a/src/styles/index.scss b/src/styles/index.scss index 93f2157e..96095ef6 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -96,14 +96,18 @@ div:focus { } } -code { +aside { background: #eef1f6; - padding: 15px 16px; + padding: 8px 24px; margin-bottom: 20px; + border-radius: 2px; display: block; - line-height: 36px; - font-size: 15px; - font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; + line-height: 32px; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: #2c3e50; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; a { color: #337ab7; @@ -115,20 +119,6 @@ code { } } -.warn-content { - background: rgba(66, 185, 131, .1); - border-radius: 2px; - padding: 16px; - padding: 1rem; - line-height: 1.6rem; - word-spacing: .05rem; - - a { - color: #42b983; - font-weight: 600; - } -} - //main-container全局样式 .app-container { padding: 20px; diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss index 03449706..e55f8656 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 ab68317d..4cb27cc8 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); } -/*breadcrumb transition*/ +/* breadcrumb transition */ .breadcrumb-enter-active, .breadcrumb-leave-active { transition: all .5s; diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 98d7b672..a19c27c1 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -8,10 +8,10 @@ $tiffany: #4AB7BD; $yellow:#FEC171; $panGreen: #30B08F; -//sidebar +// sidebar $menuText:#bfcbd9; $menuActiveText:#409EFF; -$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 +$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 $menuBg:#304156; $menuHover:#263445; diff --git a/src/utils/error-log.js b/src/utils/error-log.js new file mode 100644 index 00000000..a7f5b55f --- /dev/null +++ b/src/utils/error-log.js @@ -0,0 +1,35 @@ +import Vue from 'vue' +import store from '@/store' +import { isString, isArray } from '@/utils/validate' +import settings from '@/settings' + +// you can set in settings.js +// errorLog:'production' | ['production', 'development'] +const { errorLog: needErrorLog } = settings + +function checkNeed() { + const env = process.env.NODE_ENV + if (isString(needErrorLog)) { + return env === needErrorLog + } + if (isArray(needErrorLog)) { + return needErrorLog.includes(env) + } + return false +} + +if (checkNeed()) { + Vue.config.errorHandler = function(err, vm, info, a) { + // Don't ask me why I use Vue.nextTick, it just a hack. + // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500 + Vue.nextTick(() => { + store.dispatch('errorLog/addErrorLog', { + err, + vm, + info, + url: window.location.href + }) + console.error(err, info) + }) + } +} diff --git a/src/utils/get-page-title.js b/src/utils/get-page-title.js new file mode 100644 index 00000000..4b6604cd --- /dev/null +++ b/src/utils/get-page-title.js @@ -0,0 +1,13 @@ +import defaultSettings from '@/settings' +import i18n from '@/lang' + +const title = defaultSettings.title || 'Vue Element Admin' + +export default function getPageTitle(key) { + const hasKey = i18n.te(`route.${key}`) + if (hasKey) { + const pageName = i18n.t(`route.${key}`) + return `${pageName} - ${title}` + } + return `${title}` +} diff --git a/src/utils/index.js b/src/utils/index.js index fbcb4602..c0ee5026 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -2,6 +2,12 @@ * Created by jiachenpan 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 @@ -40,8 +46,17 @@ export function parseTime(time, cFormat) { return time_str } +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ export function formatTime(time, option) { - time = +time * 1000 + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } const d = new Date(time) const now = Date.now() @@ -74,7 +89,10 @@ export function formatTime(time, option) { } } -// 格式化时间 +/** + * @param {string} url + * @returns {Object} + */ export function getQueryObject(url) { url = url == null ? window.location.href : url const search = url.substring(url.lastIndexOf('?') + 1) @@ -91,7 +109,7 @@ export function getQueryObject(url) { } /** - * @param {Sting} input value + * @param {string} input value * @returns {number} output value */ export function byteLength(str) { @@ -106,6 +124,10 @@ export function byteLength(str) { return s } +/** + * @param {Array} actual + * @returns {Array} + */ export function cleanArray(actual) { const newArray = [] for (let i = 0; i < actual.length; i++) { @@ -116,6 +138,10 @@ export function cleanArray(actual) { return newArray } +/** + * @param {Object} json + * @returns {Array} + */ export function param(json) { if (!json) return '' return cleanArray( @@ -126,6 +152,10 @@ export function param(json) { ).join('&') } +/** + * @param {string} url + * @returns {Object} + */ export function param2Obj(url) { const search = url.split('?')[1] if (!search) { @@ -136,21 +166,29 @@ export function param2Obj(url) { decodeURIComponent(search) .replace(/"/g, '\\"') .replace(/&/g, '","') - .replace(/=/g, '":"') + + .replace(/=/g, '":"') + .replace(/\+/g, ' ') + '"}' ) } +/** + * @param {string} val + * @returns {string} + */ export function html2Text(val) { const div = document.createElement('div') div.innerHTML = val return div.textContent || div.innerText } +/** + * Merges two objects, giving the last one precedence + * @param {Object} target + * @param {(Object|Array)} source + * @returns {Object} + */ export function objectMerge(target, source) { - /* Merges two objects, - giving the last one precedence */ - if (typeof target !== 'object') { target = {} } @@ -168,6 +206,10 @@ export function objectMerge(target, source) { return target } +/** + * @param {HTMLElement} element + * @param {string} className + */ export function toggleClass(element, className) { if (!element || !className) { return @@ -184,45 +226,10 @@ export function toggleClass(element, className) { element.className = classString } -export const pickerOptions = [ - { - text: '今天', - onClick(picker) { - const end = new Date() - const start = new Date(new Date().toDateString()) - end.setTime(start.getTime()) - picker.$emit('pick', [start, end]) - } - }, - { - text: '最近一周', - onClick(picker) { - const end = new Date(new Date().toDateString()) - const start = new Date() - start.setTime(end.getTime() - 3600 * 1000 * 24 * 7) - picker.$emit('pick', [start, end]) - } - }, - { - text: '最近一个月', - onClick(picker) { - const end = new Date(new Date().toDateString()) - const start = new Date() - start.setTime(start.getTime() - 3600 * 1000 * 24 * 30) - picker.$emit('pick', [start, end]) - } - }, - { - text: '最近三个月', - onClick(picker) { - const end = new Date(new Date().toDateString()) - const start = new Date() - start.setTime(start.getTime() - 3600 * 1000 * 24 * 90) - picker.$emit('pick', [start, end]) - } - } -] - +/** + * @param {string} type + * @returns {Date} + */ export function getTime(type) { if (type === 'start') { return new Date().getTime() - 3600 * 1000 * 24 * 90 @@ -231,6 +238,12 @@ export function getTime(type) { } } +/** + * @param {Function} func + * @param {number} wait + * @param {boolean} immediate + * @return {*} + */ export function debounce(func, wait, immediate) { let timeout, args, context, timestamp, result @@ -270,10 +283,12 @@ export function debounce(func, wait, immediate) { * This is just a simple version of deep copy * Has a lot of edge cases bug * If you want to use a perfect deep copy, use lodash's _.cloneDeep + * @param {Object} source + * @returns {Object} */ export function deepClone(source) { if (!source && typeof source !== 'object') { - throw new Error('error arguments', 'shallowClone') + throw new Error('error arguments', 'deepClone') } const targetObj = source.constructor === Array ? [] : {} Object.keys(source).forEach(keys => { @@ -286,12 +301,50 @@ export function deepClone(source) { return targetObj } +/** + * @param {Array} arr + * @returns {Array} + */ export function uniqueArr(arr) { return Array.from(new Set(arr)) } +/** + * @returns {string} + */ export function createUniqueString() { const timestamp = +new Date() + '' const randomNum = parseInt((1 + Math.random()) * 65536) + '' return (+(randomNum + timestamp)).toString(32) } + +/** + * Check if an element has a class + * @param {HTMLElement} elm + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele, cls) { + return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) +} + +/** + * Add class to element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function addClass(ele, cls) { + if (!hasClass(ele, cls)) ele.className += ' ' + cls +} + +/** + * Remove class from element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function removeClass(ele, cls) { + if (hasClass(ele, cls)) { + const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)') + ele.className = ele.className.replace(reg, ' ') + } +} diff --git a/src/utils/openWindow.js b/src/utils/open-window.js similarity index 99% rename from src/utils/openWindow.js rename to src/utils/open-window.js index b63dfbb4..657bd190 100644 --- a/src/utils/openWindow.js +++ b/src/utils/open-window.js @@ -5,7 +5,6 @@ * @param {Number} w * @param {Number} h */ - export default function openWindow(url, title, w, h) { // Fixes dual-screen position Most browsers Firefox const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left diff --git a/src/utils/request.js b/src/utils/request.js index 50f9ecec..d3c25c00 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -1,67 +1,75 @@ import axios from 'axios' -import { Message } from 'element-ui' +import { MessageBox, Message } from 'element-ui' import store from '@/store' import { getToken } from '@/utils/auth' // create an axios instance const service = axios.create({ - baseURL: process.env.BASE_API, // api 的 base_url + 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 interceptor service.interceptors.request.use( config => { - // Do something before request is sent + // do something before request is sent + if (store.getters.token) { - // 让每个请求携带token-- ['X-Token']为自定义key 请根据实际情况自行修改 + // let each request carry token --['X-Token'] as a custom 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 interceptor service.interceptors.response.use( - response => response, /** - * 下面的注释为通过在response里,自定义code来标示请求状态 - * 当code返回如下情况则说明权限有问题,登出并返回到登录页 - * 如想通过 xmlhttprequest 来状态码标识 逻辑可写在下面error中 - * 以下代码均为样例,请结合自生需求加以修改,若不需要,则可删除 + * If you want to get information such as headers or status + * Please return response => response + */ + + /** + * Determine the request status by custom code + * Here is just an example + * You can also judge the status by HTTP Status Code. */ - // response => { - // const res = response.data - // if (res.code !== 20000) { - // Message({ - // message: res.message, - // type: 'error', - // duration: 5 * 1000 - // }) - // // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; - // if (res.code === 50008 || res.code === 50012 || res.code === 50014) { - // // 请自行在引入 MessageBox - // // import { Message, MessageBox } from 'element-ui' - // MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', { - // confirmButtonText: '重新登录', - // cancelButtonText: '取消', - // type: 'warning' - // }).then(() => { - // store.dispatch('FedLogOut').then(() => { - // location.reload() // 为了重新实例化vue-router对象 避免bug - // }) - // }) - // } - // return Promise.reject('error') - // } else { - // return response.data - // } - // }, + response => { + 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 || 'error', + type: 'error', + duration: 5 * 1000 + }) + + // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; + if (res.code === 50008 || res.code === 50012 || res.code === 50014) { + // 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(res.message || 'error') + } else { + return res + } + }, error => { console.log('err' + error) // for debug Message({ diff --git a/src/utils/scrollTo.js b/src/utils/scroll-to.js similarity index 89% rename from src/utils/scrollTo.js rename to src/utils/scroll-to.js index 8affede6..c5d8e04e 100644 --- a/src/utils/scrollTo.js +++ b/src/utils/scroll-to.js @@ -12,7 +12,10 @@ var requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } })() -// because it's so fucking difficult to detect the scrolling element, just move them all +/** + * Because it's so fucking difficult to detect the scrolling element, just move them all + * @param {number} amount + */ function move(amount) { document.documentElement.scrollTop = amount document.body.parentNode.scrollTop = amount @@ -23,6 +26,11 @@ function position() { return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop } +/** + * @param {number} to + * @param {number} duration + * @param {Function} callback + */ export function scrollTo(to, duration, callback) { const start = position() const change = to - start diff --git a/src/utils/validate.js b/src/utils/validate.js index 5e4056f5..e04f292e 100644 --- a/src/utils/validate.js +++ b/src/utils/validate.js @@ -1,46 +1,86 @@ /** * Created by jiachenpan on 16/11/18. */ - +/** + * @param {string} path + * @returns {Boolean} + */ export function isExternal(path) { return /^(https?:|mailto:|tel:)/.test(path) } +/** + * @param {string} str + * @returns {Boolean} + */ export function validUsername(str) { const valid_map = ['admin', 'editor'] return valid_map.indexOf(str.trim()) >= 0 } -/* 合法uri*/ +/** + * @param {string} url + * @returns {Boolean} + */ export function validURL(url) { const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ return reg.test(url) } -/* 小写字母*/ +/** + * @param {string} str + * @returns {Boolean} + */ export function validLowerCase(str) { const reg = /^[a-z]+$/ return reg.test(str) } -/* 大写字母*/ +/** + * @param {string} str + * @returns {Boolean} + */ export function validUpperCase(str) { const reg = /^[A-Z]+$/ return reg.test(str) } -/* 大小写字母*/ +/** + * @param {string} str + * @returns {Boolean} + */ export function validAlphabets(str) { const reg = /^[A-Za-z]+$/ return reg.test(str) } /** - * validate email - * @param email - * @returns {boolean} + * @param {string} email + * @returns {Boolean} */ export function validEmail(email) { - const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - return re.test(email) + const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return reg.test(email) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function isString(str) { + if (typeof str === 'string' || str instanceof String) { + return true + } + return false +} + +/** + * @param {Array} arg + * @returns {Boolean} + */ +export function isArray(arg) { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) } diff --git a/src/vendor/Export2Excel.js b/src/vendor/Export2Excel.js index ba956dc1..20784f3a 100644 --- a/src/vendor/Export2Excel.js +++ b/src/vendor/Export2Excel.js @@ -145,9 +145,11 @@ export function export_table_to_excel(id) { } export function export_json_to_excel({ + multiHeader = [], header, data, filename, + merges = [], autoWidth = true, bookType= 'xlsx' } = {}) { @@ -155,10 +157,22 @@ export function export_json_to_excel({ filename = filename || 'excel-list' data = [...data] data.unshift(header); + + for (let i = multiHeader.length-1; i > -1; i--) { + data.unshift(multiHeader[i]) + } + var ws_name = "SheetJS"; var wb = new Workbook(), ws = sheet_from_array_of_arrays(data); + if (merges.length > 0) { + if (!ws['!merges']) ws['!merges'] = []; + merges.forEach(item => { + ws['!merges'].push(XLSX.utils.decode_range(item)) + }) + } + if (autoWidth) { /*设置worksheet每列的最大宽度*/ const colWidth = data.map(row => row.map(val => { diff --git a/src/views/charts/keyboard.vue b/src/views/charts/keyboard.vue index 3ea21397..917f8eeb 100644 --- a/src/views/charts/keyboard.vue +++ b/src/views/charts/keyboard.vue @@ -1,11 +1,11 @@ - - - diff --git a/src/views/excel/merge-header.vue b/src/views/excel/merge-header.vue new file mode 100644 index 00000000..c145932c --- /dev/null +++ b/src/views/excel/merge-header.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/views/excel/selectExcel.vue b/src/views/excel/select-excel.vue similarity index 88% rename from src/views/excel/selectExcel.vue rename to src/views/excel/select-excel.vue index 2695bfb4..09866e13 100644 --- a/src/views/excel/selectExcel.vue +++ b/src/views/excel/select-excel.vue @@ -1,21 +1,24 @@