Compare commits

...

187 Commits

Author SHA1 Message Date
chenghongxing
10a7613e6e Modify: Update the README preview address and document address;
修改:更新 README 预览地址及文档地址;
2024-05-08 11:35:51 +08:00
chenghongxing
3c79f89416 fix: Modify the preview site and documentation deployment configuration; 🐛
修改:预览网站及文档部署配置;
2024-05-08 11:20:13 +08:00
chenghongxing
35f0e431b8 fix: Fixed an issue where the role array configuration did not work; 🐛
修复:角色数组配置不生效的问题;
2024-05-08 11:18:56 +08:00
chenghongxing
60be8cf4ec fix: Fixed page refresh loading exception issue; 🐛
修复:修复页面刷新加载时异常问题;
2024-05-08 11:17:35 +08:00
chenghongxing
17e41ce1fc fix: Fixed the display problem of breadcrumbs on dynamic route page; 🐛
修复:修复动态路由页面的面包屑显示问题;
2023-04-11 10:28:01 +08:00
chenghongxing
baae56f715 add: support parameterized route multi-tab title setting; #339 🐛
新增:支持带参路由多页签标题设置;
2023-04-10 23:27:37 +08:00
iczer
9a7493c99c
Merge pull request #250 from matrix-zyh/patch-1
Fix: document errors;
修复:文档错误;
2023-03-29 10:53:27 +08:00
iczer
cc60ff1a27
Merge pull request #186 from fqucuo/patch-1
Optimization: Add beforeEnter parsing to asynchronous routing;
优化:异步路由中增加对beforeEnter的解析;
2023-03-29 10:51:14 +08:00
iczer
c0e5f2ccec
Update README.md 2023-03-27 12:19:23 +08:00
iczer
3fa8995690
Update README.md 2023-03-27 12:14:59 +08:00
chenghongxing
a4d4b28ce3 fix: fixed the issue that the router permission configuration does not support arrays
; 🐛
修复:修复路由权限配置不支持数组的问题;
2023-03-15 14:50:41 +08:00
chenghongxing
d7a530db46 fix: fixed the color problem in night mode; 🐛
修复:修复黑夜模式下颜色问题;
2023-03-12 17:24:48 +08:00
iczer
7f5fbb7426 fix: fixed border color consistent with vue3 version; 🐛
修复:修正边框颜色与 vue3 版本一致;
2023-03-12 14:40:07 +08:00
iczer
6000da4220 fix: the problem of disable cache of dynamic route; 🐛
修复:动态路由配置禁用缓存无效问题;
2023-01-12 18:14:59 +08:00
iczer
c0aec854af Merge branch 'master' of https://github.com/iczer/vue-antd-admin
 Conflicts:
	yarn.lock
2022-09-21 16:26:00 +08:00
iczer
8062905b17 fix: the check problem of standard table with pagination; 🐛
修复:标准表格分页勾选问题;
2022-09-21 16:24:30 +08:00
iczer
56635a948b
Merge pull request #265 from xcchcaptain/patch-1
更新文本错误
2021-11-23 11:11:34 +08:00
iczer
2d0c6c2a2b
Merge pull request #275 from Ten-K/fix/notice
fix(HeaderNotice.vue): change loadding to loading
2021-11-23 11:10:49 +08:00
iczer
5d4bd75b22
Merge pull request #274 from ggymm/patch-1
修复混合导航,头部菜单没有选中状态问题
2021-11-23 11:10:08 +08:00
lrl
6d43e9af42 fix(HeaderNotice.vue): change loadding to loading 2021-11-13 22:08:05 +08:00
ggymm
c2d2c2c686
修复混合导航,头部菜单没有选中状态问题 2021-11-02 15:20:16 +08:00
xcchcaptain
a9b3da4d19
更新文本错误
依据上下文,此处应为“permission”
2021-09-26 11:15:12 +08:00
Matrix
baf063f9ea
onFulfilled -> onRejected
响应拦截器  - onRejected 那部分,单词写错了,大大你把写成了 onRejected 写成了 onFulfilled
2021-08-06 14:31:23 +08:00
chenghongxing
816d19f7da 修复:带查询参数的路由导致多页签出现页面多开问题; 2021-05-08 20:41:57 +08:00
chenghongxing
ce83564335 修复:异步路由部分配置不生效问题; 2021-05-08 20:41:02 +08:00
chenghongxing
1345a02cd0 update version 2021-05-06 12:19:30 +08:00
chenghongxing
449fd99f9d 新增:增加列表详情页demo、动态路由菜单demo、带参路由菜单demo; 2021-05-06 12:11:55 +08:00
chenghongxing
97a1417112 新增:路由配置增加动态路由参数支持 和 高亮菜单配置; 2021-05-06 11:13:35 +08:00
chenghongxing
ba89880736 修复:混合模式下菜单权限过滤无效问题; 2021-05-05 22:07:25 +08:00
chenghongxing
aa4e3d93d5 Merge branch 'master' of https://github.com/iczer/vue-antd-admin
 Conflicts:
	README.md
2021-04-20 12:17:58 +08:00
chenghongxing
d730f6d783 修复:npm 安装 webpack-theme-color-replacer 依赖启动报错问题; 2021-04-20 12:11:05 +08:00
iczer
8d42b936e5 Merge remote-tracking branch 'origin/master' 2021-04-20 12:07:27 +08:00
iczer
da1dafda54 修复:npm 安装 webpack-theme-color-replacer 插件启动报错问题;🐛 2021-04-20 12:07:06 +08:00
iczer
8127121ab6
Add files via upload 2021-04-12 15:53:30 +08:00
iczer
d29a14936a
Update README.md 2021-04-12 15:51:56 +08:00
chenghongxing
80e3ad42bc 新增:打赏二维码; 2021-04-12 15:39:06 +08:00
chenghongxing
bb7fa9abb6 修复:异步路由加载异常问题;
fix: the loading exception of async routes;
2021-04-05 09:51:52 +08:00
chenghongxing
990daf2d27 修复:页面布局面包屑重复问题;
fix: the repeat problem of breadcrumbs in PageLayout.vue;
2021-04-05 09:50:27 +08:00
chenghongxing
939f8640d3 修复:高级表格组件分页属性不支持 boolean 类型的问题;
fix: the pagination props of AdvancedTable.vue does not support boolean value;
2021-04-05 09:46:27 +08:00
chenghongxing
a1ae7d1e3f Merge remote-tracking branch 'origin/master' 2021-03-31 11:33:42 +08:00
chenghongxing
8ddc7c167c 修复:更改角色后菜单不能正确显示的问题;#179
fix: the menu does not display correctly after changing roles;
2021-03-12 14:45:35 +08:00
iczer
ce536b95c2
Update README.en-US.md 2021-02-25 16:38:18 +08:00
iczer
4dbbb852a9
Update README.md 2021-02-25 16:37:26 +08:00
fqucuo
f0d60a8242
异步路由中增加对beforeEnter的解析
异步路由中增加对beforeEnter的解析
2021-01-24 00:19:22 +08:00
chenghongxing
9ddd117d5e 修复:更改角色后菜单不能正确显示的问题;#179
fix: the menu does not display correctly after changing roles;
2021-01-15 09:52:20 +08:00
chenghongxing
2fc5b9d594 修复:更改角色后菜单不能正确显示的问题;#179
fix: the menu does not display correctly after changing roles;
2021-01-12 11:49:16 +08:00
chenghongxing
83c6381a4b 修复:加载异步路由不能正确提示的问题;
fix: the problem that it does not prompt correctly when loading routes;
2021-01-12 11:47:26 +08:00
chenghongxing
0c41878174 优化:切换布局时触发 resize 事件,以解决部分页面响应时布局问题;
optimize: resize event is triggered when switching layout to solve the layout problem when part of the page responds;
2021-01-12 11:45:43 +08:00
chenghongxing
a5c34a8514 优化:引入 moment 组件库中文包;
optimize: import chinese language for moment library;
2021-01-12 11:41:36 +08:00
chenghongxing
df076bda24 新增:request.js 增加请求配置参数;
feat: add configuration parameter for request.js;
2021-01-12 11:40:17 +08:00
chenghongxing
62b57a97cb 新增:增加 Mock 登录用户角色 test;
feat: add new role for login user;
2021-01-12 11:38:03 +08:00
chenghongxing
867377a6d2 优化:调整 copyright 间距;
optimize: adjust the spacing of copyright;
2020-12-18 09:26:17 +08:00
iczer
a2e5370ae8
Merge pull request #174 from AshenOneOrz/dev-ashen
refactor(querylist): 使用组件库的间隔组件包裹按钮组
2020-12-18 09:21:59 +08:00
AshenOneOrz
39b64d0704 style(pagefooter): 修改页脚 copyright 版权外观 2020-12-17 21:47:09 +08:00
AshenOneOrz
d2b2631fb1 refactor(querylist): 使用组件库的间隔组件包裹按钮组 2020-12-17 21:31:43 +08:00
chenghongxing
345b46bf6f chore: update configuration of environment; 2020-12-13 11:21:53 +08:00
chenghongxing
b0fc3a943e 修复:修改页签头标题不生效的bug;
fix: the problem that modify page title not effective;
2020-12-11 16:16:07 +08:00
chenghongxing
37f66c8786 update version 2020-12-06 16:12:10 +08:00
chenghongxing
5a333faa2b 新增:增加多页签模式下配置是否缓存页面的功能; #154
feat: add the function to configure whether to cache pages in multi tab mode;
2020-12-06 14:49:25 +08:00
chenghongxing
f2d3823069 修复:当路由有查询参数时,设置页签标题不生效的问题;🐛 #166
fix: the problem that the setting of tab title is not effective when the route has query parameters;
2020-12-06 11:31:31 +08:00
chenghongxing
33179d96b7 update docs; 2020-12-06 11:14:16 +08:00
chenghongxing
935cd77d4f 修复:标准表格不支持嵌套属性的问题;🐛
fix: the problem that StandardTable.vue not support nested attributes;
2020-12-06 10:33:02 +08:00
iczer
010ffdaacb
Merge pull request #155 from yexk/master
【进阶->国际化】文档 & 修复needTotal计算问题
2020-12-06 10:28:08 +08:00
iczer
5c6b2a2048
Merge pull request #165 from lsvih/patch-1
修复一个路由解析时候丢失 meta 值的问题
2020-12-06 10:22:21 +08:00
chenghongxing
34a76d5894 修复:更正 axios 拦截器demo; 2020-12-04 21:04:08 +08:00
lsvih
0f1a845189
Update routerUtil.js 2020-12-03 10:10:26 +08:00
chenghongxing
5de611523c 修复:弹出层挂载容器问题;🐛
fix: the problem of pop up layer mount container;
2020-12-02 22:30:49 +08:00
chenghongxing
c2915c93d3 修复:高级表格部分api默认值错误问题;🐛
fix: wrong default value of some api about AdvanceTable.vue;
2020-11-30 11:21:49 +08:00
chenghongxing
e661ae0813 修复:设置页签标题不生效的bug;🐛
fix: the problem that setting tabs title not affect;
2020-11-29 20:09:31 +08:00
chenghongxing
915c2078cb update docs 2020-11-29 14:45:05 +08:00
chenghongxing
407c2719cb update docs 2020-11-29 14:31:42 +08:00
chenghongxing
d9b5e4b766 优化:刷新页签 api 增加路由对象参数支持;
optimize: add support of route parameter for refresh page api;
2020-11-29 14:31:22 +08:00
chenghongxing
75a510edbd update version 2020-11-29 12:30:00 +08:00
chenghongxing
31e22aaf0e 修复:AdvanceTable 组件异步获取列配置不生效的问题;🐛 #159 #160 #161
fix: the async configuration of columns not to take effect in AdvanceTable.vue;
2020-11-29 11:54:48 +08:00
chenghongxing
63ea2a9459 修复:Modal 组件导致的白边问题;🐛 #162 #149
fix: white edge problems caused by Modal component;
2020-11-28 23:07:12 +08:00
chenghongxing
ae2b7a86ef 新增:动态修改标题的 api; #150
feat: add api of dynamic modify tab title;
2020-11-28 19:47:23 +08:00
chenghongxing
23b7dfe2a4 修复:混合导航模式下,菜单数据为空时导致的异常;🐛
fix: the exception caused by empty menuData in mix navigation mode;
2020-11-25 20:39:21 +08:00
chenghongxing
10296fd022 chore: update the version of ant-design-vue from 1.6.2 to 1.7.2; #156 2020-11-22 17:21:35 +08:00
chenghongxing
9e7a03fcd8 修复:菜单组件 openChang 事件的 bug;🐛 #156
fix: the problem of openChange event in menu.js;
2020-11-22 17:17:06 +08:00
chenghongxing
f74d08248e 修复:切换语言时浏览器页签标题语言不改变的问题;🐛
fix: the problem that the title language of the browser tab does not change when switching languages;
2020-11-22 16:54:10 +08:00
chenghongxing
d638eaa6bf 修复:头像列表组件 tooltip 不生效的问题; 🐛
fix: tooltip function not affect of AvatarList.vue;
2020-11-22 16:52:24 +08:00
Yexk
344ad1d1f6 fix: 修复needTotal求和的时候不支持多维数组以及字符串求和问题 2020-11-13 21:48:27 +08:00
Yexk
c82103d64d [add] i18n 2020-11-13 01:06:57 +08:00
Yexk
74c54cbf73 [add] 进阶国际化 2020-11-13 01:05:58 +08:00
chenghongxing
a8dab1687a update docs 2020-11-09 09:45:12 +08:00
iczer
19ab79c88c
Update README.md 2020-11-09 09:42:13 +08:00
iczer
2262cb98ef
Update README.en-US.md 2020-11-09 09:41:30 +08:00
iczer
7cea44c216
Update README.md 2020-11-09 09:39:59 +08:00
chenghongxing
6c79a54a53 update version 2020-11-08 16:32:11 +08:00
chenghongxing
97979c5059 fix: the problem of hiding column of AdvancedTable.vue; 🐛 #139
修复:高级表格无法隐藏列的bug;
2020-11-08 16:18:43 +08:00
chenghongxing
8130f9f250 fix: the style problem of tree checkbox when toggling theme color; 🐛
修复:切换主题色时树选择框的样式问题;
2020-11-08 14:35:52 +08:00
chenghongxing
57dc7fe2e8 fix: the style problem of tabs head; 🐛
修复:页签头样式问题;
2020-11-08 14:20:23 +08:00
chenghongxing
e841ac77fd bug: fix the problem that page content become blank when modifying the code; 🐛 #141
修复:修改代码时页面内容变为空白的问题;
2020-11-08 14:09:04 +08:00
chenghongxing
ec2c70d181 Merge branch 'master' of https://github.com/iczer/vue-antd-admin 2020-11-08 12:33:57 +08:00
chenghongxing
3a7c51cd8a bug: fix the problem that the error color be changed when toggle theme color; 🐛
修复:切换主题色时,错误色被改变的bug;
2020-11-08 12:31:09 +08:00
chenghongxing
a4b0785f0f bug: fix the problem that custom page title not effect;
修复:自定义页面标题不起作用的问题;
2020-11-08 12:20:40 +08:00
iczer
c1e956a5a0 Merge branch 'master' of https://github.com/iczer/vue-antd-admin 2020-10-30 08:54:45 +08:00
iczer
acd66d7d6c fix: redirect problem in mix navigation mode; 🐛
修复:混合模式下重定向问题;
2020-10-30 08:53:01 +08:00
chenghongxing
533890a376 update docs 2020-10-22 11:42:27 +08:00
iczer
10fc9c11b8 chore: remove useless code; 2020-10-18 19:32:28 +08:00
iczer
c4e81a1a61 feat: add function of link for out website in menu; #135
新增:菜单增加外链功能;
2020-10-18 19:26:07 +08:00
iczer
b021ce4f0b Merge branch 'master' of https://github.com/iczer/vue-antd-admin 2020-10-17 16:39:38 +08:00
iczer
59bb834da8 fix: the problem of calculating minimum height of page; 🐛
修复:页面内容最小高度计算错误问题;
2020-10-17 16:39:08 +08:00
chenghongxing
1a434dbd2a chore: update default html title; 2020-10-06 20:27:35 +08:00
chenghongxing
501bd23c20 fix: the events bug of AdvanceTable.vue;
修复:AdvanceTable.vue 组件的事件问题;
2020-10-06 16:50:25 +08:00
chenghongxing
3619242076 feat: add document for AdvanceTable.vue; 🐛
新增:给 AdvanceTable.vue 增加说明文档;
2020-10-05 16:48:50 +08:00
iczer
a4281b62dc fix: scroll problem of page; 🐛
修复:页面滚动问题;
2020-10-03 10:52:01 +08:00
iczer
6b41fa7f31 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/components/table/advance/SearchArea.vue
2020-10-03 01:30:28 +08:00
chenghongxing
463fc93af9 fix: popup container problem of AdvanceTable.vue in full screen mode; 🐛
fix:高级表格全屏模式下浮层的显示问题;
2020-10-03 01:25:52 +08:00
chenghongxing
4d69602595 fix: popup container problem of AdvanceTable.vue in full screen mode; 🐛
新增:高级表格全屏模式下浮层的显示问题;
2020-10-03 00:20:30 +08:00
chenghongxing
517c1959d8 feat: add AdvanceTable.vue component;
新增:高级表格;
2020-10-02 23:24:26 +08:00
chenghongxing
3d3e56de12 feat: add function of disable closing page;
新增:禁用关闭页面功能;
2020-09-30 21:50:20 +08:00
chenghongxing
313af63f33 feat: add function of refreshing page; #132
新增:刷新页面的功能;
2020-09-30 19:39:26 +08:00
chenghongxing
83576d88d7 feat: add function of fixing the head of tabs;
新增:固定页签头功能。
2020-09-30 16:38:37 +08:00
chenghongxing
9df2666304 fix: problem that old routes configuration not clearing when merging async routes; 🐛
修复:合并异步路由时,旧的路由配置未及时清除的问题。
2020-09-28 21:11:47 +08:00
chenghongxing
a03b37ff30 chore: update version; 2020-09-28 20:19:46 +08:00
chenghongxing
435b2ff585 chore: optimize the code of caching tabs; 2020-09-26 23:20:28 +08:00
chenghongxing
bad44562e7 feat: add function of caching tabs in multi page mode;
新增:多页签模式下增加缓存页签头功能(刷新时签头不再丢失);
2020-09-26 23:10:08 +08:00
chenghongxing
1738f601a3 chore: update yarn.lock and guards.js; 🐛 2020-09-26 17:41:48 +08:00
iczer
7b23be808f
Merge pull request #123 from guoranred/master
add nprogress and cdn assets
2020-09-26 17:35:32 +08:00
chenghongxing
d14d1ee9b8 fix: the setting of fixed width do not effect in some page; 🐛
修复:部分页面固定宽度设置不生效的bug;
2020-09-26 15:40:49 +08:00
chenghongxing
a19e8a226f chore: optimize min height of page content; 2020-09-26 15:15:40 +08:00
chenghongxing
7bfb900e59 fix: problem of tooltip color in collapsed menu; 🐛
修复:折叠菜单下的气泡提示窗文字颜色问题;
2020-09-26 15:13:38 +08:00
chenghongxing
d593670835 fix: setting of local saving do not work in some case; 🐛
修复:有些情况下本地保存配置不生效的问题;
2020-09-26 11:05:52 +08:00
chenghongxing
80baf8d202 fix: layout problem of head navigation in narrow screen; 🐛
修复:窄屏下顶部导航布局问题;
2020-09-25 22:09:55 +08:00
guoranred
dcf65935e1 optimize production environment packaging 2020-09-22 22:52:17 +08:00
chenghongxing
b8ccb5c6a1 fix: layout problem of head navigation in narrow screen; 🐛
修复:窄屏下顶部导航布局问题;
2020-09-17 13:25:27 +08:00
chenghongxing
556b5bf6fc fix: display problem of menu item's title in collapsed state; 🐛
修复:折叠状态下菜单项标题的显示问题;
2020-09-17 13:22:59 +08:00
chenghongxing
f0e9a50919 fix: problem that it's not effective of authority in button;
修复:按钮中的权限不起作用的问题;
2020-09-16 23:04:27 +08:00
guoranred
d23cfef147 Merge branch 'master' of https://github.com/guoranred/vue-antd-admin 2020-09-15 21:35:17 +08:00
guoranred
bdde606ee5 rename progress guard 2020-09-15 21:34:35 +08:00
guoranred
e2e92e9df9 fix: fixed search item 2020-09-15 09:11:07 +08:00
guoranred
4c7c5c1da1 add cdn assets 2020-09-15 00:03:10 +08:00
guoranred
e414f89e27 add nprogress 2020-09-14 23:47:33 +08:00
iczer
aa25af0252 fix: exception of saving local setting;
修复:保存配置到本地导致的异常;
2020-09-14 11:29:54 +08:00
iczer
6a0ef90d5c chore: update version of project; 2020-09-13 17:41:39 +08:00
iczer
15d18db7e0 fix: exception caused by mix layout mode; 🐛 #121
修复:混合布局模式下导致的异常;
2020-09-13 17:37:06 +08:00
iczer
c61570d2ff feat: add function of saving personal setting to local;
新增:增加保存个人主题配置到本地的功能;
2020-09-13 17:14:28 +08:00
iczer
fb146e2a36 fix: the problem z level of layout and components; 🐛
修复:布局和组件的层级问题;
2020-09-13 10:58:53 +08:00
iczer
1ebed9eaec fix: the style problem of danger button when switching theme mode; 🐛
修复:切换主题模式时危险按钮的样式问题;
2020-09-12 23:37:08 +08:00
iczer
f9aa9e2c53 fix: table header style problem in night mode; 🐛
修复:表头黑夜模式下的样式问题;
2020-09-12 21:18:07 +08:00
iczer
4877f13239 fix: style problem of SearchLayout.vue in head navigation mode; 🐛 #116
修复:搜索表单页顶部导航模式下的样式问题;
2020-09-12 19:54:09 +08:00
iczer
75c165c83f fix: the shake problem of head menu when mouse hover; 🐛 #115
修复:鼠标移入时顶部菜单抖动问题;
2020-09-12 18:35:27 +08:00
iczer
6a3ec944c6 fix: the collapsed problem of drawer menu; 🐛 #114
修复:抽屉菜单折叠问题;
2020-09-12 17:26:10 +08:00
chenghongxing
5a65032772 chore: optimize the code of page width style; 2020-09-06 12:06:20 +08:00
iczer
346c1b8a87
Merge pull request #112 from samulle/master
feat: add configuration for width of page content;  
新增:增加页面内容宽度配置;
2020-09-06 11:09:13 +08:00
iczer
a84952577f
Merge branch 'master' into master 2020-09-06 11:06:58 +08:00
chenghongxing
fd13c7d042 update docs 2020-09-05 21:56:17 +08:00
chenghongxing
8b38e77922 update docs 2020-09-05 21:07:00 +08:00
chenghongxing
8a7b82ac50 update docs 2020-09-05 12:36:37 +08:00
chenghongxing
69b514ee10 chore: optimize the code of routerUtil.js 2020-09-03 19:01:16 +08:00
chenghongxing
4856f06f41 fix: configuration problem of first route's path; 🐛
修复:一级路由 path 配置问题;
2020-09-03 18:47:03 +08:00
chenghongxing
8102c8a924 feat: add deep merge routes function for routerUtil.js;
新增:routerUtil.js 工具新增深度合并路由方法;
2020-09-03 18:24:28 +08:00
iczer
4db7c2deff fix: problem that child route can't inherit authority configuration of it's parent; 🐛
修复:子路由无法继承其父路由的权限配置的问题;
2020-09-02 18:55:28 +08:00
iczer
5ec6f73d6f fix: problem tha the authority config of route be overwritten; 🐛
修复:路由权限配置被覆盖的问题;
2020-09-01 19:59:50 +08:00
iczer
1fb75f491d feat: add function of filtering menu data through authority;
新增:增加通过权限过滤菜单数据的功能;
2020-08-31 20:51:58 +08:00
iczer
cbda23e3db fix: style problem of tooltip in setting pane; 🐛
修复:设置面板中 tooltip 组件的样式问题;
2020-08-31 12:38:49 +08:00
iczer
094935b758 feat: add mixed navigation mode; #102
新增:混合导航菜单模式;
2020-08-31 12:23:56 +08:00
samulle
b2b62914ef 增加设置面板‘流式’、‘定宽’切换界面随之修改。 2020-08-28 16:25:04 +08:00
samulle
2436182baf 增加设置面板‘流式’、‘定宽’切换界面随之修改。 2020-08-28 15:21:16 +08:00
iczer
002cf50440 update docs; 2020-08-28 14:25:54 +08:00
iczer
365dde7e66 refactor: remove enquireScreen function from device.js to util.js; 🌟
重构:把 enquireScreen 功能从 device.js 迁移至 util.js;
2020-08-28 12:18:59 +08:00
iczer
d4261bc401 refactor: config of async router and guards of router; 🌟
重构:异步路由配置和路由守卫配置;
2020-08-28 11:18:44 +08:00
iczer
88c7653dab update docs; 2020-08-28 11:09:07 +08:00
iczer
04be83ac7c fix: style problem of message component; 🐛
修复:message 组件样式问题;
2020-08-27 22:35:07 +08:00
iczer
ad11e315e6 update the version of project 2020-08-27 21:30:59 +08:00
iczer
a44c82f937 fix: the exception of authority plugin; 🐛
修复:权限认证插件异常问题;
2020-08-27 21:30:10 +08:00
iczer
902afbe47a fix: problem that title of menu item can't be hidden when using slot icon in side menu; 🐛
修复:在侧边栏菜单中使用插槽图标时,菜单项标题不能隐藏的问题;
2020-08-27 19:55:44 +08:00
iczer
3b3cd76e51 feat: add scoped slot for icon in menu.js;
新增:为 menus.js 的 icon 增加 scoped slot API;
2020-08-27 19:05:48 +08:00
iczer
58784c81fd feat: add scoped slot for icon in menu.js;
新增:为 menus.js 的 icon 增加 scoped slot API;
2020-08-27 18:55:31 +08:00
iczer
d1f84218c6 fix: problem that can't receive rejected message caused by interceptors; 🐛
修复:无法接受响应失败的消息;
2020-08-27 15:36:46 +08:00
chenghongxing
2021fb575d fix: problem that can't set roles array for route's authority; 🐛
修复:路由权限认证无法设置角色数组的问题;
2020-08-26 21:53:38 +08:00
chenghongxing
ec3e4b5571 feat: add watch method for i18n and options of menu.js;
新增:为 menu.js 组件增加 i18n 和 options 选项的监听;
2020-08-26 18:52:39 +08:00
chenghongxing
1ba8f20b76 fix: problem that the menu item can't be activated when the route changes; 🐛
修复:路由切换时,菜单项无法激活的问题;
2020-08-26 17:29:11 +08:00
iczer
431c78ff26 fix: style problem in mobile; 🐛 #108
修复:移动端样式问题;
2020-08-26 13:34:27 +08:00
iczer
51c2c354ba feat: add interceptors for axios; 🐛
新增:axios 添加拦截器配置;
2020-08-26 13:17:56 +08:00
chenghongxing
9f034092a9 feat: add configuration of hiding page title in PageLayout; #106
新增:增加隐藏 PageLayout 布局中页面标题的配置;
2020-08-25 19:22:08 +08:00
chenghongxing
de925b254e fix: style problem of dropdown components in Setting.vue; 🐛 #105
修复:设置面板里下拉组件的样式问题;
2020-08-25 18:42:18 +08:00
chenghongxing
f777920f89 fix: style problem caused by layout;
修复:布局调整导致的样式问题;
2020-08-24 15:56:08 +08:00
chenghongxing
d0281e5707 update docs 2020-08-23 17:50:23 +08:00
chenghongxing
8c3afcc3ab fix: the position problem of pop component in fixed side mode;🐛
修复:固定侧边栏模式下弹出式组件的位置问题;
2020-08-23 17:41:06 +08:00
iczer
4e29918bac fix: the i18n problem of router; 🐛
修复:路由的国际化问题;
2020-08-21 16:04:17 +08:00
iczer
e5f2eb9955 fix: exception in single tab mode; 🐛 #103
修复:单页签模式下的异常问问题;
2020-08-19 10:00:00 +08:00
iczer
93eaf9d36f fix: problem of expandedRowRender in StandardTable.vue; 🐛
修复:StandardTable.vue 组件展开行插槽问题;
2020-08-18 10:51:44 +08:00
iczer
5b5a5ea3ef feat: add responsive support for slots of StandardTable.vue; 🌟
新增:StandardTable.vue 组件的插槽增加响应式支持;
2020-08-18 09:37:19 +08:00
iczer
b2317e3209 fix: the problem that select events can't be triggered in menu.js component; 🐛 #99
修复:menu.js 组件中 select 事件无法触发的问题;
2020-08-12 13:19:29 +08:00
iczer
088b36aec1 feat: add IInput.vue component; 🌟
新增:IInput.vue 组件;
2020-08-12 11:25:18 +08:00
97 changed files with 3987 additions and 561 deletions

