Compare commits

...

73 Commits

Author SHA1 Message Date
花裤衩
378ca2c217 update to webpack4 (#889) 2018-08-15 15:33:34 +08:00
Andrey Dos
1df59cc4b6 fix typo(#965) 2018-08-15 14:51:08 +08:00
shufangyi
312a2ca8ed fixBug[waves.js]: wave's position (#949)
* fixBug[waves.js]-wave's position

* fix[waves.js]-wave's position
2018-08-13 13:45:05 +08:00
Pan
f3733c0b37 fix: demo links 2018-08-13 13:13:41 +08:00
Pan
0ef14ff5c6 tweak 2018-08-09 11:03:13 +08:00
Pan
c57c6045c9 docs: tweak 2018-08-08 13:42:12 +08:00
_xiaotian
fe190b6188 perf[login.vue]: Improve input background and cursor color (#927)
* 完善input背景和光标色;

1.完善在Chrome浏览器时登陆界面的input标签 使用记住密码之后颜色和背景不一致;
2.目前的rgb值是 #2d3a4b  修改的rgb值是 #283443;
3.修复光标使用Chrome记录的账号之后变黑色;
4.移除 .title-container .title 重复的font-weight: 400;
5.不用IE我们大家都是好朋友!~

* Update index.vue
2018-08-01 11:03:29 +08:00
Pan
48a966fe1c docs: add gitee 2018-07-31 11:26:45 +08:00
Pan
63d39727ac [release] 3.7.3 2018-07-31 11:14:18 +08:00
Pan
1e0b9c0055 fix[Tinymce]: fixed tinymce upload dialog bug #654 2018-07-31 11:12:38 +08:00
mimimi
5f20bfc780 fixed[tagsView]: DEL_OTHERS_VIEWS cachedViews bug (#913)
* mutations DEL_OTHERS_VIEWS state.cachedViews -> i type is string slice(begin: number, end: number)
2018-07-25 17:20:32 +08:00
mimimi
8851a68066 tweak setLocalStorgae -> setLocalStorage (#894) 2018-07-20 16:02:38 +08:00
Pan
878628b0ed tweak code comments 2018-07-20 10:25:14 +08:00
Pan
e254fc6c1a fix[Sidebar]: fixed sidebar bug when set hidden:true #880 2018-07-17 18:13:29 +08:00
花裤衩
513eb66d97 fix[UploadExcel]: add file type check (#878) 2018-07-17 13:30:52 +08:00
Pan
62e1c851c8 tweak 2018-07-16 11:18:13 +08:00
Pan
f0a01f0fd1 [release] 3.7.2 2018-07-13 13:38:43 +08:00
博文
aa7eab58f9 refactor(SidebarItem): optimizate SidebarItem (#845)
* refactor(sidebar-item): optimizate SiderbarItem.

* chore: update nested examples.

* fix: fix a wrong path.

* fix: fix a transition bug.

* fix: fix a wrong path.

* perf: using "router" mode of el-menu.

* Revert "perf: using "router" mode of el-menu."

This reverts commit cef02a30b0.

* fix: complement i18n text.
2018-07-13 11:23:06 +08:00
花裤衩
77cb6b1f43 fix[excel]: default filename bug && format code (#857)
* fix filename bug

* format

* format
2018-07-11 14:16:28 +08:00
iGoo丶
5fbf1cf5da 修正README.zh-CN.md (#856) 2018-07-11 00:18:34 +08:00
Pan
6a5197ad51 perf[css]: refine style 2018-07-10 13:22:16 +08:00
Pan
9b7a9a64e5 fix[css]: css bug in mobile #852 2018-07-10 13:02:19 +08:00
Pan
89ce53e185 fix[TagsView]: fix contextmenu position bug #850 2018-07-09 17:25:45 +08:00
Pan
9e04f58163 perf[login]: i18n of input placeholder #844 2018-07-07 01:44:29 +08:00
花裤衩
d98c5032f8 refine style (#843)
* refine style

* refine 404 style

* refine
2018-07-06 11:19:53 +08:00
ZYSzys
a575670cef perf[ArticleDetail]: refine el-col :span (#841) 2018-07-06 10:07:43 +08:00
花裤衩
44fa96f142 chore: add lint-staged (#818) 2018-07-03 16:31:29 +08:00
Pan
e4481a9d34 fix[build.js]: fixed build bug in preview mode #819 2018-07-02 14:02:42 +08:00
Pan
572a2d9c34 perf[Tinymce]: set nonbreaking_force_tab to true 2018-06-30 17:26:31 +08:00
Pan
5070e20dea update docs 2018-06-28 13:20:44 +08:00
Pan
59789d92cf perf[uploadExcel]: fix typo 2018-06-28 09:53:55 +08:00
Pan
775f6f5f3a [release] 3.7.1 2018-06-27 10:37:49 +08:00
Pan
2687b2eb3c perf[v-loading]: remove .body modifier #779 2018-06-26 17:10:21 +08:00
Pan
76327a8f26 update doc 2018-06-25 15:04:40 +08:00
Pan
03b708870b update doc 2018-06-25 14:54:02 +08:00
花裤衩
bdc31cea1a chore: use Runtime-only (#799)
Detail see [Runtime-Compiler-vs-Runtime-only](https://vuejs.org/v2/guide/installation.html#Runtime-Compiler-vs-Runtime-only)
2018-06-25 11:17:47 +08:00
Liu Xinyu
ae2ca072f5 fix: typo in readme (#798) 2018-06-25 10:09:19 +08:00
花裤衩
d995cdb332 add[example]: add nested routes example (#789) 2018-06-21 14:26:25 +08:00
Pan
cbc3ddd827 perf[uploadExcel]: add beforeUpload hock 2018-06-20 13:29:57 +08:00
HiiTea
9cf00fd63a fix typo (#784) 2018-06-19 17:53:30 +08:00
Pan
03691739e1 add:[permission]: add checkPermission function 2018-06-19 17:52:05 +08:00
Pan
3f479664b6 perf[utils.js]: Add code comments to deepClone and remove redundant code 2018-06-19 11:26:46 +08:00
临书
cbee7b6f20 fix[TagView-component]: loss route querystring when operating tags (#768) 2018-06-13 16:57:07 +08:00
临书
2a590a2087 Support route query in TagView (#765) 2018-06-12 13:53:56 +08:00
Pan
c93fcefe54 perf[UploadExcel-component]: tweaks 2018-06-12 10:53:08 +08:00
Pan
9f8ac37497 perf[UploadExcel-component]: set readerData to promise 2018-06-12 09:56:53 +08:00
Pan
8c685cc4c6 doc: add code comments to request.js 2018-06-11 13:04:11 +08:00
kuviki
e40fd27775 Fix typo (#747) 2018-06-07 13:26:10 +08:00
Pan
f3ccd9f04e perf[Tinymce]: remove legacyoutput #745 2018-06-06 19:46:31 +08:00
Pan
914a4ec62c chore: assetsPublicPath 2018-06-04 13:07:53 +08:00
Pan
739aef4387 Remove redundant code 2018-06-01 17:04:33 +08:00
Insua
a7942636c6 fix(Tinymce): When content is null,set content to '' to avoid error (#732)
When get rich editor value(null) from backend,it will occur error
```
Error in nextTick: "TypeError: Cannot read property 'replace' of null"
```
So I set val to '' to avoid it
2018-06-01 15:38:08 +08:00
Pan
31d9da8b9f [release] 3.7.0 2018-06-01 14:11:02 +08:00
Pan
600e75d0a2 bump jsonlint #728 2018-06-01 13:34:45 +08:00
Pan
9ba1ea6933 perf[el-dragDialog]: add dragDialog callback function 2018-06-01 10:38:27 +08:00
Pan
320e941d9a fix[Sticky]: fixed bug in resize #725 2018-05-31 17:16:24 +08:00
花裤衩
d0f6d3f1f6 chore: use babel-plugin-dynamic-import-node to lazy-loading (#727)
detail see https://panjiachen.github.io/vue-element-admin-site/#/zh-cn/lazy-loading?id=%E8%B7%AF%E7%94%B1%E6%87%92%E5%8A%A0%E8%BD%BD
2018-05-31 16:53:08 +08:00
Pan
0375542009 fix[Sticky]: fixed bug in resize #724 2018-05-31 13:22:12 +08:00
Pan
03e5f762b3 fix[Sticky]: fixed bug in resize #721 2018-05-31 10:50:38 +08:00
Heedong Im
bd0227feed Upgrade vue-loader (#723)
The vue-loader build error caused by Prettier have been fixed
2018-05-31 10:08:00 +08:00
Pan
20aad46416 fix[permission]: directive demo typo 2018-05-30 15:27:44 +08:00
花裤衩
4fc25241fe refactor example demo (#713)
* refactor: Adjust the example directory structure

* perf form demo

* refine editor-slide-upload css

* refine demo
2018-05-30 15:25:08 +08:00
Pan
6327869106 perf[transition]: refine transition animation 2018-05-29 14:31:55 +08:00
Pan
c861dd10cf fix: [404.vue]: return-home button href bug 2018-05-28 15:33:47 +08:00
花裤衩
0a196f79ba Add guide page #534 (#707)
detail see #534
2018-05-28 14:36:06 +08:00
Pan
d2d323bb02 fix: [sidebar.css] : style bug in windows #702 2018-05-28 11:01:07 +08:00
Pan
66613f0373 perf[filter]: remove duplicate code #661 2018-05-12 23:17:14 +08:00
Pan
6795c26d02 fix[Tinymce]: custom-btn bug in fullscreen 2018-05-09 14:36:17 +08:00
花裤衩
597df4844a feature[permission]: add v-permission (#653) 2018-05-08 22:15:34 +08:00
Pan
1e103cf151 perf[dashboard]: add resonsive dashboard 2018-05-05 15:42:46 +08:00
花裤衩
99d53ee0ca refactor[ScrollBar]: use el-scrollbar (#646) 2018-05-05 15:26:08 +08:00
Pan
f9d510ea78 perf[el-dragDialog]: use curring 2018-05-04 10:26:29 +08:00
Pan
9fbb028124 perf[permission]: add the verification of roles 2018-05-02 17:37:47 +08:00
114 changed files with 2071 additions and 1257 deletions

View File

@@ -8,5 +8,10 @@
}], }],
"stage-2" "stage-2"
], ],
"plugins": ["transform-vue-jsx", "transform-runtime"] "plugins": ["transform-vue-jsx", "transform-runtime"],
"env": {
"development":{
"plugins": ["dynamic-import-node"]
}
}
} }

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ dist/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
**/*.log
test/unit/coverage test/unit/coverage
test/e2e/reports test/e2e/reports

112
README.md
View File

@@ -7,7 +7,7 @@
<img src="https://img.shields.io/badge/vue-2.5.10-brightgreen.svg" alt="vue"> <img src="https://img.shields.io/badge/vue-2.5.10-brightgreen.svg" alt="vue">
</a> </a>
<a href="https://github.com/ElemeFE/element"> <a href="https://github.com/ElemeFE/element">
<img src="https://img.shields.io/badge/element--ui-2.3.0-brightgreen.svg" alt="element-ui"> <img src="https://img.shields.io/badge/element--ui-2.3.2-brightgreen.svg" alt="element-ui">
</a> </a>
<a href="https://travis-ci.org/PanJiaChen/vue-element-admin" rel="nofollow"> <a href="https://travis-ci.org/PanJiaChen/vue-element-admin" rel="nofollow">
<img src="https://travis-ci.org/PanJiaChen/vue-element-admin.svg?branch=master" alt="Build Status"> <img src="https://travis-ci.org/PanJiaChen/vue-element-admin.svg?branch=master" alt="Build Status">
@@ -24,33 +24,36 @@ English | [简体中文](./README.zh-CN.md)
## Introduction ## Introduction
`vue-element-admin` is a production-ready solution for admin interfaces. Based on [Vue.js](https://github.com/vuejs/vue) and use the UI Toolkit -- [element](https://github.com/ElemeFE/element). `vue-element-admin` is a magical vue admin, it 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. [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).
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](http://panjiachen.github.io/vue-element-admin)
- [Documentation](https://panjiachen.github.io/vue-element-admin-site/#/) - [Documentation](https://panjiachen.github.io/vue-element-admin-site/)
- [Gitter](https://gitter.im/vue-element-admin/discuss) - [Gitter](https://gitter.im/vue-element-admin/discuss)
- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki) - [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki)
- [Donate](https://panjiachen.github.io/vue-element-admin-site/#/donate) - [Donate](https://panjiachen.github.io/vue-element-admin-site/donate/)
**vue-element-admin is a admin interfaces integration solution, which is not suitable for secondary development as a base template.** - [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 国内用户可访问该地址在线预览
**This project is positioned as a background integration solution and is not suitable for secondary development as a basic template.**
- Base template recommends using: [vueAdmin-template](https://github.com/PanJiaChen/vueAdmin-template)   - Base template recommends using: [vueAdmin-template](https://github.com/PanJiaChen/vueAdmin-template)  
- Desktop: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) - Desktop: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
**Note: This project uses element-ui@2.3.0+ version, so the minimum compatible vue@2.5.0+**
## Preparation ## 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/) [element-ui](https://github.com/ElemeFE/element). All data requests for this project are simulated using [Mock.js](https://github.com/nuysoft/Mock). It would be helpful if you have pre-existing knowledge on those. 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).
Understanding and learning this knowledge in advance will greatly help the use of this project.
**This project is not a scaffolding and is more of an integrated solution.**
**This project does not support low version browsers (e.g. IE). Please add polyfill yourself if you need them.** **This project does not support low version browsers (e.g. IE). Please add polyfill yourself if you need them.**
**Note: This project uses element-ui@2.3.0+ version, so the minimum compatible vue@2.5.0+**
<p align="center"> <p align="center">
<img width="900" src="https://wpimg.wallstcn.com/a5894c1b-f6af-456e-82df-1151da0839bf.png"> <img width="900" src="https://wpimg.wallstcn.com/a5894c1b-f6af-456e-82df-1151da0839bf.png">
</p> </p>
@@ -58,40 +61,64 @@ You need to install [node](http://nodejs.org/) and [git](https://git-scm.com/) l
## Features ## Features
``` ```
- Login / Logout - Login / Logout
- Permission authentication
- Permission Authentication
- Page permission
- Directive permission
- Two-step login
- Multi-environment build - Multi-environment build
- Dynamic sidebar (supports multi-level routing) - dev sit stage prod
- Dynamic breadcrumb
- I18n - Global Features
- Customizable theme - I18n
- Tags-view(Tab page Support right-click operation) - Multiple dynamic themes
- Rich text editor - Dynamic sidebar (supports multi-level routing)
- Markdown editor - Dynamic breadcrumb
- JSON editor - Tags-view(Tab page Support right-click operation)
- Screenfull - Svg Sprite
- Drag and drop list - Mock data
- Svg Sprite - Screenfull
- Responsive Sidebar
- Editor
- Rich Text Editor
- Markdown Editor
- JSON Editor
- Excel
- Export Excel
- Export zip
- Upload Excel
- Visualization Excel
- Table
- Dynamic Table
- Drag And Drop Table
- Tree Table
- Inline Edit Table
- Error Page
- 401
- 404
- Components
- Avatar Upload
- Back To Top
- Drag Dialog
- Drag Kanban
- Drag List
- SplitPane
- Dropzone
- Sticky
- CountTo
- Advanced Example
- Error Log
- Dashboard - Dashboard
- Mock data - Guide Page
- Echarts - Echarts
- Clipboard - Clipboard
- 401/404 error page
- Error log
- Export excel
- Export zip
- Front-end visualization excel
- Tree Table
- Table example
- Dynamictable example
- Drag and drop table example
- Inline edit table example
- Form example
- Two-step login
- SplitPane
- Drag Dialog
- Dropzone
- Sticky
- CountTo
- Markdown to html - Markdown to html
``` ```
@@ -124,6 +151,9 @@ npm run build:prod
# --report to build with bundle size analytics # --report to build with bundle size analytics
npm run build:prod --report npm run build:prod --report
# --generate a bundle size analytics. default: bundle-report.html
npm run build:prod --generate_report
# --preview to start a server in local to preview # --preview to start a server in local to preview
npm run build:prod --preview npm run build:prod --preview
@@ -134,7 +164,7 @@ npm run lint
npm run lint -- --fix npm run lint -- --fix
``` ```
Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/#/deploy) for more information Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
## Changelog ## Changelog
Detailed changes for each release are documented in the [release notes](https://github.com/PanJiaChen/vue-element-admin/releases). Detailed changes for each release are documented in the [release notes](https://github.com/PanJiaChen/vue-element-admin/releases).

View File

@@ -7,7 +7,7 @@
<img src="https://img.shields.io/badge/vue-2.5.10-brightgreen.svg" alt="vue"> <img src="https://img.shields.io/badge/vue-2.5.10-brightgreen.svg" alt="vue">
</a> </a>
<a href="https://github.com/ElemeFE/element"> <a href="https://github.com/ElemeFE/element">
<img src="https://img.shields.io/badge/element--ui-2.3.0-brightgreen.svg" alt="element-ui"> <img src="https://img.shields.io/badge/element--ui-2.3.2-brightgreen.svg" alt="element-ui">
</a> </a>
<a href="https://travis-ci.org/PanJiaChen/vue-element-admin" rel="nofollow"> <a href="https://travis-ci.org/PanJiaChen/vue-element-admin" rel="nofollow">
<img src="https://travis-ci.org/PanJiaChen/vue-element-admin.svg?branch=master" alt="Build Status"> <img src="https://travis-ci.org/PanJiaChen/vue-element-admin.svg?branch=master" alt="Build Status">
@@ -24,27 +24,27 @@
## 简介 ## 简介
`vue-element-admin` 是一个后台集成解决方案,它基于 [Vue.js](https://github.com/vuejs/vue) 和 [element](https://github.com/ElemeFE/element)。它使用了最新的前端技术栈内置了i18国际化解决方案动态路由权限验证等很多功能特性,相信不管你的需求是什么,本项目都能帮助到你。 [vue-element-admin](http://panjiachen.github.io/vue-element-admin) 是一个后台集成解决方案,它基于 [vue](https://github.com/vuejs/vue) 和 [element](https://github.com/ElemeFE/element)。它使用了最新的前端技术栈内置了i18国际化解决方案动态路由权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。相信不管你的需求是什么,本项目都能帮助到你。
- [在线访问](http://panjiachen.github.io/vue-element-admin) - [在线访问](http://panjiachen.github.io/vue-element-admin)
- [使用文档](https://panjiachen.github.io/vue-element-admin-site/#/zh-cn/) - [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
- [Gitter讨论组](https://gitter.im/vue-element-admin/discuss) - [Gitter讨论组](https://gitter.im/vue-element-admin/discuss)
- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki) - [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki)
- [Donate](https://panjiachen.github.io/vue-element-admin-site/#/zh-cn/donate) - [Donate](https://panjiachen.github.io/vue-element-admin-site/zh/donate/)
- [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 国内用户可访问该地址在线预览
**本项目的定位是后台集成方案,不适合当基础模板来开发。** **本项目的定位是后台集成方案,不适合当基础模板来开发。**
- 模板建议使用: [vueAdmin-template](https://github.com/PanJiaChen/vueAdmin-template)   - 模板建议使用: [vueAdmin-template](https://github.com/PanJiaChen/vueAdmin-template)  
- 桌面端: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) - 桌面端: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
**注意:该项目使用 element-ui@2.3.0+ 版本,所以最低兼容 vue@2.5.0+**
## 前序准备 ## 前序准备
的本地环境需要安装 [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/) and [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/) 、[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/59097cd7a22b9d0065fb61d2)
@@ -53,7 +53,8 @@
- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
- [手摸手,带你优雅的使用 icon](https://juejin.im/post/59bb864b5188257e7a427c09) - [手摸手,带你优雅的使用 icon](https://juejin.im/post/59bb864b5188257e7a427c09)
- [手摸手带你用合理的姿势使用webpack4](https://juejin.im/post/5b56909a518825195f499806)
- [手摸手带你用合理的姿势使用webpack4](https://juejin.im/post/5b5d6d6f6fb9a04fea58aabc)
或者加入该群主 **[圈子](https://jianshiapp.com/circles/1209)** 楼主会经常分享一些技术相关的东西 或者加入该群主 **[圈子](https://jianshiapp.com/circles/1209)** 楼主会经常分享一些技术相关的东西
@@ -61,6 +62,8 @@
**本项目并不是一个脚手架,更倾向于是一个集成解决方案** **本项目并不是一个脚手架,更倾向于是一个集成解决方案**
**注意:该项目使用 element-ui@2.3.0+ 版本,所以最低兼容 vue@2.5.0+**
**该项目不支持低版本浏览器(如ie)有需求请自行添加polyfill [详情](https://github.com/PanJiaChen/vue-element-admin/wiki#babel-polyfill)** **该项目不支持低版本浏览器(如ie)有需求请自行添加polyfill [详情](https://github.com/PanJiaChen/vue-element-admin/wiki#babel-polyfill)**
<p align="center"> <p align="center">
@@ -69,41 +72,65 @@
## 功能 ## 功能
``` ```
- 登录/注销 - 登录 / 注销
- 权限验证 - 权限验证
- 页面权限
- 指令权限
- 二步登录
- 多环境发布 - 多环境发布
- 动态侧边栏(支持多级路由) - dev sit stage prod
- 动态面包屑
- 国际化多语言 - 全局功能
- 多种动态换肤 - 国际化多语言
- 快捷导航(标签页) - 多种动态换肤
- 富文本编辑器 - 动态侧边栏(支持多级路由嵌套)
- Markdown编辑器 - 动态面包屑
- JSON编辑器 - 快捷导航(标签页)
- Screenfull全屏 - Svg Sprite 图标
- 列表拖拽 - 本地mock数据
- Svg Sprite 图标 - Screenfull全屏
- 自适应收缩侧边栏
- 编辑器
- 富文本
- Markdown
- JSON 等多格式
- Excel
- 导出excel
- 导出zip
- 导入excel
- 前端可视化excel
- 表格
- 动态表格
- 拖拽表格
- 树形表格
- 内联编辑
- 错误页面
- 401
- 404
- 組件
- 头像上传
- 返回顶部
- 拖拽Dialog
- 拖拽看板
- 列表拖拽
- SplitPane
- Dropzone
- Sticky
- CountTo
- 综合实例
- 错误日志
- Dashboard - Dashboard
- 本地mock数据 - 引导页
- Echarts 图表 - Echarts 图表
- Clipboard(剪贴复制) - Clipboard(剪贴复制)
- 401/404错误页面
- 错误日志
- 导出excel
- 导出zip
- 前端可视化excel
- 树形table
- Table example
- 动态table example
- 拖拽table example
- 内联编辑table example
- Form example
- 二步登录
- SplitPane
- 拖拽 Dialog
- Dropzone
- Sticky
- CountTo
- Markdown2html - Markdown2html
``` ```
@@ -128,14 +155,17 @@ npm run dev
# 构建测试环境 # 构建测试环境
npm run build:sit npm run build:sit
# 构建生环境 # 构建生环境
npm run build:prod npm run build:prod
``` ```
## 其它 ## 其它
```bash ```bash
# --report to build with bundle size analytics # --report to build with bundle size analytics
npm run build:prod --report npm run build:prod
# --generate a bundle size analytics. default: bundle-report.html
npm run build:prod --generate_report
# --preview to start a server in local to preview # --preview to start a server in local to preview
npm run build:prod --preview npm run build:prod --preview
@@ -147,7 +177,7 @@ npm run lint
npm run lint -- --fix npm run lint -- --fix
``` ```
更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/#/zh-cn/deploy) 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
## Changelog ## Changelog
Detailed changes for each release are documented in the [release notes](https://github.com/PanJiaChen/vue-element-admin/releases). Detailed changes for each release are documented in the [release notes](https://github.com/PanJiaChen/vue-element-admin/releases).

View File

@@ -8,9 +8,12 @@ const chalk = require('chalk')
const webpack = require('webpack') const webpack = require('webpack')
const config = require('../config') const config = require('../config')
const webpackConfig = require('./webpack.prod.conf') const webpackConfig = require('./webpack.prod.conf')
const server = require('pushstate-server') var connect = require('connect')
var serveStatic = require('serve-static')
var spinner = ora('building for '+ process.env.env_config+ ' environment...' ) const spinner = ora(
'building for ' + process.env.env_config + ' environment...'
)
spinner.start() spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
@@ -18,31 +21,47 @@ rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
webpack(webpackConfig, (err, stats) => { webpack(webpackConfig, (err, stats) => {
spinner.stop() spinner.stop()
if (err) throw err if (err) throw err
process.stdout.write(stats.toString({ process.stdout.write(
colors: true, stats.toString({
modules: false, colors: true,
children: false, modules: false,
chunks: false, children: false,
chunkModules: false chunks: false,
}) + '\n\n') chunkModules: false
}) + '\n\n'
)
if (stats.hasErrors()) { if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n')) console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1) process.exit(1)
} }
console.log(chalk.cyan(' Build complete.\n')) console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow( console.log(
' Tip: built files are meant to be served over an HTTP server.\n' + chalk.yellow(
' Opening index.html over file:// won\'t work.\n' ' 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){ )
server.start({ )
port: 9526,
directory: './dist', if (process.env.npm_config_preview) {
file: '/index.html' const port = 9526
}); const host = 'http://localhost:' + port
console.log('> Listening at ' + 'http://localhost:9526' + '\n') 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}`)
)
})
} }
}) })
}) })

View File

@@ -4,8 +4,11 @@ const semver = require('semver')
const packageConfig = require('../package.json') const packageConfig = require('../package.json')
const shell = require('shelljs') const shell = require('shelljs')
function exec (cmd) { function exec(cmd) {
return require('child_process').execSync(cmd).toString().trim() return require('child_process')
.execSync(cmd)
.toString()
.trim()
} }
const versionRequirements = [ const versionRequirements = [
@@ -24,23 +27,30 @@ if (shell.which('npm')) {
}) })
} }
module.exports = function () { module.exports = function() {
const warnings = [] const warnings = []
for (let i = 0; i < versionRequirements.length; i++) { for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i] const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' + warnings.push(
chalk.red(mod.currentVersion) + ' should be ' + mod.name +
chalk.green(mod.versionRequirement) ': ' +
chalk.red(mod.currentVersion) +
' should be ' +
chalk.green(mod.versionRequirement)
) )
} }
} }
if (warnings.length) { if (warnings.length) {
console.log('') console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:')) console.log(
chalk.yellow(
'To use this template, you must update following to modules:'
)
)
console.log() console.log()
for (let i = 0; i < warnings.length; i++) { for (let i = 0; i < warnings.length; i++) {

View File

@@ -1,18 +1,19 @@
'use strict' 'use strict'
const path = require('path') const path = require('path')
const config = require('../config') const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const packageConfig = require('../package.json') const packageConfig = require('../package.json')
exports.assetsPath = function (_path) { exports.assetsPath = function(_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production' const assetsSubDirectory =
? config.build.assetsSubDirectory process.env.NODE_ENV === 'production'
: config.dev.assetsSubDirectory ? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path) return path.posix.join(assetsSubDirectory, _path)
} }
exports.cssLoaders = function (options) { exports.cssLoaders = function(options) {
options = options || {} options = options || {}
const cssLoader = { const cssLoader = {
@@ -30,8 +31,22 @@ exports.cssLoaders = function (options) {
} }
// generate loader string to be used with extract text plugin // generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) { function generateLoaders(loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 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) { if (loader) {
loaders.push({ loaders.push({
@@ -42,24 +57,16 @@ exports.cssLoaders = function (options) {
}) })
} }
// Extract CSS when that option is specified return loaders
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
} }
// https://vue-loader.vuejs.org/en/configurations/extract-css.html // https://vue-loader.vuejs.org/en/configurations/extract-css.html
return { return {
css: generateLoaders(), css: generateLoaders(),
postcss: generateLoaders(), postcss: generateLoaders(),
less: generateLoaders('less'), less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }), sass: generateLoaders('sass', {
indentedSyntax: true
}),
scss: generateLoaders('sass'), scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'), stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus') styl: generateLoaders('stylus')
@@ -67,7 +74,7 @@ exports.cssLoaders = function (options) {
} }
// Generate loaders for standalone style files (outside of .vue) // Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) { exports.styleLoaders = function(options) {
const output = [] const output = []
const loaders = exports.cssLoaders(options) const loaders = exports.cssLoaders(options)

View File

@@ -1,22 +1,5 @@
'use strict' 'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = { module.exports = {
loaders: utils.cssLoaders({ //You can set the vue-loader configuration by yourself.
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
} }

View File

@@ -2,9 +2,10 @@
const path = require('path') const path = require('path')
const utils = require('./utils') const utils = require('./utils')
const config = require('../config') const config = require('../config')
const { VueLoaderPlugin } = require('vue-loader')
const vueLoaderConfig = require('./vue-loader.conf') const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) { function resolve(dir) {
return path.join(__dirname, '..', dir) return path.join(__dirname, '..', dir)
} }
@@ -27,15 +28,15 @@ module.exports = {
output: { output: {
path: config.build.assetsRoot, path: config.build.assetsRoot,
filename: '[name].js', filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production' publicPath:
? config.build.assetsPublicPath process.env.NODE_ENV === 'production'
: config.dev.assetsPublicPath ? config.build.assetsPublicPath
: config.dev.assetsPublicPath
}, },
resolve: { resolve: {
extensions: ['.js', '.vue', '.json'], extensions: ['.js', '.vue', '.json'],
alias: { alias: {
'vue$': 'vue/dist/vue.esm.js', '@': resolve('src')
'@': resolve('src'),
} }
}, },
module: { module: {
@@ -49,7 +50,11 @@ module.exports = {
{ {
test: /\.js$/, test: /\.js$/,
loader: 'babel-loader?cacheDirectory', loader: 'babel-loader?cacheDirectory',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] include: [
resolve('src'),
resolve('test'),
resolve('node_modules/webpack-dev-server/client')
]
}, },
{ {
test: /\.svg$/, test: /\.svg$/,
@@ -86,6 +91,7 @@ module.exports = {
} }
] ]
}, },
plugins: [new VueLoaderPlugin()],
node: { node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue // prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native). // source contains it (although only uses it if it's native).

View File

@@ -9,7 +9,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder') const portfinder = require('portfinder')
function resolve (dir) { function resolve(dir) {
return path.join(__dirname, '..', dir) return path.join(__dirname, '..', dir)
} }
@@ -17,8 +17,12 @@ const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT) const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, { const devWebpackConfig = merge(baseWebpackConfig, {
mode: 'development',
module: { module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) rules: utils.styleLoaders({
sourceMap: config.dev.cssSourceMap,
usePostCSS: true
})
}, },
// cheap-module-eval-source-map is faster for development // cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool, devtool: config.dev.devtool,
@@ -39,7 +43,7 @@ const devWebpackConfig = merge(baseWebpackConfig, {
proxy: config.dev.proxyTable, proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: { watchOptions: {
poll: config.dev.poll, poll: config.dev.poll
} }
}, },
plugins: [ plugins: [
@@ -47,8 +51,6 @@ const devWebpackConfig = merge(baseWebpackConfig, {
'process.env': require('../config/dev.env') 'process.env': require('../config/dev.env')
}), }),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin // https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: 'index.html', filename: 'index.html',
@@ -57,7 +59,7 @@ const devWebpackConfig = merge(baseWebpackConfig, {
favicon: resolve('favicon.ico'), favicon: resolve('favicon.ico'),
title: 'vue-element-admin', title: 'vue-element-admin',
path: config.dev.assetsPublicPath + config.dev.assetsSubDirectory path: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
}), })
] ]
}) })
@@ -73,14 +75,20 @@ module.exports = new Promise((resolve, reject) => {
devWebpackConfig.devServer.port = port devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin // Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ devWebpackConfig.plugins.push(
compilationSuccessInfo: { new FriendlyErrorsPlugin({
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], compilationSuccessInfo: {
}, messages: [
onErrors: config.dev.notifyOnErrors `Your application is running here: http://${
? utils.createNotifierCallback() devWebpackConfig.devServer.host
: undefined }:${port}`
})) ]
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
})
)
resolve(devWebpackConfig) resolve(devWebpackConfig)
} }

View File

@@ -7,17 +7,23 @@ const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf') const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin') const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
function resolve (dir) { function resolve(dir) {
return path.join(__dirname, '..', dir) return path.join(__dirname, '..', dir)
} }
const env = require('../config/'+process.env.env_config+'.env') const env = require('../config/' + process.env.env_config + '.env')
// For NamedChunksPlugin
const seen = new Set()
const nameLength = 4
const webpackConfig = merge(baseWebpackConfig, { const webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
module: { module: {
rules: utils.styleLoaders({ rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap, sourceMap: config.build.productionSourceMap,
@@ -28,37 +34,18 @@ const webpackConfig = merge(baseWebpackConfig, {
devtool: config.build.productionSourceMap ? config.build.devtool : false, devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: { output: {
path: config.build.assetsRoot, path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'), filename: utils.assetsPath('js/[name].[chunkhash:8].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js')
}, },
plugins: [ plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html // http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': env 'process.env': env
}), }),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file // extract css into its own file
new ExtractTextPlugin({ new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'), filename: utils.assetsPath('css/[name].[contenthash:8].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks. chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css')
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: false,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}), }),
// generate dist index.html with correct asset hash for caching. // generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html // you can customize output by editing /index.html
@@ -76,68 +63,34 @@ const webpackConfig = merge(baseWebpackConfig, {
removeAttributeQuotes: true removeAttributeQuotes: true
// more options: // more options:
// https://github.com/kangax/html-minifier#options-quick-reference // https://github.com/kangax/html-minifier#options-quick-reference
}, }
// necessary to consistently work with multiple chunks via CommonsChunkPlugin // default sort mode uses toposort which cannot handle cyclic deps
chunksSortMode: 'dependency' // 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 // keep module.id stable when vender modules does not change
new webpack.HashedModuleIdsPlugin(), new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// split echarts into its own file
new webpack.optimize.CommonsChunkPlugin({
async: 'echarts',
minChunks(module) {
var context = module.context;
return context && (context.indexOf('echarts') >= 0 || context.indexOf('zrender') >= 0);
}
}),
// split xlsx into its own file
new webpack.optimize.CommonsChunkPlugin({
async: 'xlsx',
minChunks(module) {
var context = module.context;
return context && (context.indexOf('xlsx') >= 0);
}
}),
// split codemirror into its own file
new webpack.optimize.CommonsChunkPlugin({
async: 'codemirror',
minChunks(module) {
var context = module.context;
return context && (context.indexOf('codemirror') >= 0);
}
}),
// copy custom static assets // copy custom static assets
new CopyWebpackPlugin([ new CopyWebpackPlugin([
{ {
@@ -146,7 +99,48 @@ const webpackConfig = merge(baseWebpackConfig, {
ignore: ['.*'] 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-comomns',
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) { if (config.build.productionGzip) {
@@ -157,9 +151,7 @@ if (config.build.productionGzip) {
asset: '[path].gz[query]', asset: '[path].gz[query]',
algorithm: 'gzip', algorithm: 'gzip',
test: new RegExp( test: new RegExp(
'\\.(' + '\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
config.build.productionGzipExtensions.join('|') +
')$'
), ),
threshold: 10240, threshold: 10240,
minRatio: 0.8 minRatio: 0.8
@@ -167,9 +159,28 @@ if (config.build.productionGzip) {
) )
} }
if (config.build.bundleAnalyzerReport) { if (config.build.generateAnalyzerReport || config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
webpackConfig.plugins.push(new BundleAnalyzerPlugin()) .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 module.exports = webpackConfig

View File

@@ -13,7 +13,10 @@ module.exports = {
proxyTable: {}, proxyTable: {},
// Various Dev Server settings // Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
// 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 port: 9527, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: true, autoOpenBrowser: true,
errorOverlay: true, errorOverlay: true,
@@ -33,12 +36,7 @@ module.exports = {
*/ */
// https://webpack.js.org/configuration/devtool/#development // https://webpack.js.org/configuration/devtool/#development
devtool: '#cheap-source-map', devtool: 'cheap-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
// CSS Sourcemaps off by default because relative paths are "buggy" // CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README // with this option, according to the CSS-Loader README
@@ -56,16 +54,21 @@ module.exports = {
assetsRoot: path.resolve(__dirname, '../dist'), assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static', assetsSubDirectory: 'static',
// you can set by youself according to actual condition /**
assetsPublicPath: './', * 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: '/vue-element-admin/', // If you are deployed on the root path, please use '/'
/** /**
* Source Maps * Source Maps
*/ */
productionSourceMap: false, productionSourceMap: false,
// https://webpack.js.org/configuration/devtool/#production // https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map', devtool: 'source-map',
// Gzip off by default as many popular static hosts such as // Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you. // Surge or Netlify already gzip all static assets for you.
@@ -76,8 +79,11 @@ module.exports = {
// Run the build command with an extra argument to // Run the build command with an extra argument to
// View the bundle analyzer report after build finishes: // View the bundle analyzer report after build finishes:
// `npm run build --report` // `npm run build:prod --report`
// Set to `true` or `false` to always turn it on or off // Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report bundleAnalyzerReport: process.env.npm_config_report || false,
// `npm run build:prod --generate_report`
generateAnalyzerReport: process.env.npm_config_generate_report || false
} }
} }

View File

@@ -1,15 +1,22 @@
{ {
"name": "vue-element-admin", "name": "vue-element-admin",
"version": "3.6.6", "version": "3.7.3",
"description": "A magical vue admin. Typical templates for enterprise applications. Newest development stack of vue. Lots of awesome features", "description": "A magical vue admin. Typical templates for enterprise applications. Newest development stack of vue. Lots of awesome features",
"author": "Pan <panfree23@gmail.com>", "author": "Pan <panfree23@gmail.com>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "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: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", "build:sit": "cross-env NODE_ENV=production env_config=sit node build/build.js",
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"test": "npm run lint" "test": "npm run lint",
"precommit": "lint-staged"
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
}, },
"keywords": [ "keywords": [
"vue", "vue",
@@ -26,28 +33,30 @@
"url": "https://github.com/PanJiaChen/vue-element-admin/issues" "url": "https://github.com/PanJiaChen/vue-element-admin/issues"
}, },
"dependencies": { "dependencies": {
"axios": "0.17.1", "axios": "0.18.0",
"clipboard": "1.7.1", "clipboard": "1.7.1",
"codemirror": "5.32.0", "codemirror": "5.39.2",
"connect": "3.6.6",
"driver.js": "0.5.2",
"dropzone": "5.2.0", "dropzone": "5.2.0",
"echarts": "3.8.5", "echarts": "4.1.0",
"element-ui": "2.3.2", "element-ui": "2.4.6",
"file-saver": "1.3.3", "file-saver": "1.3.8",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"js-cookie": "2.2.0", "js-cookie": "2.2.0",
"jsonlint": "1.6.2", "jsonlint": "1.6.3",
"jszip": "3.1.5", "jszip": "3.1.5",
"mockjs": "1.0.1-beta3", "mockjs": "1.0.1-beta3",
"normalize.css": "7.0.0", "normalize.css": "7.0.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"screenfull": "3.3.2", "screenfull": "3.3.2",
"showdown": "1.8.5", "showdown": "1.8.6",
"simplemde": "1.11.2", "simplemde": "1.11.2",
"sortablejs": "1.7.0", "sortablejs": "1.7.0",
"vue": "2.5.10", "vue": "2.5.17",
"vue-count-to": "1.0.13", "vue-count-to": "1.0.13",
"vue-i18n": "7.3.2", "vue-i18n": "7.3.2",
"vue-multiselect": "2.0.8", "vue-multiselect": "2.1.0",
"vue-router": "3.0.1", "vue-router": "3.0.1",
"vue-splitpane": "1.0.2", "vue-splitpane": "1.0.2",
"vuedraggable": "^2.16.0", "vuedraggable": "^2.16.0",
@@ -55,55 +64,61 @@
"xlsx": "^0.11.16" "xlsx": "^0.11.16"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "7.2.3", "autoprefixer": "8.5.0",
"babel-core": "6.26.0", "babel-core": "6.26.3",
"babel-eslint": "8.0.3", "babel-eslint": "8.2.6",
"babel-helper-vue-jsx-merge-props": "2.0.3", "babel-helper-vue-jsx-merge-props": "2.0.3",
"babel-loader": "7.1.2", "babel-loader": "7.1.5",
"babel-plugin-dynamic-import-node": "2.0.0",
"babel-plugin-syntax-jsx": "6.18.0", "babel-plugin-syntax-jsx": "6.18.0",
"babel-plugin-transform-runtime": "6.23.0", "babel-plugin-transform-runtime": "6.23.0",
"babel-plugin-transform-vue-jsx": "3.5.0", "babel-plugin-transform-vue-jsx": "3.7.0",
"babel-preset-env": "1.6.1", "babel-preset-env": "1.7.0",
"babel-preset-stage-2": "6.24.1", "babel-preset-stage-2": "6.24.1",
"chalk": "2.3.0", "chalk": "2.4.1",
"copy-webpack-plugin": "4.3.0", "copy-webpack-plugin": "4.5.2",
"cross-env": "5.1.1", "cross-env": "5.2.0",
"css-loader": "0.28.7", "css-loader": "1.0.0",
"eslint": "4.13.1", "eslint": "4.19.1",
"eslint-friendly-formatter": "3.0.0", "eslint-friendly-formatter": "4.0.1",
"eslint-loader": "1.9.0", "eslint-loader": "2.0.0",
"eslint-plugin-html": "4.0.1", "eslint-plugin-html": "4.0.5",
"extract-text-webpack-plugin": "3.0.2", "file-loader": "1.1.11",
"file-loader": "1.1.5", "friendly-errors-webpack-plugin": "1.7.0",
"friendly-errors-webpack-plugin": "1.6.1", "hash-sum": "^1.0.2",
"html-webpack-plugin": "2.30.1", "html-webpack-plugin": "^4.0.0-alpha",
"node-notifier": "5.1.2", "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", "node-sass": "^4.7.2",
"optimize-css-assets-webpack-plugin": "3.2.0", "optimize-css-assets-webpack-plugin": "5.0.0",
"ora": "1.3.0", "ora": "3.0.0",
"portfinder": "1.0.13", "portfinder": "1.0.13",
"postcss-import": "11.0.0", "postcss-import": "11.1.0",
"postcss-loader": "2.0.9", "postcss-loader": "2.1.6",
"postcss-url": "7.3.0", "postcss-url": "7.3.2",
"pushstate-server": "3.0.1",
"rimraf": "2.6.2", "rimraf": "2.6.2",
"sass-loader": "6.0.6", "sass-loader": "7.0.3",
"script-ext-html-webpack-plugin": "2.0.1",
"script-loader": "0.7.2", "script-loader": "0.7.2",
"semver": "5.4.1", "semver": "5.5.0",
"shelljs": "0.7.8", "serve-static": "1.13.2",
"svg-sprite-loader": "3.5.2", "shelljs": "0.8.2",
"uglifyjs-webpack-plugin": "1.1.3", "svg-sprite-loader": "3.8.0",
"url-loader": "0.6.2", "uglifyjs-webpack-plugin": "1.2.7",
"vue-loader": "13.5.0", "url-loader": "1.0.1",
"vue-style-loader": "3.0.3", "vue-loader": "15.3.0",
"vue-template-compiler": "2.5.10", "vue-style-loader": "4.1.2",
"webpack": "3.10.0", "vue-template-compiler": "2.5.17",
"webpack-bundle-analyzer": "2.9.1", "webpack": "4.16.5",
"webpack-dev-server": "2.9.7", "webpack-bundle-analyzer": "2.13.1",
"webpack-merge": "4.1.1" "webpack-cli": "3.1.0",
"webpack-dev-server": "3.1.5",
"webpack-merge": "4.1.4"
}, },
"engines": { "engines": {
"node": ">= 4.0.0", "node": ">= 6.0.0",
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
}, },
"browserslist": [ "browserslist": [

View File

@@ -8,10 +8,11 @@ export function fetchList(query) {
}) })
} }
export function fetchArticle() { export function fetchArticle(id) {
return request({ return request({
url: '/article/detail', url: '/article/detail',
method: 'get' method: 'get',
params: { id }
}) })
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github"> <a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#40c9c6; color:#fff; position: absolute; top: 84px; border: 0; right: 0;" <svg width="80" height="80" viewBox="0 0 250 250" style="fill:#40c9c6; color:#fff;"
aria-hidden="true"> aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"

View File

@@ -1,57 +0,0 @@
<template>
<div class="scroll-container" ref="scrollContainer" @wheel.prevent="handleScroll" >
<div class="scroll-wrapper" ref="scrollWrapper" :style="{top: top + 'px'}">
<slot></slot>
</div>
</div>
</template>
<script>
const delta = 15
export default {
name: 'scrollBar',
data() {
return {
top: 0
}
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 3
const $container = this.$refs.scrollContainer
const $containerHeight = $container.offsetHeight
const $wrapper = this.$refs.scrollWrapper
const $wrapperHeight = $wrapper.offsetHeight
if (eventDelta > 0) {
this.top = Math.min(0, this.top + eventDelta)
} else {
if ($containerHeight - delta < $wrapperHeight) {
if (this.top < -($wrapperHeight - $containerHeight + delta)) {
this.top = this.top
} else {
this.top = Math.max(this.top + eventDelta, $containerHeight - $wrapperHeight - delta)
}
} else {
this.top = 0
}
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
@import '../../styles/variables.scss';
.scroll-container {
position: relative;
width: 100%;
height: 100%;
background-color: $menuBg;
.scroll-wrapper {
position: absolute;
width: 100%!important;
}
}
</style>

View File

@@ -35,7 +35,7 @@ export default {
</script> </script>
<style rel="stylesheet/scss" lang="scss" > <style rel="stylesheet/scss" lang="scss" >
$n: 6; //和items.length 相同 $n: 8; //和items.length 相同
$t: .1s; $t: .1s;
.share-dropdown-menu { .share-dropdown-menu {
width: 250px; width: 250px;

View File

@@ -28,22 +28,22 @@ export default {
return { return {
active: false, active: false,
position: '', position: '',
currentTop: '',
width: undefined, width: undefined,
height: undefined, height: undefined,
child: null, isSticky: false
stickyHeight: 0
} }
}, },
mounted() { mounted() {
this.height = this.$el.getBoundingClientRect().height this.height = this.$el.getBoundingClientRect().height
window.addEventListener('scroll', this.handleScroll) window.addEventListener('scroll', this.handleScroll)
window.addEventListener('resize', this.handleReize)
}, },
activated() { activated() {
this.handleScroll() this.handleScroll()
}, },
destroyed() { destroyed() {
window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleReize)
}, },
methods: { methods: {
sticky() { sticky() {
@@ -53,6 +53,7 @@ export default {
this.position = 'fixed' this.position = 'fixed'
this.active = true this.active = true
this.width = this.width + 'px' this.width = this.width + 'px'
this.isSticky = true
}, },
reset() { reset() {
if (!this.active) { if (!this.active) {
@@ -61,15 +62,21 @@ export default {
this.position = '' this.position = ''
this.width = 'auto' this.width = 'auto'
this.active = false this.active = false
this.isSticky = false
}, },
handleScroll() { handleScroll() {
this.width = this.$el.getBoundingClientRect().width this.width = this.$el.getBoundingClientRect().width
const offsetTop = this.$el.getBoundingClientRect().top const offsetTop = this.$el.getBoundingClientRect().top
if (offsetTop <= this.stickyTop) { if (offsetTop < this.stickyTop) {
this.sticky() this.sticky()
return return
} }
this.reset() this.reset()
},
handleReize() {
if (this.isSticky) {
this.width = this.$el.getBoundingClientRect().width + 'px'
}
} }
} }
} }

View File

@@ -23,6 +23,7 @@ export default {
<style> <style>
/* Mallki */ /* Mallki */
.link--mallki { .link--mallki {
font-weight: 800; font-weight: 800;
color: #4dd9d5; color: #4dd9d5;
@@ -31,10 +32,10 @@ export default {
transition: color 0.5s 0.25s; transition: color 0.5s 0.25s;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
display: inline-block; display: inline-block;
line-height: 1; line-height: 1;
outline: none; outline: none;
text-decoration: none; text-decoration: none;
} }
.link--mallki:hover { .link--mallki:hover {
@@ -109,5 +110,4 @@ display: inline-block;
-webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1); -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1); transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<div class="upload-container"> <div class="upload-container">
<el-button icon='el-icon-upload' size="mini" :style="{background:color,borderColor:color}" @click=" dialogVisible=true" type="primary">上传图片 <el-button icon='el-icon-upload' size="mini" :style="{background:color,borderColor:color}" @click=" dialogVisible=true" type="primary">上传图片
</el-button> </el-button>
<el-dialog append-to-body :visible.sync="dialogVisible"> <el-dialog :visible.sync="dialogVisible">
<el-upload class="editor-slide-upload" action="https://httpbin.org/post" :multiple="true" :file-list="fileList" :show-file-list="true" <el-upload class="editor-slide-upload" action="https://httpbin.org/post" :multiple="true" :file-list="fileList" :show-file-list="true"
list-type="picture-card" :on-remove="handleRemove" :on-success="handleSuccess" :before-upload="beforeUpload"> list-type="picture-card" :on-remove="handleRemove" :on-success="handleSuccess" :before-upload="beforeUpload">
<el-button size="small" type="primary">点击上传</el-button> <el-button size="small" type="primary">点击上传</el-button>
@@ -87,9 +87,10 @@ export default {
</script> </script>
<style rel="stylesheet/scss" lang="scss" scoped> <style rel="stylesheet/scss" lang="scss" scoped>
.upload-container { .editor-slide-upload {
.editor-slide-upload { margin-bottom: 20px;
margin-bottom: 20px; /deep/ .el-upload--picture-card {
width: 100%;
} }
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="tinymce-container editor-container"> <div class="tinymce-container editor-container" :class="{fullscreen:fullscreen}">
<textarea class="tinymce-textarea" :id="tinymceId"></textarea> <textarea class="tinymce-textarea" :id="tinymceId"></textarea>
<div class="editor-custom-btn-container"> <div class="editor-custom-btn-container">
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"></editorImage> <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"></editorImage>
@@ -43,13 +43,15 @@ export default {
return { return {
hasChange: false, hasChange: false,
hasInit: false, hasInit: false,
tinymceId: this.id || 'vue-tinymce-' + +new Date() tinymceId: this.id || 'vue-tinymce-' + +new Date(),
fullscreen: false
} }
}, },
watch: { watch: {
value(val) { value(val) {
if (!this.hasChange && this.hasInit) { if (!this.hasChange && this.hasInit) {
this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(val)) this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
} }
} }
}, },
@@ -82,6 +84,7 @@ export default {
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'], imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank', default_link_target: '_blank',
link_title: false, link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => { init_instance_callback: editor => {
if (_this.value) { if (_this.value) {
editor.setContent(_this.value) editor.setContent(_this.value)
@@ -91,6 +94,11 @@ export default {
this.hasChange = true this.hasChange = true
this.$emit('input', editor.getContent()) this.$emit('input', editor.getContent())
}) })
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
})
} }
// 整合七牛上传 // 整合七牛上传
// images_dataimg_filter(img) { // images_dataimg_filter(img) {
@@ -168,6 +176,10 @@ export default {
top: 4px; top: 4px;
/*z-index: 2005;*/ /*z-index: 2005;*/
} }
.fullscreen .editor-custom-btn-container {
z-index: 10000;
position: fixed;
}
.editor-upload-btn { .editor-upload-btn {
display: inline-block; display: inline-block;
} }

View File

@@ -2,6 +2,6 @@
// Detail plugins list see https://www.tinymce.com/docs/plugins/ // Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/ // Custom builds see https://www.tinymce.com/download/custom-builds/
const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools importcss insertdatetime legacyoutput link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'] const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools importcss insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
export default plugins export default plugins

View File

@@ -1,6 +1,6 @@
// Here is a list of the toolbar // Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak fullscreen insertdatetime media table emoticons forecolor backcolor'] const toolbar = ['bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
export default toolbar export default toolbar

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="upload-container"> <div class="upload-container">
<el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post" <el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post"
:on-success="handleImageScucess"> :on-success="handleImageSuccess">
<i class="el-icon-upload"></i> <i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div> <div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
</el-upload> </el-upload>
@@ -43,7 +43,7 @@ export default {
emitInput(val) { emitInput(val) {
this.$emit('input', val) this.$emit('input', val)
}, },
handleImageScucess() { handleImageSuccess() {
this.emitInput(this.tempUrl) this.emitInput(this.tempUrl)
}, },
beforeUpload() { beforeUpload() {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="singleImageUpload2 upload-container"> <div class="singleImageUpload2 upload-container">
<el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post" <el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post"
:on-success="handleImageScucess"> :on-success="handleImageSuccess">
<i class="el-icon-upload"></i> <i class="el-icon-upload"></i>
<div class="el-upload__text">Drag或<em>点击上传</em></div> <div class="el-upload__text">Drag或<em>点击上传</em></div>
</el-upload> </el-upload>
@@ -42,7 +42,7 @@ export default {
emitInput(val) { emitInput(val) {
this.$emit('input', val) this.$emit('input', val)
}, },
handleImageScucess() { handleImageSuccess() {
this.emitInput(this.tempUrl) this.emitInput(this.tempUrl)
}, },
beforeUpload() { beforeUpload() {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="upload-container"> <div class="upload-container">
<el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post" <el-upload class="image-uploader" :data="dataObj" drag :multiple="false" :show-file-list="false" action="https://httpbin.org/post"
:on-success="handleImageScucess"> :on-success="handleImageSuccess">
<i class="el-icon-upload"></i> <i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div> <div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
</el-upload> </el-upload>
@@ -50,7 +50,7 @@ export default {
emitInput(val) { emitInput(val) {
this.$emit('input', val) this.$emit('input', val)
}, },
handleImageScucess(file) { handleImageSuccess(file) {
this.emitInput(file.files.file) this.emitInput(file.files.file)
}, },
beforeUpload() { beforeUpload() {

View File

@@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<input id="excel-upload-input" ref="excel-upload-input" type="file" accept=".xlsx, .xls" class="c-hide" @change="handkeFileChange"> <input id="excel-upload-input" ref="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
<div id="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover"> <div id="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
Drop excel file here or Drop excel file here or
<el-button style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">browse</el-button> <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">Browse</el-button>
</div> </div>
</div> </div>
</template> </template>
@@ -12,6 +12,10 @@
import XLSX from 'xlsx' import XLSX from 'xlsx'
export default { export default {
props: {
beforeUpload: Function,
onSuccess: Function
},
data() { data() {
return { return {
loading: false, loading: false,
@@ -25,18 +29,24 @@ export default {
generateDate({ header, results }) { generateDate({ header, results }) {
this.excelData.header = header this.excelData.header = header
this.excelData.results = results this.excelData.results = results
this.$emit('on-selected-file', this.excelData) this.onSuccess && this.onSuccess(this.excelData)
}, },
handleDrop(e) { handleDrop(e) {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
if (this.loading) return
const files = e.dataTransfer.files const files = e.dataTransfer.files
if (files.length !== 1) { if (files.length !== 1) {
this.$message.error('Only support uploading one file!') this.$message.error('Only support uploading one file!')
return return
} }
const itemFile = files[0] // only use files[0] const rawFile = files[0] // only use files[0]
this.readerData(itemFile)
if (!this.isExcel(rawFile)) {
this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
return false
}
this.upload(rawFile)
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
}, },
@@ -48,26 +58,42 @@ export default {
handleUpload() { handleUpload() {
document.getElementById('excel-upload-input').click() document.getElementById('excel-upload-input').click()
}, },
handkeFileChange(e) { handleClick(e) {
const files = e.target.files const files = e.target.files
const itemFile = files[0] // only use files[0] const rawFile = files[0] // only use files[0]
if (!itemFile) return if (!rawFile) return
this.readerData(itemFile) this.upload(rawFile)
this.$refs['excel-upload-input'].value = null // fix can't select the same excel
}, },
readerData(itemFile) { upload(rawFile) {
const reader = new FileReader() this.$refs['excel-upload-input'].value = null // fix can't select the same excel
reader.onload = e => {
const data = e.target.result if (!this.beforeUpload) {
const fixedData = this.fixdata(data) this.readerData(rawFile)
const workbook = XLSX.read(btoa(fixedData), { type: 'base64' }) return
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const header = this.get_header_row(worksheet)
const results = XLSX.utils.sheet_to_json(worksheet)
this.generateDate({ header, results })
} }
reader.readAsArrayBuffer(itemFile) const before = this.beforeUpload(rawFile)
if (before) {
this.readerData(rawFile)
}
},
readerData(rawFile) {
this.loading = true
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = e => {
const data = e.target.result
const fixedData = this.fixdata(data)
const workbook = XLSX.read(btoa(fixedData), { type: 'base64' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const header = this.get_header_row(worksheet)
const results = XLSX.utils.sheet_to_json(worksheet)
this.generateDate({ header, results })
this.loading = false
resolve()
}
reader.readAsArrayBuffer(rawFile)
})
}, },
fixdata(data) { fixdata(data) {
let o = '' let o = ''
@@ -89,6 +115,9 @@ export default {
headers.push(hdr) headers.push(hdr)
} }
return headers return headers
},
isExcel(file) {
return /\.(xlsx|xls|csv)$/.test(file.name)
} }
} }
} }

View File

@@ -1,18 +1,18 @@
export default{ export default{
bind(el, binding) { bind(el, binding, vnode) {
const dialogHeaderEl = el.querySelector('.el-dialog__header') const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog') const dragDom = el.querySelector('.el-dialog')
dialogHeaderEl.style.cssText += ';cursor:move;' dialogHeaderEl.style.cssText += ';cursor:move;'
dragDom.style.cssText += ';top:0px;' dragDom.style.cssText += ';top:0px;'
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
function getStyle(dom, attr) { const getStyle = (function() {
if (dom.currentStyle) { if (window.document.currentStyle) {
return dom.currentStyle[attr] return (dom, attr) => dom.currentStyle[attr]
} else { } else {
return getComputedStyle(dom, false)[attr] return (dom, attr) => getComputedStyle(dom, false)[attr]
} }
} })()
dialogHeaderEl.onmousedown = (e) => { dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离 // 鼠标按下,计算当前元素距离可视区的距离
@@ -63,6 +63,9 @@ export default{
// 移动当前元素 // 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
// emit onDrag event
vnode.child.$emit('dragDialog')
} }
document.onmouseup = function(e) { document.onmouseup = function(e) {

View File

@@ -0,0 +1,13 @@
import permission from './permission'
const install = function(Vue) {
Vue.directive('permission', permission)
}
if (window.Vue) {
window['permission'] = permission
Vue.use(install); // eslint-disable-line
}
permission.install = install
export default permission

View File

@@ -0,0 +1,23 @@
import store from '@/store'
export default{
inserted(el, binding, vnode) {
const { value } = binding
const roles = store.getters && store.getters.roles
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`need roles! Like v-permission="['admin','editor']"`)
}
}
}

View File

@@ -29,8 +29,8 @@ export default{
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px' ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
break break
default: default:
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px' ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || document.body.scrollTop) + 'px'
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px' ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || document.body.scrollLeft) + 'px'
} }
ripple.style.backgroundColor = opts.color ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active' ripple.className = 'waves-ripple z-active'

View File

@@ -1,3 +1,6 @@
// set function parseTime,formatTime to filter
export { parseTime, formatTime } from '@/utils'
function pluralize(time, label) { function pluralize(time, label) {
if (time === 1) { if (time === 1) {
return time + label return time + label
@@ -16,67 +19,8 @@ export function timeAgo(time) {
} }
} }
export function parseTime(time, cFormat) {
if (arguments.length === 0) {
return null
}
if ((time + '').length === 10) {
time = +time * 1000
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
date = new Date(parseInt(time))
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
export function formatTime(time, option) {
time = +time * 1000
const d = new Date(time)
const now = Date.now()
const diff = (now - d) / 1000
if (diff < 30) {
return '刚刚'
} else if (diff < 3600) { // less 1 hour
return Math.ceil(diff / 60) + '分钟前'
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
}
if (option) {
return parseTime(time, option)
} else {
return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分'
}
}
/* 数字 格式化*/ /* 数字 格式化*/
export function nFormatter(num, digits) { export function numberFormatter(num, digits) {
const si = [ const si = [
{ value: 1E18, symbol: 'E' }, { value: 1E18, symbol: 'E' },
{ value: 1E15, symbol: 'P' }, { value: 1E15, symbol: 'P' },
@@ -93,12 +37,6 @@ export function nFormatter(num, digits) {
return num.toString() return num.toString()
} }
export function html2Text(val) {
const div = document.createElement('div')
div.innerHTML = val
return div.textContent || div.innerText
}
export function toThousandslsFilter(num) { export function toThousandslsFilter(num) {
return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ',')) return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
} }

1
src/icons/svg/edit.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525760397212" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2919" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M860 504c-19.9 0-36 16.1-36 36 0 1.4 0.1 2.7 0.2 4h-0.2v344H136V200h376c19.9 0 36-16.1 36-36s-16.1-36-36-36H136c-39.8 0-72 32.2-72 72v688c0 39.8 32.2 72 72 72h688c39.8 0 72-32.2 72-72V544h-0.2c0.1-1.3 0.2-2.6 0.2-4 0-19.9-16.1-36-36-36z" p-id="2920"></path><path d="M1002.7 100.3L923.4 21c-28.1-28.1-73.9-27.9-102 0.2L424.2 418.4c-2.9 2.9-5.2 6.4-6.8 10.2L317.6 664c-5.6 13.2-1.7 26.5 6.8 35.1 8.5 8.6 21.9 12.5 35.2 6.9l235.5-99.7c3.8-1.6 7.2-3.9 10.2-6.8l397.2-397.2c28.1-28.1 28.3-73.9 0.2-102zM559.8 543l-137.4 58.2 58.2-137.4L759.4 185l79.2 79.2L559.8 543z m391.7-391.7l-62 62-79.2-79.2 62-62 0.2-0.2 79.2 79.2-0.2 0.2z" p-id="2921"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
src/icons/svg/guide.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1526033837694" class="icon" style="" viewBox="0 0 1117 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10274" xmlns:xlink="http://www.w3.org/1999/xlink" width="218.1640625" height="200"><defs><style type="text/css"></style></defs><path d="M53.865 558.08l289.92 121.6 560-492.16-491.52 530.56 371.84 140.8c8.96 3.2 19.2-1.28 22.4-10.24V848l260.48-816.64-1014.4 494.72c-8.96 4.48-12.16 14.72-8.32 23.68 2.56 3.84 5.76 7.04 9.6 8.32z m357.76 434.56l144.64-155.52-144.64-58.88v214.4z" p-id="10275"></path></svg>

After

Width:  |  Height:  |  Size: 664 B

1
src/icons/svg/list.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1525761666409" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10880" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M107.2 212.8m-67.2 0a4.2 4.2 0 1 0 134.4 0 4.2 4.2 0 1 0-134.4 0Z" p-id="10881"></path><path d="M980.8 145.6 297.6 145.6c-9.6 0-16 8-16 16l0 102.4c0 9.6 8 16 16 16l683.2 0c9.6 0 16-8 16-16l0-102.4C996.8 152 988.8 145.6 980.8 145.6z" p-id="10882"></path><path d="M96 497.6m-67.2 0a4.2 4.2 0 1 0 134.4 0 4.2 4.2 0 1 0-134.4 0Z" p-id="10883"></path><path d="M968 430.4 284.8 430.4c-9.6 0-16 8-16 16l0 102.4c0 9.6 8 16 16 16l683.2 0c9.6 0 16-8 16-16l0-102.4C984 438.4 977.6 430.4 968 430.4z" p-id="10884"></path><path d="M96 795.2m-67.2 0a4.2 4.2 0 1 0 134.4 0 4.2 4.2 0 1 0-134.4 0Z" p-id="10885"></path><path d="M968 728 284.8 728c-9.6 0-16 8-16 16l0 102.4c0 9.6 8 16 16 16l683.2 0c9.6 0 16-8 16-16l0-102.4C984 736 977.6 728 968 728z" p-id="10886"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
src/icons/svg/nested.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1529559567446" class="icon" style="" viewBox="0 0 1167 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1767" xmlns:xlink="http://www.w3.org/1999/xlink" width="227.9296875" height="200"><defs><style type="text/css"></style></defs><path d="M0.015952 74.459413A2.286 2.286 1440 1 0 145.85218 74.459413 2.286 2.286 1440 1 0 0.015952 74.459413zM291.720312 1.525347 1166.801488 1.525347 1166.801488 147.361574 291.720312 147.361574zM291.720312 366.163773A2.286 2.286 1440 1 0 437.55654 366.163773 2.286 2.286 1440 1 0 291.720312 366.163773zM583.424672 293.229707 1166.801488 293.229707 1166.801488 439.065934 583.424672 439.065934zM291.720312 949.540588A2.286 2.286 1440 1 0 437.55654 949.540588 2.286 2.286 1440 1 0 291.720312 949.540588zM583.424672 876.638427 1166.801488 876.638427 1166.801488 1022.474654 583.424672 1022.474654zM583.424672 657.836228A2.286 2.286 1440 1 0 729.2609 657.836228 2.286 2.286 1440 1 0 583.424672 657.836228zM875.129032 584.934067 1166.801488 584.934067 1166.801488 730.770294 875.129032 730.770294z" p-id="1768"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -3,7 +3,10 @@ export default {
dashboard: 'Dashboard', dashboard: 'Dashboard',
introduction: 'Introduction', introduction: 'Introduction',
documentation: 'Documentation', documentation: 'Documentation',
guide: 'Guide',
permission: 'Permission', permission: 'Permission',
pagePermission: 'Page Permission',
directivePermission: 'Directive Permission',
icons: 'Icons', icons: 'Icons',
components: 'Components', components: 'Components',
componentIndex: 'Introduction', componentIndex: 'Introduction',
@@ -25,6 +28,14 @@ export default {
lineChart: 'Line Chart', lineChart: 'Line Chart',
mixChart: 'Mix Chart', mixChart: 'Mix Chart',
example: 'Example', example: 'Example',
nested: 'Nested Routes',
menu1: 'Menu 1',
'menu1-1': 'Menu 1-1',
'menu1-2': 'Menu 1-2',
'menu1-2-1': 'Menu 1-2-1',
'menu1-2-2': 'Menu 1-2-2',
'menu1-3': 'Menu 1-3',
menu2: 'Menu 2',
Table: 'Table', Table: 'Table',
dynamicTable: 'Dynamic Table', dynamicTable: 'Dynamic Table',
dragTable: 'Drag Table', dragTable: 'Drag Table',
@@ -34,8 +45,9 @@ export default {
customTreeTable: 'Custom TreeTable', customTreeTable: 'Custom TreeTable',
tab: 'Tab', tab: 'Tab',
form: 'Form', form: 'Form',
createForm: 'Create Form', createArticle: 'Create Article',
editForm: 'Edit Form', editArticle: 'Edit Article',
articleList: 'Article List',
errorPages: 'Error Pages', errorPages: 'Error Pages',
page401: '401', page401: '401',
page404: '404', page404: '404',
@@ -74,6 +86,10 @@ export default {
roles: 'Your roles', roles: 'Your roles',
switchRoles: 'Switch roles' switchRoles: 'Switch roles'
}, },
guide: {
description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',
button: 'Show Guide'
},
components: { components: {
documentation: 'Documentation', documentation: 'Documentation',
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.', tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',

View File

@@ -20,8 +20,11 @@ const messages = {
} }
const i18n = new VueI18n({ const i18n = new VueI18n({
locale: Cookies.get('language') || 'en', // set locale // set locale
messages // set locale messages // options: en or zh
locale: Cookies.get('language') || 'en',
// set locale messages
messages
}) })
export default i18n export default i18n

View File

@@ -3,7 +3,10 @@ export default {
dashboard: '首页', dashboard: '首页',
introduction: '简述', introduction: '简述',
documentation: '文档', documentation: '文档',
guide: '引导页',
permission: '权限测试页', permission: '权限测试页',
pagePermission: '页面权限',
directivePermission: '指令权限',
icons: '图标', icons: '图标',
components: '组件', components: '组件',
componentIndex: '介绍', componentIndex: '介绍',
@@ -25,6 +28,14 @@ export default {
lineChart: '折线图', lineChart: '折线图',
mixChart: '混合图表', mixChart: '混合图表',
example: '综合实例', example: '综合实例',
nested: '路由嵌套',
menu1: '菜单1',
'menu1-1': '菜单1-1',
'menu1-2': '菜单1-2',
'menu1-2-1': '菜单1-2-1',
'menu1-2-2': '菜单1-2-2',
'menu1-3': '菜单1-3',
menu2: '菜单2',
Table: 'Table', Table: 'Table',
dynamicTable: '动态Table', dynamicTable: '动态Table',
dragTable: '拖拽Table', dragTable: '拖拽Table',
@@ -34,8 +45,9 @@ export default {
customTreeTable: '自定义树表', customTreeTable: '自定义树表',
tab: 'Tab', tab: 'Tab',
form: '表单', form: '表单',
createForm: '创建表单', createArticle: '创建文章',
editForm: '编辑表单', editArticle: '编辑文章',
articleList: '文章列表',
errorPages: '错误页面', errorPages: '错误页面',
page401: '401', page401: '401',
page404: '404', page404: '404',
@@ -74,6 +86,10 @@ export default {
roles: '你的权限', roles: '你的权限',
switchRoles: '切换权限' switchRoles: '切换权限'
}, },
guide: {
description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于',
button: '打开引导'
},
components: { components: {
documentation: '文档', documentation: '文档',
tinymceTips: '富文本是管理后台一个核心的功能但同时又是一个有很多坑的地方。在选择富文本的过程中我也走了不少的弯路市面上常见的富文本都基本用过了最终权衡了一下选择了Tinymce。更详细的富文本比较和介绍见', tinymceTips: '富文本是管理后台一个核心的功能但同时又是一个有很多坑的地方。在选择富文本的过程中我也走了不少的弯路市面上常见的富文本都基本用过了最终权衡了一下选择了Tinymce。更详细的富文本比较和介绍见',

View File

@@ -36,6 +36,5 @@ new Vue({
router, router,
store, store,
i18n, i18n,
template: '<App/>', render: h => h(App)
components: { App }
}) })

View File

@@ -4,6 +4,9 @@ import { param2Obj } from '@/utils'
const List = [] const List = []
const count = 100 const count = 100
const baseContent = '<p>我是测试数据我是测试数据</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
List.push(Mock.mock({ List.push(Mock.mock({
id: '@increment', id: '@increment',
@@ -11,12 +14,17 @@ for (let i = 0; i < count; i++) {
author: '@first', author: '@first',
reviewer: '@first', reviewer: '@first',
title: '@title(5, 10)', title: '@title(5, 10)',
content_short: '我是测试数据',
content: baseContent,
forecast: '@float(0, 100, 2, 2)', forecast: '@float(0, 100, 2, 2)',
importance: '@integer(1, 3)', importance: '@integer(1, 3)',
'type|1': ['CN', 'US', 'JP', 'EU'], 'type|1': ['CN', 'US', 'JP', 'EU'],
'status|1': ['published', 'draft', 'deleted'], 'status|1': ['published', 'draft', 'deleted'],
display_time: '@datetime', display_time: '@datetime',
pageviews: '@integer(300, 5000)' comment_disabled: true,
pageviews: '@integer(300, 5000)',
image_uri,
platforms: ['a-platform']
})) }))
} }
@@ -45,22 +53,14 @@ export default {
getPv: () => ({ getPv: () => ({
pvData: [{ key: 'PC', pv: 1024 }, { key: 'mobile', pv: 1024 }, { key: 'ios', pv: 1024 }, { key: 'android', pv: 1024 }] pvData: [{ key: 'PC', pv: 1024 }, { key: 'mobile', pv: 1024 }, { key: 'ios', pv: 1024 }, { key: 'android', pv: 1024 }]
}), }),
getArticle: () => ({ getArticle: (config) => {
id: 120000000001, const { id } = param2Obj(config.url)
author: { key: 'mockPan' }, for (const article of List) {
source_name: '原创作者', if (article.id === +id) {
category_item: [{ key: 'global', name: '全球' }], return article
comment_disabled: true, }
content: '<p>我是测试数据我是测试数据</p><p><img class="wscnph" src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943" data-wscntype="image" data-wscnh="300" data-wscnw="400" data-mce-src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>"', }
content_short: '我是测试数据', },
display_time: +new Date(),
image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
platforms: ['a-platform'],
source_uri: 'https://github.com/PanJiaChen/vue-element-admin',
status: 'published',
tags: [],
title: 'vue-element-admin'
}),
createArticle: () => ({ createArticle: () => ({
data: 'success' data: 'success'
}), }),

View File

@@ -7,7 +7,7 @@ import { getToken } from '@/utils/auth' // getToken from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration NProgress.configure({ showSpinner: false })// NProgress Configuration
// permissiom judge function // permission judge function
function hasPermission(roles, permissionRoles) { function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (!permissionRoles) return true if (!permissionRoles) return true
@@ -31,10 +31,10 @@ router.beforeEach((to, from, next) => {
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
}) })
}).catch(() => { }).catch((err) => {
store.dispatch('FedLogOut').then(() => { store.dispatch('FedLogOut').then(() => {
Message.error('Verification failed, please login again') Message.error(err || 'Verification failed, please login again')
next({ path: '/login' }) next({ path: '/' })
}) })
}) })
} else { } else {

View File

@@ -1 +0,0 @@
module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+

View File

@@ -1 +0,0 @@
module.exports = file => () => import('@/views/' + file + '.vue')

View File

@@ -1,16 +1,13 @@
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
const _import = require('./_import_' + process.env.NODE_ENV)
// in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading;
// detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading
Vue.use(Router) Vue.use(Router)
/* Layout */ /* Layout */
import Layout from '../views/layout/Layout' import Layout from '@/views/layout/Layout'
/** note: submenu only apppear when children.length>=1 /** note: submenu only apppear when children.length>=1
* detail see https://panjiachen.github.io/vue-element-admin-site/#/router-and-nav?id=sidebar * detail see https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
**/ **/
/** /**
@@ -28,17 +25,17 @@ import Layout from '../views/layout/Layout'
} }
**/ **/
export const constantRouterMap = [ export const constantRouterMap = [
{ path: '/login', component: _import('login/index'), hidden: true }, { path: '/login', component: () => import('@/views/login/index'), hidden: true },
{ path: '/authredirect', component: _import('login/authredirect'), hidden: true }, { path: '/authredirect', component: () => import('@/views/login/authredirect'), hidden: true },
{ path: '/404', component: _import('errorPage/404'), hidden: true }, { path: '/404', component: () => import('@/views/errorPage/404'), hidden: true },
{ path: '/401', component: _import('errorPage/401'), hidden: true }, { path: '/401', component: () => import('@/views/errorPage/401'), hidden: true },
{ {
path: '', path: '',
component: Layout, component: Layout,
redirect: 'dashboard', redirect: 'dashboard',
children: [{ children: [{
path: 'dashboard', path: 'dashboard',
component: _import('dashboard/index'), component: () => import('@/views/dashboard/index'),
name: 'dashboard', name: 'dashboard',
meta: { title: 'dashboard', icon: 'dashboard', noCache: true } meta: { title: 'dashboard', icon: 'dashboard', noCache: true }
}] }]
@@ -49,10 +46,21 @@ export const constantRouterMap = [
redirect: '/documentation/index', redirect: '/documentation/index',
children: [{ children: [{
path: 'index', path: 'index',
component: _import('documentation/index'), component: () => import('@/views/documentation/index'),
name: 'documentation', name: 'documentation',
meta: { title: 'documentation', icon: 'documentation', noCache: true } meta: { title: 'documentation', icon: 'documentation', noCache: true }
}] }]
},
{
path: '/guide',
component: Layout,
redirect: '/guide/index',
children: [{
path: 'index',
component: () => import('@/views/guide/index'),
name: 'guide',
meta: { title: 'guide', icon: 'guide', noCache: true }
}]
} }
] ]
@@ -67,16 +75,28 @@ export const asyncRouterMap = [
path: '/permission', path: '/permission',
component: Layout, component: Layout,
redirect: '/permission/index', redirect: '/permission/index',
meta: { roles: ['admin'] }, // you can set roles in root nav alwaysShow: true, // will always show the root menu
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor'] // you can set roles in root nav
},
children: [{ children: [{
path: 'index', path: 'page',
component: _import('permission/index'), component: () => import('@/views/permission/page'),
name: 'permission', name: 'pagePermission',
meta: { meta: {
title: 'permission', title: 'pagePermission',
icon: 'lock',
roles: ['admin'] // or you can only set roles in sub nav roles: ['admin'] // or you can only set roles in sub nav
} }
}, {
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'directivePermission',
meta: {
title: 'directivePermission'
// if do not set roles, means: this page does not require permission
}
}] }]
}, },
@@ -85,7 +105,7 @@ export const asyncRouterMap = [
component: Layout, component: Layout,
children: [{ children: [{
path: 'index', path: 'index',
component: _import('svg-icons/index'), component: () => import('@/views/svg-icons/index'),
name: 'icons', name: 'icons',
meta: { title: 'icons', icon: 'icon', noCache: true } meta: { title: 'icons', icon: 'icon', noCache: true }
}] }]
@@ -101,19 +121,19 @@ export const asyncRouterMap = [
icon: 'component' icon: 'component'
}, },
children: [ children: [
{ path: 'tinymce', component: _import('components-demo/tinymce'), name: 'tinymce-demo', meta: { title: 'tinymce' }}, { path: 'tinymce', component: () => import('@/views/components-demo/tinymce'), name: 'tinymce-demo', meta: { title: 'tinymce' }},
{ path: 'markdown', component: _import('components-demo/markdown'), name: 'markdown-demo', meta: { title: 'markdown' }}, { path: 'markdown', component: () => import('@/views/components-demo/markdown'), name: 'markdown-demo', meta: { title: 'markdown' }},
{ path: 'json-editor', component: _import('components-demo/jsonEditor'), name: 'jsonEditor-demo', meta: { title: 'jsonEditor' }}, { path: 'json-editor', component: () => import('@/views/components-demo/jsonEditor'), name: 'jsonEditor-demo', meta: { title: 'jsonEditor' }},
{ path: 'dnd-list', component: _import('components-demo/dndList'), name: 'dndList-demo', meta: { title: 'dndList' }}, { path: 'splitpane', component: () => import('@/views/components-demo/splitpane'), name: 'splitpane-demo', meta: { title: 'splitPane' }},
{ path: 'splitpane', component: _import('components-demo/splitpane'), name: 'splitpane-demo', meta: { title: 'splitPane' }}, { path: 'avatar-upload', component: () => import('@/views/components-demo/avatarUpload'), name: 'avatarUpload-demo', meta: { title: 'avatarUpload' }},
{ path: 'avatar-upload', component: _import('components-demo/avatarUpload'), name: 'avatarUpload-demo', meta: { title: 'avatarUpload' }}, { path: 'dropzone', component: () => import('@/views/components-demo/dropzone'), name: 'dropzone-demo', meta: { title: 'dropzone' }},
{ path: 'dropzone', component: _import('components-demo/dropzone'), name: 'dropzone-demo', meta: { title: 'dropzone' }}, { path: 'sticky', component: () => import('@/views/components-demo/sticky'), name: 'sticky-demo', meta: { title: 'sticky' }},
{ path: 'sticky', component: _import('components-demo/sticky'), name: 'sticky-demo', meta: { title: 'sticky' }}, { path: 'count-to', component: () => import('@/views/components-demo/countTo'), name: 'countTo-demo', meta: { title: 'countTo' }},
{ path: 'count-to', component: _import('components-demo/countTo'), name: 'countTo-demo', meta: { title: 'countTo' }}, { path: 'mixin', component: () => import('@/views/components-demo/mixin'), name: 'componentMixin-demo', meta: { title: 'componentMixin' }},
{ path: 'mixin', component: _import('components-demo/mixin'), name: 'componentMixin-demo', meta: { title: 'componentMixin' }}, { path: 'back-to-top', component: () => import('@/views/components-demo/backToTop'), name: 'backToTop-demo', meta: { title: 'backToTop' }},
{ path: 'back-to-top', component: _import('components-demo/backToTop'), name: 'backToTop-demo', meta: { title: 'backToTop' }}, { path: 'drag-dialog', component: () => import('@/views/components-demo/dragDialog'), name: 'dragDialog-demo', meta: { title: 'dragDialog' }},
{ path: 'drag-dialog', component: _import('components-demo/dragDialog'), name: 'dragDialog-demo', meta: { title: 'dragDialog' }}, { path: 'dnd-list', component: () => import('@/views/components-demo/dndList'), name: 'dndList-demo', meta: { title: 'dndList' }},
{ path: 'drag-kanban', component: _import('components-demo/dragKanban'), name: 'dragKanban-demo', meta: { title: 'dragKanban' }} { path: 'drag-kanban', component: () => import('@/views/components-demo/dragKanban'), name: 'dragKanban-demo', meta: { title: 'dragKanban' }}
] ]
}, },
@@ -127,56 +147,116 @@ export const asyncRouterMap = [
icon: 'chart' icon: 'chart'
}, },
children: [ children: [
{ path: 'keyboard', component: _import('charts/keyboard'), name: 'keyboardChart', meta: { title: 'keyboardChart', noCache: true }}, { path: 'keyboard', component: () => import('@/views/charts/keyboard'), name: 'keyboardChart', meta: { title: 'keyboardChart', noCache: true }},
{ path: 'line', component: _import('charts/line'), name: 'lineChart', meta: { title: 'lineChart', noCache: true }}, { path: 'line', component: () => import('@/views/charts/line'), name: 'lineChart', meta: { title: 'lineChart', noCache: true }},
{ path: 'mixchart', component: _import('charts/mixChart'), name: 'mixChart', meta: { title: 'mixChart', noCache: true }} { path: 'mixchart', component: () => import('@/views/charts/mixChart'), name: 'mixChart', meta: { title: 'mixChart', noCache: true }}
]
},
{
path: '/tab',
component: Layout,
children: [{
path: 'index',
component: () => import('@/views/tab/index'),
name: 'tab',
meta: { title: 'tab', icon: 'tab' }
}]
},
{
path: '/table',
component: Layout,
redirect: '/table/complex-table',
name: 'table',
meta: {
title: 'Table',
icon: 'table'
},
children: [
{ path: 'dynamic-table', component: () => import('@/views/table/dynamicTable/index'), name: 'dynamicTable', meta: { title: 'dynamicTable' }},
{ path: 'drag-table', component: () => import('@/views/table/dragTable'), name: 'dragTable', meta: { title: 'dragTable' }},
{ path: 'inline-edit-table', component: () => import('@/views/table/inlineEditTable'), name: 'inlineEditTable', meta: { title: 'inlineEditTable' }},
{ path: 'tree-table', component: () => import('@/views/table/treeTable/treeTable'), name: 'treeTableDemo', meta: { title: 'treeTable' }},
{ path: 'custom-tree-table', component: () => import('@/views/table/treeTable/customTreeTable'), name: 'customTreeTableDemo', meta: { title: 'customTreeTable' }},
{ path: 'complex-table', component: () => import('@/views/table/complexTable'), name: 'complexTable', meta: { title: 'complexTable' }}
] ]
}, },
{ {
path: '/example', path: '/example',
component: Layout, component: Layout,
redirect: '/example/table/complex-table', redirect: '/example/list',
name: 'example', name: 'example',
meta: { meta: {
title: 'example', title: 'example',
icon: 'example' icon: 'example'
}, },
children: [ children: [
{ { path: 'create', component: () => import('@/views/example/create'), name: 'createArticle', meta: { title: 'createArticle', icon: 'edit' }},
path: '/example/table', { path: 'edit/:id(\\d+)', component: () => import('@/views/example/edit'), name: 'editArticle', meta: { title: 'editArticle', noCache: true }, hidden: true },
component: _import('example/table/index'), { path: 'list', component: () => import('@/views/example/list'), name: 'articleList', meta: { title: 'articleList', icon: 'list' }}
redirect: '/example/table/complex-table',
name: 'Table',
meta: {
title: 'Table',
icon: 'table'
},
children: [
{ path: 'dynamic-table', component: _import('example/table/dynamicTable/index'), name: 'dynamicTable', meta: { title: 'dynamicTable' }},
{ path: 'drag-table', component: _import('example/table/dragTable'), name: 'dragTable', meta: { title: 'dragTable' }},
{ path: 'inline-edit-table', component: _import('example/table/inlineEditTable'), name: 'inlineEditTable', meta: { title: 'inlineEditTable' }},
{ path: 'tree-table', component: _import('example/table/treeTable/treeTable'), name: 'treeTableDemo', meta: { title: 'treeTable' }},
{ path: 'custom-tree-table', component: _import('example/table/treeTable/customTreeTable'), name: 'customTreeTableDemo', meta: { title: 'customTreeTable' }},
{ path: 'complex-table', component: _import('example/table/complexTable'), name: 'complexTable', meta: { title: 'complexTable' }}
]
},
{ path: 'tab/index', icon: 'tab', component: _import('example/tab/index'), name: 'tab', meta: { title: 'tab' }}
] ]
}, },
{ {
path: '/form', path: '/nested',
component: Layout, component: Layout,
redirect: 'noredirect', redirect: '/nested/menu1/menu1-1',
name: 'form', name: 'nested',
meta: { meta: {
title: 'form', title: 'nested',
icon: 'form' icon: 'nested'
}, },
children: [ children: [
{ path: 'create-form', component: _import('form/create'), name: 'createForm', meta: { title: 'createForm', icon: 'table' }}, {
{ path: 'edit-form', component: _import('form/edit'), name: 'editForm', meta: { title: 'editForm', icon: 'table' }} path: 'menu1',
component: () => import('@/views/nested/menu1/index'), // Parent router-view
name: 'menu1',
meta: { title: 'menu1' },
redirect: '/nested/menu1/menu1-1',
children: [
{
path: 'menu1-1',
component: () => import('@/views/nested/menu1/menu1-1'),
name: 'menu1-1',
meta: { title: 'menu1-1' }
},
{
path: 'menu1-2',
component: () => import('@/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: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
name: 'menu1-2-1',
meta: { title: 'menu1-2-1' }
},
{
path: 'menu1-2-2',
component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
name: 'menu1-2-2',
meta: { title: 'menu1-2-2' }
}
]
},
{
path: 'menu1-3',
component: () => import('@/views/nested/menu1/menu1-3'),
name: 'menu1-3',
meta: { title: 'menu1-3' }
}
]
},
{
path: 'menu2',
name: 'menu2',
component: () => import('@/views/nested/menu2/index'),
meta: { title: 'menu2' }
}
] ]
}, },
@@ -190,8 +270,8 @@ export const asyncRouterMap = [
icon: '404' icon: '404'
}, },
children: [ children: [
{ path: '401', component: _import('errorPage/401'), name: 'page401', meta: { title: 'page401', noCache: true }}, { path: '401', component: () => import('@/views/errorPage/401'), name: 'page401', meta: { title: 'page401', noCache: true }},
{ path: '404', component: _import('errorPage/404'), name: 'page404', meta: { title: 'page404', noCache: true }} { path: '404', component: () => import('@/views/errorPage/404'), name: 'page404', meta: { title: 'page404', noCache: true }}
] ]
}, },
@@ -199,7 +279,7 @@ export const asyncRouterMap = [
path: '/error-log', path: '/error-log',
component: Layout, component: Layout,
redirect: 'noredirect', redirect: 'noredirect',
children: [{ path: 'log', component: _import('errorLog/index'), name: 'errorLog', meta: { title: 'errorLog', icon: 'bug' }}] children: [{ path: 'log', component: () => import('@/views/errorLog/index'), name: 'errorLog', meta: { title: 'errorLog', icon: 'bug' }}]
}, },
{ {
@@ -212,9 +292,9 @@ export const asyncRouterMap = [
icon: 'excel' icon: 'excel'
}, },
children: [ children: [
{ path: 'export-excel', component: _import('excel/exportExcel'), name: 'exportExcel', meta: { title: 'exportExcel' }}, { path: 'export-excel', component: () => import('@/views/excel/exportExcel'), name: 'exportExcel', meta: { title: 'exportExcel' }},
{ path: 'export-selected-excel', component: _import('excel/selectExcel'), name: 'selectExcel', meta: { title: 'selectExcel' }}, { path: 'export-selected-excel', component: () => import('@/views/excel/selectExcel'), name: 'selectExcel', meta: { title: 'selectExcel' }},
{ path: 'upload-excel', component: _import('excel/uploadExcel'), name: 'uploadExcel', meta: { title: 'uploadExcel' }} { path: 'upload-excel', component: () => import('@/views/excel/uploadExcel'), name: 'uploadExcel', meta: { title: 'uploadExcel' }}
] ]
}, },
@@ -224,27 +304,27 @@ export const asyncRouterMap = [
redirect: '/zip/download', redirect: '/zip/download',
alwaysShow: true, alwaysShow: true,
meta: { title: 'zip', icon: 'zip' }, meta: { title: 'zip', icon: 'zip' },
children: [{ path: 'download', component: _import('zip/index'), name: 'exportZip', meta: { title: 'exportZip' }}] children: [{ path: 'download', component: () => import('@/views/zip/index'), name: 'exportZip', meta: { title: 'exportZip' }}]
}, },
{ {
path: '/theme', path: '/theme',
component: Layout, component: Layout,
redirect: 'noredirect', redirect: 'noredirect',
children: [{ path: 'index', component: _import('theme/index'), name: 'theme', meta: { title: 'theme', icon: 'theme' }}] children: [{ path: 'index', component: () => import('@/views/theme/index'), name: 'theme', meta: { title: 'theme', icon: 'theme' }}]
}, },
{ {
path: '/clipboard', path: '/clipboard',
component: Layout, component: Layout,
redirect: 'noredirect', redirect: 'noredirect',
children: [{ path: 'index', component: _import('clipboard/index'), name: 'clipboardDemo', meta: { title: 'clipboardDemo', icon: 'clipboard' }}] children: [{ path: 'index', component: () => import('@/views/clipboard/index'), name: 'clipboardDemo', meta: { title: 'clipboardDemo', icon: 'clipboard' }}]
}, },
{ {
path: '/i18n', path: '/i18n',
component: Layout, component: Layout,
children: [{ path: 'index', component: _import('i18n-demo/index'), name: 'i18n', meta: { title: 'i18n', icon: 'international' }}] children: [{ path: 'index', component: () => import('@/views/i18n-demo/index'), name: 'i18n', meta: { title: 'i18n', icon: 'international' }}]
}, },
{ path: '*', redirect: '/404', hidden: true } { path: '*', redirect: '/404', hidden: true }

View File

@@ -6,11 +6,9 @@ const tagsView = {
mutations: { mutations: {
ADD_VISITED_VIEWS: (state, view) => { ADD_VISITED_VIEWS: (state, view) => {
if (state.visitedViews.some(v => v.path === view.path)) return if (state.visitedViews.some(v => v.path === view.path)) return
state.visitedViews.push({ state.visitedViews.push(Object.assign({}, view, {
name: view.name,
path: view.path,
title: view.meta.title || 'no-name' title: view.meta.title || 'no-name'
}) }))
if (!view.meta.noCache) { if (!view.meta.noCache) {
state.cachedViews.push(view.name) state.cachedViews.push(view.name)
} }
@@ -40,7 +38,7 @@ const tagsView = {
for (const i of state.cachedViews) { for (const i of state.cachedViews) {
if (i === view.name) { if (i === view.name) {
const index = state.cachedViews.indexOf(i) const index = state.cachedViews.indexOf(i)
state.cachedViews = state.cachedViews.slice(index, i + 1) state.cachedViews = state.cachedViews.slice(index, index + 1)
break break
} }
} }

View File

@@ -67,7 +67,13 @@ const user = {
reject('error') reject('error')
} }
const data = response.data const data = response.data
commit('SET_ROLES', data.roles)
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_NAME', data.name)
commit('SET_AVATAR', data.avatar) commit('SET_AVATAR', data.avatar)
commit('SET_INTRODUCTION', data.introduction) commit('SET_INTRODUCTION', data.introduction)

View File

@@ -1,18 +1,14 @@
#app { #app {
// 主体区域 // 主体区域
.main-container { .main-container {
min-height: 100%; min-height: 100%;
transition: margin-left .28s; transition: margin-left .28s;
margin-left: 180px; margin-left: 180px;
position: relative;
} }
// 侧边栏
// 侧边栏
.sidebar-container { .sidebar-container {
.horizontal-collapse-transition { transition: width 0.28s;
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
transition: width .28s;
width: 180px !important; width: 180px !important;
height: 100%; height: 100%;
position: fixed; position: fixed;
@@ -22,19 +18,33 @@
left: 0; left: 0;
z-index: 1001; z-index: 1001;
overflow: hidden; overflow: hidden;
//reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden!important;
.el-scrollbar__view {
height: 100%;
}
}
.is-horizontal {
display: none;
}
a { a {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
overflow: hidden;
} }
.svg-icon { .svg-icon {
margin-right: 16px; margin-right: 16px;
} }
.el-menu { .el-menu {
border: none; border: none;
height: 100%;
width: 100% !important; width: 100% !important;
} }
} }
.hideSidebar { .hideSidebar {
.sidebar-container { .sidebar-container {
width: 36px !important; width: 36px !important;
@@ -50,22 +60,28 @@
} }
} }
.el-submenu { .el-submenu {
overflow: hidden;
&>.el-submenu__title { &>.el-submenu__title {
padding-left: 10px !important; padding-left: 10px !important;
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
.el-submenu__icon-arrow { .el-submenu__icon-arrow {
display: none; display: none;
} }
} }
} }
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
} }
.sidebar-container .nest-menu .el-submenu>.el-submenu__title, .sidebar-container .nest-menu .el-submenu>.el-submenu__title,
.sidebar-container .el-submenu .el-menu-item { .sidebar-container .el-submenu .el-menu-item {
min-width: 180px !important; min-width: 180px !important;
@@ -84,7 +100,6 @@
margin-left: 0px; margin-left: 0px;
} }
.sidebar-container { .sidebar-container {
top: 50px;
transition: transform .28s; transition: transform .28s;
width: 180px !important; width: 180px !important;
} }
@@ -95,7 +110,6 @@
} }
} }
} }
.withoutAnimation { .withoutAnimation {
.main-container, .main-container,
.sidebar-container { .sidebar-container {

View File

@@ -11,7 +11,21 @@
opacity: 0; opacity: 0;
} }
/*fade*/ /*fade-transform*/
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/*breadcrumb transition*/
.breadcrumb-enter-active, .breadcrumb-enter-active,
.breadcrumb-leave-active { .breadcrumb-leave-active {
transition: all .5s; transition: all .5s;

View File

@@ -246,6 +246,11 @@ 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
*/
export function deepClone(source) { export function deepClone(source) {
if (!source && typeof source !== 'object') { if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'shallowClone') throw new Error('error arguments', 'shallowClone')
@@ -253,7 +258,6 @@ export function deepClone(source) {
const targetObj = source.constructor === Array ? [] : {} const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach((keys) => { Object.keys(source).forEach((keys) => {
if (source[keys] && typeof source[keys] === 'object') { if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = source[keys].constructor === Array ? [] : {}
targetObj[keys] = deepClone(source[keys]) targetObj[keys] = deepClone(source[keys])
} else { } else {
targetObj[keys] = source[keys] targetObj[keys] = source[keys]
@@ -261,3 +265,7 @@ export function deepClone(source) {
}) })
return targetObj return targetObj
} }
export function uniqueArr(arr) {
return Array.from(new Set(arr))
}

26
src/utils/permission.js Normal file
View File

@@ -0,0 +1,26 @@
import store from '@/store'
/**
* @param {Array} value
* @returns {Boolean}
* @example see @/views/permission/directive.vue
*/
export default function checkPermission(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
return false
}
return true
} else {
console.error(`need roles! Like v-permission="['admin','editor']"`)
return false
}
}

View File

@@ -13,7 +13,8 @@ const service = axios.create({
service.interceptors.request.use(config => { service.interceptors.request.use(config => {
// Do something before request is sent // Do something before request is sent
if (store.getters.token) { if (store.getters.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带token-- ['X-Token']为自定义key 请根据实际情况自行修改 // 让每个请求携带token-- ['X-Token']为自定义key 请根据实际情况自行修改
config.headers['X-Token'] = getToken()
} }
return config return config
}, error => { }, error => {
@@ -26,34 +27,40 @@ service.interceptors.request.use(config => {
service.interceptors.response.use( service.interceptors.response.use(
response => response, response => response,
/** /**
* 下面的注释为通过response自定义code来标示请求状态当code返回如下情况为权限有问题登出并返回到登录页 * 下面的注释为通过response里,自定义code来标示请求状态
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 * 当code返回如下情况则说明权限有问题登出并返回到登录页
*/ * 如想通过xmlhttprequest来状态码标识 逻辑可写在下面error中
// const res = response.data; * 以下代码均为样例,请结合自生需求加以修改,若不需要,则可删除
// if (res.code !== 20000) { */
// Message({ // response => {
// message: res.message, // const res = response.data
// type: 'error', // if (res.code !== 20000) {
// duration: 5 * 1000 // Message({
// }); // message: res.message,
// // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; // type: 'error',
// if (res.code === 50008 || res.code === 50012 || res.code === 50014) { // duration: 5 * 1000
// MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', { // })
// confirmButtonText: '重新登录', // // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
// cancelButtonText: '取消', // if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// type: 'warning' // // 请自行在引入 MessageBox
// }).then(() => { // // import { Message, MessageBox } from 'element-ui'
// store.dispatch('FedLogOut').then(() => { // MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
// location.reload();// 为了重新实例化vue-router对象 避免bug // confirmButtonText: '重新登录',
// }); // cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
// store.dispatch('FedLogOut').then(() => {
// location.reload() // 为了重新实例化vue-router对象 避免bug
// }) // })
// } // })
// return Promise.reject('error');
// } else {
// return response.data;
// } // }
// return Promise.reject('error')
// } else {
// return response.data
// }
// },
error => { error => {
console.log('err' + error)// for debug console.log('err' + error) // for debug
Message({ Message({
message: error.message, message: error.message,
type: 'error', type: 'error',

307
src/vendor/Blob.js vendored
View File

@@ -16,164 +16,161 @@
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
(function (view) { (function (view) {
"use strict"; "use strict";
view.URL = view.URL || view.webkitURL; view.URL = view.URL || view.webkitURL;
if (view.Blob && view.URL) { if (view.Blob && view.URL) {
try { try {
new Blob; new Blob;
return; return;
} catch (e) {} } catch (e) {}
}
// Internally we use a BlobBuilder implementation to base Blob off of
// in order to support older browsers that only have BlobBuilder
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function (view) {
var
get_class = function (object) {
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
},
FakeBlobBuilder = function BlobBuilder() {
this.data = [];
},
FakeBlob = function Blob(data, type, encoding) {
this.data = data;
this.size = data.length;
this.type = type;
this.encoding = encoding;
},
FBB_proto = FakeBlobBuilder.prototype,
FB_proto = FakeBlob.prototype,
FileReaderSync = view.FileReaderSync,
FileException = function (type) {
this.code = this[this.name = type];
},
file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " +
"NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" "),
file_ex_code = file_ex_codes.length,
real_URL = view.URL || view.webkitURL || view,
real_create_object_URL = real_URL.createObjectURL,
real_revoke_object_URL = real_URL.revokeObjectURL,
URL = real_URL,
btoa = view.btoa,
atob = view.atob
,
ArrayBuffer = view.ArrayBuffer,
Uint8Array = view.Uint8Array;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
} }
if (!real_URL.createObjectURL) {
// Internally we use a BlobBuilder implementation to base Blob off of URL = view.URL = {};
// in order to support older browsers that only have BlobBuilder }
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { URL.createObjectURL = function (blob) {
var var
get_class = function(object) { type = blob.type,
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; data_URI_header;
} if (type === null) {
, FakeBlobBuilder = function BlobBuilder() { type = "application/octet-stream";
this.data = []; }
} if (blob instanceof FakeBlob) {
, FakeBlob = function Blob(data, type, encoding) { data_URI_header = "data:" + type;
this.data = data; if (blob.encoding === "base64") {
this.size = data.length; return data_URI_header + ";base64," + blob.data;
this.type = type; } else if (blob.encoding === "URI") {
this.encoding = encoding; return data_URI_header + "," + decodeURIComponent(blob.data);
}
, FBB_proto = FakeBlobBuilder.prototype
, FB_proto = FakeBlob.prototype
, FileReaderSync = view.FileReaderSync
, FileException = function(type) {
this.code = this[this.name = type];
}
, file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" ")
, file_ex_code = file_ex_codes.length
, real_URL = view.URL || view.webkitURL || view
, real_create_object_URL = real_URL.createObjectURL
, real_revoke_object_URL = real_URL.revokeObjectURL
, URL = real_URL
, btoa = view.btoa
, atob = view.atob
, ArrayBuffer = view.ArrayBuffer
, Uint8Array = view.Uint8Array
;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
}
if (!real_URL.createObjectURL) {
URL = view.URL = {};
}
URL.createObjectURL = function(blob) {
var
type = blob.type
, data_URI_header
;
if (type === null) {
type = "application/octet-stream";
}
if (blob instanceof FakeBlob) {
data_URI_header = "data:" + type;
if (blob.encoding === "base64") {
return data_URI_header + ";base64," + blob.data;
} else if (blob.encoding === "URI") {
return data_URI_header + "," + decodeURIComponent(blob.data);
} if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
};
URL.revokeObjectURL = function(object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function(data/*, endings*/) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = ""
, buf = new Uint8Array(data)
, i = 0
, buf_len = buf.length
;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function(type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function() {
return "[object BlobBuilder]";
};
FB_proto.slice = function(start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length)
, type
, this.encoding
);
};
FB_proto.toString = function() {
return "[object Blob]";
};
FB_proto.close = function() {
this.size = this.data.length = 0;
};
return FakeBlobBuilder;
}(view));
view.Blob = function Blob(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
builder.append(blobParts[i]);
}
} }
return builder.getBlob(type); if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
}; };
URL.revokeObjectURL = function (object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function (data /*, endings*/ ) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = "",
buf = new Uint8Array(data),
i = 0,
buf_len = buf.length;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function (type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function () {
return "[object BlobBuilder]";
};
FB_proto.slice = function (start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length), type, this.encoding
);
};
FB_proto.toString = function () {
return "[object Blob]";
};
FB_proto.close = function () {
this.size = this.data.length = 0;
};
return FakeBlobBuilder;
}(view));
view.Blob = function Blob(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
builder.append(blobParts[i]);
}
}
return builder.getBlob(type);
};
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

View File

@@ -4,155 +4,203 @@ require('script-loader!@/vendor/Blob');
import XLSX from 'xlsx' import XLSX from 'xlsx'
function generateArray(table) { function generateArray(table) {
var out = []; var out = [];
var rows = table.querySelectorAll('tr'); var rows = table.querySelectorAll('tr');
var ranges = []; var ranges = [];
for (var R = 0; R < rows.length; ++R) { for (var R = 0; R < rows.length; ++R) {
var outRow = []; var outRow = [];
var row = rows[R]; var row = rows[R];
var columns = row.querySelectorAll('td'); var columns = row.querySelectorAll('td');
for (var C = 0; C < columns.length; ++C) { for (var C = 0; C < columns.length; ++C) {
var cell = columns[C]; var cell = columns[C];
var colspan = cell.getAttribute('colspan'); var colspan = cell.getAttribute('colspan');
var rowspan = cell.getAttribute('rowspan'); var rowspan = cell.getAttribute('rowspan');
var cellValue = cell.innerText; var cellValue = cell.innerText;
if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue; if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
//Skip ranges //Skip ranges
ranges.forEach(function (range) { ranges.forEach(function (range) {
if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) { if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null); for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
}
});
//Handle Row Span
if (rowspan || colspan) {
rowspan = rowspan || 1;
colspan = colspan || 1;
ranges.push({s: {r: R, c: outRow.length}, e: {r: R + rowspan - 1, c: outRow.length + colspan - 1}});
}
;
//Handle Value
outRow.push(cellValue !== "" ? cellValue : null);
//Handle Colspan
if (colspan) for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
} }
out.push(outRow); });
//Handle Row Span
if (rowspan || colspan) {
rowspan = rowspan || 1;
colspan = colspan || 1;
ranges.push({
s: {
r: R,
c: outRow.length
},
e: {
r: R + rowspan - 1,
c: outRow.length + colspan - 1
}
});
};
//Handle Value
outRow.push(cellValue !== "" ? cellValue : null);
//Handle Colspan
if (colspan)
for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
} }
return [out, ranges]; out.push(outRow);
}
return [out, ranges];
}; };
function datenum(v, date1904) { function datenum(v, date1904) {
if (date1904) v += 1462; if (date1904) v += 1462;
var epoch = Date.parse(v); var epoch = Date.parse(v);
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
} }
function sheet_from_array_of_arrays(data, opts) { function sheet_from_array_of_arrays(data, opts) {
var ws = {}; var ws = {};
var range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}}; var range = {
for (var R = 0; R != data.length; ++R) { s: {
for (var C = 0; C != data[R].length; ++C) { c: 10000000,
if (range.s.r > R) range.s.r = R; r: 10000000
if (range.s.c > C) range.s.c = C; },
if (range.e.r < R) range.e.r = R; e: {
if (range.e.c < C) range.e.c = C; c: 0,
var cell = {v: data[R][C]}; r: 0
if (cell.v == null) continue;
var cell_ref = XLSX.utils.encode_cell({c: C, r: R});
if (typeof cell.v === 'number') cell.t = 'n';
else if (typeof cell.v === 'boolean') cell.t = 'b';
else if (cell.v instanceof Date) {
cell.t = 'n';
cell.z = XLSX.SSF._table[14];
cell.v = datenum(cell.v);
}
else cell.t = 's';
ws[cell_ref] = cell;
}
} }
if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); };
return ws; for (var R = 0; R != data.length; ++R) {
for (var C = 0; C != data[R].length; ++C) {
if (range.s.r > R) range.s.r = R;
if (range.s.c > C) range.s.c = C;
if (range.e.r < R) range.e.r = R;
if (range.e.c < C) range.e.c = C;
var cell = {
v: data[R][C]
};
if (cell.v == null) continue;
var cell_ref = XLSX.utils.encode_cell({
c: C,
r: R
});
if (typeof cell.v === 'number') cell.t = 'n';
else if (typeof cell.v === 'boolean') cell.t = 'b';
else if (cell.v instanceof Date) {
cell.t = 'n';
cell.z = XLSX.SSF._table[14];
cell.v = datenum(cell.v);
} else cell.t = 's';
ws[cell_ref] = cell;
}
}
if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
return ws;
} }
function Workbook() { function Workbook() {
if (!(this instanceof Workbook)) return new Workbook(); if (!(this instanceof Workbook)) return new Workbook();
this.SheetNames = []; this.SheetNames = [];
this.Sheets = {}; this.Sheets = {};
} }
function s2ab(s) { function s2ab(s) {
var buf = new ArrayBuffer(s.length); var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf); var view = new Uint8Array(buf);
for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
return buf; return buf;
} }
export function export_table_to_excel(id) { export function export_table_to_excel(id) {
var theTable = document.getElementById(id); var theTable = document.getElementById(id);
var oo = generateArray(theTable); var oo = generateArray(theTable);
var ranges = oo[1]; var ranges = oo[1];
/* original data */ /* original data */
var data = oo[0]; var data = oo[0];
var ws_name = "SheetJS"; var ws_name = "SheetJS";
var wb = new Workbook(), ws = sheet_from_array_of_arrays(data); var wb = new Workbook(),
ws = sheet_from_array_of_arrays(data);
/* add ranges to worksheet */ /* add ranges to worksheet */
// ws['!cols'] = ['apple', 'banan']; // ws['!cols'] = ['apple', 'banan'];
ws['!merges'] = ranges; ws['!merges'] = ranges;
/* add worksheet to workbook */ /* add worksheet to workbook */
wb.SheetNames.push(ws_name); wb.SheetNames.push(ws_name);
wb.Sheets[ws_name] = ws; wb.Sheets[ws_name] = ws;
var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'}); var wbout = XLSX.write(wb, {
bookType: 'xlsx',
bookSST: false,
type: 'binary'
});
saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), "test.xlsx") saveAs(new Blob([s2ab(wbout)], {
type: "application/octet-stream"
}), "test.xlsx")
} }
export function export_json_to_excel({header, data, filename='excel-list', autoWidth=true}={}) { export function export_json_to_excel({
/* original data */ header,
data=[...data] data,
data.unshift(header); filename,
var ws_name = "SheetJS"; autoWidth = true
var wb = new Workbook(), ws = sheet_from_array_of_arrays(data); } = {}) {
/* original data */
filename = filename || 'excel-list'
data = [...data]
data.unshift(header);
var ws_name = "SheetJS";
var wb = new Workbook(),
ws = sheet_from_array_of_arrays(data);
if(autoWidth){ if (autoWidth) {
/*设置worksheet每列的最大宽度*/ /*设置worksheet每列的最大宽度*/
const colWidth = data.map(row => row.map(val => { const colWidth = data.map(row => row.map(val => {
/*先判断是否为null/undefined*/ /*先判断是否为null/undefined*/
if (val == null) { if (val == null) {
return {'wch': 10}; return {
} 'wch': 10
/*再判断是否为中文*/ };
else if (val.toString().charCodeAt(0) > 255) { }
return {'wch': val.toString().length * 2}; /*再判断是否为中文*/
} else { else if (val.toString().charCodeAt(0) > 255) {
return {'wch': val.toString().length}; return {
} 'wch': val.toString().length * 2
})) };
/*以第一行为初始值*/ } else {
let result = colWidth[0]; return {
for (let i = 1; i < colWidth.length; i++) { 'wch': val.toString().length
for (let j = 0; j < colWidth[i].length; j++) { };
if (result[j]['wch'] < colWidth[i][j]['wch']) { }
result[j]['wch'] = colWidth[i][j]['wch']; }))
} /*以第一行为初始值*/
let result = colWidth[0];
for (let i = 1; i < colWidth.length; i++) {
for (let j = 0; j < colWidth[i].length; j++) {
if (result[j]['wch'] < colWidth[i][j]['wch']) {
result[j]['wch'] = colWidth[i][j]['wch'];
} }
} }
ws['!cols'] = result;
} }
ws['!cols'] = result;
}
/* add worksheet to workbook */ /* add worksheet to workbook */
wb.SheetNames.push(ws_name); wb.SheetNames.push(ws_name);
wb.Sheets[ws_name] = ws; wb.Sheets[ws_name] = ws;
var wbout = XLSX.write(wb, {bookType: 'xlsx', bookSST: false, type: 'binary'}); var wbout = XLSX.write(wb, {
saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), filename + ".xlsx"); bookType: 'xlsx',
bookSST: false,
type: 'binary'
});
saveAs(new Blob([s2ab(wbout)], {
type: "application/octet-stream"
}), filename + ".xlsx");
} }

View File

@@ -14,7 +14,9 @@ export function export_txt_to_zip(th, jsonData, txtName, zipName) {
txtData += `${tempStr}\r\n` txtData += `${tempStr}\r\n`
}) })
zip.file(`${txt_name}.txt`, txtData) zip.file(`${txt_name}.txt`, txtData)
zip.generateAsync({type:"blob"}).then((blob) => { zip.generateAsync({
type: "blob"
}).then((blob) => {
saveAs(blob, `${zip_name}.zip`) saveAs(blob, `${zip_name}.zip`)
}, (err) => { }, (err) => {
alert('导出失败') alert('导出失败')

View File

@@ -34,7 +34,7 @@ export default {
}, },
clipboardSuccess() { clipboardSuccess() {
this.$message({ this.$message({
message: '复制成功', message: 'Copy successfully',
type: 'success', type: 'success',
duration: 1500 duration: 1500
}) })

View File

@@ -1,7 +1,11 @@
<template> <template>
<div class="components-container"> <div class="components-container">
<el-button type="primary" @click="dialogTableVisible = true">open a Drag Dialog</el-button> <el-button type="primary" @click="dialogTableVisible = true">open a Drag Dialog</el-button>
<el-dialog v-el-drag-dialog title="Shipping address" :visible.sync="dialogTableVisible"> <el-dialog v-el-drag-dialog @dragDialog="handleDrag" title="Shipping address" :visible.sync="dialogTableVisible">
<el-select ref="select" v-model="value" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<el-table :data="gridData"> <el-table :data="gridData">
<el-table-column property="date" label="Date" width="150"></el-table-column> <el-table-column property="date" label="Date" width="150"></el-table-column>
<el-table-column property="name" label="Name" width="200"></el-table-column> <el-table-column property="name" label="Name" width="200"></el-table-column>
@@ -20,6 +24,13 @@ export default {
data() { data() {
return { return {
dialogTableVisible: false, dialogTableVisible: false,
options: [
{ value: '选项1', label: '黄金糕' },
{ value: '选项2', label: '双皮奶' },
{ value: '选项3', label: '蚵仔煎' },
{ value: '选项4', label: '龙须面' }
],
value: '',
gridData: [{ gridData: [{
date: '2016-05-02', date: '2016-05-02',
name: 'John Smith', name: 'John Smith',
@@ -38,6 +49,12 @@ export default {
address: 'No.1518, Jinshajiang Road, Putuo District' address: 'No.1518, Jinshajiang Road, Putuo District'
}] }]
} }
},
methods: {
// v-el-drag-dialog onDrag callback function
handleDrag() {
this.$refs.select.blur()
}
} }
} }
</script> </script>

View File

@@ -2,7 +2,7 @@
<div class="components-container"> <div class="components-container">
<code>Markdown is based on <code>Markdown is based on
<a href="https://github.com/sparksuite/simplemde-markdown-editor" target="_blank">simplemde-markdown-editor</a> Simply encapsulated in Vue. <a href="https://github.com/sparksuite/simplemde-markdown-editor" target="_blank">simplemde-markdown-editor</a> Simply encapsulated in Vue.
<a target="_blank" href="https://segmentfault.com/a/1190000009762198#articleHeader14"> <a target="_blank" href="https://juejin.im/post/593121aa0ce4630057f70d35#heading-15">
相关文章 </a> 相关文章 </a>
</code> </code>
<div class="editor-container"> <div class="editor-container">

View File

@@ -7,19 +7,19 @@
</div> </div>
<div style="margin-bottom:50px;"> <div style="margin-bottom:50px;">
<el-col :span="4" class="text-center"> <el-col :span="4" class="text-center">
<router-link class="pan-btn blue-btn" to="/components/index">Components</router-link> <router-link class="pan-btn blue-btn" to="/documentation/index">Documentation</router-link>
</el-col> </el-col>
<el-col :span="4" class="text-center"> <el-col :span="4" class="text-center">
<router-link class="pan-btn light-blue-btn" to="/charts/index">Charts</router-link> <router-link class="pan-btn light-blue-btn" to="/icon/index">Icons</router-link>
</el-col> </el-col>
<el-col :span="4" class="text-center"> <el-col :span="4" class="text-center">
<router-link class="pan-btn pink-btn" to="/excel/download">Excel</router-link> <router-link class="pan-btn pink-btn" to="/excel/export-excel">Excel</router-link>
</el-col> </el-col>
<el-col :span="4" class="text-center"> <el-col :span="4" class="text-center">
<router-link class="pan-btn green-btn" to="/example/table/complex-table">Table</router-link> <router-link class="pan-btn green-btn" to="/table/complex-table">Table</router-link>
</el-col> </el-col>
<el-col :span="4" class="text-center"> <el-col :span="4" class="text-center">
<router-link class="pan-btn tiffany-btn" to="/form/edit-form">Form</router-link> <router-link class="pan-btn tiffany-btn" to="/example/create">Form</router-link>
</el-col> </el-col>
<el-col :span="4" class="text-center"> <el-col :span="4" class="text-center">
<router-link class="pan-btn yellow-btn" to="/theme/index">Theme</router-link> <router-link class="pan-btn yellow-btn" to="/theme/index">Theme</router-link>
@@ -130,12 +130,14 @@ export default {
title: [{ required: true, trigger: 'change', validator: validate }] title: [{ required: true, trigger: 'change', validator: validate }]
}, },
articleList: [ articleList: [
{ title: '基础篇', href: 'https://segmentfault.com/a/1190000009275424' }, { title: '基础篇', href: 'https://juejin.im/post/59097cd7a22b9d0065fb61d2' },
{ title: '登录权限篇', href: 'https://segmentfault.com/a/1190000009506097' }, { title: '登录权限篇', href: 'https://juejin.im/post/591aa14f570c35006961acac' },
{ title: '实战篇', href: 'https://segmentfault.com/a/1190000009762198' }, { title: '实战篇', href: 'https://juejin.im/post/593121aa0ce4630057f70d35' },
{ title: 'vueAdmin-template 篇', href: 'https://segmentfault.com/a/1190000010043013' }, { title: 'vueAdmin-template 篇', href: 'https://juejin.im/post/595b4d776fb9a06bbe7dba56' },
{ title: '自行封装 component', href: 'https://segmentfault.com/a/1190000009090836' }, { title: '自行封装 component', href: 'https://segmentfault.com/a/1190000009090836' },
{ title: '优雅的使用 icon', href: 'https://segmentfault.com/a/https://segmentfault.com/a/1190000012213278' } { title: '优雅的使用 icon', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
{ title: 'webpack4', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
{ title: 'webpack4', href: 'https://juejin.im/post/5b5d6d6f6fb9a04fea58aabc' }
] ]
} }
} }

View File

@@ -2,7 +2,7 @@
<div class="components-container"> <div class="components-container">
<code> <code>
{{$t('components.tinymceTips')}} {{$t('components.tinymceTips')}}
<a target="_blank" class="link-type" href="https://panjiachen.github.io/vue-element-admin-site/#/rich-editor"> {{$t('components.documentation')}}</a> <a target="_blank" class="link-type" href="https://panjiachen.github.io/vue-element-admin-site/component/rich-editor.html"> {{$t('components.documentation')}}</a>
</code> </code>
<div> <div>
<tinymce :height="300" v-model="content"></tinymce> <tinymce :height="300" v-model="content"></tinymce>

View File

@@ -72,7 +72,7 @@ export default {
} }
}, },
methods: { methods: {
setLocalStorgae() { setLocalStorage() {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos)) window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos))
}, },
addTodo(e) { addTodo(e) {
@@ -82,30 +82,30 @@ export default {
text, text,
done: false done: false
}) })
this.setLocalStorgae() this.setLocalStorage()
} }
e.target.value = '' e.target.value = ''
}, },
toggleTodo(val) { toggleTodo(val) {
val.done = !val.done val.done = !val.done
this.setLocalStorgae() this.setLocalStorage()
}, },
deleteTodo(todo) { deleteTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1) this.todos.splice(this.todos.indexOf(todo), 1)
this.setLocalStorgae() this.setLocalStorage()
}, },
editTodo({ todo, value }) { editTodo({ todo, value }) {
todo.text = value todo.text = value
this.setLocalStorgae() this.setLocalStorage()
}, },
clearCompleted() { clearCompleted() {
this.todos = this.todos.filter(todo => !todo.done) this.todos = this.todos.filter(todo => !todo.done)
this.setLocalStorgae() this.setLocalStorage()
}, },
toggleAll({ done }) { toggleAll({ done }) {
this.todos.forEach(todo => { this.todos.forEach(todo => {
todo.done = done todo.done = done
this.setLocalStorgae() this.setLocalStorage()
}) })
} }
}, },

View File

@@ -1,8 +1,8 @@
<template> <template>
<el-table :data="list" style="width: 100%;padding-top: 15px;"> <el-table :data="list" style="width: 100%;padding-top: 15px;">
<el-table-column label="Order_No" show-overflow-tooltip> <el-table-column label="Order_No" min-width="200">
<template slot-scope="scope"> <template slot-scope="scope">
{{scope.row.order_no}} {{scope.row.order_no | orderNoFilter}}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="Price" width="195" align="center"> <el-table-column label="Price" width="195" align="center">
@@ -34,6 +34,9 @@ export default {
pending: 'danger' pending: 'danger'
} }
return statusMap[status] return statusMap[status]
},
orderNoFilter(str) {
return str.substring(0, 30)
} }
}, },
created() { created() {
@@ -42,7 +45,7 @@ export default {
methods: { methods: {
fetchData() { fetchData() {
fetchList().then(response => { fetchList().then(response => {
this.list = response.data.items.slice(0, 7) this.list = response.data.items.slice(0, 8)
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="dashboard-editor-container"> <div class="dashboard-editor-container">
<github-corner></github-corner>
<github-corner style="position: absolute; top: 0px; border: 0; right: 0;"></github-corner>
<panel-group @handleSetLineChartData="handleSetLineChartData"></panel-group> <panel-group @handleSetLineChartData="handleSetLineChartData"></panel-group>
@@ -30,10 +31,10 @@
<el-col :xs="{span: 24}" :sm="{span: 24}" :md="{span: 24}" :lg="{span: 12}" :xl="{span: 12}" style="padding-right:8px;margin-bottom:30px;"> <el-col :xs="{span: 24}" :sm="{span: 24}" :md="{span: 24}" :lg="{span: 12}" :xl="{span: 12}" style="padding-right:8px;margin-bottom:30px;">
<transaction-table></transaction-table> <transaction-table></transaction-table>
</el-col> </el-col>
<el-col :xs="{span: 12}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 5}"> <el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
<todo-list></todo-list> <todo-list></todo-list>
</el-col> </el-col>
<el-col :xs="{span: 12}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 5}"> <el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
<box-card></box-card> <box-card></box-card>
</el-col> </el-col>
</el-row> </el-row>
@@ -41,6 +42,7 @@
</div> </div>
</template> </template>
<script> <script>
import GithubCorner from '@/components/GithubCorner' import GithubCorner from '@/components/GithubCorner'
import PanelGroup from './components/PanelGroup' import PanelGroup from './components/PanelGroup'

View File

@@ -4,10 +4,10 @@
<pan-thumb style="float: left" :image="avatar"> Your roles: <pan-thumb style="float: left" :image="avatar"> Your roles:
<span class="pan-info-roles" :key='item' v-for="item in roles">{{item}}</span> <span class="pan-info-roles" :key='item' v-for="item in roles">{{item}}</span>
</pan-thumb> </pan-thumb>
<github-corner></github-corner> <github-corner style="position: absolute; top: 0px; border: 0; right: 0;"></github-corner>
<div class="info-container"> <div class="info-container">
<span class="display_name">{{name}}</span> <span class="display_name">{{name}}</span>
<span style="font-size:20px;padding-top:20px;display:inline-block;">editor : dashboard</span> <span style="font-size:20px;padding-top:20px;display:inline-block;">Editor's Dashboard</span>
</div> </div>
</div> </div>
<div> <div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="app-container documentation-container"> <div class="app-container documentation-container">
<a class="document-btn" target='_blank' href="https://panjiachen.github.io/vue-element-admin-site/#/">{{$t('documentation.documentation')}}</a> <a class="document-btn" target='_blank' href="https://panjiachen.github.io/vue-element-admin-site/">{{$t('documentation.documentation')}}</a>
<a class="document-btn" target='_blank' href="https://github.com/PanJiaChen/vue-element-admin/">{{$t('documentation.github')}}</a> <a class="document-btn" target='_blank' href="https://github.com/PanJiaChen/vue-element-admin/">{{$t('documentation.github')}}</a>
<dropdown-menu style="float:left;margin-left:50px;" title='系列文章' :items='articleList'></dropdown-menu> <dropdown-menu style="float:left;margin-left:50px;" title='系列文章' :items='articleList'></dropdown-menu>
</div> </div>
@@ -14,12 +14,14 @@ export default {
data() { data() {
return { return {
articleList: [ articleList: [
{ title: '基础篇', href: 'https://segmentfault.com/a/1190000009275424' }, { title: '基础篇', href: 'https://juejin.im/post/59097cd7a22b9d0065fb61d2' },
{ title: '登录权限篇', href: 'https://segmentfault.com/a/1190000009506097' }, { title: '登录权限篇', href: 'https://juejin.im/post/591aa14f570c35006961acac' },
{ title: '实战篇', href: 'https://segmentfault.com/a/1190000009762198' }, { title: '实战篇', href: 'https://juejin.im/post/593121aa0ce4630057f70d35' },
{ title: 'vueAdmin-template 篇', href: 'https://segmentfault.com/a/1190000010043013' }, { title: 'vueAdmin-template 篇', href: 'https://juejin.im/post/595b4d776fb9a06bbe7dba56' },
{ title: '自行封装 component', href: 'https://segmentfault.com/a/1190000009090836' }, { title: '自行封装 component', href: 'https://segmentfault.com/a/1190000009090836' },
{ title: '优雅的使用 icon', href: 'https://segmentfault.com/a/1190000012213278' } { title: '优雅的使用 icon', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
{ title: 'webpack4', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
{ title: 'webpack4', href: 'https://juejin.im/post/5b5d6d6f6fb9a04fea58aabc' }
] ]
} }
} }

View File

@@ -6,7 +6,7 @@
<h3>{{$t('errorLog.tips')}}</h3> <h3>{{$t('errorLog.tips')}}</h3>
<code> <code>
{{$t('errorLog.description')}} {{$t('errorLog.description')}}
<a target="_blank" class="link-type" href="https://panjiachen.github.io/vue-element-admin-site/#/error?id=%e4%bb%a3%e7%a0%81"> <a target="_blank" class="link-type" href="https://panjiachen.github.io/vue-element-admin-site/guide/advanced/error.html">
{{$t('errorLog.documentation')}} {{$t('errorLog.documentation')}}
</a> </a>
</code> </code>

View File

@@ -57,6 +57,7 @@ export default {
.pan-back-btn { .pan-back-btn {
background: #008489; background: #008489;
color: #fff; color: #fff;
border: none!important;
} }
.pan-gif { .pan-gif {
margin: 0 auto; margin: 0 auto;

View File

@@ -1,11 +1,11 @@
<template> <template>
<div style="background:#f0f2f5;margin-top: -20px;height:100%;"> <div class="wscn-http404-container">
<div class="wscn-http404"> <div class="wscn-http404">
<div class="pic-404"> <div class="pic-404">
<img class="pic-404__parent" :src="img_404" alt="404"> <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" :src="img_404_cloud" alt="404"> <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" :src="img_404_cloud" alt="404"> <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" :src="img_404_cloud" alt="404"> <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div> </div>
<div class="bullshit"> <div class="bullshit">
<div class="bullshit__oops">OOPS!</div> <div class="bullshit__oops">OOPS!</div>
@@ -14,44 +14,40 @@
</div> </div>
<div class="bullshit__headline">{{ message }}</div> <div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">请检查您输入的网址是否正确请点击以下按钮返回主页或者发送错误报告</div> <div class="bullshit__info">请检查您输入的网址是否正确请点击以下按钮返回主页或者发送错误报告</div>
<a href="/" class="bullshit__return-home">返回首页</a> <a href="" class="bullshit__return-home">返回首页</a>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import img_404 from '@/assets/404_images/404.png'
import img_404_cloud from '@/assets/404_images/404_cloud.png'
export default { export default {
name: 'page404', name: 'page404',
data() {
return {
img_404,
img_404_cloud
}
},
computed: { computed: {
message() { message() {
return '特朗普说这个页面你不能进......' return '网管说这个页面你不能进......'
} }
} }
} }
</script> </script>
<style rel="stylesheet/scss" lang="scss" scoped> <style rel="stylesheet/scss" lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 { .wscn-http404 {
position: relative; position: relative;
width: 1200px; width: 1200px;
margin: 20px auto 60px; padding: 0 50px;
padding: 0 100px;
overflow: hidden; overflow: hidden;
.pic-404 { .pic-404 {
position: relative; position: relative;
float: left; float: left;
width: 600px; width: 600px;
padding: 150px 0;
overflow: hidden; overflow: hidden;
&__parent { &__parent {
width: 100%; width: 100%;
@@ -163,7 +159,7 @@ export default {
position: relative; position: relative;
float: left; float: left;
width: 300px; width: 300px;
padding: 150px 0; padding: 30px 0;
overflow: hidden; overflow: hidden;
&__oops { &__oops {
font-size: 32px; font-size: 32px;
@@ -179,7 +175,8 @@ export default {
&__headline { &__headline {
font-size: 20px; font-size: 20px;
line-height: 24px; line-height: 24px;
color: #1482f0; color: #222;
font-weight: bold;
opacity: 0; opacity: 0;
margin-bottom: 10px; margin-bottom: 10px;
animation-name: slideUp; animation-name: slideUp;

View File

@@ -3,100 +3,51 @@
<el-form class="form-container" :model="postForm" :rules="rules" ref="postForm"> <el-form class="form-container" :model="postForm" :rules="rules" ref="postForm">
<sticky :className="'sub-navbar '+postForm.status"> <sticky :className="'sub-navbar '+postForm.status">
<template v-if="fetchSuccess"> <CommentDropdown v-model="postForm.comment_disabled" />
<PlatformDropdown v-model="postForm.platforms" />
<router-link style="margin-right:15px;" v-show='isEdit' :to="{ path:'create-form'}"> <SourceUrlDropdown v-model="postForm.source_uri" />
<el-button type="info">创建form</el-button> <el-button v-loading="loading" style="margin-left: 10px;" type="success" @click="submitForm">发布
</router-link> </el-button>
<el-button v-loading="loading" type="warning" @click="draftForm">草稿</el-button>
<el-dropdown trigger="click">
<el-button plain>{{!postForm.comment_disabled?'评论已打开':'评论已关闭'}}
<i class="el-icon-caret-bottom el-icon--right"></i>
</el-button>
<el-dropdown-menu class="no-padding" slot="dropdown">
<el-dropdown-item>
<el-radio-group style="padding: 10px;" v-model="postForm.comment_disabled">
<el-radio :label="true">关闭评论</el-radio>
<el-radio :label="false">打开评论</el-radio>
</el-radio-group>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown trigger="click">
<el-button plain>平台
<i class="el-icon-caret-bottom el-icon--right"></i>
</el-button>
<el-dropdown-menu class="no-border" slot="dropdown">
<el-checkbox-group v-model="postForm.platforms" style="padding: 5px 15px;">
<el-checkbox v-for="item in platformsOptions" :label="item.key" :key="item.key">
{{item.name}}
</el-checkbox>
</el-checkbox-group>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown trigger="click">
<el-button plain>
外链
<i class="el-icon-caret-bottom el-icon--right"></i>
</el-button>
<el-dropdown-menu class="no-padding no-border" style="width:300px" slot="dropdown">
<el-form-item label-width="0px" style="margin-bottom: 0px" prop="source_uri">
<el-input placeholder="请输入内容" v-model="postForm.source_uri">
<template slot="prepend">填写url</template>
</el-input>
</el-form-item>
</el-dropdown-menu>
</el-dropdown>
<el-button v-loading="loading" style="margin-left: 10px;" type="success" @click="submitForm()">发布
</el-button>
<el-button v-loading="loading" type="warning" @click="draftForm">草稿</el-button>
</template>
<template v-else>
<el-tag>发送异常错误,刷新页面,或者联系程序员</el-tag>
</template>
</sticky> </sticky>
<div class="createPost-main-container"> <div class="createPost-main-container">
<el-row> <el-row>
<el-col :span="21">
<Warning />
<el-col :span="24">
<el-form-item style="margin-bottom: 40px;" prop="title"> <el-form-item style="margin-bottom: 40px;" prop="title">
<MDinput name="name" v-model="postForm.title" required :maxlength="100"> <MDinput name="name" v-model="postForm.title" required :maxlength="100">
标题 标题
</MDinput> </MDinput>
<span v-show="postForm.title.length>=26" class='title-prompt'>app可能会显示不全</span>
</el-form-item> </el-form-item>
<div class="postInfo-container"> <div class="postInfo-container">
<el-row> <el-row>
<el-col :span="8"> <el-col :span="8">
<el-form-item label-width="45px" label="作者:" class="postInfo-container-item"> <el-form-item label-width="45px" label="作者:" class="postInfo-container-item">
<multiselect v-model="postForm.author" :options="userLIstOptions" @search-change="getRemoteUserList" placeholder="搜索用户" selectLabel="选择" <el-select v-model="postForm.author" filterable remote placeholder="搜索用户" :remote-method="getRemoteUserList">
deselectLabel="删除" track-by="key" :internalSearch="false" label="key"> <el-option v-for="(item,index) in userListOptions" :key="item+index" :label="item" :value="item">
<span slot='noResult'>无结果</span> </el-option>
</multiselect> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="10">
<el-tooltip class="item" effect="dark" content="将替换作者" placement="top">
<el-form-item label-width="50px" label="来源:" class="postInfo-container-item">
<el-input placeholder="将替换作者" style='min-width:150px;' v-model="postForm.source_name">
</el-input>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="8">
<el-form-item label-width="80px" label="发布时间:" class="postInfo-container-item"> <el-form-item label-width="80px" label="发布时间:" class="postInfo-container-item">
<el-date-picker v-model="postForm.display_time" type="datetime" format="yyyy-MM-dd HH:mm:ss" placeholder="选择日期时间"> <el-date-picker v-model="postForm.display_time" type="datetime" format="yyyy-MM-dd HH:mm:ss" placeholder="选择日期时间">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6">
<el-form-item label-width="60px" label="重要性:" class="postInfo-container-item">
<el-rate style="margin-top:8px;" v-model="postForm.importance" :max='3' :colors="['#99A9BF', '#F7BA2A', '#FF9900']" :low-threshold="1"
:high-threshold="3">
</el-rate>
</el-form-item>
</el-col>
</el-row> </el-row>
</div> </div>
</el-col> </el-col>
@@ -109,11 +60,11 @@
</el-form-item> </el-form-item>
<div class="editor-container"> <div class="editor-container">
<tinymce :height=400 ref="editor" v-model="postForm.content"></tinymce> <Tinymce :height=400 ref="editor" v-model="postForm.content" />
</div> </div>
<div style="margin-bottom: 20px;"> <div style="margin-bottom: 20px;">
<Upload v-model="postForm.image_uri"></Upload> <Upload v-model="postForm.image_uri" />
</div> </div>
</div> </div>
</el-form> </el-form>
@@ -131,6 +82,8 @@ import Sticky from '@/components/Sticky' // 粘性header组件
import { validateURL } from '@/utils/validate' import { validateURL } from '@/utils/validate'
import { fetchArticle } from '@/api/article' import { fetchArticle } from '@/api/article'
import { userSearch } from '@/api/remoteSearch' import { userSearch } from '@/api/remoteSearch'
import Warning from './Warning'
import { CommentDropdown, PlatformDropdown, SourceUrlDropdown } from './Dropdown'
const defaultForm = { const defaultForm = {
status: 'draft', status: 'draft',
@@ -139,16 +92,16 @@ const defaultForm = {
content_short: '', // content_short: '', //
source_uri: '', // source_uri: '', //
image_uri: '', // image_uri: '', //
source_name: '', //
display_time: undefined, // display_time: undefined, //
id: undefined, id: undefined,
platforms: ['a-platform'], platforms: ['a-platform'],
comment_disabled: false comment_disabled: false,
importance: 0
} }
export default { export default {
name: 'articleDetail', name: 'articleDetail',
components: { Tinymce, MDinput, Upload, Multiselect, Sticky }, components: { Tinymce, MDinput, Upload, Multiselect, Sticky, Warning, CommentDropdown, PlatformDropdown, SourceUrlDropdown },
props: { props: {
isEdit: { isEdit: {
type: Boolean, type: Boolean,
@@ -184,14 +137,8 @@ export default {
} }
return { return {
postForm: Object.assign({}, defaultForm), postForm: Object.assign({}, defaultForm),
fetchSuccess: true,
loading: false, loading: false,
userLIstOptions: [], userListOptions: [],
platformsOptions: [
{ key: 'a-platform', name: 'a-platform' },
{ key: 'b-platform', name: 'b-platform' },
{ key: 'c-platform', name: 'c-platform' }
],
rules: { rules: {
image_uri: [{ validator: validateRequire }], image_uri: [{ validator: validateRequire }],
title: [{ validator: validateRequire }], title: [{ validator: validateRequire }],
@@ -207,17 +154,20 @@ export default {
}, },
created() { created() {
if (this.isEdit) { if (this.isEdit) {
this.fetchData() const id = this.$route.params && this.$route.params.id
this.fetchData(id)
} else { } else {
this.postForm = Object.assign({}, defaultForm) this.postForm = Object.assign({}, defaultForm)
} }
}, },
methods: { methods: {
fetchData() { fetchData(id) {
fetchArticle().then(response => { fetchArticle(id).then(response => {
this.postForm = response.data this.postForm = response.data
// Just for test
this.postForm.title += ` Article Id:${this.postForm.id}`
this.postForm.content_short += ` Article Id:${this.postForm.id}`
}).catch(err => { }).catch(err => {
this.fetchSuccess = false
console.log(err) console.log(err)
}) })
}, },
@@ -260,10 +210,7 @@ export default {
getRemoteUserList(query) { getRemoteUserList(query) {
userSearch(query).then(response => { userSearch(query).then(response => {
if (!response.data.items) return if (!response.data.items) return
console.log(response) this.userListOptions = response.data.items.map(v => v.name)
this.userLIstOptions = response.data.items.map(v => ({
key: v.name
}))
}) })
} }
} }
@@ -271,44 +218,36 @@ export default {
</script> </script>
<style rel="stylesheet/scss" lang="scss" scoped> <style rel="stylesheet/scss" lang="scss" scoped>
@import "src/styles/mixin.scss"; @import "src/styles/mixin.scss";
.title-prompt{ .createPost-container {
position: relative;
.createPost-main-container {
padding: 40px 45px 20px 50px;
.postInfo-container {
position: relative;
@include clearfix;
margin-bottom: 10px;
.postInfo-container-item {
float: left;
}
}
.editor-container {
min-height: 500px;
margin: 0 0 30px;
.editor-upload-btn-container {
text-align: right;
margin-right: 10px;
.editor-upload-btn {
display: inline-block;
}
}
}
}
.word-counter {
width: 40px;
position: absolute; position: absolute;
right: 0px; right: -10px;
font-size: 12px; top: 0px;
top:10px;
color:#ff4949;
}
.createPost-container {
position: relative;
.createPost-main-container {
padding: 40px 45px 20px 50px;
.postInfo-container {
position: relative;
@include clearfix;
margin-bottom: 10px;
.postInfo-container-item {
float: left;
}
}
.editor-container {
min-height: 500px;
margin: 0 0 30px;
.editor-upload-btn-container {
text-align: right;
margin-right: 10px;
.editor-upload-btn {
display: inline-block;
}
}
}
}
.word-counter {
width: 40px;
position: absolute;
right: -10px;
top: 0px;
}
} }
}
</style> </style>

View File

@@ -0,0 +1,31 @@
<template>
<el-dropdown trigger="click" :show-timeout="100">
<el-button plain>{{!comment_disabled?'评论已打开':'评论已关闭'}}
<i class="el-icon-caret-bottom el-icon--right"></i>
</el-button>
<el-dropdown-menu class="no-padding" slot="dropdown">
<el-dropdown-item>
<el-radio-group style="padding: 10px;" v-model="comment_disabled">
<el-radio :label="true">关闭评论</el-radio>
<el-radio :label="false">打开评论</el-radio>
</el-radio-group>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
props: ['value'],
computed: {
comment_disabled: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
}
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<el-dropdown :hide-on-click="false" :show-timeout="100" trigger="click">
<el-button plain>
平台({{platforms.length}})
<i class="el-icon-caret-bottom el-icon--right"></i>
</el-button>
<el-dropdown-menu class="no-border" slot="dropdown">
<el-checkbox-group v-model="platforms" style="padding: 5px 15px;">
<el-checkbox v-for="item in platformsOptions" :label="item.key" :key="item.key">
{{item.name}}
</el-checkbox>
</el-checkbox-group>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
props: ['value'],
data() {
return {
platformsOptions: [
{ key: 'a-platform', name: 'a-platform' },
{ key: 'b-platform', name: 'b-platform' },
{ key: 'c-platform', name: 'c-platform' }
]
}
},
computed: {
platforms: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
}
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<el-dropdown :show-timeout="100" trigger="click">
<el-button plain>
外链
<i class="el-icon-caret-bottom el-icon--right"></i>
</el-button>
<el-dropdown-menu class="no-padding no-border" style="width:400px" slot="dropdown">
<el-form-item label-width="0px" style="margin-bottom: 0px" prop="source_uri">
<el-input placeholder="请输入内容" v-model="source_uri">
<template slot="prepend">填写url</template>
</el-input>
</el-form-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
props: ['value'],
computed: {
source_uri: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
}
}
</script>

View File

@@ -0,0 +1,3 @@
export { default as CommentDropdown } from './Comment'
export { default as PlatformDropdown } from './Platform'
export { default as SourceUrlDropdown } from './SourceUrl'

View File

@@ -0,0 +1,9 @@
<template>
<p class="warn-content">
创建和编辑页面是不能被keep-alive 缓存的因为keep-alive 的include 目前不支持根据路由来缓存所以目前都是基于component name 来缓存的如果你想要实现缓存的效果可以使用localstorage 等游览器缓存方案或者不要使用keep-alive
的include直接缓存所有页面详情见
<a href="https://panjiachen.github.io/vue-element-admin-site/guide/essentials/tags-view.html"
target="_blank">文档</a>
</p>
</template>

121
src/views/example/list.vue Normal file
View File

@@ -0,0 +1,121 @@
<template>
<div class="app-container">
<el-table :data="list" v-loading.body="listLoading" border fit highlight-current-row style="width: 100%">
<el-table-column align="center" label="ID" width="80">
<template slot-scope="scope">
<span>{{scope.row.id}}</span>
</template>
</el-table-column>
<el-table-column width="180px" align="center" label="Date">
<template slot-scope="scope">
<span>{{scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}')}}</span>
</template>
</el-table-column>
<el-table-column width="120px" align="center" label="Author">
<template slot-scope="scope">
<span>{{scope.row.author}}</span>
</template>
</el-table-column>
<el-table-column width="100px" label="Importance">
<template slot-scope="scope">
<svg-icon v-for="n in +scope.row.importance" icon-class="star" class="meta-item__icon" :key="n"></svg-icon>
</template>
</el-table-column>
<el-table-column class-name="status-col" label="Status" width="110">
<template slot-scope="scope">
<el-tag :type="scope.row.status | statusFilter">{{scope.row.status}}</el-tag>
</template>
</el-table-column>
<el-table-column min-width="300px" label="Title">
<template slot-scope="scope">
<router-link class="link-type" :to="'/example/edit/'+scope.row.id">
<span>{{ scope.row.title }}</span>
</router-link>
</template>
</el-table-column>
<el-table-column align="center" label="Actions" width="120">
<template slot-scope="scope">
<router-link :to="'/example/edit/'+scope.row.id">
<el-button type="primary" size="small" icon="el-icon-edit">Edit</el-button>
</router-link>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="listQuery.page"
:page-sizes="[10,20,30, 50]" :page-size="listQuery.limit" layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
</div>
</div>
</template>
<script>
import { fetchList } from '@/api/article'
export default {
name: 'articleList',
data() {
return {
list: null,
total: 0,
listLoading: true,
listQuery: {
page: 1,
limit: 10
}
}
},
filters: {
statusFilter(status) {
const statusMap = {
published: 'success',
draft: 'info',
deleted: 'danger'
}
return statusMap[status]
}
},
created() {
this.getList()
},
methods: {
getList() {
this.listLoading = true
fetchList(this.listQuery).then(response => {
this.list = response.data.items
this.total = response.data.total
this.listLoading = false
})
},
handleSizeChange(val) {
this.listQuery.limit = val
this.getList()
},
handleCurrentChange(val) {
this.listQuery.page = val
this.getList()
}
}
}
</script>
<style scoped>
.edit-input {
padding-right: 100px;
}
.cancel-btn {
position: absolute;
right: 15px;
top: 10px;
}
</style>

View File

@@ -1,18 +0,0 @@
<template>
<transition name="fade" mode="out-in">
<keep-alive :include='cachedViews'>
<router-view></router-view>
</keep-alive>
</transition>
</template>
<script>
export default {
name: 'TableMain',
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
}
}
}
</script>

View File

@@ -11,7 +11,7 @@
</el-radio-group> </el-radio-group>
<el-button style='margin:0 0 20px 20px;' type="primary" icon="document" @click="handleDownload" :loading="downloadLoading">{{$t('excel.export')}} excel</el-button> <el-button style='margin:0 0 20px 20px;' type="primary" icon="document" @click="handleDownload" :loading="downloadLoading">{{$t('excel.export')}} excel</el-button>
<el-table :data="list" v-loading.body="listLoading" element-loading-text="拼命加载中" border fit highlight-current-row> <el-table :data="list" v-loading="listLoading" element-loading-text="拼命加载中" border fit highlight-current-row>
<el-table-column align="center" label='Id' width="95"> <el-table-column align="center" label='Id' width="95">
<template slot-scope="scope"> <template slot-scope="scope">
{{scope.$index}} {{scope.$index}}

View File

@@ -3,7 +3,7 @@
<!-- $t is vue-i18n global function to translate lang --> <!-- $t is vue-i18n global function to translate lang -->
<el-input style='width:340px;' :placeholder="$t('excel.placeholder')" prefix-icon="el-icon-document" v-model="filename"></el-input> <el-input style='width:340px;' :placeholder="$t('excel.placeholder')" prefix-icon="el-icon-document" v-model="filename"></el-input>
<el-button style='margin-bottom:20px' type="primary" icon="document" @click="handleDownload" :loading="downloadLoading">{{$t('excel.selectedExport')}}</el-button> <el-button style='margin-bottom:20px' type="primary" icon="document" @click="handleDownload" :loading="downloadLoading">{{$t('excel.selectedExport')}}</el-button>
<el-table :data="list" v-loading.body="listLoading" element-loading-text="拼命加载中" border fit highlight-current-row @selection-change="handleSelectionChange" <el-table :data="list" v-loading="listLoading" element-loading-text="拼命加载中" border fit highlight-current-row @selection-change="handleSelectionChange"
ref="multipleTable"> ref="multipleTable">
<el-table-column type="selection" align="center"></el-table-column> <el-table-column type="selection" align="center"></el-table-column>
<el-table-column align="center" label='Id' width="95"> <el-table-column align="center" label='Id' width="95">

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<upload-excel-component @on-selected-file='selected'></upload-excel-component> <upload-excel-component :on-success='handleSuccess' :before-upload="beforeUpload"></upload-excel-component>
<el-table :data="tableData" border highlight-current-row style="width: 100%;margin-top:20px;"> <el-table :data="tableData" border highlight-current-row style="width: 100%;margin-top:20px;">
<el-table-column v-for='item of tableHeader' :prop="item" :label="item" :key='item'> <el-table-column v-for='item of tableHeader' :prop="item" :label="item" :key='item'>
</el-table-column> </el-table-column>
@@ -21,9 +21,22 @@ export default {
} }
}, },
methods: { methods: {
selected(data) { beforeUpload(file) {
this.tableData = data.results const isLt1M = file.size / 1024 / 1024 < 1
this.tableHeader = data.header
if (isLt1M) {
return true
}
this.$message({
message: 'Please do not upload files larger than 1m in size.',
type: 'warning'
})
return false
},
handleSuccess({ results, header }) {
this.tableData = results
this.tableHeader = header
} }
} }
} }

View File

@@ -0,0 +1,52 @@
const steps = [
{
element: '.hamburger-container',
popover: {
title: 'Hamburger',
description: 'Open && Close sidebar',
position: 'bottom'
}
},
{
element: '.breadcrumb-container',
popover: {
title: 'Breadcrumb',
description: 'Indicate the current page location',
position: 'bottom'
}
},
{
element: '.screenfull',
popover: {
title: 'Screenfull',
description: 'Bring the page into fullscreen',
position: 'left'
}
},
{
element: '.international-icon',
popover: {
title: 'Switch language',
description: 'Switch the system language',
position: 'left'
}
},
{
element: '.theme-switch',
popover: {
title: 'Theme Switch',
description: 'Custom switch system theme',
position: 'left'
}
},
{
element: '.tags-view-container',
popover: {
title: 'Tags view',
description: 'The history of the page you visited',
position: 'bottom'
}
}
]
export default steps

34
src/views/guide/index.vue Normal file
View File

@@ -0,0 +1,34 @@
<template>
<div class="app-container">
<p class="warn-content">
{{$t('guide.description')}}
<a href="https://github.com/kamranahmedse/driver.js" target="_blank">driver.js.
</a>
</p>
<el-button icon='el-icon-question' type="primary" @click.prevent.stop="guide">{{$t('guide.button')}}</el-button>
</div>
</template>
<script>
import * as Driver from 'driver.js' // import driver.js
import 'driver.js/dist/driver.min.css' // import driver.js css
import steps from './defineSteps'
export default {
name: 'guide',
data() {
return {
driver: null
}
},
mounted() {
this.driver = new Driver()
},
methods: {
guide() {
this.driver.defineSteps(steps)
this.driver.start()
}
}
}
</script>

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="app-wrapper" :class="classObj"> <div class="app-wrapper" :class="classObj">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"></div>
<sidebar class="sidebar-container"></sidebar> <sidebar class="sidebar-container"></sidebar>
<div class="main-container"> <div class="main-container">
<navbar></navbar> <navbar></navbar>
@@ -32,10 +33,16 @@ export default {
classObj() { classObj() {
return { return {
hideSidebar: !this.sidebar.opened, hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation, withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile' mobile: this.device === 'mobile'
} }
} }
},
methods: {
handleClickOutside() {
this.$store.dispatch('closeSideBar', { withoutAnimation: false })
}
} }
} }
</script> </script>
@@ -47,5 +54,18 @@ export default {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
&.mobile.openSidebar{
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
} }
</style> </style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<section class="app-main" style="min-height: 100%"> <section class="app-main">
<transition name="fade" mode="out-in"> <transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews"> <keep-alive :include="cachedViews">
<router-view></router-view> <router-view :key="key"></router-view>
</keep-alive> </keep-alive>
</transition> </transition>
</section> </section>
@@ -14,10 +14,20 @@ export default {
computed: { computed: {
cachedViews() { cachedViews() {
return this.$store.state.tagsView.cachedViews return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.fullPath
} }
// key() {
// return this.$route.name !== undefined ? this.$route.name + +new Date() : this.$route + +new Date()
// }
} }
} }
</script> </script>
<style scoped>
.app-main {
/*84 = navbar + tags-view = 50 +34 */
min-height: calc(100vh - 84px);
position: relative;
overflow: hidden;
}
</style>

View File

@@ -1,26 +1,24 @@
<template> <template>
<div class="menu-wrapper"> <div v-if="!item.hidden&&item.children" class="menu-wrapper">
<template v-for="item in routes" v-if="!item.hidden&&item.children">
<router-link v-if="hasOneShowingChildren(item.children) && !item.children[0].children&&!item.alwaysShow" :to="item.path+'/'+item.children[0].path" <router-link v-if="hasOneShowingChild(item.children) && !onlyOneChild.children&&!item.alwaysShow" :to="resolvePath(onlyOneChild.path)">
:key="item.children[0].name"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<el-menu-item :index="item.path+'/'+item.children[0].path" :class="{'submenu-title-noDropdown':!isNest}"> <svg-icon v-if="onlyOneChild.meta&&onlyOneChild.meta.icon" :icon-class="onlyOneChild.meta.icon"></svg-icon>
<svg-icon v-if="item.children[0].meta&&item.children[0].meta.icon" :icon-class="item.children[0].meta.icon"></svg-icon> <span v-if="onlyOneChild.meta&&onlyOneChild.meta.title" slot="title">{{generateTitle(onlyOneChild.meta.title)}}</span>
<span v-if="item.children[0].meta&&item.children[0].meta.title" slot="title">{{generateTitle(item.children[0].meta.title)}}</span>
</el-menu-item> </el-menu-item>
</router-link> </router-link>
<el-submenu v-else :index="item.name||item.path" :key="item.name"> <el-submenu v-else :index="item.name||item.path">
<template slot="title"> <template slot="title">
<svg-icon v-if="item.meta&&item.meta.icon" :icon-class="item.meta.icon"></svg-icon> <svg-icon v-if="item.meta&&item.meta.icon" :icon-class="item.meta.icon"></svg-icon>
<span v-if="item.meta&&item.meta.title" slot="title">{{generateTitle(item.meta.title)}}</span> <span v-if="item.meta&&item.meta.title" slot="title">{{generateTitle(item.meta.title)}}</span>
</template> </template>
<template v-for="child in item.children" v-if="!child.hidden"> <template v-for="child in item.children" v-if="!child.hidden">
<sidebar-item :is-nest="true" class="nest-menu" v-if="child.children&&child.children.length>0" :routes="[child]" :key="child.path"></sidebar-item> <sidebar-item :is-nest="true" class="nest-menu" v-if="child.children&&child.children.length>0" :item="child" :key="child.path" :base-path="resolvePath(child.path)"></sidebar-item>
<router-link v-else :to="item.path+'/'+child.path" :key="child.name"> <router-link v-else :to="resolvePath(child.path)" :key="child.name">
<el-menu-item :index="item.path+'/'+child.path"> <el-menu-item :index="resolvePath(child.path)">
<svg-icon v-if="child.meta&&child.meta.icon" :icon-class="child.meta.icon"></svg-icon> <svg-icon v-if="child.meta&&child.meta.icon" :icon-class="child.meta.icon"></svg-icon>
<span v-if="child.meta&&child.meta.title" slot="title">{{generateTitle(child.meta.title)}}</span> <span v-if="child.meta&&child.meta.title" slot="title">{{generateTitle(child.meta.title)}}</span>
</el-menu-item> </el-menu-item>
@@ -28,34 +26,54 @@
</template> </template>
</el-submenu> </el-submenu>
</template>
</div> </div>
</template> </template>
<script> <script>
import path from 'path'
import { generateTitle } from '@/utils/i18n' import { generateTitle } from '@/utils/i18n'
export default { export default {
name: 'SidebarItem', name: 'SidebarItem',
props: { props: {
routes: { // route object
type: Array item: {
type: Object,
required: true
}, },
isNest: { isNest: {
type: Boolean, type: Boolean,
default: false default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
return {
onlyOneChild: null
} }
}, },
methods: { methods: {
hasOneShowingChildren(children) { hasOneShowingChild(children) {
const showingChildren = children.filter(item => { const showingChildren = children.filter(item => {
return !item.hidden if (item.hidden) {
return false
} else {
// temp set(will be used if only has one showing child )
this.onlyOneChild = item
return true
}
}) })
if (showingChildren.length === 1) { if (showingChildren.length === 1) {
return true return true
} }
return false return false
}, },
resolvePath(...paths) {
return path.resolve(this.basePath, ...paths)
},
generateTitle generateTitle
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<scroll-bar> <el-scrollbar wrapClass="scrollbar-wrapper">
<el-menu <el-menu
mode="vertical" mode="vertical"
:show-timeout="200" :show-timeout="200"
@@ -9,18 +9,17 @@
text-color="#bfcbd9" text-color="#bfcbd9"
active-text-color="#409EFF" active-text-color="#409EFF"
> >
<sidebar-item :routes="permission_routers"></sidebar-item> <sidebar-item v-for="route in permission_routers" :key="route.name" :item="route" :base-path="route.path"></sidebar-item>
</el-menu> </el-menu>
</scroll-bar> </el-scrollbar>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
import ScrollBar from '@/components/ScrollBar'
export default { export default {
components: { SidebarItem, ScrollBar }, components: { SidebarItem },
computed: { computed: {
...mapGetters([ ...mapGetters([
'permission_routers', 'permission_routers',

View File

@@ -2,7 +2,7 @@
<div class="tags-view-container"> <div class="tags-view-container">
<scroll-pane class='tags-view-wrapper' ref='scrollPane'> <scroll-pane class='tags-view-wrapper' ref='scrollPane'>
<router-link ref='tag' class="tags-view-item" :class="isActive(tag)?'active':''" v-for="tag in Array.from(visitedViews)" <router-link ref='tag' class="tags-view-item" :class="isActive(tag)?'active':''" v-for="tag in Array.from(visitedViews)"
:to="tag.path" :key="tag.path" @contextmenu.prevent.native="openMenu(tag,$event)"> :to="tag" :key="tag.path" @contextmenu.prevent.native="openMenu(tag,$event)">
{{generateTitle(tag.title)}} {{generateTitle(tag.title)}}
<span class='el-icon-close' @click.prevent.stop='closeSelectedTag(tag)'></span> <span class='el-icon-close' @click.prevent.stop='closeSelectedTag(tag)'></span>
</router-link> </router-link>
@@ -59,7 +59,7 @@ export default {
return false return false
}, },
isActive(route) { isActive(route) {
return route.path === this.$route.path || route.name === this.$route.name return route.path === this.$route.path
}, },
addViewTags() { addViewTags() {
const route = this.generateRoute() const route = this.generateRoute()
@@ -72,7 +72,7 @@ export default {
const tags = this.$refs.tag const tags = this.$refs.tag
this.$nextTick(() => { this.$nextTick(() => {
for (const tag of tags) { for (const tag of tags) {
if (tag.to === this.$route.path) { if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag.$el) this.$refs.scrollPane.moveToTarget(tag.$el)
break break
} }
@@ -84,7 +84,7 @@ export default {
if (this.isActive(view)) { if (this.isActive(view)) {
const latestView = views.slice(-1)[0] const latestView = views.slice(-1)[0]
if (latestView) { if (latestView) {
this.$router.push(latestView.path) this.$router.push(latestView)
} else { } else {
this.$router.push('/') this.$router.push('/')
} }
@@ -92,7 +92,7 @@ export default {
}) })
}, },
closeOthersTags() { closeOthersTags() {
this.$router.push(this.selectedTag.path) this.$router.push(this.selectedTag)
this.$store.dispatch('delOthersViews', this.selectedTag).then(() => { this.$store.dispatch('delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag() this.moveToCurrentTag()
}) })
@@ -104,7 +104,8 @@ export default {
openMenu(tag, e) { openMenu(tag, e) {
this.visible = true this.visible = true
this.selectedTag = tag this.selectedTag = tag
this.left = e.clientX const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
this.left = e.clientX - offsetLeft + 15 // 15: margin right
this.top = e.clientY this.top = e.clientY
}, },
closeMenu() { closeMenu() {

View File

@@ -1,22 +1,27 @@
<template> <template>
<div class="login-container"> <div class="login-container">
<el-form class="login-form" autoComplete="on" :model="loginForm" :rules="loginRules" ref="loginForm" label-position="left"> <el-form class="login-form" autoComplete="on" :model="loginForm" :rules="loginRules" ref="loginForm" label-position="left">
<div class="title-container"> <div class="title-container">
<h3 class="title">{{$t('login.title')}}</h3> <h3 class="title">{{$t('login.title')}}</h3>
<lang-select class="set-language"></lang-select> <lang-select class="set-language"></lang-select>
</div> </div>
<el-form-item prop="username"> <el-form-item prop="username">
<span class="svg-container svg-container_login"> <span class="svg-container svg-container_login">
<svg-icon icon-class="user" /> <svg-icon icon-class="user" />
</span> </span>
<el-input name="username" type="text" v-model="loginForm.username" autoComplete="on" placeholder="username" /> <el-input name="username" type="text" v-model="loginForm.username" autoComplete="on" :placeholder="$t('login.username')"
/>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<span class="svg-container"> <span class="svg-container">
<svg-icon icon-class="password" /> <svg-icon icon-class="password" />
</span> </span>
<el-input name="password" :type="passwordType" @keyup.enter.native="handleLogin" v-model="loginForm.password" autoComplete="on" placeholder="password" /> <el-input name="password" :type="passwordType" @keyup.enter.native="handleLogin" v-model="loginForm.password" autoComplete="on"
:placeholder="$t('login.password')" />
<span class="show-pwd" @click="showPwd"> <span class="show-pwd" @click="showPwd">
<svg-icon icon-class="eye" /> <svg-icon icon-class="eye" />
</span> </span>
@@ -137,36 +142,50 @@ export default {
</script> </script>
<style rel="stylesheet/scss" lang="scss"> <style rel="stylesheet/scss" lang="scss">
$bg:#2d3a4b; /* 修复input 背景不协调 和光标变色 */
$light_gray:#eee; /* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
$bg:#283443;
$light_gray:#eee;
$cursor: #fff;
/* reset element-ui css */ @supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
.login-container { .login-container .el-input input{
.el-input { color: $cursor;
display: inline-block; &::first-line {
height: 47px; color: $light_gray;
width: 85%;
input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: $light_gray;
height: 47px;
&:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
-webkit-text-fill-color: #fff !important;
} }
} }
} }
.el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1); /* reset element-ui css */
background: rgba(0, 0, 0, 0.1); .login-container {
border-radius: 5px; .el-input {
color: #454545; display: inline-block;
height: 47px;
width: 85%;
input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: $light_gray;
height: 47px;
caret-color: $cursor;
&:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
-webkit-text-fill-color: $cursor !important;
}
}
}
.el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
color: #454545;
}
} }
}
</style> </style>
<style rel="stylesheet/scss" lang="scss" scoped> <style rel="stylesheet/scss" lang="scss" scoped>
@@ -211,7 +230,6 @@ $light_gray:#eee;
position: relative; position: relative;
.title { .title {
font-size: 26px; font-size: 26px;
font-weight: 400;
color: $light_gray; color: $light_gray;
margin: 0px auto 40px auto; margin: 0px auto 40px auto;
text-align: center; text-align: center;

View File

@@ -0,0 +1,7 @@
<template >
<div style="padding:30px;">
<el-alert title="menu 1" :closable="false">
<router-view />
</el-alert>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template >
<div style="padding:30px;">
<el-alert title="menu 1-1" type="success" :closable="false">
<router-view />
</el-alert>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<div style="padding:30px;">
<el-alert title="menu 1-2" type="success" :closable="false">
<router-view />
</el-alert>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert title="menu 1-2-1" type="warning" :closable="false" />
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert title="menu 1-2-2" type="warning" :closable="false" />
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert title="menu 1-3" type="success" :closable="false" />
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<div style="padding:30px;">
<el-alert title="menu 2" :closable="false" />
</div>
</template>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<div style="margin-bottom:15px;">{{$t('permission.roles')}} {{roles}}</div>
{{$t('permission.switchRoles')}}
<el-radio-group v-model="switchRoles">
<el-radio-button label="editor"></el-radio-button>
<el-radio-button label="admin"></el-radio-button>
</el-radio-group>
</div>
</template>
<script>
export default {
computed: {
roles() {
return this.$store.getters.roles
},
switchRoles: {
get() {
return this.roles[0]
},
set(val) {
this.$store.dispatch('ChangeRoles', val).then(() => {
this.$emit('change')
})
}
}
}
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div class="app-container">
<switch-roles @change="handleRolesChange" />
<div :key="key" style="margin-top:30px;">
<span v-permission="['admin']" class="permission-alert">
Only
<el-tag class="permission-tag" size="small">admin</el-tag> can see this
</span>
<span v-permission="['editor']" class="permission-alert">
Only
<el-tag class="permission-tag" size="small">editor</el-tag> can see this
</span>
<span v-permission="['admin','editor']" class="permission-alert">
Both
<el-tag class="permission-tag" size="small">admin</el-tag> and
<el-tag class="permission-tag" size="small">editor</el-tag> can see this
</span>
</div>
<div style="margin-top:30px;" :key="'checkPermission'+key">
<code>In some cases it is not suitable to use v-permission, such as element Tab component which can only be achieved by manually setting the v-if.
<br> e.g.
</code>
<el-tabs type="border-card" style="width:500px;">
<el-tab-pane v-if="checkPermission(['admin'])" label="Admin">Admin can see this</el-tab-pane>
<el-tab-pane v-if="checkPermission(['editor'])" label="Editor">Editor can see this</el-tab-pane>
<el-tab-pane v-if="checkPermission(['admin','editor'])" label="Admin-OR-Editor">Both admin or editor can see this</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
import permission from '@/directive/permission/index.js' // 权限判断指令
import checkPermission from '@/utils/permission' // 权限判断函数
import SwitchRoles from './components/SwitchRoles'
export default{
name: 'directivePermission',
components: { SwitchRoles },
directives: { permission },
data() {
return {
key: 1 // 为了能每次切换权限的时候重新初始化指令
}
},
methods: {
checkPermission,
handleRolesChange() {
this.key++
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.app-container {
/deep/ .permission-alert {
width: 320px;
margin-top: 30px;
background-color: #f0f9eb;
color: #67c23a;
padding: 8px 16px;
border-radius: 4px;
display: block;
}
/deep/ .permission-tag{
background-color: #ecf5ff;
}
}
</style>

View File

@@ -1,34 +0,0 @@
<template>
<div class="app-container">
<div style="margin-bottom:15px;">{{$t('permission.roles')}} {{roles}}</div>
{{$t('permission.switchRoles')}}
<el-radio-group v-model="switchRoles">
<el-radio-button label="editor"></el-radio-button>
</el-radio-group>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default{
name: 'permission',
data() {
return {
switchRoles: ''
}
},
computed: {
...mapGetters([
'roles'
])
},
watch: {
switchRoles(val) {
this.$store.dispatch('ChangeRoles', val).then(() => {
this.$router.push({ path: '/permission/index?' + +new Date() })
})
}
}
}
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div class="app-container">
<switch-roles @change="handleRolesChange" />
</div>
</template>
<script>
import SwitchRoles from './components/SwitchRoles'
export default{
name: 'pagePermission',
components: { SwitchRoles },
methods: {
handleRolesChange() {
this.$router.push({ path: '/permission/index?' + +new Date() })
}
}
}
</script>

Some files were not shown because too many files have changed in this diff Show More