4
.env
View File

@ -1,6 +1,10 @@
VUE_APP_PUBLIC_PATH=/
VUE_APP_NAME=Admin
VUE_APP_ROUTES_KEY=admin.routes
VUE_APP_PERMISSIONS_KEY=admin.permissions
VUE_APP_ROLES_KEY=admin.roles
VUE_APP_USER_KEY=admin.user
VUE_APP_SETTING_KEY=admin.setting
VUE_APP_TBAS_KEY=admin.tabs
VUE_APP_TBAS_TITLES_KEY=admin.tabs.titles
VUE_APP_API_BASE_URL=http://api.iczer.com

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ selenium-debug.log
*.njsproj
*.sln
package-lock.json
.env.production.local

View File

@ -16,9 +16,9 @@ Multiple theme modes available
![image](./src/assets/img/preview-nine.png)
</div>
- Previewhttps://iczer.gitee.io/vue-antd-admin
- Documentationhttps://iczer.gitee.io/vue-antd-admin-docs
- FAQhttps://iczer.github.io/vue-antd-admin/start/faq.html
- Previewhttps://vue-antd-admin.pages.dev
- Documentationhttps://doc.vue-antd-admin.pages.dev
- FAQhttps://doc.vue-antd-admin.pages.dev/start/faq.html
- Mirror Repo in Chinahttps://gitee.com/iczer/vue-antd-admin
## Browsers support
@ -43,11 +43,11 @@ $ yarn serve
$ npm install
$ npm run serve
```
More instructions at [documentation](https://iczer.github.io/vue-antd-admin).
More instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs).
## Contributing
Any type of contribution is welcome, here are some examples of how you may contribute to this project: :star2:
- Use Vue Antd Admin in your daily work.
- Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions.
- Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code.
- Join the community and share your experiences with us. QQ Group: 610090280
- Join the community and share your experiences with us. QQ Group:942083829、812277510(full)、610090280(full)

View File

@ -1,3 +1,5 @@
简体中文 | [English](./README.en-US.md)
<h1 align="center">Vue Antd Admin</h1>
@ -6,6 +8,12 @@
[Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 实现版本
开箱即用的中后台前端/设计解决方案
:star::star::star:
vue3 版本现已推出,更名为
[stepin-template](https://github.com/stepui/stepin-template),欢迎体验,
[立即前往](https://github.com/stepui/stepin-template)
--
[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)
[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)
[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)
@ -16,9 +24,9 @@
![image](./src/assets/img/preview-nine.png)
</div>
- 预览地址https://iczer.gitee.io/vue-antd-admin
- 使用文档https://iczer.gitee.io/vue-antd-admin-docs
- 常见问题https://iczer.github.io/vue-antd-admin/start/faq.html
- 预览地址https://vue-antd-admin.pages.dev
- 使用文档https://doc.vue-antd-admin.pages.dev
- 常见问题https://doc.vue-antd-admin.pages.dev/start/faq.html
- 国内镜像https://gitee.com/iczer/vue-antd-admin
## 浏览器支持
@ -43,11 +51,18 @@ $ yarn serve
$ npm install
$ npm run serve
```
更多信息参考 [使用文档](https://iczer.github.io/vue-antd-admin)
更多信息参考 [使用文档](https://iczer.gitee.io/vue-antd-admin-docs)
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2:
- 在你的公司或个人项目中使用 Vue Antd Admin。
- 通过 [Issue](https://github.com/iczer/vue-antd-admin/issues) 报告:bug:或进行咨询。
- 提交 [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) 改进 Admin 的代码。
- 加入社群与小伙伴们一同交流心得。QQ群610090280
- 加入社群与小伙伴们一同交流心得。QQ群942083829、 812277510已满、610090280已满
## 打赏
如果该项目对您有所帮助,可以请作者喝一杯咖啡。
<p>
<img src="./src/assets/img/alipay.png" width="320px" style="display: inline-block;" />
<img src="./src/assets/img/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px;" />
</p>

View File

@ -1,5 +1,13 @@
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
const plugins = []
if (IS_PROD) {
plugins.push('transform-remove-console')
}
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
],
plugins
}

View File

@ -1,7 +1,7 @@
module.exports = {
title: 'Vue Antd Admin',
description: 'Vue Antd Admin',
base: '/vue-antd-admin-docs/',
base: '/',
head: [
['link', { rel: 'icon', href: '/favicon.ico' }]
],
@ -36,7 +36,8 @@ module.exports = {
title: '进阶',
collapsable: false,
children: [
'/advance/i18n', '/advance/async', '/advance/authority', '/advance/error'
'/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors',
'/advance/api'
]
},
{

42
docs/advance/api.md Normal file
View File

@ -0,0 +1,42 @@
---
title: 全局API
lang: zn-CN
---
# 全局API
我们提供了一些全局Api在日常功能开发中或许会有帮助它们均被绑定到了页面组件或子组件实例上。
在组件内可以直接通过`this.$[apiName]`的方式调用。如下:
## 多页签
### $closePage(closeRoute, nextRoute)
该api用于关闭当前已打开的页签接收两个参数
* **closeRoute**
要关闭的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
* **nextRoute**
关闭页签要后跳转的 route 对象,可不传,不传则会自动选择打开页签(临近原则)。
### $refreshPage(route)
该api用于刷新路由对应的页签接收一个参数
* **route**
要刷新的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
### $openPage(route, title)
该api用于打开一个新页签接收两个参数
* **route**
要打开的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
* **title**
设置打开页签的标题,可不传。
### $setPageTitle(route, title)
该api用于设置页签的标题接收两个参数
* **route**
要设置的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
* **title**
页签的标题。
## 权限
### $auth(check, type)
该api可以用于操作权限校验接收两个参数
* **check**
需要要校验的操作权限
* **type**
操作权限校验类别,可选 `permission``role`,即通过权限校验还是角色进行校验,可不传(不传的话,会对两种类型都进行匹配,任意一种匹配成功即校验通过)。

View File

@ -24,7 +24,7 @@ module.exports = {
}
```
### 注册路由组件
基础路由组件包含路由基本配置和对应的视图组件,我们统一在 `/router/router.map.js` 文件中注册它们。它和正常的路由配置基本无异,相当于把完整的路由拆分成单个的路由配置进行注册,为后面的路由动态配置打好基础。
基础路由组件包含路由基本配置和对应的视图组件,我们统一在 `/router/async/router.map.js` 文件中注册它们。它和正常的路由配置基本无异,相当于把完整的路由拆分成单个的路由配置进行注册,为后面的路由动态配置打好基础。
一个单独的路由组件注册示例如下:
```jsx
registerName: { //路由组件注册名称,唯一标识
@ -108,7 +108,7 @@ export default routerMap
```
:::
### 配置基本路由
如果没有任何路由你的应用是无法访问的所以我们需要在本地配置一些基本的路由比如登录页、404、403 等。你可以在 `/router/config.async.js` 文件中配置一些本地必要的路由。如下:
如果没有任何路由你的应用是无法访问的所以我们需要在本地配置一些基本的路由比如登录页、404、403 等。你可以在 `/router/async/config.async.js` 文件中配置一些本地必要的路由。如下:
```js
const routesConfig = [
'login', //匹配 router.map.js 中注册的 registerName = login 的路由
@ -163,25 +163,25 @@ export default options
那么我们就需要先从后端服务获取异步路由配置,后端返回的异步路由配置 `routesConfig` 是一个异步路由配置数组, 应当如下格式:
```jsx
[{
router: 'root', //匹配 /router/router.map.js 中注册名 registerName = root 的路由
router: 'root', //匹配 router.map.js 中注册名 registerName = root 的路由
children: [ //root 路由的子路由配置
{
router: 'dashboard', //匹配 /router/router.map.js 中注册名 registerName = dashboard 的路由
router: 'dashboard', //匹配 router.map.js 中注册名 registerName = dashboard 的路由
children: ['workplace', 'analysis'], //dashboard 路由的子路由配置,依次匹配 registerName 为 workplace 和 analysis 的路由
},
{
router: 'form', //匹配 /router/router.map.js 中注册名 registerName = form 的路由
router: 'form', //匹配 router.map.js 中注册名 registerName = form 的路由
children: [ //form 路由的子路由配置
'basicForm', //匹配 /router/router.map.js 中注册名 registerName = basicForm 的路由
'stepForm', //匹配 /router/router.map.js 中注册名 registerName = stepForm 的路由
'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由
'stepForm', //匹配 router.map.js 中注册名 registerName = stepForm 的路由
{
router: 'advanceForm', //匹配 /router/router.map.js 中注册名 registerName = advanceForm 的路由
router: 'advanceForm', //匹配 router.map.js 中注册名 registerName = advanceForm 的路由
path: 'advance' //重写 advanceForm 路由的 path 属性
}
]
},
{
router: 'basicForm', //匹配 /router/router.map.js 中注册名 registerName = basicForm 的路由
router: 'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由
name: '验权表单', //重写 basicForm 路由的 name 属性
icon: 'file-excel', //重写 basicForm 路由的 icon 属性
authority: 'form' //重写 basicForm 路由的 authority 属性
@ -197,24 +197,21 @@ export default options
```js {3}
getRoutesConfig().then(result => {
const routesConfig = result.data.data
loadRoutes({router: this.$router, store: this.$store, i18n: this.$i18n}, routesConfig)
loadRoutes(routesConfig)
})
```
至此,异步路由的加载就完成了,你可以访问异步加载的路由了。
:::tip
上面获取异步路由的代码,在 /pages/login/Login.vue 文件中可以找到。
loadRoutes 方法会合并 /router/config.async.js 文件中配置的基本路由。
loadRoutes 方法会合并 /router/async/config.async.js 文件中配置的基本路由。
:::
:::details 点击查看 loadRoutes 的详细代码
```js
/**
* 加载路由
* @param router 应用路由实例
* @param store 应用的 vuex.store 实例
* @param i18n 应用的 vue-i18n 实例
* @param routesConfig 路由配置
*/
function loadRoutes({router, store, i18n}, routesConfig) {
function loadRoutes(routesConfig) {
// 如果 routesConfig 有值,则更新到本地,否则从本地获取
if (routesConfig) {
store.commit('account/setRoutesConfig', routesConfig)

View File

@ -27,11 +27,11 @@ role = {
Vue Antd Admin 的 `权限/permission` 也包含 `id``operation` 两个属性。其中 `id``权限/permission` 的 id`operation``权限/permission` 下的操作权限,是一个字符串数组。
```js
permission = {
id: 'form', //角色ID
id: 'form', //权限ID
operation: ['add', 'delete', 'edit', 'close'] //权限下的操作权限
}
```
你也可以设置 role 的值为字符串,比如 permission = 'form', 它等同于:
你也可以设置 permission 的值为字符串,比如 permission = 'form', 它等同于:
```js
permission = {
id: 'form'

109
docs/advance/guard.md Normal file
View File

@ -0,0 +1,109 @@
---
title: 路由守卫
lang: zn-CN
---
# 路由守卫
Vue Antd Admin 使用 vue-router 实现路由导航功能,因此可以为路由配置一些守卫。
我们统一把导航守卫配置在 router/guards.js 文件中。
## 前置守卫
Vue Antd Admin 为每个前置导航守卫函数注入 to,from,next,options 四个参数:
* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)
* `from: Route`: 当前导航正要离开的路由对象
* `next: Function`: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。详情查看 [Vue Router #导航守卫](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,是登录拦截导航守卫的定义
```js
const loginGuard = (to, from, next, options) => {
const {message} = options
if (!loginIgnore.includes(to) && !checkAuthorization()) {
message.warning('登录已失效,请重新登录')
next({path: '/login'})
} else {
next()
}
}
```
## 后置守卫
你也可以定义后置导航守卫Vue Antd Admin 为每个后置导航函数注入 to,from,options 三个参数:
* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)
* `from: Route`: 当前导航正要离开的路由对象
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,是一个后置导航守卫的定义
```js
const afterGuard = (to, from, options) => {
const {store, message} = options
// 做些什么
message.info('do something')
}
```
## 导出守卫配置
定义好导航守卫后,只需按照类别在 guard.js 中导出即可。分为两类,`前置守卫``后置守卫`。如下:
```js
export default {
beforeEach: [loginGuard, authorityGuard],
afterEach: [afterGuard]
}
```
:::details 点击查看完整的导航守卫配置
```js
import {loginIgnore} from '@/router/index'
import {checkAuthorization} from '@/utils/request'
/**
* 登录守卫
* @param to
* @param form
* @param next
* @param options
*/
const loginGuard = (to, from, next, options) => {
const {message} = options
if (!loginIgnore.includes(to) && !checkAuthorization()) {
message.warning('登录已失效,请重新登录')
next({path: '/login'})
} else {
next()
}
}
/**
* 权限守卫
* @param to
* @param form
* @param next
* @param options
*/
const authorityGuard = (to, from, next, options) => {
const {store, message} = options
const permissions = store.getters['account/permissions']
const roles = store.getters['account/roles']
if (!hasAuthority(to, permissions, roles)) {
message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`)
next({path: '/403'})
} else {
next()
}
}
/**
* 后置守卫
* @param to
* @param form
* @param options
*/
const afterGuard = (to, from, options) => {
const {store, message} = options
// 做些什么
message.info('do something')
}
export default {
beforeEach: [loginGuard, authorityGuard],
afterEach: [afterGuard]
}
```
:::

View File

@ -3,5 +3,127 @@ title: 国际化
lang: zn-CN
---
# 国际化
vue-antd-admin 采用 [vue-i18n](https://kazupon.github.io/vue-i18n/) 插件来实现国际化,该项目已经内置并且加载好了基础配置。可以直接上手使用。
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
> 如果你还没有看快速入门,请先移步查看: [页面 -> i18n国际化配置](../develop/page.html#i18n国际化配置)
## 菜单和路由
### 默认情况
如果你没有对菜单进行国际化配置admin 默认会从路由数据中提取数据作为国际化配置。route.name 作为中文语言route.path 作为英文语言。
国际化提取函数定义在 `@/utils/i18n.js` 文件中,会在路由加载时调用,如下:
```js
/**
* 从路由提取国际化数据
* @param i18n
* @param routes
*/
function mergeI18nFromRoutes(i18n, routes) {
formatFullPath(routes)
const CN = generateI18n(new Object(), routes, 'name')
const US = generateI18n(new Object(), routes, 'path')
i18n.mergeLocaleMessage('CN', CN)
i18n.mergeLocaleMessage('US', US)
const messages = routesI18n.messages
Object.keys(messages).forEach(lang => {
i18n.mergeLocaleMessage(lang, messages[lang])
})
}
```
### 自定义
如果你想自定义菜单国际化数据,可在 `@/router/i18n.js` 文件中配置。我们以路由的 path 作为 key嵌套path 的写法也会被解析name 作为 国际化语言的值。
假设你有一个路由的配置如下:
```js
[{
path: 'parent',
...
children: [{
path: 'self',
...
}]
}]
or
[{
path: 'other',
...
children: [{
path: '/parent/self', // 在国际化配置中 key 会解析为 parent.self
...
}]
}]
```
那么你需要在 `@/router/i18n.js` 中这样配置:
```jsx
messages: {
CN: {
parent: {
name: '父級菜單',
self: {name: '菜單名'},
},
US: {
parent: {
name: 'parent menu',
self: {name: 'menu name'},
},
HK: {
parent: {
name: '父級菜單',
self: {name: '菜單名'},
},
```
## 添加语言
首先在 `@/layouts/header/AdminHeader.vue` ,新增一门语言 (多个同理)。
```vue {15}
<template>
...
</template>
<script>
...
export default {
...
data() {
return {
langList: [
{key: 'CN', name: '简体中文', alias: '简体'},
{key: 'HK', name: '繁體中文', alias: '繁體'},
{key: 'US', name: 'English', alias: 'English'},
// 新增一个语言选项, key是i18n的索引name是菜单显示名称
{key: 'JP', name: 'Japanese', alias: 'Japanese'}
],
searchActive: false
}
},
}
</script>
```
> TIP: 后续开发建议把这里改成动态配置的方式!
然后开始往 `@/router/i18n.js``@/pages/你的页面/i18n.js` 里面分别添加上语言的翻译。
```vue {12,13,14}
module.exports = {
messages: {
CN: {
home: {name: '首页'},
},
US: {
home: {name: 'home'},
},
HK: {
home: {name: '首頁'},
},
JP: {
home: {name: '最初のページ'},
},
}
}
```
> Notice: 更多用法请移步到 [vue-i18n](https://kazupon.github.io/vue-i18n/) 。

View File

@ -0,0 +1,131 @@
---
title: 拦截器配置
lang: zn-CN
---
# 拦截器配置
Vue Antd Admin 基于 aixos 封装了 http 通信功能,我们可以为 http 请求响应配置一些拦截器。拦截器统一配置在 /utils/axios-interceptors.js 文件中。
## 请求拦截器
你可以为每个请求拦截器配置 `onFulfilled``onRejected` 两个钩子函数。
### onFulfilled
我们会为 onFulfilled 钩子函数注入 config 和 options 两个参数:
* `config: AxiosRequestConfig`: axios 请求配置,详情参考 [axios 请求配置](http://www.axios-js.com/zh-cn/docs/#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE)
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
### onRejected
我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数:
* `error: Error`: axios 请求错误对象
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,为一个完整的请求拦截器配置:
```js
const tokenCheck = {
// 发送请求之前做些什么
onFulfilled(config, options) {
const {message} = options
const {url, xsrfCookieName} = config
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
message.warning('认证 token 已过期,请重新登录')
}
return config
},
// 请求出错时做点什么
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
```
## 响应拦截器
响应拦截器也同样可以配置 `onFulfilled``onRejected` 两个钩子函数。
### onFulfilled
我们会为 onFulfilled 钩子函数注入 response 和 options 两个参数:
* `response: AxiosResponse`: axios 响应对象,详情参考 [axios 响应对象](http://www.axios-js.com/zh-cn/docs/#%E5%93%8D%E5%BA%94%E7%BB%93%E6%9E%84)
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
### onRejected
我们会为 onRejected 钩子函数注入 error 和 options 两个参数:
* `error: Error`: axios 请求错误对象
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,为一个完整的响应拦截器配置:
```js
const resp401 = {
// 响应数据之前做点什么
onFulfilled(response, options) {
const {message} = options
if (response.status === 401) {
message.error('无此接口权限')
}
return response
},
// 响应出错时做点什么
onRejected(error, options) {
const {message} = options
if (response.status === 401) {
message.error('无此接口权限')
}
return Promise.reject(error)
}
}
```
## 导出拦截器
定义好拦截器后,只需在 axios-interceptors.js 文件中导出即可。分为两类,`请求拦截器``响应拦截器`。如下:
```js
export default {
request: [tokenCheck], // 请求拦截
response: [resp401] // 响应拦截
}
```
:::details 点击查看完整的拦截器配置示例
```js
import Cookie from 'js-cookie'
// 401拦截
const resp401 = {
onFulfilled(response, options) {
const {message} = options
if (response.status === 401) {
message.error('无此接口权限')
}
return response
},
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
const resp403 = {
onFulfilled(response, options) {
const {message} = options
if (response.status === 403) {
message.error(`请求被拒绝`)
}
return response
}
}
const reqCommon = {
onFulfilled(config, options) {
const {message} = options
const {url, xsrfCookieName} = config
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
message.warning('认证 token 已过期,请重新登录')
}
return config
},
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
export default {
request: [reqCommon], // 请求拦截
response: [resp401, resp403] // 响应拦截
}
```
:::

76
docs/advance/login.md Normal file
View File

@ -0,0 +1,76 @@
---
title: 登录认证
lang: zn-CN
---
# 登录认证
Vue Antd Admin 使用 js-cookie.js 管理用户的 token结合 axios 配置,可以为每个请求头加上 token 信息。
## token名称
后端系统通常会从请求 header 中获取用户的 token因此我们需要配置好 token 名称,好让后端能正确的识别到用户 token。
Vue Antd Admin 默认token 名称为 `Authorization`,你可以在 /utils/request.js 中修改它。
```js{5}
import axios from 'axios'
import Cookie from 'js-cookie'
// 跨域认证信息 header 名
const xsrfHeaderName = 'Authorization'
...
```
## token 设置
调用登录接口后拿到用户的 token 和 token 过期时间(如无过期时间,可忽略),并使用 /utils/request.js #setAuthorization 方法保存token。
```js{5}
import {setAuthorization} from '@/utils/request'
login(name, password).then(res => {
const {token, expireAt} = res.data
setAuthorization({token, expireAt: new Date(expireAt)})
})
```
## token 校验
Vue Antd Admin 默认添加了登录导航守卫如检查到本地cookie 中不包含 token 信息,则会拦截跳转至登录页。你可以在 /router/index.js 中配置
不需要登录拦截的路由
```js
// 不需要登录拦截的路由配置
const loginIgnore = {
names: ['404', '403'], //根据路由名称匹配
paths: ['/login'], //根据路由fullPath匹配
/**
* 判断路由是否包含在该配置中
* @param route vue-router 的 route 对象
* @returns {boolean}
*/
includes(route) {
return this.names.includes(route.name) || this.paths.includes(route.path)
}
}
```
或者在 /router/guards.js 中移出登录守卫
```diff
...
export default {
- beforeEach: [loginGuard, authorityGuard, redirectGuard],
+ beforeEach: [authorityGuard, redirectGuard],
afterEach: []
}
```
## Api
### setAuthorization(auth, authType)
来源:/utils/request.js
该方法用于保存用户 token接收两个参数:
* **auth**
认证信息,包含 token、expireAt 等认证数据。
* **authType**
认证类型,默认为 `AUTH_TYPE.BEARER`AUTH_TYPE.BEARER 默认会给token 加上 Bearer 识别前缀),可根据自己的认证类型自行扩展。
### checkAuthorization(authType)
该方法用于校验用户 token 是否过期,接收一个参数:
* **authType**
认证类型,默认为 `AUTH_TYPE.BEARER`
### removeAuthorization(authType)
该方法用于移出用户本地存储的 token接收一个参数:
* **authType**
认证类型,默认为 `AUTH_TYPE.BEARER`
:::tip
以上 Api 均可在 /utils/request.js 文件中找到。
:::

10
docs/advance/skill.md Normal file
View File

@ -0,0 +1,10 @@
---
title: 108个小技巧
lang: zn-CN
---
# 108个小技巧
## 自定义菜单icon
## 隐藏页面标题
## 关闭页签API
## 权限校验PI

View File

@ -5,4 +5,4 @@ lang: zh-CN
# 社区
## 交流学习
### QQ群610090280
### QQ群812277510、610090280(已满)

View File

@ -1,29 +1,32 @@
{
"name": "vue-antd-admin",
"version": "0.2.1",
"version": "0.7.4",
"homepage": "https://iczer.github.io/vue-antd-admin",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"serve": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
"lint": "vue-cli-service lint",
"predeploy": "yarn build",
"deploy": "gh-pages -d dist -b pages -r https://gitee.com/iczer/vue-antd-admin.git",
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs",
"docs:deploy": "vuepress build docs && gh-pages -d docs/.vuepress/dist -b master -r https://gitee.com/iczer/vue-antd-admin-docs.git"
"deploy": "gh-pages -d dist -b pages -r https://github.com/iczer/vue-antd-admin.git",
"docs:dev": "export NODE_OPTIONS=--openssl-legacy-provider && vuepress dev docs",
"docs:build": "export NODE_OPTIONS=--openssl-legacy-provider && vuepress build docs",
"predocs:deploy": "yarn docs:build",
"docs:deploy": "gh-pages -d docs/.vuepress/dist -b doc -r https://github.com/iczer/vue-antd-admin.git"
},
"dependencies": {
"@antv/data-set": "^0.11.4",
"animate.css": "^4.1.0",
"ant-design-vue": "^1.6.2",
"ant-design-vue": "1.7.2",
"axios": "^0.19.2",
"clipboard": "^2.0.6",
"core-js": "^3.6.5",
"date-fns": "^2.14.0",
"enquire.js": "^2.1.6",
"highlight.js": "^10.2.1",
"js-cookie": "^2.2.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"viser-vue": "^2.4.8",
"vue": "^2.6.11",
"vue-i18n": "^8.18.2",
@ -38,7 +41,9 @@
"@vue/cli-service": "^4.4.0",
"@vuepress/plugin-back-to-top": "^1.5.2",
"babel-eslint": "^10.1.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"babel-polyfill": "^6.26.0",
"compression-webpack-plugin": "^2.0.0",
"deepmerge": "^4.2.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
@ -49,7 +54,7 @@
"vue-cli-plugin-style-resources-loader": "^0.1.4",
"vue-template-compiler": "^2.6.11",
"vuepress": "^1.5.2",
"webpack-theme-color-replacer": "^1.3.12",
"webpack-theme-color-replacer": "1.3.18",
"whatwg-fetch": "^3.0.0"
},
"eslintConfig": {

View File

@ -5,13 +5,23 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<title><%= process.env.VUE_APP_NAME %></title>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
</head>
<body class="beauty-scroll">
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<div id="popContainer" class="beauty-scroll" style="height: 100vh; overflow-y: scroll">
<div id="app"></div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,12 +1,12 @@
<template>
<a-config-provider :locale="locale">
<a-config-provider :locale="locale" :get-popup-container="popContainer">
<router-view/>
</a-config-provider>
</template>
<script>
import enquireScreen from './utils/device'
import {mapState} from 'vuex'
import {enquireScreen} from './utils/util'
import {mapState, mapMutations} from 'vuex'
import themeUtil from '@/utils/themeUtil';
import {getI18nKey} from '@/utils/routerUtil'
@ -20,9 +20,7 @@ export default {
created () {
this.setHtmlTitle()
this.setLanguage(this.lang)
enquireScreen(isMobile => {
this.$store.commit('setting/setDevice', isMobile)
})
enquireScreen(isMobile => this.setDevice(isMobile))
},
mounted() {
this.setWeekModeTheme(this.weekMode)
@ -33,27 +31,28 @@ export default {
},
lang(val) {
this.setLanguage(val)
this.setHtmlTitle()
},
$route() {
this.setHtmlTitle()
},
'theme.mode': function(val) {
let closeMessage = this.$message.loading(`您选择了主题模式 ${val}, 正在切换...`)
themeUtil.changeThemeColor(this.theme.color, val).then(() => {
setTimeout(closeMessage, 1000)
})
themeUtil.changeThemeColor(this.theme.color, val).then(closeMessage)
},
'theme.color': function(val) {
let closeMessage = this.$message.loading(`您选择了主题色 ${val}, 正在切换...`)
themeUtil.changeThemeColor(val, this.theme.mode).then(() => {
setTimeout(closeMessage, 1000)
})
themeUtil.changeThemeColor(val, this.theme.mode).then(closeMessage)
},
'layout': function() {
window.dispatchEvent(new Event('resize'))
}
},
computed: {
...mapState('setting', ['theme', 'weekMode', 'lang'])
...mapState('setting', ['layout', 'theme', 'weekMode', 'lang'])
},
methods: {
...mapMutations('setting', ['setDevice']),
setWeekModeTheme(weekMode) {
if (weekMode) {
document.body.classList.add('week-mode')
@ -81,6 +80,9 @@ export default {
const key = route.path === '/' ? 'home.name' : getI18nKey(route.matched[route.matched.length - 1].path)
document.title = process.env.VUE_APP_NAME + ' | ' + this.$t(key)
},
popContainer() {
return document.getElementById("popContainer")
}
}
}
</script>

BIN
src/assets/img/alipay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

19
src/bootstrap.js vendored
View File

@ -1,4 +1,7 @@
import {loadRoutes, loginGuard, authorityGuard} from '@/utils/routerUtil'
import {loadRoutes, loadGuards, setAppOptions} from '@/utils/routerUtil'
import {loadInterceptors} from '@/utils/request'
import guards from '@/router/guards'
import interceptors from '@/utils/axios-interceptors'
/**
* 启动引导方法
@ -6,13 +9,17 @@ import {loadRoutes, loginGuard, authorityGuard} from '@/utils/routerUtil'
* @param router 应用的路由实例
* @param store 应用的 vuex.store 实例
* @param i18n 应用的 vue-i18n 实例
* @param i18n 应用的 message 实例
*/
function bootstrap({router, store, i18n}) {
function bootstrap({router, store, i18n, message}) {
// 设置应用配置
setAppOptions({router, store, i18n})
// 加载 axios 拦截器
loadInterceptors(interceptors, {router, store, i18n, message})
// 加载路由
loadRoutes({router, store, i18n})
// 添加路由守卫
loginGuard(router)
authorityGuard(router, store)
loadRoutes()
// 加载路由守卫
loadGuards(guards, {router, store, i18n, message})
}
export default bootstrap

View File

@ -4,7 +4,16 @@ const patternTypes = [String, RegExp, Array]
function matches (pattern, name) {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
if (pattern.indexOf(name) > -1) {
return true
} else {
for (let item of pattern) {
if (isRegExp(item) && item.test(name)) {
return true
}
}
return false
}
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
@ -18,6 +27,13 @@ function getComponentName (opts) {
return opts && (opts.Ctor.options.name || opts.tag)
}
function getComponentKey (vnode) {
const {componentOptions, key} = vnode
return key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: key + componentOptions.Ctor.cid
}
function getFirstComponentChild (children) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
@ -35,13 +51,23 @@ function pruneCache (keepAliveInstance, filter) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
const componentKey = getComponentKey(cachedNode)
if (name && !filter(name, componentKey)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry2(cache, key, keys) {
const cached = cache[key]
if (cached) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
function pruneCacheEntry (cache, key, keys, current) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
@ -61,16 +87,16 @@ export default {
props: {
include: patternTypes,
exclude: patternTypes,
excludeKeys: patternTypes,
max: [String, Number],
clearCaches: Array
},
watch: {
clearCaches: function(val) {
if (val && val.length > 0) {
const {cache, keys} = this
val.forEach(key => {
pruneCacheEntry(cache, key, keys, this._vnode)
pruneCacheEntry2(cache, key, keys)
})
this.$emit('clear', [])
}
@ -90,10 +116,13 @@ export default {
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
pruneCache(this, (name) => !matches(val, name))
})
this.$watch('excludeKeys', val => {
pruneCache(this, (name, key) => !matches(val, key))
})
},
@ -104,12 +133,14 @@ export default {
if (componentOptions) {
// check pattern
const name = getComponentName(componentOptions)
const { include, exclude } = this
const componentKey = getComponentKey(vnode)
const { include, exclude, excludeKeys } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
(exclude && name && matches(exclude, name)) ||
(excludeKeys && componentKey && matches(excludeKeys, componentKey))
) {
return vnode
}
@ -119,7 +150,7 @@ export default {
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
: vnode.key + componentOptions.Ctor.cid
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest

View File

@ -1,5 +1,5 @@
<template>
<a-tooltip :title="title">
<a-tooltip :title="title" :overlayStyle="{zIndex: 2001}">
<div class="img-check-box" @click="toggle">
<img :src="img" />
<div v-if="sChecked" class="check-item">

View File

@ -0,0 +1,66 @@
<template>
<a-input
:addon-after="addonAfter"
:addon-before="addonBefore"
:default-value="defaultValue"
:disabled="disabled"
:id="id"
:max-length="maxLength"
:prefix="prefix"
:size="size"
:suffix="suffix || lenSuffix"
:type="type"
:allow-clear="allowClear"
v-model="sValue"
:value="value"
@change="onChange"
@input="onInput"
@pressEnter="onPressEnter"
@keydown="onKeydown"
>
<template :slot="slot" v-for="slot in Object.keys($slots)">
<slot :name="slot"></slot>
</template>
</a-input>
</template>
<script>
export default {
name: 'IInput',
model: {
prop: 'value',
event: 'change.value'
},
props: ['addonAfter', 'addonBefore', 'defaultValue', 'disabled', 'id', 'maxLength', 'prefix', 'size', 'suffix', 'type', 'value', 'allowClear'],
data() {
return {
sValue: this.value || this.defaultValue || ''
}
},
watch: {
value(val) {
this.sValue = val
}
},
computed: {
lenSuffix() {
return this.maxLength && `${(this.sValue + '').length}/${this.maxLength}`
}
},
methods: {
onChange(e) {
this.$emit('change', e)
this.$emit('change.value', e.target.value)
},
onInput(e) {
this.$emit('input', e)
},
onPressEnter(e) {
this.$emit('pressEnter', e)
},
onKeydown(e) {
this.$emit('keydown', e)
}
}
}
</script>

View File

@ -33,6 +33,7 @@ export default {
left: 0,
top: 0,
target: null,
meta: null,
selectedKeys: []
}
},
@ -45,14 +46,12 @@ export default {
}
},
created () {
const clickHandler = () => this.closeMenu()
const contextMenuHandler = e => this.setPosition(e)
window.addEventListener('click', clickHandler)
window.addEventListener('contextmenu', contextMenuHandler)
this.$emit('hook:beforeDestroy', () => {
window.removeEventListener('click', clickHandler)
window.removeEventListener('contextmenu', contextMenuHandler)
})
window.addEventListener('click', this.closeMenu)
window.addEventListener('contextmenu', this.setPosition)
},
beforeDestroy() {
window.removeEventListener('click', this.closeMenu)
window.removeEventListener('contextmenu', this.setPosition)
},
methods: {
closeMenu () {
@ -62,9 +61,10 @@ export default {
this.left = e.clientX
this.top = e.clientY
this.target = e.target
this.meta = e.meta
},
handleClick ({ key }) {
this.$emit('select', key, this.target)
this.$emit('select', key, this.target, this.meta)
this.closeMenu()
}
}

View File

@ -38,6 +38,26 @@ import {getI18nKey} from '@/utils/routerUtil'
const {Item, SubMenu} = Menu
const resolvePath = (path, params = {}) => {
let _path = path
Object.entries(params).forEach(([key, value]) => {
_path = _path.replace(new RegExp(`:${key}`, 'g'), value)
})
return _path
}
const toRoutesMap = (routes) => {
const map = {}
routes.forEach(route => {
map[route.fullPath] = route
if (route.children && route.children.length > 0) {
const childrenMap = toRoutesMap(route.children)
Object.assign(map, childrenMap)
}
})
return map
}
export default {
name: 'IMenu',
props: {
@ -73,11 +93,14 @@ export default {
computed: {
menuTheme() {
return this.theme == 'light' ? this.theme : 'dark'
},
routesMap() {
return toRoutesMap(this.options)
}
},
created () {
this.updateMenu()
if (!this.options[0].fullPath) {
if (this.options.length > 0 && !this.options[0].fullPath) {
this.formatOptions(this.options, '')
}
// 自定义国际化配置
@ -89,6 +112,19 @@ export default {
}
},
watch: {
options(val) {
if (val.length > 0 && !val[0].fullPath) {
this.formatOptions(this.options, '')
}
},
i18n(val) {
if(val && val.messages) {
const messages = this.i18n.messages
Object.keys(messages).forEach(key => {
this.$i18n.mergeLocaleMessage(key, messages[key])
})
}
},
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.sOpenKeys
@ -106,17 +142,32 @@ export default {
}
},
methods: {
renderIcon: function (h, icon) {
renderIcon: function (h, icon, key) {
if (this.$scopedSlots.icon && icon && icon !== 'none') {
const vnodes = this.$scopedSlots.icon({icon, key})
vnodes.forEach(vnode => {
vnode.data.class = vnode.data.class ? vnode.data.class : []
vnode.data.class.push('anticon')
})
return vnodes
}
return !icon || icon == 'none' ? null : h(Icon, {props: {type: icon}})
},
renderMenuItem: function (h, menu) {
let tag = 'router-link'
const path = resolvePath(menu.fullPath, menu.meta.params)
let config = {props: {to: {path, query: menu.meta.query}, }, attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}}
if (menu.meta && menu.meta.link) {
tag = 'a'
config = {attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank'}}
}
return h(
Item, {key: menu.fullPath},
[
h('router-link', {props: {to: menu.fullPath}},
h(tag, config,
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none'),
h('span', [this.$t(getI18nKey(menu.fullPath))])
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
]
)
]
@ -124,10 +175,10 @@ export default {
},
renderSubMenu: function (h, menu) {
let this_ = this
let subItem = [h('span', {slot: 'title'},
let subItem = [h('span', {slot: 'title', attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}},
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none'),
h('span', [this.$t(getI18nKey(menu.fullPath))])
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
]
)]
let itemArr = []
@ -173,19 +224,23 @@ export default {
})
},
updateMenu () {
const menuRoutes = this.$route.matched.filter(item => item.path !== '')
const route = menuRoutes.pop()
this.selectedKeys = [this.getSelectedKey(route)]
let openKeys = menuRoutes.map(item => item.path)
this.selectedKeys = this.getSelectedKeys()
let openKeys = this.selectedKeys.filter(item => item !== '')
openKeys = openKeys.slice(0, openKeys.length -1)
if (!fastEqual(openKeys, this.sOpenKeys)) {
this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys
}
},
getSelectedKey (route) {
if (route.meta.invisible && route.parent) {
return this.getSelectedKey(route.parent)
getSelectedKeys() {
let matches = this.$route.matched
const route = matches[matches.length - 1]
let chose = this.routesMap[route.path]
if (chose && chose.meta && chose.meta.highlight) {
chose = this.routesMap[chose.meta.highlight]
const resolve = this.$router.resolve({path: chose.fullPath})
matches = (resolve.resolved && resolve.resolved.matched) || matches
}
return route.path
return matches.map(item => item.path)
}
},
render (h) {
@ -199,12 +254,12 @@ export default {
openKeys: this.openKeys ? this.openKeys : this.sOpenKeys
},
on: {
select: (obj) => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
},
'update:openKeys': (val) => {
this.sOpenKeys = val
},
click: (obj) => {
obj.selectedKeys = [obj.key]
this.$emit('select', obj)
}
}
}, this.renderMenu(h, this.options)

View File

@ -1,6 +1,6 @@
<template>
<div class="page-header">
<div :class="['page-header-wide', layout]">
<div :class="['page-header', layout, pageWidth]">
<div class="page-header-wide">
<div class="breadcrumb">
<a-breadcrumb>
<a-breadcrumb-item :key="index" v-for="(item, index) in breadcrumb">
@ -11,7 +11,7 @@
<div class="detail">
<div class="main">
<div class="row">
<h1 v-if="title" class="title">{{title}}</h1>
<h1 v-if="showPageTitle && title" class="title">{{title}}</h1>
<div class="action"><slot name="action"></slot></div>
</div>
<div class="row">
@ -28,11 +28,12 @@
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'PageHeader',
props: {
title: {
type: String,
type: [String, Boolean],
required: false
},
breadcrumb: {
@ -49,9 +50,7 @@ export default {
},
},
computed: {
layout () {
return this.$store.state.setting.layout
}
...mapState('setting', ['layout', 'showPageTitle', 'pageWidth'])
}
}
</script>

View File

@ -1,13 +1,11 @@
.page-header{
background: @base-bg-color;
padding: 16px 24px;
&.head.fixed{
margin: auto;
max-width: 1400px;
}
.page-header-wide{
&.head{
margin: auto;
max-width: 1400px;
}
&.side{
}
.breadcrumb{
margin-bottom: 20px;
}

View File

@ -1,5 +1,9 @@
<template>
<div class="side-setting">
<setting-item>
<a-button @click="saveSetting" type="primary" icon="save">{{$t('save')}}</a-button>
<a-button @click="resetSetting" type="dashed" icon="redo" style="float: right">{{$t('reset')}}</a-button>
</setting-item>
<setting-item :title="$t('theme.title')">
<img-checkbox-group
@change="values => setTheme({...theme, mode: values[0]})"
@ -26,15 +30,21 @@
>
<img-checkbox :title="$t('navigate.side')" img="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" value="side"/>
<img-checkbox :title="$t('navigate.head')" img="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" value="head"/>
<img-checkbox :title="$t('navigate.mix')" img="https://gw.alipayobjects.com/zos/antfincdn/x8Ob%26B8cy8/LCkqqYNmvBEbokSDscrm.svg" value="mix"/>
</img-checkbox-group>
</setting-item>
<setting-item>
<a-list :split="false">
<a-list-item>
{{$t('navigate.content.title')}}
<a-select size="small" defaultValue="1" slot="actions" style="width: 80px">
<a-select-option value="1">{{$t('navigate.content.fluid')}}</a-select-option>
<a-select-option value="2">{{$t('navigate.content.fixed')}}</a-select-option>
<a-select
:getPopupContainer="getPopupContainer"
:value="pageWidth"
@change="setPageWidth"
class="select-item" size="small" slot="actions"
>
<a-select-option value="fluid">{{$t('navigate.content.fluid')}}</a-select-option>
<a-select-option value="fixed">{{$t('navigate.content.fixed')}}</a-select-option>
</a-select>
</a-list-item>
<a-list-item>
@ -75,6 +85,7 @@
{{$t('animate.effect')}}
<a-select
:value="animate.name"
:getPopupContainer="getPopupContainer"
@change="val => setAnimate({...animate, name: val})"
class="select-item" size="small" slot="actions"
>
@ -85,6 +96,7 @@
{{$t('animate.direction')}}
<a-select
:value="animate.direction"
:getPopupContainer="getPopupContainer"
@change="val => setAnimate({...animate, direction: val})"
class="select-item" size="small" slot="actions"
>
@ -94,12 +106,13 @@
</a-list>
</setting-item>
<a-alert
v-if="isDev"
style="max-width: 240px; margin: -16px 0 8px; word-break: break-all"
type="warning"
:message="$t('alert')"
>
</a-alert>
<a-button id="copyBtn" :data-clipboard-text="copyConfig" @click="copyCode" style="width: 100%" icon="copy" >{{$t('copy')}}</a-button>
<a-button v-if="isDev" id="copyBtn" :data-clipboard-text="copyConfig" @click="copyCode" style="width: 100%" icon="copy" >{{$t('copy')}}</a-button>
</div>
</template>
@ -110,7 +123,9 @@ import Clipboard from 'clipboard'
import { mapState, mapMutations } from 'vuex'
import {formatConfig} from '@/utils/formatter'
import {setting} from '@/config/default'
import sysConfig from '@/config/config'
import fastEqual from 'fast-deep-equal'
import deepMerge from 'deepmerge'
const ColorCheckboxGroup = ColorCheckbox.Group
const ImgCheckboxGroup = ImgCheckbox.Group
@ -120,14 +135,15 @@ export default {
components: {ImgCheckboxGroup, ImgCheckbox, ColorCheckboxGroup, ColorCheckbox, SettingItem},
data() {
return {
copyConfig: 'Sorry, you have copied nothing O(∩_∩)O~'
copyConfig: 'Sorry, you have copied nothing O(∩_∩)O~',
isDev: process.env.NODE_ENV === 'development'
}
},
computed: {
directions() {
return this.animates.find(item => item.name == this.animate.name).directions
},
...mapState('setting', ['theme', 'layout', 'animate', 'animates', 'palettes', 'multiPage', 'weekMode', 'fixedHeader', 'fixedSideBar', 'hideSetting'])
...mapState('setting', ['theme', 'layout', 'animate', 'animates', 'palettes', 'multiPage', 'weekMode', 'fixedHeader', 'fixedSideBar', 'hideSetting', 'pageWidth'])
},
watch: {
'animate.name': function(val) {
@ -135,30 +151,56 @@ export default {
}
},
methods: {
getPopupContainer() {
return this.$el.parentNode
},
copyCode () {
let config = this.extractConfig(false)
this.copyConfig = `// 自定义配置,参考 ./default/setting.config.js需要自定义的属性在这里配置即可
module.exports = ${formatConfig(config)}
`
let clipboard = new Clipboard('#copyBtn')
clipboard.on('success', () => {
this.$message.success(`复制成功,覆盖文件 src/config/config.js 然后重启项目即可生效`).then(() => {
const localConfig = localStorage.getItem(process.env.VUE_APP_SETTING_KEY)
if (localConfig) {
console.warn('检测到本地有历史保存的主题配置,想要要拷贝的配置代码生效,您可能需要先重置配置')
this.$message.warn('检测到本地有历史保存的主题配置,想要要拷贝的配置代码生效,您可能需要先重置配置', 5)
}
})
clipboard.destroy()
})
},
saveSetting() {
const closeMessage = this.$message.loading('正在保存到本地,请稍后...', 0)
const config = this.extractConfig(true)
localStorage.setItem(process.env.VUE_APP_SETTING_KEY, JSON.stringify(config))
setTimeout(closeMessage, 800)
},
resetSetting() {
this.$confirm({
title: '重置主题会刷新页面,当前页面内容不会保留,确认重置?',
onOk() {
localStorage.removeItem(process.env.VUE_APP_SETTING_KEY)
window.location.reload()
}
})
},
//
extractConfig(local = false) {
let config = {}
//
let mySetting = this.$store.state.setting
let dftSetting = local ? deepMerge(setting, sysConfig) : setting
Object.keys(mySetting).forEach(key => {
const dftValue = setting[key], myValue = mySetting[key]
//
const dftValue = dftSetting[key], myValue = mySetting[key]
if (dftValue != undefined && !fastEqual(dftValue, myValue)) {
config[key] = myValue
}
})
this.copyConfig = '// 自定义配置,参考 ./default/setting.config.js需要自定义的属性在这里配置即可\n'
this.copyConfig += 'module.exports = '
this.copyConfig += formatConfig(config)
let clipboard = new Clipboard('#copyBtn')
const _this = this
clipboard.on('success', function () {
_this.$message.success(`复制成功,覆盖文件 src/config/config.js 然后重启项目即可生效`)
clipboard.destroy()
})
return config
},
...mapMutations('setting', ['setTheme', 'setLayout', 'setMultiPage', 'setWeekMode',
'setFixedSideBar', 'setFixedHeader', 'setAnimate', 'setHideSetting'])
'setFixedSideBar', 'setFixedHeader', 'setAnimate', 'setHideSetting', 'setPageWidth'])
}
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="setting-item">
<h3 class="title">{{title}}</h3>
<h3 v-if="title" class="title">{{title}}</h3>
<slot></slot>
</div>
</template>

View File

@ -12,6 +12,7 @@ module.exports = {
title: '导航设置',
side: '侧边导航',
head: '顶部导航',
mix: '混合导航',
content: {
title: '内容区域宽度',
fluid: '流式',
@ -33,7 +34,9 @@ module.exports = {
direction: '动画方向'
},
alert: '拷贝配置后,直接覆盖文件 src/config/config.js 中的全部内容,然后重启即可。(注意:仅会拷贝与默认配置不同的项)',
copy: '拷贝配置'
copy: '拷贝配置',
save: '保存配置',
reset: '重置配置',
},
HK: {
theme: {
@ -68,7 +71,9 @@ module.exports = {
direction: '動畫方向'
},
alert: '拷貝配置后,直接覆蓋文件 src/config/config.js 中的全部內容,然後重啟即可。(注意:僅會拷貝與默認配置不同的項)',
copy: '拷貝配置'
copy: '拷貝配置',
save: '保存配置',
reset: '重置配置',
},
US: {
theme: {
@ -82,6 +87,7 @@ module.exports = {
title: 'Navigation Mode',
side: 'Side Menu Layout',
head: 'Top Menu Layout',
mix: 'Mix Menu Layout',
content: {
title: 'Content Width',
fluid: 'Fluid',
@ -103,7 +109,9 @@ module.exports = {
direction: 'Direction'
},
alert: 'After copying the configuration code, directly cover all contents in the file src/config/config.js, then restart the server. (Note: only items that are different from the default configuration will be copied)',
copy: 'Copy Setting'
copy: 'Copy Setting',
save: 'Save',
reset: 'Reset',
}
}
}

View File

@ -20,15 +20,20 @@
:dataSource="dataSource"
:rowKey="rowKey"
:pagination="pagination"
:expandedRowKeys="expandedRowKeys"
:expandedRowRender="expandedRowRender"
@change="onChange"
:rowSelection="selectedRows ? {selectedRowKeys: selectedRowKeys, onChange: updateSelect} : undefined"
:rowSelection="selectedRows ? {selectedRowKeys, onSelect, onSelectAll} : undefined"
>
<template slot-scope="text, record, index" :slot="slot" v-for="slot in scopedSlots">
<template slot-scope="text, record, index" :slot="slot" v-for="slot in Object.keys($scopedSlots).filter(key => key !== 'expandedRowRender') ">
<slot :name="slot" v-bind="{text, record, index}"></slot>
</template>
<template :slot="slot" v-for="slot in slots">
<template :slot="slot" v-for="slot in Object.keys($slots)">
<slot :name="slot"></slot>
</template>
<template slot-scope="record, index, indent, expanded" :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''">
<slot v-bind="{record, index, indent, expanded}" :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"></slot>
</template>
</a-table>
</div>
</template>
@ -49,42 +54,80 @@ export default {
type: [Object, Boolean],
default: true
},
selectedRows: Array
selectedRows: Array,
expandedRowKeys: Array,
expandedRowRender: Function
},
data () {
return {
needTotalList: [],
scopedSlots: [],
slots: []
needTotalList: []
}
},
methods: {
updateSelect (selectedRowKeys, selectedRows) {
this.$emit('update:selectedRows', selectedRows)
this.$emit('selectedRowChange', selectedRowKeys, selectedRows)
equals(record1, record2) {
if (record1 === record2) {
return true
}
const {rowKey} = this
if (rowKey && typeof rowKey === 'string') {
return record1[rowKey] === record2[rowKey]
} else if (rowKey && typeof rowKey === 'function') {
return rowKey(record1) === rowKey(record2)
}
return false
},
contains(arr, item) {
if (!arr || arr.length === 0) {
return false
}
const {equals} = this
for (let i = 0; i < arr.length; i++) {
if (equals(arr[i], item)) {
return true
}
}
return false
},
onSelectAll(selected, rows) {
const {getKey, contains} = this
const unselected = this.dataSource.filter(item => !contains(rows, item, this.rowKey))
const _selectedRows = this.selectedRows.filter(item => !contains(unselected, item, this.rowKey))
const set = {}
_selectedRows.forEach(item => set[getKey(item)] = item)
rows.forEach(item => set[getKey(item)] = item)
const _rows = Object.values(set)
this.$emit('update:selectedRows', _rows)
this.$emit('selectedRowChange', _rows.map(item => getKey(item)), _rows)
},
getKey(record) {
const {rowKey} = this
if (!rowKey || !record) {
return undefined
}
if (typeof rowKey === 'string') {
return record[rowKey]
} else {
return rowKey(record)
}
},
onSelect(record, selected) {
const {equals, selectedRows, getKey} = this
const _selectedRows = selected ? [...selectedRows, record] : selectedRows.filter(row => !equals(row, record))
this.$emit('update:selectedRows', _selectedRows)
this.$emit('selectedRowChange', _selectedRows.map(item => getKey(item)), _selectedRows)
},
initTotalList (columns) {
const totalList = columns.filter(item => item.needTotal)
return columns.filter(item => item.needTotal)
.map(item => {
return {
...item,
total: 0
}
})
return totalList
},
getScopedSlots(columns) {
let scopedSlots = columns.filter(item => item.scopedSlots).map(item => item.scopedSlots)
scopedSlots = scopedSlots.flatMap(item => Object.values(item))
return scopedSlots
},
getSlots(columns) {
let slots = columns.filter(item => item.slots).map(item => item.slots)
slots = slots.flatMap(item => Object.values(item))
return slots
},
onClear() {
this.updateSelect([], [])
this.$emit('update:selectedRows', [])
this.$emit('selectedRowChange', [], [])
this.$emit('clear')
},
onChange(pagination, filters, sorter, {currentDataSource}) {
@ -92,8 +135,6 @@ export default {
}
},
created () {
this.scopedSlots = this.getScopedSlots(this.columns)
this.slots = this.getSlots(this.columns)
this.needTotalList = this.initTotalList(this.columns)
},
watch: {
@ -102,7 +143,14 @@ export default {
return {
...item,
total: selectedRows.reduce((sum, val) => {
return sum + val[item.dataIndex]
let v
try{
v = val[item.dataIndex] ? val[item.dataIndex] : eval(`val.${item.dataIndex}`);
}catch(_){
v = val[item.dataIndex];
}
v = !isNaN(parseFloat(v)) ? parseFloat(v) : 0;
return sum + v
}, 0)
}
})
@ -110,10 +158,8 @@ export default {
},
computed: {
selectedRowKeys() {
return this.selectedRows.map(record => {
return (typeof this.rowKey === 'function') ? this.rowKey(record) : record[this.rowKey]
})
}
return this.selectedRows.map(record => this.getKey(record))
},
}
}
</script>

View File

@ -0,0 +1,155 @@
<template>
<div class="action-columns" ref="root">
<a-popover v-model="visible" placement="bottomRight" trigger="click" :get-popup-container="() => $refs.root">
<div slot="title">
<a-checkbox :indeterminate="indeterminate" :checked="checkAll" @change="onCheckAllChange" class="check-all" />列展示
<a-button @click="resetColumns" style="float: right" type="link" size="small">重置</a-button>
</div>
<a-list style="width: 100%" size="small" :key="i" v-for="(col, i) in columns" slot="content">
<a-list-item>
<a-checkbox v-model="col.visible" @change="e => onCheckChange(e, col)"/>
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<template slot="actions">
<a-tooltip title="固定在列头" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
<a-icon :class="['left', {active: col.fixed === 'left'}]" @click="fixColumn('left', col)" type="vertical-align-top" />
</a-tooltip>
<a-tooltip title="固定在列尾" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
<a-icon :class="['right', {active: col.fixed === 'right'}]" @click="fixColumn('right', col)" type="vertical-align-bottom" />
</a-tooltip>
<a-tooltip title="添加搜索" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
<a-icon :class="{active: col.searchAble}" @click="setSearch(col)" type="search" />
</a-tooltip>
</template>
</a-list-item>
</a-list>
<a-icon class="action" type="setting" />
</a-popover>
</div>
</template>
<script>
import cloneDeep from 'lodash.clonedeep'
export default {
name: 'ActionColumns',
props: ['columns', 'visibleColumns'],
data() {
return {
visible: false,
indeterminate: false,
checkAll: true,
checkedCounts: this.columns.length,
backColumns: cloneDeep(this.columns)
}
},
watch: {
checkedCounts(val) {
this.checkAll = val === this.columns.length
this.indeterminate = val > 0 && val < this.columns.length
},
columns(newVal, oldVal) {
if (newVal != oldVal) {
this.checkedCounts = newVal.length
this.formatColumns(newVal)
}
}
},
created() {
this.formatColumns(this.columns)
},
methods: {
onCheckChange(e, col) {
if (!col.visible) {
this.checkedCounts -= 1
} else {
this.checkedCounts += 1
}
},
fixColumn(fixed, col) {
if (fixed !== col.fixed) {
this.$set(col, 'fixed', fixed)
} else {
this.$set(col, 'fixed', undefined)
}
},
setSearch(col) {
this.$set(col, 'searchAble', !col.searchAble)
if (!col.searchAble && col.search) {
this.resetSearch(col)
}
},
resetSearch(col) {
// col.search.value = col.dataType === 'boolean' ? false : undefined
col.search.value = undefined
col.search.backup = undefined
},
resetColumns() {
const {columns, backColumns} = this
let counts = columns.length
backColumns.forEach((back, index) => {
const column = columns[index]
column.visible = back.visible === undefined || back.visible
if (!column.visible) {
counts -= 1
}
if (back.fixed !== undefined) {
column.fixed = back.fixed
} else {
this.$set(column, 'fixed', undefined)
}
this.$set(column, 'searchAble', back.searchAble)
// column.searchAble = back.searchAble
this.resetSearch(column)
})
this.checkedCounts = counts
this.visible = false
this.$emit('reset', this.getConditions(columns))
},
onCheckAllChange(e) {
if (e.target.checked) {
this.checkedCounts = this.columns.length
this.columns.forEach(col => col.visible = true)
} else {
this.checkedCounts = 0
this.columns.forEach(col => col.visible = false)
}
},
getConditions(columns) {
const conditions = {}
columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.forEach(col => {
conditions[col.dataIndex] = col.search.value
})
return conditions
},
formatColumns(columns) {
for (let col of columns) {
if (col.visible === undefined) {
this.$set(col, 'visible', true)
}
if (!col.visible) {
this.checkedCounts -= 1
}
}
}
}
}
</script>
<style scoped lang="less">
.action-columns{
display: inline-block;
.check-all{
margin-right: 8px;
}
.left,.right{
transform: rotate(-90deg);
}
.active{
color: @primary-color;
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="action-size" ref="root">
<a-tooltip title="密度">
<a-dropdown placement="bottomCenter" :trigger="['click']" :get-popup-container="() => $refs.root">
<a-icon class="action" type="column-height" />
<a-menu :selected-keys="[value]" slot="overlay" @click="onClick">
<a-menu-item key="default">
默认
</a-menu-item>
<a-menu-item key="middle">
中等
</a-menu-item>
<a-menu-item key="small">
紧密
</a-menu-item>
</a-menu>
</a-dropdown>
</a-tooltip>
</div>
</template>
<script>
export default {
name: 'ActionSize',
props: ['value'],
inject: ['table'],
data() {
return {
selectedKeys: ['middle']
}
},
methods: {
onClick({key}) {
this.$emit('input', key)
}
}
}
</script>
<style scoped lang="less">
.action-size{
display: inline-block;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<div ref="table" :id="id" class="advanced-table">
<a-spin :spinning="loading">
<div :class="['header-bar', size]">
<div class="title">
<template v-if="title">{{title}}</template>
<slot v-else-if="$slots.title" name="title"></slot>
<template v-else>高级表格</template>
</div>
<div class="search">
<search-area :format-conditions="formatConditions" @change="onSearchChange" :columns="columns" >
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
</search-area>
</div>
<div class="actions">
<a-tooltip title="刷新">
<a-icon @click="refresh" class="action" :type="loading ? 'loading' : 'reload'" />
</a-tooltip>
<action-size v-model="sSize" class="action" />
<a-tooltip title="列配置">
<action-columns :columns="columns" @reset="onColumnsReset" class="action">
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
</action-columns>
</a-tooltip>
<a-tooltip title="全屏">
<a-icon @click="toggleScreen" class="action" :type="fullScreen ? 'fullscreen-exit' : 'fullscreen'" />
</a-tooltip>
</div>
</div>
<a-table
v-bind="{...$props, columns: visibleColumns, title: undefined, loading: false}"
:size="sSize"
@expandedRowsChange="onExpandedRowsChange"
@change="onChange"
@expand="onExpand"
>
<template slot-scope="text, record, index" :slot="slot" v-for="slot in scopedSlots ">
<slot :name="slot" v-bind="{text, record, index}"></slot>
</template>
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
<template slot-scope="record, index, indent, expanded" :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''">
<slot v-bind="{record, index, indent, expanded}" :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"></slot>
</template>
</a-table>
</a-spin>
</div>
</template>
<script>
import ActionSize from '@/components/table/advance/ActionSize'
import ActionColumns from '@/components/table/advance/ActionColumns'
import SearchArea from '@/components/table/advance/SearchArea'
export default {
name: 'AdvanceTable',
components: {SearchArea, ActionColumns, ActionSize},
props: {
tableLayout: String,
bordered: Boolean,
childrenColumnName: {type: String, default: 'children'},
columns: Array,
components: Object,
dataSource: Array,
defaultExpandAllRows: Array[String],
expandedRowKeys: Array[String],
expandedRowRender: Function,
expandIcon: Function,
expandRowByClick: Boolean,
expandIconColumnIndex: Number,
footer: Function,
indentSize: Number,
loading: Boolean,
locale: Object,
pagination: [Object, Boolean],
rowClassName: Function,
rowKey: [String, Function],
rowSelection: Object,
scroll: Object,
showHeader: {type: Boolean, default: true},
size: String,
title: String,
customHeaderRow: Function,
customRow: Function,
getPopupContainer: Function,
transformCellText: Function,
formatConditions: Boolean
},
provide() {
return {
table: this
}
},
data() {
return {
id: `${new Date().getTime()}-${Math.floor(Math.random() * 10)}`,
sSize: this.size || 'default',
fullScreen: false,
conditions: {}
}
},
computed: {
slots() {
return Object.keys(this.$slots).filter(slot => slot !== 'title')
},
scopedSlots() {
return Object.keys(this.$scopedSlots).filter(slot => slot !== 'expandedRowRender' && slot !== 'title')
},
visibleColumns(){
return this.columns.filter(col => col.visible)
}
},
created() {
this.addListener()
},
beforeDestroy() {
this.removeListener()
},
methods: {
refresh() {
this.$emit('refresh', this.conditions)
},
onSearchChange(conditions, searchOptions) {
this.conditions = conditions
this.$emit('search', conditions, searchOptions)
},
toggleScreen() {
if (this.fullScreen) {
this.outFullScreen()
} else {
this.inFullScreen()
}
},
inFullScreen() {
const el = this.$refs.table
el.classList.add('beauty-scroll')
if (el.requestFullscreen) {
el.requestFullscreen()
return true
} else if (el.webkitRequestFullScreen) {
el.webkitRequestFullScreen()
return true
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
return true
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen()
return true
}
this.$message.warn('对不起,您的浏览器不支持全屏模式')
el.classList.remove('beauty-scroll')
return false
},
outFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
this.$refs.table.classList.remove('beauty-scroll')
},
onColumnsReset(conditions) {
this.$emit('reset', conditions)
},
onExpandedRowsChange(expandedRows) {
this.$emit('expandedRowsChange', expandedRows)
},
onChange(pagination, filters, sorter, options) {
this.$emit('change', pagination, filters, sorter, options)
},
onExpand(expanded, record) {
this.$emit('expand', expanded, record)
},
addListener() {
document.addEventListener('fullscreenchange', this.fullScreenListener)
document.addEventListener('webkitfullscreenchange', this.fullScreenListener)
document.addEventListener('mozfullscreenchange', this.fullScreenListener)
document.addEventListener('msfullscreenchange', this.fullScreenListener)
},
removeListener() {
document.removeEventListener('fullscreenchange', this.fullScreenListener)
document.removeEventListener('webkitfullscreenchange', this.fullScreenListener)
document.removeEventListener('mozfullscreenchange', this.fullScreenListener)
document.removeEventListener('msfullscreenchange', this.fullScreenListener)
},
fullScreenListener(e) {
if (e.target.id === this.id) {
this.fullScreen = !this.fullScreen
}
}
}
}
</script>
<style scoped lang="less">
.advanced-table{
overflow-y: auto;
background-color: @component-background;
.header-bar{
padding: 16px 24px;
display: flex;
align-items: center;
border-radius: 4px;
transition: all 0.3s;
&.middle{
padding: 12px 16px;
}
&.small{
padding: 8px 12px;
border: 1px solid @border-color;
border-bottom: 0;
.title{
font-size: 16px;
}
}
.title{
transition: all 0.3s;
font-size: 18px;
color: @title-color;
font-weight: 700;
}
.search{
flex: 1;
text-align: right;
margin: 0 24px;
}
.actions{
text-align: right;
font-size: 17px;
color: @text-color;
.action{
margin: 0 8px;
cursor: pointer;
&:hover{
color: @primary-color;
}
}
}
}
}
</style>

View File

@ -0,0 +1,313 @@
<template>
<div class="search-area" ref="root">
<div class="select-root" ref="selectRoot"></div>
<div class="search-item" :key="index" v-for="(col, index) in searchCols">
<div v-if="col.dataType === 'boolean'" :class="['title', {active: col.search.value !== undefined}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-switch @change="onSwitchChange(col)" class="switch" v-model="col.search.value" size="small"
:checked-children="(col.search.switchOptions && col.search.switchOptions.checkedText) || '是'"
:un-checked-children="(col.search.switchOptions && col.search.switchOptions.uncheckedText) || '否'"
/>
<a-icon v-if="col.search.value !== undefined" class="close" @click="e => onCloseClick(e, col)" type="close-circle" theme="filled" />
</div>
<div v-else-if="col.dataType === 'time'" :class="['title', {active: col.search.value}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-time-picker :format="col.search.format" v-model="col.search.value" placeholder="选择时间" @change="(time, timeStr) => onCalendarChange(time, timeStr, col)" @openChange="open => onCalendarOpenChange(open, col)" class="time-picker" size="small" :get-popup-container="() => $refs.root"/>
</div>
<div v-else-if="col.dataType === 'date'" :class="['title', {active: col.search.value}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-date-picker :format="col.search.format" v-model="col.search.value" @change="onDateChange(col)" class="date-picker" size="small" :getCalendarContainer="() => $refs.root"/>
</div>
<div v-else-if="col.dataType === 'datetime'" class="title datetime active">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-date-picker :format="col.search.format" v-model="col.search.value" @change="(date, dateStr) => onCalendarChange(date, dateStr, col)" @openChange="open => onCalendarOpenChange(open, col)" class="datetime-picker" size="small" show-time :getCalendarContainer="() => $refs.root"/>
</div>
<div v-else-if="col.dataType === 'select'" :class="['title', {active: col.search.value !== undefined}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-select :allowClear="true" :options="col.search.selectOptions" v-model="col.search.value" placeholder="请选择..." @change="onSelectChange(col)" class="select" slot="content" size="small" :get-popup-container="() => $refs.selectRoot">
</a-select>
</div>
<div v-else :class="['title', {active: col.search.value}]">
<a-popover @visibleChange="onVisibleChange(col, index)" v-model="col.search.visible" placement="bottom" :trigger="['click']" :get-popup-container="() => $refs.root">
<template v-if="col.title">
{{col.title}}
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<div class="value " v-if="col.search.value">:&nbsp;&nbsp;{{col.search.format && typeof col.search.format === 'function' ? col.search.format(col.search.value) : col.search.value}}</div>
<a-icon v-if="!col.search.value" class="icon-down" type="down"/>
<div class="operations" slot="content">
<a-button @click="onCancel(col)" class="btn" size="small" type="link">取消</a-button>
<a-button @click="onConfirm(col)" class="btn" size="small" type="primary">确认</a-button>
</div>
<div class="search-overlay" slot="title">
<a-input :id="`${searchIdPrefix}${index}`" :allow-clear="true" @keyup.esc="onCancel(col)" @keyup.enter="onConfirm(col)" v-model="col.search.value" size="small" />
</div>
</a-popover>
<a-icon v-if="col.search.value" @click="e => onCloseClick(e, col)" class="close" type="close-circle" theme="filled"/>
</div>
</div>
</div>
</template>
<script>
import fastEqual from 'fast-deep-equal'
import moment from 'moment'
export default {
name: 'SearchArea',
props: ['columns', 'formatConditions'],
inject: ['table'],
created() {
this.formatColumns(this.columns)
},
watch: {
columns(newVal, oldVal) {
if (newVal != oldVal) {
this.formatColumns(newVal)
}
},
searchCols(newVal, oldVal) {
if (newVal.length != oldVal.length) {
const newConditions = this.getConditions(newVal)
const newSearchOptions = this.getSearchOptions(newVal)
if (!fastEqual(newConditions, this.conditions)) {
this.conditions = newConditions
this.searchOptions = newSearchOptions
this.$emit('change', this.conditions, this.searchOptions)
}
}
}
},
data() {
return {
conditions: {},
searchOptions: []
}
},
computed: {
searchCols() {
return this.columns.filter(item => item.searchAble)
},
searchIdPrefix() {
return this.table.id + '-ipt-'
}
},
methods: {
onCloseClick(e, col) {
e.preventDefault()
e.stopPropagation()
col.search.value = undefined
const {backup, value} = col.search
if (backup !== value) {
this.backupAndEmitChange(col)
}
},
onCancel(col) {
col.search.value = col.search.backup
col.search.visible = false
},
onConfirm(col) {
const {backup, value} = col.search
col.search.visible = false
if (backup !== value) {
this.backupAndEmitChange(col)
}
},
onSwitchChange(col) {
const {backup, value} = col.search
if (backup !== value) {
this.backupAndEmitChange(col)
}
},
onSelectChange(col) {
this.backupAndEmitChange(col)
},
onCalendarOpenChange(open, col) {
col.search.visible = open
const {momentEqual, backupAndEmitChange} = this
const {value, backup, format} = col.search
if (!open && !momentEqual(value, backup, format)) {
backupAndEmitChange(col, moment(value))
}
},
onCalendarChange(date, dateStr, col) {
const {momentEqual, backupAndEmitChange} = this
const {value, backup, format} = col.search
if (!col.search.visible && !momentEqual(value, backup, format)) {
backupAndEmitChange(col, moment(value))
}
},
onDateChange(col) {
const {momentEqual, backupAndEmitChange} = this
const {value, backup, format} = col.search
if (!momentEqual(value, backup, format)) {
backupAndEmitChange(col, moment(value))
}
},
getFormat(col) {
if (col.search && col.search.format) {
return col.search.format
}
const dataType = col.dataType
switch(dataType) {
case 'time': return 'HH:mm:ss'
case 'date': return 'YYYY-MM-DD'
case 'datetime': return 'YYYY-MM-DD HH:mm:ss'
default: return undefined
}
},
backupAndEmitChange(col, backValue = col.search.value) {
const {getConditions, getSearchOptions} = this
col.search.backup = backValue
this.conditions = getConditions(this.searchCols)
this.searchOptions = getSearchOptions(this.searchCols)
this.$emit('change', this.conditions, this.searchOptions)
},
getConditions(columns) {
const conditions = {}
columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.forEach(col => {
const {value, format} = col.search
if (this.formatConditions && format) {
if (typeof format === 'function') {
conditions[col.dataIndex] = format(col.search.value)
} else if (typeof format === 'string' && value.constructor.name === 'Moment') {
conditions[col.dataIndex] = value.format(format)
} else {
conditions[col.dataIndex] = value
}
} else {
conditions[col.dataIndex] = value
}
})
return conditions
},
getSearchOptions(columns) {
return columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.map(({dataIndex, search}) => ({field: dataIndex, value: search.value, format: search.format}))
},
onVisibleChange(col, index) {
if (!col.search.visible) {
col.search.value = col.search.backup
} else {
let input = document.getElementById(`${this.searchIdPrefix}${index}`)
if (input) {
setTimeout(() => {input.focus()}, 0)
} else {
this.$nextTick(() => {
input = document.getElementById(`${this.searchIdPrefix}${index}`)
input.focus()
})
}
}
},
momentEqual(target, source, format) {
if (target === source) {
return true
} else if (target && source && target.format(format) === source.format(format)) {
return true
}
return false
},
formatColumns(columns) {
columns.forEach(item => {
this.$set(item, 'search', {...item.search, visible: false, value: undefined, format: this.getFormat(item)})
})
}
}
}
</script>
<style scoped lang="less">
.search-area{
.select-root{
text-align: left;
}
margin: -4px 0;
.search-item{
margin: 4px 4px;
display: inline-block;
.title{
padding: 4px 8px;
cursor: pointer;
border-radius: 4px;
user-select: none;
display: inline-flex;
align-items: center;
.close{
color: @text-color-second;
margin-left: 4px;
font-size: 12px;
vertical-align: middle;
:hover{
color: @text-color;
}
}
.switch{
margin-left: 4px;
}
.time-picker{
margin-left: 4px;
width: 96px;
}
.date-picker{
margin-left: 4px;
width: 120px;
}
.datetime-picker{
margin-left: 4px;
width: 195px;
}
.value{
display: inline-block;
overflow: hidden;
flex:1;
vertical-align: middle;
max-width: 144px;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
&.active{
background-color: @layout-bg-color;
}
}
.icon-down{
vertical-align: middle;
font-size: 12px;
}
}
.search-overlay{
padding: 8px 0px;
text-align: center;
}
.select{
margin-left: 4px;
max-width: 144px;
min-width: 96px;
text-align: left;
}
.operations{
display: flex;
margin: -6px 0;
justify-content: space-between;
.btn{
}
}
}
</style>

View File

@ -0,0 +1,2 @@
import AdvanceTable from './AdvanceTable'
export default AdvanceTable

View File

@ -0,0 +1,49 @@
<template>
<a-table :data-source="apiSource" :pagination="false">
<h2 v-if="title" style="margin: 0 16px 0" slot="title">{{title}}</h2>
<a-table-column width="20%" data-index="param" title="参数">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column width="50%" data-index="desc" title="说明">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column v-if="isApi" width="15%" data-index="type" title="类型">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column v-if="isApi" width="15%" data-index="default" title="默认值">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column v-if="!isApi" width="30%" data-index="callback" title="回调函数">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
</a-table>
</template>
<script>
export default {
name: 'ApiTable',
props: {
title: {
type: String,
default: 'API'
},
type: {
type: String,
default: 'api',
validator(value) {
return ['api', 'event'].includes(value)
}
},
apiSource: Array
},
computed: {
isApi() {
return this.type === 'api'
}
}
}
</script>
<style scoped>
</style>

View File

@ -35,7 +35,7 @@ const Item = {
return h(
'li',
{class: 'avatar-item'},
[!this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar]
[this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar]
)
}
}

View File

@ -2,11 +2,11 @@
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night'
mode: 'dark',
},
multiPage: true,
animate: {
name: 'roll',
direction: 'default'
name: 'lightSpeed',
direction: 'left'
}
}

View File

@ -22,10 +22,11 @@ const ANTD = {
'component-background': '#fff',
'heading-color': 'rgba(0, 0, 0, 0.85)',
'text-color': 'rgba(0, 0, 0, 0.65)',
'text-color-inverse': '#fff',
'text-color-inverse': '#fefefe',
'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
'shadow-color': 'rgba(0, 0, 0, 0.15)',
'border-color-split': '#f0f0f0',
'border-color-base': '#d9d9d9',
'background-color-light': '#fafafa',
'background-color-base': '#f5f5f5',
'table-selected-row-bg': '#fafafa',
@ -34,8 +35,9 @@ const ANTD = {
'disabled-color': 'rgba(0, 0, 0, 0.25)',
'menu-dark-color': 'rgba(254, 254, 254, 0.65)',
'menu-dark-highlight-color': '#fefefe',
'menu-dark-selected-item-icon-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#fff',
'btn-primary-color': '#fefefe',
},
light: {
'layout-body-background': '#f0f2f5',
@ -43,10 +45,11 @@ const ANTD = {
'component-background': '#fff',
'heading-color': 'rgba(0, 0, 0, 0.85)',
'text-color': 'rgba(0, 0, 0, 0.65)',
'text-color-inverse': '#fff',
'text-color-inverse': '#fefefe',
'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
'shadow-color': 'rgba(0, 0, 0, 0.15)',
'border-color-split': '#f0f0f0',
'border-color-base': '#d9d9d9',
'background-color-light': '#fafafa',
'background-color-base': '#f5f5f5',
'table-selected-row-bg': '#fafafa',
@ -55,8 +58,9 @@ const ANTD = {
'disabled-color': 'rgba(0, 0, 0, 0.25)',
'menu-dark-color': 'rgba(1, 1, 1, 0.65)',
'menu-dark-highlight-color': '#fefefe',
'menu-dark-selected-item-icon-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#fff',
'btn-primary-color': '#fefefe',
},
night: {
'layout-body-background': '#000',
@ -64,10 +68,11 @@ const ANTD = {
'component-background': '#141414',
'heading-color': 'rgba(255, 255, 255, 0.85)',
'text-color': 'rgba(255, 255, 255, 0.85)',
'text-color-inverse': '#141414',
'text-color-inverse': '#fefefe',
'text-color-secondary': 'rgba(255, 255, 255, 0.45)',
'shadow-color': 'rgba(255, 255, 255, 0.15)',
'border-color-split': '#303030',
'border-color-base': '#282828',
'background-color-light': '#ffffff0a',
'background-color-base': '#2a2a2a',
'table-selected-row-bg': '#ffffff0a',
@ -76,8 +81,9 @@ const ANTD = {
'disabled-color': 'rgba(255, 255, 255, 0.25)',
'menu-dark-color': 'rgba(254, 254, 254, 0.65)',
'menu-dark-highlight-color': '#fefefe',
'menu-dark-selected-item-icon-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#141414',
'btn-primary-color': '#fefefe',
}
}
}

View File

@ -6,17 +6,22 @@ module.exports = {
mode: 'dark', //主题模式 可选 dark、 light 和 night
success: '#52c41a', //成功色
warning: '#faad14', //警告色
error: '#f5222d', //错误色
error: '#f5222f', //错误色
},
layout: 'side', //导航布局,可选 side 和 head分别为侧边导航和顶部导航
fixedHeader: false, //固定头部状态栏true:固定false:不固定
fixedSideBar: true, //固定侧边栏true:固定false:不固定
fixedTabs: false, //固定页签头true:固定false:不固定
pageWidth: 'fixed', //内容区域宽度fixed:固定宽度fluid:流式宽度
weekMode: false, //色弱模式true:开启false:不开启
multiPage: false, //多页签模式true:开启false:不开启
cachePage: true, //是否缓存页面数据仅多页签模式下生效true 缓存, false 不缓存
hideSetting: false, //隐藏设置抽屉true:隐藏false:不隐藏
systemName: 'Vue Antd Admin', //系统名称
copyright: '2018 ICZER 工作室出品', //copyright
asyncRoutes: false, //异步加载路由true:开启false:不开启
copyright: '2018 ICZER 工作室出品', //copyright
asyncRoutes: false, //异步加载路由true:开启false:不开启
showPageTitle: true, //是否显示页面标题PageLayout 布局中的页面标题true:显示false:不显示
filterMenu: true, //根据权限过滤菜单true:过滤false:不过滤
animate: { //动画设置
disabled: false, //禁用动画true:禁用false:启用
name: 'bounce', //动画效果,支持的动画效果可参考 ./animate.config.js

View File

@ -19,12 +19,24 @@ const cssResolve = {
return cssObj.toText()
}
},
'.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-checkbox-checked .ant-checkbox-inner:after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-menu-dark .ant-menu-inline.ant-menu-sub': {
resolve(cssText, cssObj) {
cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1)
@ -37,6 +49,12 @@ const cssResolve = {
return cssObj.toText()
}
},
'.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': {
resolve(cssText, cssObj) {
cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')
return cssObj.toText()
}
},
'.ant-layout-sider': {
resolve(cssText, cssObj) {
cssObj.selector = '.ant-layout-sider-dark'

View File

@ -1,9 +1,10 @@
<template>
<a-layout :class="['admin-layout', fixedSideBar ? 'fixed-side-bar' : '', 'beauty-scroll']">
<drawer v-if="isMobile" v-model="collapsed">
<a-layout :class="['admin-layout', 'beauty-scroll']">
<drawer v-if="isMobile" v-model="drawerOpen">
<side-menu :theme="theme.mode" :menuData="menuData" :collapsed="false" :collapsible="false" @menuSelect="onMenuSelect"/>
</drawer>
<side-menu :theme="theme.mode" v-else-if="layout === 'side'" :menuData="menuData" :collapsed="collapsed" :collapsible="true" />
<side-menu :class="[fixedSideBar ? 'fixed-side' : '']" :theme="theme.mode" v-else-if="layout === 'side' || layout === 'mix'" :menuData="sideMenuData" :collapsed="collapsed" :collapsible="true" />
<div v-if="fixedSideBar && !isMobile" :style="`width: ${sideMenuWidth}; min-width: ${sideMenuWidth};max-width: ${sideMenuWidth};`" class="virtual-side"></div>
<drawer v-if="!hideSetting" v-model="showSetting" placement="right">
<div class="setting" slot="handler">
<a-icon :type="showSetting ? 'close' : 'setting'"/>
@ -11,10 +12,10 @@
<setting />
</drawer>
<a-layout class="admin-layout-main beauty-scroll">
<admin-header :style="headerStyle" :menuData="menuData" :collapsed="collapsed" @toggleCollapse="toggleCollapse"/>
<a-layout-header v-if="fixedHeader"></a-layout-header>
<a-layout-content class="admin-layout-content">
<div :style="`min-height: ${minHeight}px; position: relative`">
<admin-header :class="[{'fixed-tabs': fixedTabs, 'fixed-header': fixedHeader, 'multi-page': multiPage}]" :style="headerStyle" :menuData="headMenuData" :collapsed="collapsed" @toggleCollapse="toggleCollapse"/>
<a-layout-header :class="['virtual-header', {'fixed-tabs' : fixedTabs, 'fixed-header': fixedHeader, 'multi-page': multiPage}]" v-show="fixedHeader"></a-layout-header>
<a-layout-content class="admin-layout-content" :style="`min-height: ${minHeight}px;`">
<div style="position: relative">
<slot></slot>
</div>
</a-layout-content>
@ -31,68 +32,127 @@ import PageFooter from './footer/PageFooter'
import Drawer from '../components/tool/Drawer'
import SideMenu from '../components/menu/SideMenu'
import Setting from '../components/setting/Setting'
import {mapState, mapMutations} from 'vuex'
import {mapState, mapMutations, mapGetters} from 'vuex'
const minHeight = window.innerHeight - 64 - 24 - 122
// const minHeight = window.innerHeight - 64 - 122
export default {
name: 'AdminLayout',
components: {Setting, SideMenu, Drawer, PageFooter, AdminHeader},
data () {
return {
minHeight: minHeight,
minHeight: window.innerHeight - 64 - 122,
collapsed: false,
showSetting: false
showSetting: false,
drawerOpen: false
}
},
provide() {
return {
adminLayout: this
}
},
watch: {
$route(val) {
this.setActivated(val)
},
layout() {
this.setActivated(this.$route)
},
isMobile(val) {
if(!val) {
this.drawerOpen = false
}
}
},
computed: {
...mapState('setting', ['isMobile', 'theme', 'layout', 'footerLinks', 'copyright', 'fixedHeader', 'fixedSideBar',
'hideSetting', 'menuData']),
'fixedTabs', 'hideSetting', 'multiPage']),
...mapGetters('setting', ['firstMenu', 'subMenu', 'menuData']),
sideMenuWidth() {
return this.collapsed ? '80px' : '256px'
},
headerStyle() {
let width = (this.fixedHeader && this.layout == 'side' && !this.isMobile) ? `calc(100% - ${this.sideMenuWidth})` : '100%'
let width = (this.fixedHeader && this.layout !== 'head' && !this.isMobile) ? `calc(100% - ${this.sideMenuWidth})` : '100%'
let position = this.fixedHeader ? 'fixed' : 'static'
let transition = this.fixedHeader ? 'transition: width 0.2s' : ''
return `width: ${width}; position: ${position}; ${transition}`
return `width: ${width}; position: ${position};`
},
headMenuData() {
const {layout, menuData, firstMenu} = this
return layout === 'mix' ? firstMenu : menuData
},
sideMenuData() {
const {layout, menuData, subMenu} = this
return layout === 'mix' ? subMenu : menuData
}
},
methods: {
...mapMutations('setting', ['correctPageMinHeight']),
...mapMutations('setting', ['correctPageMinHeight', 'setActivatedFirst']),
toggleCollapse () {
this.collapsed = !this.collapsed
},
onMenuSelect () {
this.toggleCollapse()
},
setActivated(route) {
if (this.layout === 'mix') {
let matched = route.matched
matched = matched.slice(0, matched.length - 1)
const {firstMenu} = this
for (let menu of firstMenu) {
if (matched.findIndex(item => item.path === menu.fullPath) !== -1) {
this.setActivatedFirst(menu.fullPath)
break
}
}
}
}
},
created() {
this.correctPageMinHeight(minHeight - 1)
this.correctPageMinHeight(this.minHeight - 24)
this.setActivated(this.$route)
},
beforeDestroy() {
this.correctPageMinHeight(-minHeight + 1)
this.correctPageMinHeight(-this.minHeight + 24)
}
}
</script>
<style lang="less" scoped>
.admin-layout{
&.fixed-side-bar{
height: 100vh;
.admin-layout-main{
overflow: scroll;
.side-menu{
&.fixed-side{
position: fixed;
height: 100vh;
left: 0;
top: 0;
}
}
.virtual-side{
transition: all 0.2s;
}
.virtual-header{
transition: all 0.2s;
opacity: 0;
&.fixed-tabs.multi-page:not(.fixed-header){
height: 0;
}
}
.admin-layout-main{
.admin-header{
top: 0;
right: 0;
overflow: hidden;
transition: all 0.2s;
&.fixed-tabs.multi-page:not(.fixed-header){
height: 0;
}
}
}
.admin-layout-content{
padding: 24px 24px 0;
min-height: auto;
/*overflow-x: hidden;*/
/*min-height: calc(100vh - 64px - 122px);*/
}
.setting{
background-color: @primary-color;

View File

@ -13,7 +13,7 @@
</div>
<slot v-if="this.$slots.extra" slot="extra" name="extra"></slot>
</page-header>
<div ref="page" :class="['page-content', layout]" >
<div ref="page" :class="['page-content', layout, pageWidth]" >
<slot></slot>
</div>
</div>
@ -60,10 +60,10 @@ export default {
this.updatePageHeight(0)
},
computed: {
...mapState('setting', ['layout', 'multiPage', 'pageMinHeight']),
...mapState('setting', ['layout', 'multiPage', 'pageMinHeight', 'pageWidth', 'customTitles']),
pageTitle() {
let pageTitle = this.page && this.page.title
return this.title || this.$t(pageTitle) || this.routeName
return this.customTitle || (pageTitle && this.$t(pageTitle)) || this.title || this.routeName
},
routeName() {
const route = this.$route
@ -90,11 +90,17 @@ export default {
...mapMutations('setting', ['correctPageMinHeight']),
getRouteBreadcrumb() {
let routes = this.$route.matched
const path = this.$route.path
let breadcrumb = []
routes.forEach(route => {
routes.filter(item => path.includes(item.path) || item.regex.test(path))
.forEach(route => {
const path = route.path.length === 0 ? '/home' : route.path
breadcrumb.push(this.$t(getI18nKey(path)))
})
let pageTitle = this.page && this.page.title
if (this.customTitle || pageTitle) {
breadcrumb[breadcrumb.length - 1] = this.customTitle || pageTitle
}
return breadcrumb
},
/**
@ -130,7 +136,7 @@ export default {
padding: 24px 0 0;
&.side{
}
&.head{
&.head.fixed{
margin: 0 auto;
max-width: 1400px;
}

View File

@ -25,13 +25,13 @@ export default {
computed: {
...mapState('setting', ['isMobile', 'multiPage', 'animate']),
desc() {
return this.page.desc
return this.page?.desc
},
linkList() {
return this.page.linkList
return this.page?.linkList
},
extraImage() {
return this.page.extraImage
return this.page?.extraImage
}
},
mounted () {

View File

@ -20,12 +20,15 @@ export default {
<style lang="less" scoped>
.footer{
padding: 0 16px;
margin: 48px 0 24px;
padding: 48px 16px 24px;
/*margin: 48px 0 24px;*/
text-align: center;
.copyright{
color: @text-color-second;
font-size: 14px;
i {
margin: 0 4px;
}
}
.links{
margin-bottom: 8px;

View File

@ -1,19 +1,19 @@
<template>
<a-layout-header :class="[headerTheme, 'admin-header']">
<div :class="['admin-header-wide', layout]">
<div :class="['admin-header-wide', layout, pageWidth]">
<router-link v-if="isMobile || layout === 'head'" to="/" :class="['logo', isMobile ? null : 'pc', headerTheme]">
<img width="32" src="@/assets/img/logo.png" />
<h1 v-if="!isMobile">{{systemName}}</h1>
</router-link>
<a-divider v-if="isMobile" type="vertical" />
<a-icon v-if="layout === 'side'" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/>
<div v-if="layout == 'head' && !isMobile" class="admin-header-menu">
<i-menu class="head-menu" style="height: 64px; line-height: 64px;box-shadow: none" :theme="headerTheme" mode="horizontal" :options="menuData" @select="onSelect"/>
<a-icon v-if="layout !== 'head'" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/>
<div v-if="layout !== 'side' && !isMobile" class="admin-header-menu" :style="`width: ${menuWidth};`">
<i-menu class="head-menu" :theme="headerTheme" mode="horizontal" :options="menuData" @select="onSelect"/>
</div>
<div :class="['admin-header-right', headerTheme]">
<header-search class="header-item" />
<header-search class="header-item" @active="val => searchActive = val" />
<a-tooltip class="header-item" title="帮助文档" placement="bottom" >
<a href="https://iczer.github.io/vue-antd-admin/" target="_blank">
<a href="https://iczer.gitee.io/vue-antd-admin-docs/" target="_blank">
<a-icon type="question-circle-o" />
</a>
</a-tooltip>
@ -49,11 +49,12 @@ export default {
{key: 'CN', name: '简体中文', alias: '简体'},
{key: 'HK', name: '繁體中文', alias: '繁體'},
{key: 'US', name: 'English', alias: 'English'}
]
],
searchActive: false
}
},
computed: {
...mapState('setting', ['theme', 'isMobile', 'layout', 'systemName', 'lang']),
...mapState('setting', ['theme', 'isMobile', 'layout', 'systemName', 'lang', 'pageWidth']),
headerTheme () {
if (this.layout == 'side' && this.theme.mode == 'dark' && !this.isMobile) {
return 'light'
@ -63,6 +64,12 @@ export default {
langAlias() {
let lang = this.langList.find(item => item.key == this.lang)
return lang.alias
},
menuWidth() {
const {layout, searchActive} = this
const headWidth = layout === 'head' ? '100% - 188px' : '100%'
const extraWidth = searchActive ? '600px' : '400px'
return `calc(${headWidth} - ${extraWidth})`
}
},
methods: {

View File

@ -56,9 +56,10 @@ export default {
this.loading = false
return
}
this.loadding = true
if (this.show) return
this.loading = true
setTimeout(() => {
this.loadding = false
this.loading = false
}, 1000)
}
}

View File

@ -3,6 +3,7 @@
<a-icon type="search" class="search-icon" @click="enterSearchMode"/>
<a-auto-complete
ref="input"
:getPopupContainer="e => {return e.parentNode || document.body}"
:dataSource="dataSource"
:class="['search-input', searchMode ? 'enter' : 'leave']"
placeholder="站内搜索"
@ -24,10 +25,12 @@ export default {
methods: {
enterSearchMode () {
this.searchMode = true
this.$emit('active', true)
setTimeout(() => this.$refs.input.focus(), 300)
},
leaveSearchMode () {
this.searchMode = false
setTimeout(() => this.$emit('active', false), 300)
}
}
}

View File

@ -4,6 +4,12 @@
box-shadow: @shadow-down;
position: relative;
background: @base-bg-color;
.head-menu{
height: 64px;
line-height: 64px;
vertical-align: middle;
box-shadow: none;
}
&.dark{
background: @header-bg-color-dark;
color: white;
@ -14,9 +20,11 @@
}
}
.admin-header-wide{
&.head{
padding-left: 24px;
&.head.fixed{
max-width: 1400px;
margin: auto;
padding-left: 0;
}
&.side{
padding-right: 12px;

View File

@ -0,0 +1,187 @@
<template>
<div :class="['tabs-head', layout, pageWidth]">
<a-tabs
type="editable-card"
:class="['tabs-container', layout, pageWidth, {'affixed' : affixed, 'fixed-header' : fixedHeader, 'collapsed' : adminLayout.collapsed}]"
:active-key="active"
:hide-add="true"
>
<a-tooltip placement="left" :title="lockTitle" slot="tabBarExtraContent">
<a-icon
theme="filled"
@click="onLockClick"
class="header-lock"
:type="fixedTabs ? 'lock' : 'unlock'"
/>
</a-tooltip>
<a-tab-pane v-for="page in pageList" :key="page.path">
<div slot="tab" class="tab" @contextmenu="e => onContextmenu(page.path, e)">
<a-icon @click="onRefresh(page)" :class="['icon-sync', {'hide': page.path !== active && !page.loading}]" :type="page.loading ? 'loading' : 'sync'" />
<div class="title" @click="onTabClick(page.path)" >{{pageName(page)}}</div>
<a-icon v-if="!page.unclose" @click="onClose(page.path)" class="icon-close" type="close"/>
</div>
</a-tab-pane>
</a-tabs>
<div v-if="affixed" class="virtual-tabs"></div>
</div>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
import {getI18nKey} from '@/utils/routerUtil'
export default {
name: 'TabsHead',
i18n: {
messages: {
CN: {
lock: '点击锁定页签头',
unlock: '点击解除锁定',
},
HK: {
lock: '點擊鎖定頁簽頭',
unlock: '點擊解除鎖定',
},
US: {
lock: 'click to lock the tabs head',
unlock: 'click to unlock',
}
}
},
props: {
pageList: Array,
active: String,
fixed: Boolean
},
data() {
return {
affixed: false,
}
},
inject:['adminLayout'],
created() {
this.affixed = this.fixedTabs
},
computed: {
...mapState('setting', ['layout', 'pageWidth', 'fixedHeader', 'fixedTabs', 'customTitles']),
lockTitle() {
return this.$t(this.fixedTabs ? 'unlock' : 'lock')
}
},
methods: {
...mapMutations('setting', ['setFixedTabs']),
onLockClick() {
this.setFixedTabs(!this.fixedTabs)
if (this.fixedTabs) {
setTimeout(() => {
this.affixed = true
}, 200)
} else {
this.affixed = false
}
},
onTabClick(key) {
if (this.active !== key) {
this.$emit('change', key)
}
},
onClose(key) {
this.$emit('close', key)
},
onRefresh(page) {
this.$emit('refresh', page.path, page)
},
onContextmenu(pageKey, e) {
this.$emit('contextmenu', pageKey, e)
},
pageName(page) {
const custom = this.customTitles.find(item => item.path === page.path)
return (custom && custom.title) || page.title || this.$t(getI18nKey(page.keyPath))
}
}
}
</script>
<style scoped lang="less">
.tab{
margin: 0 -16px;
padding: 0 16px;
font-size: 14px;
user-select: none;
transition: all 0.2s;
.title{
display: inline-block;
height: 100%;
}
.icon-close{
font-size: 12px;
margin-left: 6px;
margin-right: -4px !important;
color: @text-color-second;
&:hover{
color: @text-color;
}
}
.icon-sync{
margin-left: -4px;
color: @primary-4;
transition: all 0.3s ease-in-out;
&:hover{
color: @primary-color;
}
font-size: 14px;
&.hide{
font-size: 0;
}
}
}
.tabs-head{
margin: 0 auto;
&.head.fixed{
width: 1400px;
}
}
.tabs-container{
margin: -16px auto 8px;
transition: top,left 0.2s;
.header-lock{
font-size: 18px;
cursor: pointer;
color: @primary-3;
&:hover{
color: @primary-color;
}
}
&.affixed{
margin: 0 auto;
top: 0px;
padding: 8px 24px 0;
position: fixed;
height: 48px;
z-index: 1;
background-color: @layout-body-background;
&.side,&.mix{
right: 0;
left: 256px;
&.collapsed{
left: 80px;
}
}
&.head{
width: inherit;
padding: 8px 0 0;
&.fluid{
left: 0;
right: 0;
padding: 8px 24px 0;
}
}
&.fixed-header{
top: 64px;
}
}
}
.virtual-tabs{
height: 48px;
}
</style>

View File

@ -1,26 +1,21 @@
<template>
<admin-layout>
<contextmenu :itemList="menuItemList" :visible.sync="menuVisible" @select="onMenuSelect" />
<a-tabs
v-if="multiPage"
type="editable-card"
:active-key="activePage"
:style="`margin: -16px auto 8px; ${layout == 'head' ? 'max-width: 1400px;' : ''}`"
:hide-add="true"
@change="changePage"
@edit="editPage"
@contextmenu="onContextmenu"
>
<a-tab-pane :key="page.fullPath" v-for="page in pageList">
<span slot="tab" :pagekey="page.fullPath">{{pageName(page)}}</span>
</a-tab-pane>
</a-tabs>
<div class="tabs-view-content" :style="`margin-top: ${multiPage ? -24 : 0}px`">
<tabs-head
v-if="multiPage"
:active="activePage"
:page-list="pageList"
@change="changePage"
@close="remove"
@refresh="refresh"
@contextmenu="onContextmenu"
/>
<div :class="['tabs-view-content', layout, pageWidth]" :style="`margin-top: ${multiPage ? -24 : 0}px`">
<page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
<a-keep-alive v-if="multiPage" v-model="clearCaches">
<router-view ref="tabContent" :key="$route.fullPath" />
<a-keep-alive :exclude-keys="excludeKeys" v-if="multiPage && cachePage" v-model="clearCaches">
<router-view v-if="!refreshing" ref="tabContent" :key="$route.fullPath" />
</a-keep-alive>
<router-view v-else />
<router-view ref="tabContent" v-else-if="!refreshing" />
</page-toggle-transition>
</div>
</admin-layout>
@ -33,27 +28,30 @@ import PageToggleTransition from '@/components/transition/PageToggleTransition'
import {mapState, mapMutations} from 'vuex'
import {getI18nKey} from '@/utils/routerUtil'
import AKeepAlive from '@/components/cache/AKeepAlive'
import TabsHead from '@/layouts/tabs/TabsHead'
export default {
name: 'TabsView',
i18n: require('./i18n'),
components: { PageToggleTransition, Contextmenu, AdminLayout , AKeepAlive },
components: {TabsHead, PageToggleTransition, Contextmenu, AdminLayout , AKeepAlive },
data () {
return {
clearCaches: [],
pageList: [],
cachedKeys: [],
activePage: '',
menuVisible: false
menuVisible: false,
refreshing: false,
excludeKeys: []
}
},
computed: {
...mapState('setting', ['multiPage', 'animate', 'layout']),
...mapState('setting', ['multiPage', 'cachePage', 'animate', 'layout', 'pageWidth']),
menuItemList() {
return [
{ key: '1', icon: 'vertical-right', text: this.$t('closeLeft') },
{ key: '2', icon: 'vertical-left', text: this.$t('closeRight') },
{ key: '3', icon: 'close', text: this.$t('closeOthers') }
{ key: '3', icon: 'close', text: this.$t('closeOthers') },
{ key: '4', icon: 'sync', text: this.$t('refresh') },
]
},
tabsOffset() {
@ -61,39 +59,54 @@ export default {
}
},
created () {
this.loadCacheConfig(this.$router?.options?.routes)
this.loadCachedTabs()
const route = this.$route
this.pageList.push(route)
if (this.pageList.findIndex(item => item.path === route.fullPath) === -1) {
this.pageList.push(this.createPage(route))
}
this.activePage = route.fullPath
if (this.multiPage) {
window.addEventListener('page:close', this.closePageListener)
this.$nextTick(() => {
this.setCachedKey(route)
})
this.addListener()
}
},
mounted () {
this.correctPageMinHeight(-this.tabsOffset)
this.cachedKeys.push(this.$refs.tabContent.$vnode.key)
},
beforeDestroy() {
window.removeEventListener('page:close', this.closePageListener)
this.removeListener()
this.correctPageMinHeight(this.tabsOffset)
},
watch: {
'$router.options.routes': function (val) {
this.excludeKeys = []
this.loadCacheConfig(val)
},
'$route': function (newRoute) {
this.activePage = newRoute.fullPath
const page = this.pageList.find(item => item.path === newRoute.fullPath)
if (!this.multiPage) {
this.pageList = [newRoute]
} else if (this.pageList.findIndex(item => item.fullPath == newRoute.fullPath) == -1) {
this.pageList = [this.createPage(newRoute)]
} else if (page) {
page.fullPath = newRoute.fullPath
} else if (!page) {
this.pageList.push(this.createPage(newRoute))
}
if (this.multiPage) {
this.$nextTick(() => {
this.cachedKeys.push(this.$refs.tabContent.$vnode.key)
this.setCachedKey(newRoute)
})
this.pageList.push(newRoute)
}
},
'multiPage': function (newVal) {
if (!newVal) {
this.pageList = [this.$route]
window.removeEventListener('page:close', this.closePageListener)
this.pageList = [this.createPage(this.$route)]
this.removeListener()
} else {
window.addEventListener('page:close', this.closePageListener)
this.addListener()
}
},
tabsOffset(newVal, oldVal) {
@ -103,108 +116,215 @@ export default {
methods: {
changePage (key) {
this.activePage = key
this.$router.push(key)
},
editPage (key, action) {
this[action](key) // remove
const page = this.pageList.find(item => item.path === key)
this.$router.push(page.fullPath)
},
remove (key, next) {
if (this.pageList.length === 1) {
return this.$message.warning(this.$t('warn'))
}
let index = this.pageList.findIndex(item => item.fullPath === key)
//
this.clearCaches = this.cachedKeys.splice(index, 1)
this.pageList.splice(index, 1)
let index = this.pageList.findIndex(item => item.path === key)
this.clearCaches = this.pageList.splice(index, 1).map(page => page.cachedKey)
if (next) {
this.$router.push(next)
} else if (key === this.activePage) {
index = index >= this.pageList.length ? this.pageList.length - 1 : index
this.activePage = this.pageList[index].fullPath
this.activePage = this.pageList[index].path
this.$router.push(this.activePage)
}
},
onContextmenu (e) {
const pageKey = getPageKey(e.target)
refresh (key, page) {
page = page || this.pageList.find(item => item.path === key)
page.loading = true
this.clearCache(page)
if (key === this.activePage) {
this.reloadContent(() => page.loading = false)
} else {
// loading
setTimeout(() => page.loading = false, 500)
}
},
onContextmenu(pageKey, e) {
if (pageKey) {
e.preventDefault()
e.meta = pageKey
this.menuVisible = true
}
},
onMenuSelect (key, target) {
let pageKey = getPageKey(target)
onMenuSelect (key, target, pageKey) {
switch (key) {
case '1': this.closeLeft(pageKey); break
case '2': this.closeRight(pageKey); break
case '3': this.closeOthers(pageKey); break
case '4': this.refresh(pageKey); break
default: break
}
},
closeOthers (pageKey) {
const index = this.pageList.findIndex(item => item.fullPath === pageKey)
//
this.clearCaches = this.cachedKeys.filter((item, i) => i != index)
this.cachedKeys = this.cachedKeys.slice(index, index + 1)
this.pageList = this.pageList.slice(index, index + 1)
const clearPages = this.pageList.filter(item => item.path !== pageKey && !item.unclose)
this.clearCaches = clearPages.map(item => item.cachedKey)
this.pageList = this.pageList.filter(item => !clearPages.includes(item))
//
if (this.activePage != pageKey) {
this.activePage = pageKey
this.$router.push(this.activePage)
}
},
closeLeft (pageKey) {
const index = this.pageList.findIndex(item => item.fullPath === pageKey)
const index = this.pageList.findIndex(item => item.path === pageKey)
//
this.clearCaches = this.cachedKeys.filter((item, i) => i < index)
this.cachedKeys = this.cachedKeys.slice(index)
this.pageList = this.pageList.slice(index)
if (!this.pageList.find(item => item.fullPath === this.activePage)) {
const clearPages = this.pageList.filter((item, i) => i < index && !item.unclose)
this.clearCaches = clearPages.map(item => item.cachedKey)
this.pageList = this.pageList.filter(item => !clearPages.includes(item))
//
if (!this.pageList.find(item => item.path === this.activePage)) {
this.activePage = pageKey
this.$router.push(this.activePage)
}
},
closeRight (pageKey) {
const index = this.pageList.findIndex(item => item.fullPath === pageKey)
//
this.clearCaches = this.cachedKeys.filter((item, i) => i > index)
this.cachedKeys = this.cachedKeys.slice(0, index+1)
this.pageList = this.pageList.slice(0, index + 1)
if (!this.pageList.find(item => item.fullPath === this.activePage)) {
const index = this.pageList.findIndex(item => item.path === pageKey)
const clearPages = this.pageList.filter((item, i) => i > index && !item.unclose)
this.clearCaches = clearPages.map(item => item.cachedKey)
this.pageList = this.pageList.filter(item => !clearPages.includes(item))
//
if (!this.pageList.find(item => item.path === this.activePage)) {
this.activePage = pageKey
this.$router.push(this.activePage)
}
},
pageName(page) {
return this.$t(getI18nKey(page.matched[page.matched.length - 1].path))
clearCache(page) {
page._init_ = false
this.clearCaches = [page.cachedKey]
},
reloadContent(onLoaded) {
this.refreshing = true
setTimeout(() => {
this.refreshing = false
this.$nextTick(() => {
this.setCachedKey(this.$route)
if (typeof onLoaded === 'function') {
onLoaded.apply(this, [])
}
})
}, 200)
},
pageName(page) {
return this.$t(getI18nKey(page.keyPath))
},
/**
* 添加监听器
*/
addListener() {
window.addEventListener('page:close', this.closePageListener)
window.addEventListener('page:refresh', this.refreshPageListener)
window.addEventListener('unload', this.unloadListener)
},
/**
* 移出监听器
*/
removeListener() {
window.removeEventListener('page:close', this.closePageListener)
window.removeEventListener('page:refresh', this.refreshPageListener)
window.removeEventListener('unload', this.unloadListener)
},
/**
* 页签关闭事件监听
* @param event 页签关闭事件
*/
closePageListener(event) {
const {closeRoute, nextRoute} = event.detail
const closePath = typeof closeRoute === 'string' ? closeRoute : closeRoute.path
this.remove(closePath, nextRoute)
const path = closePath && closePath.split('?')[0]
this.remove(path, nextRoute)
},
/**
* 页面刷新事件监听
* @param event 页签关闭事件
*/
refreshPageListener(event) {
const {pageKey} = event.detail
const path = pageKey && pageKey.split('?')[0]
this.refresh(path)
},
/**
* 页面 unload 事件监听器添加页签到 session 缓存用于刷新时保留页签
*/
unloadListener() {
const tabs = this.pageList.map(item => ({...item, _init_: false}))
sessionStorage.setItem(process.env.VUE_APP_TBAS_KEY, JSON.stringify(tabs))
},
createPage(route) {
return {
keyPath: route.matched[route.matched.length - 1].path,
fullPath: route.fullPath, loading: false,
path: route.fullPath,
title: route.meta && route.meta.page && route.meta.page.title,
unclose: route.meta && route.meta.page && (route.meta.page.closable === false),
}
},
/**
* 设置页面缓存的key
* @param route 页面对应的路由
*/
setCachedKey(route) {
const page = this.pageList.find(item => item.path === route.fullPath)
page.unclose = route.meta && route.meta.page && (route.meta.page.closable === false)
if (!page._init_) {
const vnode = this.$refs.tabContent.$vnode
page.cachedKey = vnode.key + vnode.componentOptions.Ctor.cid
page._init_ = true
}
},
/**
* 加载缓存的 tabs
*/
loadCachedTabs() {
const cachedTabsStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_KEY)
if (cachedTabsStr) {
try {
const cachedTabs = JSON.parse(cachedTabsStr)
if (cachedTabs.length > 0) {
this.pageList = cachedTabs
}
} catch (e) {
console.warn('failed to load cached tabs, got exception:', e)
} finally {
sessionStorage.removeItem(process.env.VUE_APP_TBAS_KEY)
}
}
},
loadCacheConfig(routes, pCache = true) {
routes.forEach(item => {
const cacheAble = item.meta?.page?.cacheAble ?? pCache ?? true
if (!cacheAble) {
this.excludeKeys.push(new RegExp(`${item.path.replace(/:[^/]*/g, '[^/]*')}(\\?.*)?\\d*$`))
}
if (item.children) {
this.loadCacheConfig(item.children, cacheAble)
}
})
},
...mapMutations('setting', ['correctPageMinHeight'])
}
}
/**
* 由于ant-design-vue组件库的TabPane组件暂不支持自定义监听器无法直接获取到右键target所在标签页的 pagekey 故增加此方法用于
* 查询右键target所在标签页的标识 pagekey 以用于自定义右键菜单的事件处理
* TabPane组件支持自定义监听器后可去除该方法并重构 自定义右键菜单的事件处理
* @param target 查询开始目标
* @param depth 查询层级深度 查找层级最多不超过3层超过3层深度直接返回 null
* @returns {String}
*/
function getPageKey (target, depth = 0) {
if (depth > 2 || !target) {
return null
}
return target.getAttribute('pagekey') || getPageKey(target.firstElementChild, ++depth)
}
</script>
<style scoped lang="less">
.tabs-view{
margin: -16px auto 8px;
&.head.fixed{
max-width: 1400px;
}
}
.tabs-view-content{
position: relative;
&.head.fixed{
width: 1400px;
margin: 0 auto;
}
}
</style>

View File

@ -4,18 +4,21 @@ module.exports = {
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeOthers: '关闭其它',
refresh: '刷新页面',
warn: '这是最后一页,不能再关闭了',
},
HK: {
closeLeft: '關閉左側',
closeRight: '關閉右側',
closeOthers: '關閉其它',
refresh: '刷新頁面',
warn: '這是最後一頁,不能再關閉了',
},
US: {
closeLeft: 'close left',
closeRight: 'close right',
closeOthers: 'close others',
refresh: 'refresh the page',
warn: 'This is the last page, you can\'t close it',
},
}

View File

@ -10,16 +10,18 @@ import 'animate.css/source/animate.css'
import Plugins from '@/plugins'
import {initI18n} from '@/utils/i18n'
import bootstrap from '@/bootstrap'
import 'moment/locale/zh-cn'
const router = initRouter(store.state.setting.asyncRoutes)
const i18n = initI18n('CN', 'US')
bootstrap({router, store, i18n})
Vue.use(Antd)
Vue.config.productionTip = false
Vue.use(Viser)
Vue.use(Antd)
Vue.use(Plugins)
bootstrap({router, store, i18n, message: Vue.prototype.$message})
new Vue({
router,
store,

View File

@ -50,6 +50,8 @@ const welcomeMessages = [
}
]
const goods = ['运动鞋', '短裤', 'T恤', '七分裤', '风衣', '寸衫']
Random.extend({
admin () {
return this.pick(admins)
@ -69,6 +71,9 @@ Random.extend({
position () {
return this.pick(positions)
},
goods () {
return this.pick(goods)
},
saying () {
return this.pick(sayings)
},

106
src/mock/goods/index.js Normal file
View File

@ -0,0 +1,106 @@
import Mock from 'mockjs'
import '@/mock/extend'
import {parseUrlParams} from '@/utils/request'
const current = new Date().getTime()
const goodsList = Mock.mock({
'list|100': [{
'id|+1': 0,
'name': '@GOODS',
'orderId': `${current}-@integer(1,100)`,
'status|1-4': 1,
'send': '@BOOLEAN',
'sendTime': '@DATETIME',
'orderDate': '@DATE',
'auditTime': '@TIME'
}]
})
Mock.mock(RegExp(`${process.env.VUE_APP_API_BASE_URL}/goods` + '.*'),'get', ({url}) => {
const params = parseUrlParams(decodeURI(url))
let {page, pageSize} = params
page = eval(page) - 1 || 0
pageSize = eval(pageSize) || 10
delete params.page
delete params.pageSize
let result = goodsList.list.filter(item => {
for (let [key, value] of Object.entries(params)) {
if (item[key] != value) {
return false
}
}
return true
})
const total = result.length
if ((page) * pageSize > total) {
result = []
} else {
result = result.slice(page * pageSize, (page + 1) * pageSize)
}
return {
code: 0,
message: 'success',
data: {
page: page + 1,
pageSize,
total,
list: result
}
}
})
const columnsConfig = [
{
title: '商品名称',
dataIndex: 'name',
searchAble: true
},
{
title: '订单号',
dataIndex: 'orderId'
},
{
searchAble: true,
dataIndex: 'status',
dataType: 'select',
slots: {title: 'statusTitle'},
scopedSlots: {customRender: 'status'},
search: {
selectOptions: [
{title: '已下单', value: 1},
{title: '已付款', value: 2},
{title: '已审核', value: 3},
// {title: '已发货', value: 4}
]
}
},
{
title: '发货',
searchAble: true,
dataIndex: 'send',
dataType: 'boolean',
scopedSlots: {customRender: 'send'}
},
{
title: '发货时间',
dataIndex: 'sendTime',
dataType: 'datetime'
},
{
title: '下单日期',
searchAble: true,
dataIndex: 'orderDate',
dataType: 'date',
visible: false
},
{
title: '审核时间',
dataIndex: 'auditTime',
dataType: 'time',
},
]
Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/columns`, 'get', () => {
return columnsConfig
})

View File

@ -4,8 +4,10 @@ import '@/mock/project'
import '@/mock/user/login'
import '@/mock/workplace'
import '@/mock/user/routes'
import '@/mock/goods'
import '@/mock/list'
// 设置全局延时
Mock.setup({
timeout: '300-600'
timeout: '200-400'
})

52
src/mock/list/index.js Normal file
View File

@ -0,0 +1,52 @@
import Mock from 'mockjs'
import '@/mock/extend'
import {parseUrlParams} from '@/utils/request'
const current = new Date().getTime()
const source = Mock.mock({
'list|100': [{
'key|+1': 0,
'no': `${current}-@integer(1,100)`,
'description': '这是一段描述',
'callNo|0-50': 5,
'status|1-4': 1,
'updatedAt': '@DATETIME',
}]
})
Mock.mock(RegExp(`${process.env.VUE_APP_API_BASE_URL}/list` + '.*'),'get', ({url}) => {
const params = parseUrlParams(decodeURI(url))
let {page, pageSize} = params
page = eval(page) - 1 || 0
pageSize = eval(pageSize) || 10
delete params.page
delete params.pageSize
let result = source.list.filter(item => {
for (let [key, value] of Object.entries(params)) {
if (item[key] !== value) {
return false
}
}
return true
})
const total = result.length
if ((page) * pageSize > total) {
result = []
} else {
result = result.slice(page * pageSize, (page + 1) * pageSize)
}
return {
code: 0,
message: 'success',
data: {
page: page + 1,
pageSize,
total: 100,
list: result
}
}
})

View File

@ -8,21 +8,32 @@ const user = Mock.mock({
position: '@POSITION'
})
Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/login`, 'post', ({body}) => {
let result = {}
let result = {data: {}}
const {name, password} = JSON.parse(body)
if (name !== 'admin' || password !== '888888') {
result.code = -1
result.message = '账户名或密码错误admin/888888'
let success = false
if (name === 'admin' && password === '888888') {
success = true
result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}]
result.data.roles = [{id: 'admin', operation: ['add', 'edit', 'delete']}]
} else if (name === 'test' || password === '888888') {
success = true
result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}]
result.data.roles = [{id: 'test', operation: ['add', 'edit', 'delete']}]
} else {
success = false
}
if (success) {
result.code = 0
result.message = Mock.mock('@TIMEFIX').CN + ',欢迎回来'
result.data = {}
result.data.user = user
result.data.token = 'Authorization:' + Math.random()
result.data.expireAt = new Date(new Date().getTime() + 30 * 60 * 1000)
result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit', 'delete']}]
result.data.roles = [{id: 'admin', operation: ['add', 'edit', 'delete']}]
} else {
result.code = -1
result.message = '账户名或密码错误admin/888888 or test/888888'
}
return result
})

View File

@ -18,7 +18,21 @@ Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/routes`, 'get', () => {
router: 'basicForm',
name: '验权表单',
icon: 'file-excel',
authority: 'form'
authority: 'queryForm'
},
{
router: 'antdv',
path: 'antdv',
name: 'Ant Design Vue',
icon: 'ant-design',
link: 'https://www.antdv.com/docs/vue/introduce-cn/'
},
{
router: 'document',
path: 'document',
name: '使用文档',
icon: 'file-word',
link: 'https://iczer.gitee.io/vue-antd-admin-docs/'
}
]
}]

16
src/pages/Demo.vue Normal file
View File

@ -0,0 +1,16 @@
<template>
<div>
<p>query: {{$route.query}}</p>
<p>params: {{$route.params}}</p>
</div>
</template>
<script>
export default {
name: 'Demo'
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,152 @@
<template>
<div class="api">
<div class="introduce">
<h2 class="title">说明</h2>
<p class="content">
AdvanceTable 是基于 Ant Design Vue Table 组件封装支持其所有 API<br/>
主要添加了<em>列设置</em><em>搜索控件配置</em>的功能可用于一些需要动态配置表格展示动态配置搜索条件的场景<br/>
使用方式 antd table 基本无异添加了部分API如下
</p>
</div>
<api-table :api-source="apiSource" />
<api-table type="event" title="事件" :api-source="events" />
<api-table title="Column" :api-source="columnApi" />
<api-table title="Search" :api-source="searchApi" />
</div>
</template>
<script>
import ApiTable from '@/components/table/api/ApiTable'
export default {
name: 'Api',
components: {ApiTable},
data() {
return {
apiSource: [
{
key: 0,
param: '<a href="https://www.antdv.com/components/table-cn/#API" target="_blank">Ant Design Vue Table API</a>',
desc: '支持 Ant Design Vue Table 组件 所有 api',
type: '--',
default: '--',
},
{
key: 1,
param: 'title',
desc: '表格标题',
type: 'string | slot',
default: '\'高级表格\''
},
{
key: 2,
param: 'formatConditions',
desc: `是否格式化搜索条件的值,格式化规则参考 <a>Search 配置</a>。
<br/>false取搜索输入控件的原值 <br/>true取搜索输入控件格式化后的值`,
type: 'boolean',
default: 'false',
},
{
key: 3,
param: 'columns',
desc: `表格列配置,参考 <a>Column 配置</a>`,
type: 'array',
default: '--',
}
],
events: [
{
key: 0,
param: '<a href="https://www.antdv.com/components/table-cn/#API" target="_blank">Ant Design Vue Table Events API</a>',
desc: '支持 Ant Design Vue Table 所有事件',
callback: '--',
},
{
key: 1,
param: 'search',
desc: '搜索条件变化时触发',
callback: 'Function(conditions, searchOptions: [{field, value, format}])',
},
{
key: 2,
param: 'refresh',
desc: '表头刷新图标点击时触发',
callback: 'Function(conditions, searchOptions: [{field, value, format}])',
},
{
key: 3,
param: 'reset',
desc: '列配置重置按钮点击时触发',
callback: 'Function(conditions, searchOptions: [{field, value, format}])',
},
],
columnApi: [
{
key: 0,
param: '<a href="https://www.antdv.com/components/table-cn/#API" target="_blank">Ant Design Vue Table Column API</a>',
desc: '支持 Ant Design Vue Table 组件 Column 配置所有 api',
type: '--',
default: '--'
},
{
key: 1,
param: 'searchAble',
desc: '是否启用列搜索',
type: 'boolean',
default: 'false'
},
{
key: 2,
param: 'dataType',
desc: `数据类型,该配置将决定列搜索输入控件的类型,与列搜索输入控件对应关系如下:<br/>
string: 输入框组件<br/>
boolean: 开关组件<br/>
select: 下拉输入框组件<br/>
date: 日期选择器<br/>
time: 时间选择器<br/>
datetime: 带时间选择器的日期选择器`,
type: `'string' | 'boolean' | 'select' | 'date' | 'time' | 'datetime'`,
default: `'string'`
},
{
key: 3,
param: 'search',
desc: '列搜索配置,参考 <a>Search 配置</a>',
type: 'object',
default: '--'
},
],
searchApi: [
{
key: 0,
param: 'format',
desc: `列搜索输入控件值的格式化配置。<br/>如果输入控件支持格式化,则可设置该值为字符串,如日期输入组件,可设为为 'YYYY-MM-DD'。
<br/>不支持格式化的输入控件可设置为一个接收控件的输入值作为参数的函数 (value) => {return \`prefix\${value}\`}。`,
type: 'string | Function(value)',
default: '取输入控件默认的格式化配置'
},
{
key: 1,
param: 'selectOptions',
desc: `select 数据类型的下拉输入组件的选项配置,可参考 <a href="https://www.antdv.com/components/select-cn/#API" target="_blank">Ant Design Vue Select Option props Api</a>`,
type: 'array',
default: '--'
}
],
}
}
}
</script>
<style scoped lang="less">
.api{
.introduce{
padding: 16px;
.content{
em{
margin: 0 4px;
color: @primary-color;
}
}
}
}
</style>

View File

@ -0,0 +1,166 @@
<template>
<div class="table">
<advance-table
:columns="columns"
:data-source="dataSource"
title="高级表格-Beta"
:loading="loading"
rowKey="id"
@search="onSearch"
@refresh="onRefresh"
:format-conditions="true"
@reset="onReset"
:pagination="{
current: page,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showLessItems: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,总计 ${total}`,
onChange: onPageChange,
onShowSizeChange: onSizeChange,
}"
>
<template slot="statusTitle">
状态<a-icon style="margin: 0 4px" type="info-circle" />
</template>
<template slot="send" slot-scope="{text}">
{{text ? '是' : '否'}}
</template>
<template slot="status" slot-scope="{text}">
{{text | statusStr}}
</template>
</advance-table>
<api />
</div>
</template>
<script>
import AdvanceTable from '@/components/table/advance/AdvanceTable'
import {dataSource as ds} from '@/services'
import Api from '@/pages/components/table/Api'
export default {
name: 'Table',
components: {Api, AdvanceTable},
filters: {
statusStr(val) {
switch (val) {
case 1: return '已下单'
case 2: return '已付款'
case 3: return '已审核'
case 4: return '已发货'
}
}
},
data() {
return {
loading: false,
page: 1,
pageSize: 10,
total: 0,
columns: [
{
title: '商品名称',
dataIndex: 'name',
searchAble: true
},
{
title: '订单号',
dataIndex: 'orderId'
},
{
searchAble: true,
dataIndex: 'status',
dataType: 'select',
slots: {title: 'statusTitle'},
scopedSlots: {customRender: 'status'},
search: {
selectOptions: [
{title: '已下单', value: 1},
{title: '已付款', value: 2},
{title: '已审核', value: 3},
{title: '已发货', value: 4}
]
}
},
{
title: '发货',
searchAble: true,
dataIndex: 'send',
dataType: 'boolean',
scopedSlots: {customRender: 'send'},
search: {
switchOptions: {
checkedText: '开',
uncheckedText: '关'
}
}
},
{
title: '审核时间',
dataIndex: 'auditTime',
dataType: 'time',
}
],
dataSource: [],
conditions: {}
}
},
created() {
this.getGoodList()
this.getColumns()
},
methods: {
getGoodList() {
this.loading = true
const {page, pageSize, conditions} = this
ds.goodsList({page, pageSize, ...conditions}).then(result => {
const {list, page, pageSize, total} = result.data.data
this.dataSource = list
this.page = page
this.total = total
this.pageSize = pageSize
this.loading = false
})
},
getColumns() {
ds.goodsColumns().then(res => {
this.columns = res.data
})
},
onSearch(conditions, searchOptions) {
console.log(searchOptions)
this.page = 1
this.conditions = conditions
this.getGoodList()
},
onSizeChange(current, size) {
this.page = 1
this.pageSize = size
this.getGoodList()
},
onRefresh(conditions) {
this.conditions = conditions
this.getGoodList()
},
onReset(conditions) {
this.conditions = conditions
this.getGoodList()
},
onPageChange(page, pageSize) {
this.page = page
this.pageSize = pageSize
this.getGoodList()
}
}
}
</script>
<style scoped lang="less">
.table{
background-color: @base-bg-color;
padding: 24px;
}
</style>

View File

@ -0,0 +1,2 @@
import Table from './Table'
export default Table

View File

@ -1,6 +1,6 @@
<template>
<div class="analysis">
<a-row :gutter="[24, 24]">
<a-row style="margin-top: 0" :gutter="[24, 24]">
<a-col :sm="24" :md="12" :xl="6">
<chart-card :loading="loading" :title="$t('totalSales')" total="¥ 189,345">
<a-tooltip :title="$t('introduce')" slot="action">

View File

@ -79,7 +79,7 @@
</a-form>
</div>
<div>
<div class="operator">
<a-space class="operator">
<a-button @click="addNew" type="primary">新建</a-button>
<a-button >批量操作</a-button>
<a-dropdown>
@ -91,13 +91,14 @@
更多操作 <a-icon type="down" />
</a-button>
</a-dropdown>
</div>
</a-space>
<standard-table
:columns="columns"
:dataSource="dataSource"
:selectedRows.sync="selectedRows"
@clear="onClear"
@change="onChange"
:pagination="{...pagination, onChange: onPageChange}"
@selectedRowChange="onSelectChange"
>
<div slot="description" slot-scope="{text}">
@ -116,6 +117,7 @@
<a @click="deleteRecord(record.key)" v-auth="`delete`">
<a-icon type="delete" />删除2
</a>
<router-link :to="`/list/query/detail/${record.key}`" >详情</router-link>
</div>
<template slot="statusTitle">
<a-icon @click.native="onStatusTitleClick" type="info-circle" />
@ -127,6 +129,7 @@
<script>
import StandardTable from '@/components/table/StandardTable'
import {request} from '@/utils/request'
const columns = [
{
title: '规则编号',
@ -160,19 +163,6 @@ const columns = [
}
]
const dataSource = []
for (let i = 0; i < 100; i++) {
dataSource.push({
key: i,
no: 'NO ' + i,
description: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: '2018-07-26'
})
}
export default {
name: 'QueryList',
components: {StandardTable},
@ -180,14 +170,37 @@ export default {
return {
advanced: true,
columns: columns,
dataSource: dataSource,
selectedRows: []
dataSource: [],
selectedRows: [],
pagination: {
current: 1,
pageSize: 10,
total: 0
}
}
},
authorize: {
deleteRecord: 'delete'
},
mounted() {
this.getData()
},
methods: {
onPageChange(page, pageSize) {
this.pagination.current = page
this.pagination.pageSize = pageSize
this.getData()
},
getData() {
request(process.env.VUE_APP_API_BASE_URL + '/list', 'get', {page: this.pagination.current,
pageSize: this.pagination.pageSize}).then(res => {
const {list, page, pageSize, total} = res?.data?.data ?? {}
this.dataSource = list
this.pagination.current = page
this.pagination.pageSize = pageSize
this.pagination.total = total
})
},
deleteRecord(key) {
this.dataSource = this.dataSource.filter(item => item.key !== key)
this.selectedRows = this.selectedRows.filter(item => item.key !== key)

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="search-head">
<div :class="['search-head', layout, pageWidth]">
<div class="search-input">
<a-input-search class="search-ipt" style="width: 522px" placeholder="请输入..." size="large" enterButton="搜索" />
</div>
@ -19,9 +19,11 @@
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'SearchLayout',
computed: {
...mapState('setting', ['layout', 'pageWidth']),
activeKey () {
switch (this.$route.path) {
case '/list/search/article':
@ -58,7 +60,10 @@ export default {
<style lang="less" scoped>
.search-head{
background-color: @base-bg-color;
margin: -25px -24px -24px;
margin: -24px;
&.head.fixed{
margin: -24px 0;
}
.search-input{
text-align: center;
}

View File

@ -120,7 +120,7 @@ export default {
//
getRoutesConfig().then(result => {
const routesConfig = result.data.data
loadRoutes({router: this.$router, store: this.$store, i18n: this.$i18n}, routesConfig)
loadRoutes(routesConfig)
this.$router.push('/dashboard/workplace')
this.$message.success(loginRes.message, 3)
})

View File

@ -2,16 +2,19 @@
* 获取路由需要的权限
* @param permissions
* @param route
* @returns {*}
* @returns {Permission}
*/
const getRoutePermission = (permissions, route) => permissions.find(item => item.id === route.meta.authority.permission)
/**
* 获取路由需要的角色
* @param roles
* @param route
* @returns {*}
* @returns {Array[Role]}
*/
const getRouteRole = (roles, route) => roles.find(item => item.id === route.meta.authority.role)
const getRouteRole = (roles, route) => {
const requiredRoles = route.meta.authority.role
return requiredRoles ? roles.filter(item => requiredRoles.findIndex(required => required === item.id) !== -1) : []
}
/**
* 判断是否已为方法注入权限认证
* @param method
@ -32,11 +35,40 @@ const auth = function(authConfig, permission, role, permissions, roles) {
const {check, type} = authConfig
if (check && typeof check === 'function') {
return check.apply(this, [permission, role, permissions, roles])
}
if (type === 'permission') {
return checkFromPermission(check, permission)
} else if (type === 'role') {
return checkFromRoles(check, roles)
} else {
if (type === 'permission') {
return permission && permission.operation && permission.operation.indexOf(check) !== -1
} else if (type === 'role') {
return role && role.operation && role.operation.indexOf(check) !== -1
return checkFromPermission(check, permission) || checkFromRoles(check, roles)
}
}
/**
* 检查权限是否有操作权限
* @param check 需要检查的操作权限
* @param permission 权限
* @returns {boolean}
*/
const checkFromPermission = function(check, permission) {
return permission && permission.operation && permission.operation.indexOf(check) !== -1
}
/**
* 检查 roles 是否有操作权限
* @param check 需要检查的操作权限
* @param roles 角色数组
* @returns {boolean}
*/
const checkFromRoles = function(check, roles) {
if (!roles) {
return false
}
for (let role of roles) {
const {operation} = role
if (operation && operation.indexOf(check) !== -1) {
return true
}
}
return false
@ -56,7 +88,7 @@ const checkInject = function (el, binding,vnode) {
const addDisabled = function (el) {
if (el.tagName === 'BUTTON') {
el.setAttribute('disabled', 'disabled')
el.disabled = true
} else {
el.classList.add('disabled')
}
@ -64,8 +96,8 @@ const addDisabled = function (el) {
}
const removeDisabled = function (el) {
el.disabled = false
el.classList.remove('disabled')
el.removeAttribute('disabled')
el.removeAttribute('title')
}
@ -73,10 +105,10 @@ const AuthorityPlugin = {
install(Vue) {
Vue.directive('auth', {
bind(el, binding,vnode) {
checkInject(el, binding, vnode)
setTimeout(() => checkInject(el, binding, vnode), 10)
},
update(el, binding,vnode) {
checkInject(el, binding, vnode)
componentUpdated(el, binding,vnode) {
setTimeout(() => checkInject(el, binding, vnode), 10)
},
unbind(el) {
removeDisabled(el)
@ -125,9 +157,6 @@ const AuthorityPlugin = {
const roles = this.$store.getters['account/roles']
const permission = getRoutePermission(permissions, this.$route)
const role = getRouteRole(roles, this.$route)
if (!type) {
type = permission ? 'permission' : 'role'
}
return auth.apply(this, [{check, type}, permission, role, permissions, roles])
}
}

View File

@ -5,6 +5,31 @@ const TabsPagePlugin = {
$closePage(closeRoute, nextRoute) {
const event = new CustomEvent('page:close', {detail:{closeRoute, nextRoute}})
window.dispatchEvent(event)
},
$refreshPage(route) {
const path = typeof route === 'object' ? route.path : route
const event = new CustomEvent('page:refresh', {detail:{pageKey: path}})
window.dispatchEvent(event)
},
$openPage(route, title) {
this.$setPageTitle(route, title)
this.$router.push(route)
},
$setPageTitle(route, title) {
if (title) {
// let path = typeof route === 'object' ? route.path : route
// path = path && path.split('?')[0]
let path = typeof route === 'object' ? this.$router.resolve(route).route.fullPath : route
this.$store.commit('setting/setCustomTitle', {path, title})
}
}
},
computed: {
customTitle() {
const customTitles = this.$store.state.setting.customTitles
// const path = this.$route.path.split('?')[0]
const custom = customTitles.find(item => item.path === this.$route.fullPath)
return custom && custom.title
}
}
})

View File

@ -37,6 +37,11 @@ const options = {
{
path: 'workplace',
name: '工作台',
meta: {
page: {
closable: false
}
},
component: () => import('@/pages/dashboard/workplace'),
},
{
@ -51,6 +56,9 @@ const options = {
name: '表单页',
meta: {
icon: 'form',
page: {
cacheAble: false
}
},
component: PageView,
children: [
@ -87,6 +95,15 @@ const options = {
},
component: () => import('@/pages/list/QueryList'),
},
{
path: 'query/detail/:id',
name: '查询详情',
meta: {
highlight: '/list/query',
invisible: true
},
component: () => import('@/pages/Demo')
},
{
path: 'primary',
name: '标准列表',
@ -188,7 +205,7 @@ const options = {
},
{
path: 'components',
name: '组件',
name: '内置组件',
meta: {
icon: 'appstore-o'
},
@ -203,6 +220,11 @@ const options = {
path: 'palette',
name: '颜色复选框',
component: () => import('@/pages/components/Palette')
},
{
path: 'table',
name: '高级表格',
component: () => import('@/pages/components/table')
}
]
},
@ -216,6 +238,44 @@ const options = {
}
},
component: () => import('@/pages/form/basic')
},
{
name: '带参菜单',
path: 'router/query',
meta: {
icon: 'project',
query: {
name: '菜单默认参数'
}
},
component: () => import('@/pages/Demo')
},
{
name: '动态路由菜单',
path: 'router/dynamic/:id',
meta: {
icon: 'project',
params: {
id: 123
}
},
component: () => import('@/pages/Demo')
},
{
name: 'Ant Design Vue',
path: 'antdv',
meta: {
icon: 'ant-design',
link: 'https://www.antdv.com/docs/vue/introduce-cn/'
}
},
{
name: '使用文档',
path: 'document',
meta: {
icon: 'file-word',
link: 'https://iczer.gitee.io/vue-antd-admin-docs/'
}
}
]
},

104
src/router/guards.js Normal file
View File

@ -0,0 +1,104 @@
import {hasAuthority} from '@/utils/authority-utils'
import {loginIgnore} from '@/router/index'
import {checkAuthorization} from '@/utils/request'
import NProgress from 'nprogress'
NProgress.configure({ showSpinner: false })
/**
* 进度条开始
* @param to
* @param form
* @param next
*/
const progressStart = (to, from, next) => {
// start progress bar
if (!NProgress.isStarted()) {
NProgress.start()
}
next()
}
/**
* 登录守卫
* @param to
* @param form
* @param next
* @param options
*/
const loginGuard = (to, from, next, options) => {
const {message} = options
if (!loginIgnore.includes(to) && !checkAuthorization()) {
message.warning('登录已失效,请重新登录')
next({path: '/login'})
} else {
next()
}
}
/**
* 权限守卫
* @param to
* @param form
* @param next
* @param options
*/
const authorityGuard = (to, from, next, options) => {
const {store, message} = options
const permissions = store.getters['account/permissions']
const roles = store.getters['account/roles']
if (!hasAuthority(to, permissions, roles)) {
message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`)
next({path: '/403'})
// NProgress.done()
} else {
next()
}
}
/**
* 混合导航模式下一级菜单跳转重定向
* @param to
* @param from
* @param next
* @param options
* @returns {*}
*/
const redirectGuard = (to, from, next, options) => {
const {store} = options
const getFirstChild = (routes) => {
const route = routes[0]
if (!route.children || route.children.length === 0) {
return route
}
return getFirstChild(route.children)
}
if (store.state.setting.layout === 'mix') {
const firstMenu = store.getters['setting/firstMenu']
if (firstMenu.find(item => item.fullPath === to.fullPath)) {
store.commit('setting/setActivatedFirst', to.fullPath)
const subMenu = store.getters['setting/subMenu']
if (subMenu.length > 0) {
const redirect = getFirstChild(subMenu)
return next({path: redirect.fullPath})
}
}
}
next()
}
/**
* 进度条结束
* @param to
* @param form
* @param options
*/
const progressDone = () => {
// finish progress bar
NProgress.done()
}
export default {
beforeEach: [progressStart, loginGuard, authorityGuard, redirectGuard],
afterEach: [progressDone]
}

View File

@ -1,6 +1,6 @@
import Vue from 'vue'
import Router from 'vue-router'
import {formatAuthority} from '@/utils/routerUtil'
import {formatRoutes} from '@/utils/routerUtil'
Vue.use(Router)
@ -24,8 +24,8 @@ const loginIgnore = {
* @returns {VueRouter}
*/
function initRouter(isAsync) {
const options = isAsync ? require('./config.async').default : require('./config').default
formatAuthority(options.routes)
const options = isAsync ? require('./async/config.async').default : require('./config').default
formatRoutes(options.routes)
return new Router(options)
}
export {loginIgnore, initRouter}

View File

@ -4,5 +4,7 @@
const BASE_URL = process.env.VUE_APP_API_BASE_URL
module.exports = {
LOGIN: `${BASE_URL}/login`,
ROUTES: `${BASE_URL}/routes`
ROUTES: `${BASE_URL}/routes`,
GOODS: `${BASE_URL}/goods`,
GOODS_COLUMNS: `${BASE_URL}/columns`,
}

View File

@ -0,0 +1,12 @@
import {GOODS, GOODS_COLUMNS} from './api'
import {METHOD, request} from '@/utils/request'
export async function goodsList(params) {
return request(GOODS, METHOD.GET, params)
}
export async function goodsColumns() {
return request(GOODS_COLUMNS, METHOD.GET)
}
export default {goodsList, goodsColumns}

View File

@ -1,5 +1,7 @@
import userService from './user'
import dataSource from './dataSource'
export {
userService
userService,
dataSource
}

View File

@ -1,5 +1,14 @@
import config from '@/config'
import {ADMIN} from '@/config/default'
import {formatFullPath} from '@/utils/i18n'
import {filterMenu} from '@/utils/authority-utils'
import {getLocalSetting} from '@/utils/themeUtil'
import deepClone from 'lodash.clonedeep'
const localSetting = getLocalSetting(true)
const customTitlesStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_TITLES_KEY)
const customTitles = (customTitlesStr && JSON.parse(customTitlesStr)) || []
export default {
namespaced: true,
state: {
@ -8,7 +17,38 @@ export default {
palettes: ADMIN.palettes,
pageMinHeight: 0,
menuData: [],
activatedFirst: undefined,
customTitles,
...config,
...localSetting
},
getters: {
menuData(state, getters, rootState) {
if (state.filterMenu) {
const {permissions, roles} = rootState.account
return filterMenu(deepClone(state.menuData), permissions, roles)
}
return state.menuData
},
firstMenu(state, getters) {
const {menuData} = getters
if (menuData.length > 0 && !menuData[0].fullPath) {
formatFullPath(menuData)
}
return menuData.map(item => {
const menuItem = {...item}
delete menuItem.children
return menuItem
})
},
subMenu(state) {
const {menuData, activatedFirst} = state
if (menuData.length > 0 && !menuData[0].fullPath) {
formatFullPath(menuData)
}
const current = menuData.find(menu => menu.fullPath === activatedFirst)
return current && current.children || []
}
},
mutations: {
setDevice (state, isMobile) {
@ -49,6 +89,26 @@ export default {
},
setAsyncRoutes(state, asyncRoutes) {
state.asyncRoutes = asyncRoutes
},
setPageWidth(state, pageWidth) {
state.pageWidth = pageWidth
},
setActivatedFirst(state, activatedFirst) {
state.activatedFirst = activatedFirst
},
setFixedTabs(state, fixedTabs) {
state.fixedTabs = fixedTabs
},
setCustomTitle(state, {path, title}) {
if (title) {
const obj = state.customTitles.find(item => item.path === path)
if (obj) {
obj.title = title
} else {
state.customTitles.push({path, title})
}
sessionStorage.setItem(process.env.VUE_APP_TBAS_TITLES_KEY, JSON.stringify(state.customTitles))
}
}
}
}

View File

@ -0,0 +1,3 @@
.ant-menu-inline-collapsed-tooltip a{
color: @text-color-inverse;
}

View File

@ -0,0 +1,3 @@
.ant-message{
z-index: 1100;
}

View File

@ -0,0 +1,22 @@
.ant-table-thead{
tr{
th{
&.ant-table-column-has-actions{
&.ant-table-column-has-sorters:hover{
background-color: @background-color-base;
}
&.ant-table-column-has-filters{
&:hover{
.anticon-filter, .anticon-filter:hover{
background-color: @background-color-base;
}
}
.anticon-filter.ant-table-filter-open{
background-color: @background-color-base;
}
}
}
}
}
}

View File

@ -1 +1,4 @@
@import "ant-time-picker";
@import "ant-message";
@import "ant-table";
@import "ant-menu";

View File

@ -1,2 +1,3 @@
@import "color";
@import "style";
@import "nprogress";

View File

@ -0,0 +1,76 @@
@import '~ant-design-vue/lib/style/themes/default';
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: @primary-color;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: @primary-color;
border-left-color: @primary-color;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -0,0 +1,85 @@
/**
* 判断是否有路由的权限
* @param authority 路由权限配置
* @param permissions 用户权限集合
* @returns {boolean|*}
*/
function hasPermission(authority, permissions) {
let required = '*'
if (typeof authority === 'string') {
required = authority
} else if (Array.isArray(authority)) {
required = authority
} else if (typeof authority === 'object') {
required = authority.permission
}
return required === '*' || hasAnyItem(required, permissions, (r, t) => !!(r === t || r === t.id))
}
/**
* 判断是否有路由需要的角色
* @param authority 路由权限配置
* @param roles 用户角色集合
*/
function hasRole(authority, roles) {
let required = undefined
if (typeof authority === 'object') {
required = authority.role
}
return authority === '*' || hasAnyItem(required, roles, (r, t) => !!(r === t || r === t.id))
}
/**
* 判断目标数组是否有所需元素
* @param {String | String[]}required 所需元素数组或单个元素
* @param {String[]|Object[]} source 目标数组
* @param {Function} filter 匹配条件
* (r: String, s: String|Object) => boolean
* @returns {boolean}
*/
function hasAnyItem(required, source, filter) {
if (!required) {
return false
}
let checkedList = Array.isArray(required) ? required : [required]
return !!source.find(s => checkedList.find(r => filter(r, s)))
}
/**
* 路由权限校验
* @param route 路由
* @param permissions 用户权限集合
* @param roles 用户角色集合
* @returns {boolean}
*/
function hasAuthority(route, permissions, roles) {
const authorities = [...route.meta.pAuthorities, route.meta.authority]
for (let authority of authorities) {
if (!hasPermission(authority, permissions) && !hasRole(authority, roles)) {
return false
}
}
return true
}
/**
* 根据权限配置过滤菜单数据
* @param menuData
* @param permissions
* @param roles
*/
function filterMenu(menuData, permissions, roles) {
return menuData.filter(menu => {
if (menu.meta && menu.meta.invisible === undefined) {
if (!hasAuthority(menu, permissions, roles)) {
return false
}
}
if (menu.children && menu.children.length > 0) {
menu.children = filterMenu(menu.children, permissions, roles)
}
return true
})
}
export {filterMenu, hasAuthority}

View File

@ -0,0 +1,82 @@
import Cookie from 'js-cookie'
// 401拦截
const resp401 = {
/**
* 响应数据之前做点什么
* @param response 响应对象
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {*}
*/
onFulfilled(response, options) {
const {message} = options
if (response.code === 401) {
message.error('无此权限')
}
return response
},
/**
* 响应出错时执行
* @param error 错误对象
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {Promise<never>}
*/
onRejected(error, options) {
const {message} = options
const {response} = error
if (response.status === 401) {
message.error('无此权限')
}
return Promise.reject(error)
}
}
const resp403 = {
onFulfilled(response, options) {
const {message} = options
if (response.code === 403) {
message.error('请求被拒绝')
}
return response
},
onRejected(error, options) {
const {message} = options
const {response} = error
if (response.status === 403) {
message.error('请求被拒绝')
}
return Promise.reject(error)
}
}
const reqCommon = {
/**
* 发送请求之前做些什么
* @param config axios config
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {*}
*/
onFulfilled(config, options) {
const {message} = options
const {url, xsrfCookieName} = config
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
message.warning('认证 token 已过期,请重新登录')
}
return config
},
/**
* 请求出错时做点什么
* @param error 错误对象
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {Promise<never>}
*/
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
export default {
request: [reqCommon], // 请求拦截
response: [resp401, resp403] // 响应拦截
}

View File

@ -23,9 +23,9 @@ function getFunctionalColors(mode) {
const warningColors = generate(warning, options)
const errorColors = generate(error, options)
return {
success: [...successColors.slice(0, 3), successColors[5]],
warning: [...warningColors.slice(0, 3), warningColors[5]],
error: [...errorColors.slice(0, 3), errorColors[5]]
success: successColors,
warning: warningColors,
error: errorColors
}
}

View File

@ -1,15 +0,0 @@
import enquireJs from 'enquire.js'
const enquireScreen = function (call) {
const hanlder = {
match: function () {
call && call(true)
},
unmatch: function () {
call && call(false)
}
}
enquireJs.register('only screen and (max-width: 767.99px)', hanlder)
}
export default enquireScreen

View File

@ -16,7 +16,6 @@ function initI18n(locale, fallback) {
locale,
fallbackLocale: fallback,
silentFallbackWarn: true,
messages: routesI18n.messages
}
return new VueI18n(i18nOptions)
}
@ -66,9 +65,14 @@ function mergeI18nFromRoutes(i18n, routes) {
const US = generateI18n(new Object(), routes, 'path')
i18n.mergeLocaleMessage('CN', CN)
i18n.mergeLocaleMessage('US', US)
const messages = routesI18n.messages
Object.keys(messages).forEach(lang => {
i18n.mergeLocaleMessage(lang, messages[lang])
})
}
export {
initI18n,
mergeI18nFromRoutes
mergeI18nFromRoutes,
formatFullPath
}

View File

@ -30,14 +30,14 @@ const METHOD = {
* @param params 请求参数
* @returns {Promise<AxiosResponse<T>>}
*/
async function request(url, method, params) {
async function request(url, method, params, config) {
switch (method) {
case METHOD.GET:
return axios.get(url, {params})
return axios.get(url, {params, ...config})
case METHOD.POST:
return axios.post(url, params)
return axios.post(url, params, config)
default:
return axios.get(url, {params})
return axios.get(url, {params, ...config})
}
}
@ -97,11 +97,72 @@ function checkAuthorization(authType = AUTH_TYPE.BEARER) {
return false
}
/**
* 加载 axios 拦截器
* @param interceptors
* @param options
*/
function loadInterceptors(interceptors, options) {
const {request, response} = interceptors
// 加载请求拦截器
request.forEach(item => {
let {onFulfilled, onRejected} = item
if (!onFulfilled || typeof onFulfilled !== 'function') {
onFulfilled = config => config
}
if (!onRejected || typeof onRejected !== 'function') {
onRejected = error => Promise.reject(error)
}
axios.interceptors.request.use(
config => onFulfilled(config, options),
error => onRejected(error, options)
)
})
// 加载响应拦截器
response.forEach(item => {
let {onFulfilled, onRejected} = item
if (!onFulfilled || typeof onFulfilled !== 'function') {
onFulfilled = response => response
}
if (!onRejected || typeof onRejected !== 'function') {
onRejected = error => Promise.reject(error)
}
axios.interceptors.response.use(
response => onFulfilled(response, options),
error => onRejected(error, options)
)
})
}
/**
* 解析 url 中的参数
* @param url
* @returns {Object}
*/
function parseUrlParams(url) {
const params = {}
if (!url || url === '' || typeof url !== 'string') {
return params
}
const paramsStr = url.split('?')[1]
if (!paramsStr) {
return params
}
const paramsArr = paramsStr.replace(/&|=/g, ' ').split(' ')
for (let i = 0; i < paramsArr.length / 2; i++) {
const value = paramsArr[i * 2 + 1]
params[paramsArr[i * 2]] = value === 'true' ? true : (value === 'false' ? false : value)
}
return params
}
export {
METHOD,
AUTH_TYPE,
request,
setAuthorization,
removeAuthorization,
checkAuthorization
checkAuthorization,
loadInterceptors,
parseUrlParams
}

View File

@ -1,8 +1,26 @@
import routerMap from '@/router/router.map'
import routerMap from '@/router/async/router.map'
import {mergeI18nFromRoutes} from '@/utils/i18n'
import Router from 'vue-router'
import {loginIgnore} from '@/router'
import {checkAuthorization} from '@/utils/request'
import deepMerge from 'deepmerge'
import basicOptions from '@/router/async/config.async'
//应用配置
let appOptions = {
router: undefined,
i18n: undefined,
store: undefined
}
/**
* 设置应用配置
* @param options
*/
function setAppOptions(options) {
const {router, store, i18n} = options
appOptions.router = router
appOptions.store = store
appOptions.i18n = i18n
}
/**
* 根据 路由配置 路由组件注册 解析路由
@ -14,48 +32,83 @@ function parseRoutes(routesConfig, routerMap) {
routesConfig.forEach(item => {
// 获取注册在 routerMap 中的 router初始化 routeCfg
let router = undefined, routeCfg = {}
if (typeof item === 'string' && routerMap[item]) {
if (typeof item === 'string') {
router = routerMap[item]
routeCfg = {path: router.path || item, router: item}
routeCfg = {path: (router && router.path) || item, router: item}
} else if (typeof item === 'object') {
router = routerMap[item.router]
routeCfg = item
}
// 从 router 和 routeCfg 解析路由
if (!router) {
console.warn(`can't find register for router ${routeCfg.router}, please register it in advance.`)
} else {
const route = {
path: routeCfg.path || router.path || routeCfg.router,
name: routeCfg.name || router.name,
component: router.component,
redirect: routeCfg.redirect || router.redirect,
meta: {
authority: routeCfg.authority || router.authority || '*',
icon: routeCfg.icon || router.icon,
page: routeCfg.page || router.page
}
}
if (routeCfg.invisible || router.invisible) {
route.meta.invisible = true
}
if (routeCfg.children && routeCfg.children.length > 0) {
route.children = parseRoutes(routeCfg.children, routerMap)
}
routes.push(route)
router = typeof item === 'string' ? {path: item, name: item} : item
}
// 从 router 和 routeCfg 解析路由
const meta = {
authority: router.authority,
icon: router.icon,
page: router.page,
link: router.link,
params: router.params,
query: router.query,
...router.meta
}
const cfgMeta = {
authority: routeCfg.authority,
icon: routeCfg.icon,
page: routeCfg.page,
link: routeCfg.link,
params: routeCfg.params,
query: routeCfg.query,
...routeCfg.meta
}
Object.keys(cfgMeta).forEach(key => {
if (cfgMeta[key] === undefined || cfgMeta[key] === null || cfgMeta[key] === '') {
delete cfgMeta[key]
}
})
Object.assign(meta, cfgMeta)
const route = {
path: routeCfg.path || router.path || routeCfg.router,
name: routeCfg.name || router.name,
component: router.component,
redirect: routeCfg.redirect || router.redirect,
meta: {...meta, authority: meta.authority || '*'}
}
if (router.beforeEnter) {
route.beforeEnter = router.beforeEnter
}
if (routeCfg.invisible || router.invisible) {
route.meta.invisible = true
}
if (routeCfg.children && routeCfg.children.length > 0) {
route.children = parseRoutes(routeCfg.children, routerMap)
}
routes.push(route)
})
return routes
}
/**
* 加载路由
* @param router 应用路由实例
* @param store 应用的 vuex.store 实例
* @param i18n 应用的 vue-i18n 实例
* @param routesConfig 路由配置
* @param routesConfig {RouteConfig[]} 路由配置
*/
function loadRoutes({router, store, i18n}, routesConfig) {
function loadRoutes(routesConfig) {
//兼容 0.6.1 以下版本
/*************** 兼容 version < v0.6.1 *****************/
if (arguments.length > 0) {
const arg0 = arguments[0]
if (arg0.router || arg0.i18n || arg0.store) {
routesConfig = arguments[1]
console.error('the usage of signature loadRoutes({router, store, i18n}, routesConfig) is out of date, please use the new signature: loadRoutes(routesConfig).')
console.error('方法签名 loadRoutes({router, store, i18n}, routesConfig) 的用法已过时, 请使用新的方法签名 loadRoutes(routesConfig)。')
}
}
/*************** 兼容 version < v0.6.1 *****************/
// 应用配置
const {router, store, i18n} = appOptions
// 如果 routesConfig 有值,则更新到本地,否则从本地获取
if (routesConfig) {
store.commit('account/setRoutesConfig', routesConfig)
@ -67,8 +120,8 @@ function loadRoutes({router, store, i18n}, routesConfig) {
if (asyncRoutes) {
if (routesConfig && routesConfig.length > 0) {
const routes = parseRoutes(routesConfig, routerMap)
formatAuthority(routes)
const finalRoutes = mergeRoutes(router.options.routes, routes)
const finalRoutes = mergeRoutes(basicOptions.routes, routes)
formatRoutes(finalRoutes)
router.options = {...router.options, routes: finalRoutes}
router.matcher = new Router({...router.options, routes:[]}).matcher
router.addRoutes(finalRoutes)
@ -98,93 +151,90 @@ function mergeRoutes(target, source) {
}
/**
* 登录守卫
* @param router 应用路由实例
* 深度合并路由
* @param target {Route[]}
* @param source {Route[]}
* @returns {Route[]}
*/
function loginGuard(router) {
router.beforeEach((to, from, next) => {
if (!loginIgnore.includes(to) && !checkAuthorization()) {
next({path: '/login'})
} else {
next()
function deepMergeRoutes(target, source) {
// 映射路由数组
const mapRoutes = routes => {
const routesMap = {}
routes.forEach(item => {
routesMap[item.path] = {
...item,
children: item.children ? mapRoutes(item.children) : undefined
}
})
return routesMap
}
const tarMap = mapRoutes(target)
const srcMap = mapRoutes(source)
// 合并路由
const merge = deepMerge(tarMap, srcMap)
// 转换为 routes 数组
const parseRoutesMap = routesMap => {
return Object.values(routesMap).map(item => {
if (item.children) {
item.children = parseRoutesMap(item.children)
} else {
delete item.children
}
return item
})
}
return parseRoutesMap(merge)
}
/**
* 格式化路由
* @param routes 路由配置
*/
function formatRoutes(routes) {
routes.forEach(route => {
const {path} = route
if (!path.startsWith('/') && path !== '*') {
route.path = '/' + path
}
})
}
/**
* 权限守卫
* @param router 应用路由实例
* @param store 应用的 vuex.store 实例
*/
function authorityGuard(router, store) {
router.beforeEach((to, form, next) => {
const permissions = store.getters['account/permissions']
const roles = store.getters['account/roles']
if (!hasPermission(to, permissions) && !hasRole(to, roles)) {
next({path: '/403'})
} else {
next()
}
})
}
/**
* 判断是否有路由的权限
* @param route 路由
* @param permissions 用户权限集合
* @returns {boolean|*}
*/
function hasPermission(route, permissions) {
const authority = route.meta.authority || '*'
let required = '*'
if (typeof authority === 'string') {
required = authority
} else if (typeof authority === 'object') {
required = authority.permission
}
return required === '*' || (permissions && permissions.findIndex(item => item === required || item.id === required) !== -1)
}
/**
* 判断是否有路由需要的角色
* @param route 路由
* @param roles 用户角色集合
*/
function hasRole(route, roles) {
const authority = route.meta.authority || '*'
let required = undefined
if (typeof authority === 'object') {
required = authority.role
}
return authority === '*' || (required && roles && roles.findIndex(item => item === required || item.id === required) !== -1)
formatAuthority(routes)
}
/**
* 格式化路由的权限配置
* @param routes
* @param routes 路由
* @param pAuthorities 父级路由权限配置集合
*/
function formatAuthority(routes) {
function formatAuthority(routes, pAuthorities = []) {
routes.forEach(route => {
const meta = route.meta
const defaultAuthority = pAuthorities[pAuthorities.length - 1] || {permission: '*'}
if (meta) {
let authority = {}
if (!meta.authority) {
authority.permission = '*'
}else if (typeof meta.authority === 'string') {
authority = defaultAuthority
}else if (typeof meta.authority === 'string' || Array.isArray(meta.authority)) {
authority.permission = meta.authority
} else if (typeof meta.authority === 'object') {
authority = meta.authority
} else {
console.log(typeof meta.authority)
const {role} = authority
if (typeof role === 'string') {
authority.role = [role]
}
if (!authority.permission && !authority.role) {
authority = defaultAuthority
}
}
meta.authority = authority
} else {
route.meta = {
authority: {permission: '*'}
}
const authority = defaultAuthority
route.meta = {authority}
}
route.meta.pAuthorities = pAuthorities
if (route.children) {
formatAuthority(route.children)
formatAuthority(route.children, [...pAuthorities, route.meta.authority])
}
})
}
@ -200,4 +250,24 @@ function getI18nKey(path) {
return keys.join('.')
}
export {parseRoutes, loadRoutes, loginGuard, authorityGuard, formatAuthority, getI18nKey}
/**
* 加载导航守卫
* @param guards
* @param options
*/
function loadGuards(guards, options) {
const {beforeEach, afterEach} = guards
const {router} = options
beforeEach.forEach(guard => {
if (guard && typeof guard === 'function') {
router.beforeEach((to, from, next) => guard(to, from, next, options))
}
})
afterEach.forEach(guard => {
if (guard && typeof guard === 'function') {
router.afterEach((to, from) => guard(to, from, options))
}
})
}
export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes, setAppOptions}

View File

@ -3,62 +3,100 @@ const {theme} = require('../config')
const {getMenuColors, getAntdColors, getThemeToggleColors, getFunctionalColors} = require('../utils/colors')
const {ANTD} = require('../config/default')
module.exports = {
getThemeColors(color, $theme) {
const _color = color || theme.color
const mode = $theme || theme.mode
const replaceColors = getThemeToggleColors(_color, mode)
const themeColors = [
...replaceColors.mainColors,
...replaceColors.subColors,
...replaceColors.menuColors,
...replaceColors.contentColors,
...replaceColors.rgbColors,
...replaceColors.functionalColors.success,
...replaceColors.functionalColors.warning,
...replaceColors.functionalColors.error,
]
return themeColors
},
changeThemeColor (newColor, $theme) {
let promise = client.changer.changeColor({newColors: this.getThemeColors(newColor, $theme)})
return promise
},
modifyVars(color) {
let _color = color || theme.color
const palettes = getAntdColors(_color, theme.mode)
const menuColors = getMenuColors(_color, theme.mode)
const {success, warning, error} = getFunctionalColors(theme.mode)
const primary = palettes[5]
return {
'primary-color': primary,
'primary-1': palettes[0],
'primary-2': palettes[1],
'primary-3': palettes[2],
'primary-4': palettes[3],
'primary-5': palettes[4],
'primary-6': palettes[5],
'primary-7': palettes[6],
'primary-8': palettes[7],
'primary-9': palettes[8],
'primary-10': palettes[9],
'info-color': primary,
'success-color': success[3],
'warning-color': warning[3],
'error-color': error[3],
'alert-info-bg-color': palettes[0],
'alert-info-border-color': palettes[2],
'alert-success-bg-color': success[0],
'alert-success-border-color': success[2],
'alert-warning-bg-color': warning[0],
'alert-warning-border-color': warning[2],
'alert-error-bg-color': error[0],
'alert-error-border-color': error[2],
'processing-color': primary,
'menu-dark-submenu-bg': menuColors[0],
'layout-header-background': menuColors[1],
'layout-trigger-background': menuColors[2],
...ANTD.theme[theme.mode]
}
function getThemeColors(color, $theme) {
const _color = color || theme.color
const mode = $theme || theme.mode
const replaceColors = getThemeToggleColors(_color, mode)
const themeColors = [
...replaceColors.mainColors,
...replaceColors.subColors,
...replaceColors.menuColors,
...replaceColors.contentColors,
...replaceColors.rgbColors,
...replaceColors.functionalColors.success,
...replaceColors.functionalColors.warning,
...replaceColors.functionalColors.error,
]
return themeColors
}
function changeThemeColor(newColor, $theme) {
let promise = client.changer.changeColor({newColors: getThemeColors(newColor, $theme)})
return promise
}
function modifyVars(color) {
let _color = color || theme.color
const palettes = getAntdColors(_color, theme.mode)
const menuColors = getMenuColors(_color, theme.mode)
const {success, warning, error} = getFunctionalColors(theme.mode)
const primary = palettes[5]
return {
'primary-color': primary,
'primary-1': palettes[0],
'primary-2': palettes[1],
'primary-3': palettes[2],
'primary-4': palettes[3],
'primary-5': palettes[4],
'primary-6': palettes[5],
'primary-7': palettes[6],
'primary-8': palettes[7],
'primary-9': palettes[8],
'primary-10': palettes[9],
'info-color': primary,
'success-color': success[5],
'warning-color': warning[5],
'error-color': error[5],
'alert-info-bg-color': palettes[0],
'alert-info-border-color': palettes[2],
'alert-success-bg-color': success[0],
'alert-success-border-color': success[2],
'alert-warning-bg-color': warning[0],
'alert-warning-border-color': warning[2],
'alert-error-bg-color': error[0],
'alert-error-border-color': error[2],
'processing-color': primary,
'menu-dark-submenu-bg': menuColors[0],
'layout-header-background': menuColors[1],
'layout-trigger-background': menuColors[2],
'btn-danger-bg': error[4],
'btn-danger-border': error[4],
...ANTD.theme[theme.mode]
}
}
function loadLocalTheme(localSetting) {
if (localSetting && localSetting.theme) {
let {color, mode} = localSetting.theme
color = color || theme.color
mode = mode || theme.mode
changeThemeColor(color, mode)
}
}
/**
* 获取本地保存的配置
* @param load {boolean} 是否加载配置中的主题
* @returns {Object}
*/
function getLocalSetting(loadTheme) {
let localSetting = {}
try {
const localSettingStr = localStorage.getItem(process.env.VUE_APP_SETTING_KEY)
localSetting = JSON.parse(localSettingStr)
} catch (e) {
console.error(e)
}
if (loadTheme) {
loadLocalTheme(localSetting)
}
return localSetting
}
module.exports = {
getThemeColors,
changeThemeColor,
modifyVars,
loadLocalTheme,
getLocalSetting
}

View File

@ -1,3 +1,5 @@
import enquireJs from 'enquire.js'
export function isDef (v){
return v !== undefined && v !== null
}
@ -18,4 +20,16 @@ export function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}
export function enquireScreen(call) {
const handler = {
match: function () {
call && call(true)
},
unmatch: function () {
call && call(false)
}
}
enquireJs.register('only screen and (max-width: 767.99px)', handler)
}
const _toString = Object.prototype.toString

View File

@ -1,7 +1,39 @@
let path = require('path')
const webpack = require('webpack')
const ThemeColorReplacer = require('webpack-theme-color-replacer')
const {getThemeColors, modifyVars} = require('./src/utils/themeUtil')
const {resolveCss} = require('./src/utils/theme-color-replacer-extend')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = ['js', 'css']
const isProd = process.env.NODE_ENV === 'production'
const assetsCDN = {
// webpack build externals
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
nprogress: 'NProgress',
clipboard: 'ClipboardJS',
'@antv/data-set': 'DataSet',
'js-cookie': 'Cookies'
},
css: [
],
js: [
'//cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',
'//cdn.jsdelivr.net/npm/vue-router@3.3.4/dist/vue-router.min.js',
'//cdn.jsdelivr.net/npm/vuex@3.4.0/dist/vuex.min.js',
'//cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js',
'//cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js',
'//cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js',
'//cdn.jsdelivr.net/npm/@antv/data-set@0.11.4/build/data-set.min.js',
'//cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js'
]
}
module.exports = {
devServer: {
// proxy: {
@ -22,6 +54,9 @@ module.exports = {
},
configureWebpack: config => {
config.entry.app = ["babel-polyfill", "whatwg-fetch", "./src/main.js"];
config.performance = {
hints: false
}
config.plugins.push(
new ThemeColorReplacer({
fileName: 'css/theme-colors-[contenthash:8].css',
@ -30,16 +65,40 @@ module.exports = {
resolveCss
})
)
// Ignore all locale files of moment.js
config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
// 生产环境下将资源压缩成gzip格式
if (isProd) {
// add `CompressionWebpack` plugin to webpack plugins
config.plugins.push(new CompressionWebpackPlugin({
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8
}))
}
// if prod, add externals
if (isProd) {
config.externals = assetsCDN.externals
}
},
chainWebpack: config => {
// 生产环境下关闭css压缩的 colormin 项,因为此项优化与主题色替换功能冲突
if (process.env.NODE_ENV === 'production') {
if (isProd) {
config.plugin('optimize-css')
.tap(args => {
args[0].cssnanoOptions.preset[1].colormin = false
return args
})
}
// 生产环境下使用CDN
if (isProd) {
config.plugin('html')
.tap(args => {
args[0].cdn = assetsCDN
return args
})
}
},
css: {
loaderOptions: {
@ -51,7 +110,7 @@ module.exports = {
}
}
},
publicPath: process.env.NODE_ENV === 'production' ? '/vue-antd-admin/' : '/',
publicPath: process.env.VUE_APP_PUBLIC_PATH,
outputDir: 'dist',
assetsDir: 'static',
productionSourceMap: false

View File

@ -1146,6 +1146,14 @@
resolved "https://registry.npm.taobao.org/@nodelib/fs.stat/download/@nodelib/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha1-K1o6s/kYzKSKjHVMCBaOPwPrphs=
"@simonwep/pickr@~1.7.0":
version "1.7.4"
resolved "https://registry.npm.taobao.org/@simonwep/pickr/download/@simonwep/pickr-1.7.4.tgz#b14fcd945890388b870cd6db4d6c78d531f25141"
integrity sha1-sU/NlFiQOIuHDNbbTWx41THyUUE=
dependencies:
core-js "^3.6.5"
nanopop "^2.1.0"
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.npm.taobao.org/@sindresorhus/is/download/@sindresorhus/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@ -1959,13 +1967,14 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
"@types/color-name" "^1.1.1"
color-convert "^2.0.1"
ant-design-vue@^1.6.2:
version "1.6.2"
resolved "https://registry.npm.taobao.org/ant-design-vue/download/ant-design-vue-1.6.2.tgz?cache=0&sync_timestamp=1591081225900&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fant-design-vue%2Fdownload%2Fant-design-vue-1.6.2.tgz#983634caac9cdaca0b3a095b540105b4f76da29f"
integrity sha1-mDY0yqyc2soLOglbVAEFtPdtop8=
ant-design-vue@1.7.2:
version "1.7.2"
resolved "https://registry.npm.taobao.org/ant-design-vue/download/ant-design-vue-1.7.2.tgz#aac7ff802205711631c8698e2a0c7b4e61dfd73e"
integrity sha1-qsf/gCIFcRYxyGmOKgx7TmHf1z4=
dependencies:
"@ant-design/icons" "^2.1.1"
"@ant-design/icons-vue" "^2.0.0"
"@simonwep/pickr" "~1.7.0"
add-dom-event-listener "^1.0.2"
array-tree-filter "^2.1.0"
async-validator "^3.0.3"
@ -2240,6 +2249,11 @@ babel-plugin-dynamic-import-node@^2.3.3:
dependencies:
object.assign "^4.1.0"
babel-plugin-transform-remove-console@^6.9.4:
version "6.9.4"
resolved "https://registry.npm.taobao.org/babel-plugin-transform-remove-console/download/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780"
integrity sha1-uYA2DAZzhOJLNXpYjYB9PINSd4A=
babel-polyfill@^6.26.0:
version "6.26.0"
resolved "https://registry.npm.taobao.org/babel-polyfill/download/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153"
@ -2547,6 +2561,26 @@ cac@^6.5.6:
resolved "https://registry.npm.taobao.org/cac/download/cac-6.5.10.tgz#be1d4509af1809dca601685145cd80ea85dabe0d"
integrity sha1-vh1FCa8YCdymAWhRRc2A6oXavg0=
cacache@^11.2.0:
version "11.3.3"
resolved "https://registry.npm.taobao.org/cacache/download/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc"
integrity sha1-i9Kd+ManGKbr0tAQ2k15cq47utw=
dependencies:
bluebird "^3.5.5"
chownr "^1.1.1"
figgy-pudding "^3.5.1"
glob "^7.1.4"
graceful-fs "^4.1.15"
lru-cache "^5.1.1"
mississippi "^3.0.0"
mkdirp "^0.5.1"
move-concurrently "^1.0.1"
promise-inflight "^1.0.1"
rimraf "^2.6.3"
ssri "^6.0.1"
unique-filename "^1.1.1"
y18n "^4.0.0"
cacache@^12.0.2, cacache@^12.0.3:
version "12.0.4"
resolved "https://registry.npm.taobao.org/cacache/download/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c"
@ -2701,15 +2735,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061:
version "1.0.30001083"
resolved "https://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30001083.tgz?cache=0&sync_timestamp=1592075334738&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001083.tgz#52410c20c6f029f604f0d45eca0439a82e712442"
integrity sha1-UkEMIMbwKfYE8NReygQ5qC5xJEI=
caniuse-lite@^1.0.30001087:
version "1.0.30001088"
resolved "https://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30001088.tgz#23a6b9e192106107458528858f2c0e0dba0d9073"
integrity sha1-I6a54ZIQYQdFhSiFjywODboNkHM=
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061, caniuse-lite@^1.0.30001087:
version "1.0.30001616"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz"
integrity sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.3.0"
@ -3079,6 +3108,18 @@ compressible@~2.0.16:
dependencies:
mime-db ">= 1.43.0 < 2"
compression-webpack-plugin@^2.0.0:
version "2.0.0"
resolved "https://registry.npm.taobao.org/compression-webpack-plugin/download/compression-webpack-plugin-2.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcompression-webpack-plugin%2Fdownload%2Fcompression-webpack-plugin-2.0.0.tgz#46476350c1eb27f783dccc79ac2f709baa2cffbc"
integrity sha1-RkdjUMHrJ/eD3Mx5rC9wm6os/7w=
dependencies:
cacache "^11.2.0"
find-cache-dir "^2.0.0"
neo-async "^2.5.0"
schema-utils "^1.0.0"
serialize-javascript "^1.4.0"
webpack-sources "^1.0.1"
compression@^1.7.4:
version "1.7.4"
resolved "https://registry.npm.taobao.org/compression/download/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
@ -4727,7 +4768,7 @@ find-cache-dir@^0.1.1:
mkdirp "^0.5.1"
pkg-dir "^1.0.0"
find-cache-dir@^2.1.0:
find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
integrity sha1-jQ+UzRP+Q8bHwmGg2GEVypGMBfc=
@ -5265,6 +5306,11 @@ hex-color-regex@^1.1.0:
resolved "https://registry.npm.taobao.org/hex-color-regex/download/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4=
highlight.js@^10.2.1:
version "10.2.1"
resolved "https://registry.npm.taobao.org/highlight.js/download/highlight.js-10.2.1.tgz#09784fe2e95612abbefd510948945d4fe6fa9668"
integrity sha1-CXhP4ulWEqu+/VEJSJRdT+b6lmg=
highlight.js@^9.6.0:
version "9.18.1"
resolved "https://registry.npm.taobao.org/highlight.js/download/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c"
@ -6905,6 +6951,11 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
nanopop@^2.1.0:
version "2.1.0"
resolved "https://registry.npm.taobao.org/nanopop/download/nanopop-2.1.0.tgz#23476513cee2405888afd2e8a4b54066b70b9e60"
integrity sha1-I0dlE87iQFiIr9LopLVAZrcLnmA=
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.npm.taobao.org/natural-compare/download/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -8738,6 +8789,11 @@ send@0.17.1:
range-parser "~1.2.1"
statuses "~1.5.0"
serialize-javascript@^1.4.0:
version "1.9.1"
resolved "https://registry.npm.taobao.org/serialize-javascript/download/serialize-javascript-1.9.1.tgz?cache=0&sync_timestamp=1599740650381&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fserialize-javascript%2Fdownload%2Fserialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
integrity sha1-z8IArvd7YAxH2pu4FJyUPnmML9s=
serialize-javascript@^2.1.2:
version "2.1.2"
resolved "https://registry.npm.taobao.org/serialize-javascript/download/serialize-javascript-2.1.2.tgz?cache=0&sync_timestamp=1591622032410&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fserialize-javascript%2Fdownload%2Fserialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
@ -10354,7 +10410,7 @@ webpack-merge@^4.1.2, webpack-merge@^4.2.2:
dependencies:
lodash "^4.17.15"
webpack-sources@*, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
webpack-sources@*, webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
version "1.4.3"
resolved "https://registry.npm.taobao.org/webpack-sources/download/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
integrity sha1-7t2OwLko+/HL/plOItLYkPMwqTM=
@ -10362,10 +10418,10 @@ webpack-sources@*, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sourc
source-list-map "^2.0.0"
source-map "~0.6.1"
webpack-theme-color-replacer@^1.3.12:
version "1.3.12"
resolved "https://registry.npm.taobao.org/webpack-theme-color-replacer/download/webpack-theme-color-replacer-1.3.12.tgz#0593a3149310c0e5b6b85afeccd61925b1b8e86b"
integrity sha1-BZOjFJMQwOW2uFr+zNYZJbG46Gs=
webpack-theme-color-replacer@1.3.18:
version "1.3.18"
resolved "https://registry.npmjs.org/webpack-theme-color-replacer/-/webpack-theme-color-replacer-1.3.18.tgz#98b70eab698e40b06ea3c56a3db8590f7ccef847"
integrity sha512-z7qM3opvuSjAyJd0eLMOpZhH56r+fFctczWG6xnhUSeRsvbCg/EnFdsYoGL3xYJZNANvwLlggpJxnAcuFV5a6Q==
dependencies:
webpack-sources "*"