Compare commits

...

99 Commits

Author SHA1 Message Date
xiaoxian521
836c9e7cab release: update 2.7.0 2021-12-18 13:56:21 +08:00
xiaoxian521
4ded2a7a0c chore: update 2021-12-18 13:38:53 +08:00
xiaoxian521
dc1caecf1c fix: 修复动态路由子集设置rank为0报错 2021-12-17 18:27:06 +08:00
xiaoxian521
8de3e8b37f docs: update 2021-12-17 16:45:39 +08:00
xiaoxian521
ab93216dbe perf: breadCrumb 2021-12-17 14:46:30 +08:00
lrl
86177e430e fix: breadcrumb 2021-12-17 12:53:41 +08:00
lrl
93ac4fa813 perf: breadcrumb 2021-12-17 12:53:41 +08:00
xiaoxian521
0903008ced perf: menu tree 2021-12-16 13:47:13 +08:00
xiaoxian521
10fa0ee8c8 feat: 添加菜单树结构事例 2021-12-16 11:03:20 +08:00
xiaoxian521
eb0771e7ec feat: 打包后的文件提供传统浏览器兼容性支持 2021-12-15 13:25:18 +08:00
xiaoxian521
39159d5e7b docs: update 2021-12-15 11:41:40 +08:00
xiaoxian521
501891a21c docs: update 2021-12-15 11:36:18 +08:00
啝裳
cbffe31c70 Merge pull request #149 from xiaoxian521/fix/menuModel
fix: tag
2021-12-15 11:08:44 +08:00
xiaoxian521
3ef9444bcb docs: update 2021-12-15 11:05:50 +08:00
xiaoxian521
c81227bb4c docs: update 2021-12-15 11:04:12 +08:00
lrl
05ed941638 fix: menuModel 2021-12-14 22:04:52 +08:00
lrl
77c798eaed fix: menuModel 2021-12-14 21:59:54 +08:00
xiaoxian521
6ab9997a56 fix: router 2021-12-14 13:40:11 +08:00
lrl
b961659c2f fix: router 2021-12-14 13:23:02 +08:00
xiaoxian521
81bf66eca9 fix: router 2021-12-13 23:25:27 +08:00
啝裳
b251f8ff79 Merge pull request #148 from xiaoxian521/fix/router
fix: router
2021-12-13 21:34:29 +08:00
lrl
bae16008db fix: router 2021-12-13 21:13:54 +08:00
xiaoxian521
a0c54a6ac9 fix: types 2021-12-13 17:39:13 +08:00
xiaoxian521
438aab9bfc fix: router 2021-12-13 17:30:08 +08:00
xiaoxian521
e97bd9c8c4 feat: add hasPermissions util 2021-12-13 14:08:12 +08:00
xiaoxian521
653bafaa2b fix: permission directive 2021-12-13 13:54:28 +08:00
xiaoxian521
b82a3d3a2e docs: update 2021-12-13 11:23:12 +08:00
xiaoxian521
b8c8251c64 feat: 路由历史模式从env读取并支持base参数 2021-12-13 11:20:50 +08:00
xiaoxian521
00cc5a88e0 perf: router 2021-12-12 19:46:58 +08:00
xiaoxian521
5d6ed8da33 fix: nest menu 2021-12-12 18:25:04 +08:00
啝裳
d57e0e379e refactor: router (#145)
* refactor: router

* perf: router

* perf: router

* perf: router

Co-authored-by: lrl <742798240@qq.com>
2021-12-12 17:49:19 +08:00
xiaoxian521
7811f6bdeb style: layout 2021-12-11 11:04:00 +08:00
xiaoxian521
113e5f9db2 fix: layout style 2021-12-08 20:02:44 +08:00
xiaoxian521
1758711174 fix: vite build 2021-12-08 14:27:39 +08:00
啝裳
12879f9553 perf(router): refresh (#142)
* feat: set add multiTagsCache

* perf(router): refresh

* fix(multiTags): clear storage when close other tag

Co-authored-by: lrl <742798240@qq.com>
2021-12-07 15:40:10 +08:00
xiaoxian521
d6a358e851 fix: color name 2021-12-07 14:27:08 +08:00
xiaoxian521
9e7d78fd80 fix: layout style 2021-12-06 11:40:11 +08:00
xiaoxian521
570154a4f1 fix: delete useless pictures 2021-12-06 11:05:50 +08:00
xiaoxian521
5564250e7d fix: layout icon 2021-12-06 11:00:58 +08:00
GeeHon
8d65f8ee92 style: 解决eslint换行符告警 (#139) 2021-12-06 08:31:34 +08:00
xiaoxian521
11bf711838 refactor: axios methods and env 2021-12-04 01:16:50 +08:00
xiaoxian521
a845d4f237 fix: types 2021-12-02 10:17:05 +08:00
xiaoxian521
02c3a88ed6 perf: tag 2021-12-01 20:18:00 +08:00
xiaoxian521
a8a3e5b303 perf: tags 2021-12-01 17:20:25 +08:00
一万
cec5af55d9 fix(router): refresh (#137) 2021-11-30 22:57:28 +08:00
一万
d04ba7563a fix: router refresh (#136) 2021-11-30 21:42:30 +08:00
xiaoxian521
0450f004d3 docs: update 2021-11-28 23:43:52 +08:00
xiaoxian521
24a899bba0 chore: update wangeditor 2021-11-28 16:08:09 +08:00
啝裳
6c75296a02 Merge pull request #135 from xiaoxian521/feat/expandTag
feat: expand tag
2021-11-28 15:30:14 +08:00
lrl
6cca0d3ab2 Merge branch 'feat/expandTag' of https://github.com/xiaoxian521/vue-pure-admin into feat/expandTag 2021-11-28 15:08:02 +08:00
lrl
3d34663eda feat: tabs operation view 2021-11-28 15:06:22 +08:00
xiaoxian521
4bbf4c8548 fix: style 2021-11-28 12:07:04 +08:00
xiaoxian521
8685092260 feat: 菜单支持fontawesome、iconfont、ep/icons、自定义svg 2021-11-28 01:07:16 +08:00
啝裳
622464a8a4 Merge pull request #134 from xiaoxian521/refactor/tabs
refactor: tabs
2021-11-27 19:51:21 +08:00
lrl
be3a8a6949 perf: storage tags 2021-11-27 19:04:14 +08:00
一万
3acb65d42c perf: router (#133) 2021-11-27 16:38:18 +08:00
lrl
6d3a8c5a88 fix: update 2021-11-27 15:16:50 +08:00
xiaoxian521
b65b972353 fix: update 2021-11-27 10:05:36 +08:00
xiaoxian521
8c31ca1bad fix: update name 2021-11-26 22:54:29 +08:00
一万
8cb21b6321 Merge pull request #131 from xiaoxian521/perf/progress
perf: not show spinner
2021-11-25 21:45:30 +08:00
lrl
6d6eb98562 perf: not show Spinner 2021-11-25 21:41:33 +08:00
xiaoxian521
aca6a667f3 refactor: tabs 2021-11-25 21:30:44 +08:00
啝裳
d79e63f673 Merge pull request #130 from xiaoxian521/perf/layout
perf: layout
2021-11-25 21:11:37 +08:00
lrl
e67d2df677 perf: layout 2021-11-25 20:52:23 +08:00
xiaoxian521
d44da67dc4 perf: layout 2021-11-25 12:30:02 +08:00
啝裳
9d45e80856 Merge pull request #126 from xiaoxian521/fix/tag
fix(tag): the route is not normally closed and the close icon is repeated
2021-11-22 20:24:52 +08:00
lrl
be66c8bfb9 fix(tag): the route is not normally closed and the close icon is repeated 2021-11-22 20:15:37 +08:00
xiaoxian521
e26a0f949d perf: tag 2021-11-21 11:00:13 +08:00
啝裳
3e991e6e43 Merge pull request #125 from xiaoxian521/fix/sidebar
fix(sidebarItem): span focus and enter the border that appears
2021-11-20 22:41:05 +08:00
lrl
0337a0300c fix(sidebarItem): span focus and enter the border that appears 2021-11-20 22:29:14 +08:00
xiaoxian521
ee8e0eb733 docs: update 2021-11-20 19:47:28 +08:00
啝裳
39cca9ac25 Merge pull request #124 from xiaoxian521/refactor/tag
Refactor/tag
2021-11-20 19:19:22 +08:00
lrl
95140986b9 perf: tag 2021-11-20 19:06:23 +08:00
lrl
034f1577c2 perf: tag 2021-11-20 16:41:30 +08:00
xiaoxian521
c3645fd760 fix: 删除滚动支持 2021-11-20 11:09:52 +08:00
xiaoxian521
b1b236f736 style: fix 2021-11-20 11:00:40 +08:00
xiaoxian521
0b79b65575 refactor: tag 2021-11-19 15:13:28 +08:00
啝裳
067ed96de4 Merge pull request #120 from xiaoxian521/feat/headerNotice
feat: add header notice
2021-11-17 22:48:18 +08:00
xiaoxian521
f25e5d19a4 revert: will roll to the upper level 2021-11-17 22:45:54 +08:00
xiaoxian521
0380d4a17a style: notices style 2021-11-17 22:31:10 +08:00
lrl
89dc4e5052 perf: notice add scrollbar 2021-11-17 18:10:01 +08:00
lrl
12492a522f Merge branch 'main' into feat/headerNotice 2021-11-16 23:53:26 +08:00
lrl
5d9638758b perf: headerNotice 2021-11-16 23:46:34 +08:00
xiaoxian521
6b064bdef9 fix: icon 2021-11-16 22:21:05 +08:00
paobai
7aa895a2b7 perf: storage hooks 2021-11-16 15:52:58 +08:00
xiaoxian521
35f2f9e93f fix: i18n 2021-11-16 13:49:24 +08:00
xiaoxian521
f0a5f02588 fix: i18n 2021-11-16 13:16:04 +08:00
lrl
c4a6a337a3 Merge branch 'main' into feat/headerNotice 2021-11-16 10:04:36 +08:00
hb0730
44a4c9346b fix: i18n (#110) 2021-11-16 09:53:46 +08:00
lrl
bcf533af62 feat: add headerNotice 2021-11-15 23:19:00 +08:00
hb0730
aa8005a982 feat(i18n): 菜单动态支持i18n (#109)
* feat(i18n): 菜单动态支持i18n
2021-11-15 16:45:09 +08:00
hb0730
2d6ad99f6f feat(icon): findIcon function (#107)
* feat(icon): findIcon function

支持ElementPlus icon组件和 FontAwesomeIcon

* fix(menu): 支持第三方icon组件
2021-11-15 16:41:51 +08:00
xiaoxian521
1e1747a355 docs: update 2021-11-14 09:22:15 +08:00
xiaoxian521
6ba0bd7739 docs: update 2021-11-14 09:15:20 +08:00
xiaoxian521
b4088f4612 docs: update 2021-11-14 08:46:43 +08:00
xiaoxian521
3c4619d071 feat: 兼容fontawesome4和5版本 2021-11-13 14:39:38 +08:00
hb0730
10e8b296e3 fix: #100 (#103) 2021-11-12 12:39:08 +08:00
xiaoxian521
b702703472 docs: update 2021-11-10 20:40:06 +08:00
xiaoxian521
7590dc308c chore: update vite-plugin-theme-preprocessor 2021-11-10 18:08:40 +08:00
119 changed files with 4277 additions and 1927 deletions

14
.env
View File

@@ -1,14 +1,2 @@
# port # 项目本地运行端口号
VITE_PORT = 8848 VITE_PORT = 8848
# title
VITE_TITLE = vue-pure-admin
# version
VITE_VERSION = 2.6.0
# open
VITE_OPEN = false
# public path
VITE_PUBLIC_PATH = /
# Cross-domain proxy, you can configure multiple
VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ]

View File

@@ -1,14 +1,14 @@
# port # 项目本地运行端口号
VITE_PORT = 8848 VITE_PORT = 8848
# title
VITE_TITLE = vue-pure-admin
# version
VITE_VERSION = 2.6.0
# open
VITE_OPEN = false
# public path # 开发环境读取配置文件路径
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = /
# Cross-domain proxy, you can configure multiple # 开发环境代理
VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ] VITE_PROXY_DOMAIN = /api
# 开发环境路由历史模式
VITE_ROUTER_HISTORY = "hash"
# 开发环境后端地址
VITE_PROXY_DOMAIN_REAL = "http://127.0.0.1:3000"

View File

@@ -1,2 +1,11 @@
# public path # 线上环境项目打包路径
VITE_PUBLIC_PATH = /manages/ VITE_PUBLIC_PATH = /
# 线上环境路由历史模式
VITE_ROUTER_HISTORY = "hash"
# 线上环境后端地址
VITE_PROXY_DOMAIN_REAL = ""
# 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false
VITE_LEGACY = false

View File

@@ -70,6 +70,12 @@ module.exports = {
argsIgnorePattern: "^_", argsIgnorePattern: "^_",
varsIgnorePattern: "^_" varsIgnorePattern: "^_"
} }
],
"prettier/prettier": [
"error",
{
endOfLine: "auto"
}
] ]
} }
}; };

View File

@@ -1,3 +1,23 @@
# 2.7.0(2021-12-18)
### 🎫 Feat
- New tab reuse
- New message reminder template
- Added front-end menu tree structure example
- Refactor routing, optimize permissions modules, and bring a more convenient experience
- Refactor the env environment and http request to bring a more convenient experience
- Currently, the tabs of the platform are forced to associate with local storage. The next step is to put the tabs in the memory by default and support configurable persistent tabs
- Navigation menu icons support fontawesome, iconfont, remixicon, element-plus/icons, custom svg
- Update font-awesome to version 5.0, because versions below 5.0 are no longer officially maintained, but the platform will still be compatible with font-awesome4 version
### 🍏 Perf
- Optimize the tab page to bring a better interactive experience
- Routing title supports direct writing in Chinese, which can be separated from internationalization
- Route history mode is read from env and supports base parameter
- Packaged files provide traditional browser compatibility support, configure VITE_LEGACY to true
# 2.6.0(2021-11-10) # 2.6.0(2021-11-10)
### 🎫 Feat ### 🎫 Feat

View File

@@ -1,3 +1,23 @@
# 2.7.0(2021-12-18)
### 🎫 Feat
- New tab reuse
- New message reminder template
- Added front-end menu tree structure example
- Refactor routing, optimize permissions modules, and bring a more convenient experience
- Refactor the env environment and http request to bring a more convenient experience
- Currently, the tabs of the platform are forced to associate with local storage. The next step is to put the tabs in the memory by default and support configurable persistent tabs
- Navigation menu icons support fontawesome, iconfont, remixicon, element-plus/icons, custom svg
- Update font-awesome to version 5.0, because versions below 5.0 are no longer officially maintained, but the platform will still be compatible with font-awesome4 version
### 🍏 Perf
- Optimize the tab page to bring a better interactive experience
- Routing title supports direct writing in Chinese, which can be separated from internationalization
- Route history mode is read from env and supports base parameter
- Packaged files provide traditional browser compatibility support, configure VITE_LEGACY to true
# 2.6.0(2021-11-10) # 2.6.0(2021-11-10)
### 🎫 Feat ### 🎫 Feat

View File

@@ -1,3 +1,23 @@
# 2.7.0(2021-12-18)
### 🎫 Feat
- 新增标签页复用
- 新增消息提醒模版
- 新增前端菜单树结构例子
- 重构路由,优化权限模块,带来更方便的体验
- 重构 env 环境和 http 请求,带来更方便的体验
- 目前平台的标签页强制关联了本地存储,下一步标签页默认放到内存中并支持可配置持久化标签页
- 导航菜单图标支持 fontawesome、iconfont、remixicon、element-plus/icons、自定义 svg
- 更新 font-awesome 到 5.0 版本,因为 5.0 以下的版本官方不再维护,但平台依旧会兼容 font-awesome4 版本
### 🍏 Perf
- 优化标签页,带来更好的交互体验
- 路由 title 支持直接写中文,可脱离国际化
- 路由历史模式从 env 读取并支持 base 参数
- 打包后的文件提供传统浏览器兼容性支持,配置 VITE_LEGACY 为 true
# 2.6.0(2021-11-10) # 2.6.0(2021-11-10)
### 🎫 Feat ### 🎫 Feat

View File

@@ -8,34 +8,26 @@
vue-pure-admin is a free and open source middle and back-end template. Using the latest `vue3` `vite2` `Element-Plus` `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference. vue-pure-admin is a free and open source middle and back-end template. Using the latest `vue3` `vite2` `Element-Plus` `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
## Supporting video tutorial ## Supporting Video
bilibili<https://www.bilibili.com/video/BV1534y1S7HV/> Tutorial: <https://www.bilibili.com/video/BV1534y1S7HV/>
UI Design: <https://www.bilibili.com/video/BV17g411T7rq/>
## Docs
<https://pure-admin-doc.vercel.app/>
## Thin
Github Address: <https://github.com/xiaoxian521/pure-admin-thin>
## Preview ## Preview
- [vue-pure-admin](http://yiming_chang.gitee.io/manages) - [vue-pure-admin](http://yiming_chang.gitee.io/manages)
Click to log in without password
<p align="center"> <p align="center">
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f5ee80eee1014fb4a53c5bb37574a5f5~tplv-k3u1fbpfcp-watermark.image"> <img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4857fc7eb7d4c0f8deeefc644c1f7dd~tplv-k3u1fbpfcp-watermark.awebp?">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dec0672a62e141f3b7f626c22ff6c7ef~tplv-k3u1fbpfcp-watermark.image"> <img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/549c3184697f4d268a78c9833e5ec2ea~tplv-k3u1fbpfcp-watermark.awebp?">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f586f1353de74b1b88cc9f89fce2146e~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a28fc0af7ac44e3b8f30469cba4a9993~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a17e54329cda4d76aa9c1c4f2a4715d3~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d884fb611da74ee0bdc17c29014d0260~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/abed44ac1f2744e897c28d1689bcb517~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf2f068650f44a0787c699f5a20c75a6~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73c575dd06474731ad8ab9d853f1ddfd~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99389b90f5ac4db9b0d61d99dd9a1454~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1546f1c6014446db6c9983934aedc86~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cfb0093b77c34e87b094daaa4304bc2d~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe133cc6db3245f9b1b37f231d040550~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3110a12d63e4f6fb314e60bf18bdb66~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/45b93ec453e3406a939affe65ddcc803~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cdc2dc88c1ef4aafbaaade820442c986~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4c91b162206485b88cc58a72ff54a01~tplv-k3u1fbpfcp-watermark.image">
</p> </p>
### Use Gitpod ### Use Gitpod
@@ -129,12 +121,6 @@ If you think this project is helpful to you, you can help the author buy a cup o
<img src="http://yiming_chang.gitee.io/manages/pay.png" width="360px" height="480px" /> <img src="http://yiming_chang.gitee.io/manages/pay.png" width="360px" height="480px" />
## Exchange Group
[WeChat exchange group, click to scan the code to enter the group](https://juejin.cn/post/6948419379566477342/)
My WeChat: 18237613535, pull you into the group
## License ## License
In principle, no fees and copyrights are charged, so you can use it with confidence In principle, no fees and copyrights are charged, so you can use it with confidence

View File

@@ -8,34 +8,26 @@
vue-pure-admin 是一个免费开源的中后台模版。使用了最新的`vue3` `vite2` `Element-Plus` `TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。 vue-pure-admin 是一个免费开源的中后台模版。使用了最新的`vue3` `vite2` `Element-Plus` `TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
## 配套视频教程 ## 配套视频
bilibili<https://www.bilibili.com/video/BV1534y1S7HV/> 教程<https://www.bilibili.com/video/BV1534y1S7HV/>
UI 设计:<https://www.bilibili.com/video/BV17g411T7rq/>
## 配套文档
<https://pure-admin-doc.vercel.app/>
## 精简版
仓库地址:<https://github.com/xiaoxian521/pure-admin-thin>
## 预览 ## 预览
- [vue-pure-admin](http://yiming_chang.gitee.io/manages) - [vue-pure-admin](http://yiming_chang.gitee.io/manages)
点击免密登录
<p align="center"> <p align="center">
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f5ee80eee1014fb4a53c5bb37574a5f5~tplv-k3u1fbpfcp-watermark.image"> <img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4857fc7eb7d4c0f8deeefc644c1f7dd~tplv-k3u1fbpfcp-watermark.awebp?">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dec0672a62e141f3b7f626c22ff6c7ef~tplv-k3u1fbpfcp-watermark.image"> <img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/549c3184697f4d268a78c9833e5ec2ea~tplv-k3u1fbpfcp-watermark.awebp?">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f586f1353de74b1b88cc9f89fce2146e~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a28fc0af7ac44e3b8f30469cba4a9993~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a17e54329cda4d76aa9c1c4f2a4715d3~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d884fb611da74ee0bdc17c29014d0260~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/abed44ac1f2744e897c28d1689bcb517~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf2f068650f44a0787c699f5a20c75a6~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73c575dd06474731ad8ab9d853f1ddfd~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99389b90f5ac4db9b0d61d99dd9a1454~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1546f1c6014446db6c9983934aedc86~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cfb0093b77c34e87b094daaa4304bc2d~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe133cc6db3245f9b1b37f231d040550~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3110a12d63e4f6fb314e60bf18bdb66~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/45b93ec453e3406a939affe65ddcc803~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cdc2dc88c1ef4aafbaaade820442c986~tplv-k3u1fbpfcp-watermark.image">
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4c91b162206485b88cc58a72ff54a01~tplv-k3u1fbpfcp-watermark.image">
</p> </p>
### 使用 Gitpod ### 使用 Gitpod
@@ -125,15 +117,15 @@ pnpm build
## 捐赠 ## 捐赠
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持! 如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持
<img src="http://yiming_chang.gitee.io/manages/pay.png" width="360px" height="480px" /> <img src="http://yiming_chang.gitee.io/manages/pay.jpg" width="150px" height="150px" />
## 交流群 ## 付费咨询、需求定制
[微信交流群,点击扫码进群](https://juejin.cn/post/6948419379566477342/) 作者精力有限,需要提供技术服务的可扫下面的二维码加微信,添加请备注来意
本人微信18237613535拉你进群 <img src="http://yiming_chang.gitee.io/manages/wechat.jpg" width="150px" height="150px" />
## 许可证 ## 许可证

View File

@@ -1,5 +1,14 @@
// 处理环境变量
const warpperEnv = (envConf: Recordable): ViteEnv => { const warpperEnv = (envConf: Recordable): ViteEnv => {
const ret: any = {}; // 此处为默认值,无需修改
const ret: ViteEnv = {
VITE_PORT: 8848,
VITE_PUBLIC_PATH: "",
VITE_PROXY_DOMAIN: "",
VITE_PROXY_DOMAIN_REAL: "",
VITE_ROUTER_HISTORY: "",
VITE_LEGACY: false
};
for (const envName of Object.keys(envConf)) { for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, "\n"); let realName = envConf[envName].replace(/\\n/g, "\n");
@@ -9,13 +18,6 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
if (envName === "VITE_PORT") { if (envName === "VITE_PORT") {
realName = Number(realName); realName = Number(realName);
} }
if (envName === "VITE_PROXY" && realName) {
try {
realName = JSON.parse(realName.replace(/'/g, '"'));
} catch (error) {
realName = "";
}
}
ret[envName] = realName; ret[envName] = realName;
if (typeof realName === "string") { if (typeof realName === "string") {
process.env[envName] = realName; process.env[envName] = realName;
@@ -25,8 +27,15 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
} }
return ret; return ret;
}; };
// 跨域代理重写
const regExps = (value: string, reg: string): string => {
return value.replace(new RegExp(reg, "g"), "");
};
// 环境变量
const loadEnv = (): ViteEnv => { const loadEnv = (): ViteEnv => {
return import.meta.env; return import.meta.env;
}; };
export { loadEnv, warpperEnv }; export { warpperEnv, regExps, loadEnv };

View File

@@ -1,19 +0,0 @@
type ProxyItem = [string, string];
type ProxyList = ProxyItem[];
const regExps = (value: string, reg: string): string => {
return value.replace(new RegExp(reg, "g"), "");
};
export function createProxy(list: ProxyList = []) {
const ret: any = {};
for (const [prefix, target] of list) {
ret[prefix] = {
target: target,
changeOrigin: true,
rewrite: (path: string) => regExps(path, prefix)
};
}
return ret;
}

View File

@@ -9,6 +9,7 @@ const systemRouter = {
meta: { meta: {
icon: "Setting", icon: "Setting",
title: "message.hssysManagement", title: "message.hssysManagement",
i18n: true,
showLink: true, showLink: true,
rank: 6 rank: 6
}, },
@@ -18,6 +19,7 @@ const systemRouter = {
name: "user", name: "user",
meta: { meta: {
title: "message.hsBaseinfo", title: "message.hsBaseinfo",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -26,7 +28,9 @@ const systemRouter = {
name: "dict", name: "dict",
meta: { meta: {
title: "message.hsDict", title: "message.hsDict",
showLink: true i18n: true,
showLink: true,
keepAlive: true
} }
} }
] ]
@@ -39,6 +43,7 @@ const permissionRouter = {
meta: { meta: {
title: "message.permission", title: "message.permission",
icon: "Lollipop", icon: "Lollipop",
i18n: true,
showLink: true, showLink: true,
rank: 3 rank: 3
}, },
@@ -48,6 +53,7 @@ const permissionRouter = {
name: "permissionPage", name: "permissionPage",
meta: { meta: {
title: "message.permissionPage", title: "message.permissionPage",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -56,6 +62,7 @@ const permissionRouter = {
name: "permissionButton", name: "permissionButton",
meta: { meta: {
title: "message.permissionButton", title: "message.permissionButton",
i18n: true,
showLink: true, showLink: true,
authority: [] authority: []
} }
@@ -63,6 +70,42 @@ const permissionRouter = {
] ]
}; };
const tabsRouter = {
path: "/tabs",
name: "reTabs",
redirect: "/tabs/index",
meta: {
icon: "IF-team-icontabs",
title: "message.hstabs",
i18n: true,
showLink: true,
rank: 8
},
children: [
{
path: "/tabs/index",
name: "reTabs",
meta: {
title: "message.hstabs",
showLink: true,
i18n: true
}
},
{
path: "/tabs/detail",
name: "tabDetail",
meta: {
title: "",
showLink: false,
i18n: false,
dynamicLevel: 3,
realPath: "/tabs/detail",
refreshRedirect: "/tabs/index"
}
}
]
};
// 添加不同按钮权限到/permission/button页面中 // 添加不同按钮权限到/permission/button页面中
function setDifAuthority(authority, routes) { function setDifAuthority(authority, routes) {
routes.children[1].meta.authority = [authority]; routes.children[1].meta.authority = [authority];
@@ -77,12 +120,16 @@ export default [
if (query.name === "admin") { if (query.name === "admin") {
return { return {
code: 0, code: 0,
info: [systemRouter, setDifAuthority("v-admin", permissionRouter)] info: [
tabsRouter,
systemRouter,
setDifAuthority("v-admin", permissionRouter)
]
}; };
} else { } else {
return { return {
code: 0, code: 0,
info: [setDifAuthority("v-test", permissionRouter)] info: [tabsRouter, setDifAuthority("v-test", permissionRouter)]
}; };
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "vue-pure-admin", "name": "vue-pure-admin",
"version": "2.6.0", "version": "2.7.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">= 16", "node": ">= 16",
@@ -29,7 +29,10 @@
], ],
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1", "@amap/amap-jsapi-loader": "^1.0.1",
"@element-plus/icons": "^0.0.11", "@element-plus/icons-vue": "^0.2.4",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-5",
"@logicflow/core": "0.7.1", "@logicflow/core": "0.7.1",
"@logicflow/extension": "0.7.1", "@logicflow/extension": "0.7.1",
"@vueuse/core": "^6.7.1", "@vueuse/core": "^6.7.1",
@@ -40,9 +43,10 @@
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"echarts": "^5.2.1", "echarts": "^5.2.1",
"element-plus": "1.2.0-beta.3", "element-plus": "1.2.0-beta.6",
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lowdb": "^3.0.0", "lowdb": "^3.0.0",
"mitt": "^3.0.0", "mitt": "^3.0.0",
@@ -51,19 +55,21 @@
"path": "^0.12.7", "path": "^0.12.7",
"path-to-regexp": "^6.2.0", "path-to-regexp": "^6.2.0",
"pinia": "^2.0.0-rc.14", "pinia": "^2.0.0-rc.14",
"qs": "^6.10.1",
"remixicon": "^2.5.0",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"responsive-storage": "^1.0.11", "responsive-storage": "^1.0.11",
"sortablejs": "1.13.0", "sortablejs": "1.13.0",
"typescript-cookie": "^1.0.0", "typescript-cookie": "^1.0.0",
"v-contextmenu": "3.0.0", "v-contextmenu": "3.0.0",
"vue": "^3.2.21", "vue": "^3.2.24",
"vue-i18n": "^9.2.0-beta.3", "vue-i18n": "^9.2.0-beta.3",
"vue-json-pretty": "^2.0.2", "vue-json-pretty": "^2.0.2",
"vue-router": "^4.0.11", "vue-router": "^4.0.12",
"vue-types": "^4.1.0", "vue-types": "^4.1.0",
"vuedraggable": "4.1.0", "vuedraggable": "4.1.0",
"vxe-table": "4.0.30", "vxe-table": "4.0.30",
"wangeditor": "4.7.7", "wangeditor": "^4.7.9",
"xe-ajax": "4.0.5", "xe-ajax": "4.0.5",
"xe-utils": "3.4.0", "xe-utils": "3.4.0",
"xgplayer": "2.28.0" "xgplayer": "2.28.0"
@@ -72,17 +78,20 @@
"@commitlint/cli": "13.1.0", "@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0", "@commitlint/config-conventional": "13.1.0",
"@types/element-resize-detector": "1.1.3", "@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
"@types/mockjs": "1.0.3", "@types/mockjs": "1.0.3",
"@types/node": "14.14.14", "@types/node": "14.14.14",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "4.31.0", "@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0", "@typescript-eslint/parser": "4.31.0",
"@vitejs/plugin-vue": "^1.9.4", "@vitejs/plugin-legacy": "^1.6.4",
"@vitejs/plugin-vue-jsx": "^1.2.0", "@vitejs/plugin-vue": "^1.10.2",
"@vue/compiler-sfc": "^3.2.21", "@vitejs/plugin-vue-jsx": "^1.3.1",
"@vue/compiler-sfc": "^3.2.24",
"@vue/eslint-config-prettier": "6.0.0", "@vue/eslint-config-prettier": "6.0.0",
"@vue/eslint-config-typescript": "7.0.0", "@vue/eslint-config-typescript": "7.0.0",
"@zougt/vite-plugin-theme-preprocessor": "^1.3.10", "@zougt/vite-plugin-theme-preprocessor": "^1.4.0",
"autoprefixer": "10.2.4", "autoprefixer": "10.2.4",
"babel-plugin-transform-remove-console": "6.9.4", "babel-plugin-transform-remove-console": "6.9.4",
"chalk": "2.4.2", "chalk": "2.4.2",
@@ -97,15 +106,15 @@
"prettier": "2.3.2", "prettier": "2.3.2",
"pretty-quick": "3.1.1", "pretty-quick": "3.1.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"sass": "^1.43.4", "sass": "^1.45.0",
"sass-loader": "^12.3.0", "sass-loader": "^12.3.0",
"stylelint": "13.13.1", "stylelint": "13.13.1",
"stylelint-config-prettier": "8.0.2", "stylelint-config-prettier": "8.0.2",
"stylelint-config-standard": "22.0.0", "stylelint-config-standard": "22.0.0",
"stylelint-order": "4.1.0", "stylelint-order": "4.1.0",
"typescript": "4.4.2", "typescript": "4.4.2",
"unplugin-element-plus": "^0.1.0", "unplugin-element-plus": "^0.1.3",
"vite": "latest", "vite": "2.6.14",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vite-plugin-style-import": "^1.2.1", "vite-plugin-style-import": "^1.2.1",
"vite-svg-loader": "^2.2.0", "vite-svg-loader": "^2.2.0",

1341
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
{ {
"Version": "2.6.0", "Version": "2.7.0",
"Title": "PureAdmin", "Title": "PureAdmin",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,
"MultiTagsCache": false,
"KeepAlive": true, "KeepAlive": true,
"Locale": "zh", "Locale": "zh",
"Layout": "vertical", "Layout": "vertical",
@@ -12,7 +13,6 @@
"HideTabs": false, "HideTabs": false,
"MapConfigure": { "MapConfigure": {
"amapKey": "97b3248d1553172e81f168cf94ea667e", "amapKey": "97b3248d1553172e81f168cf94ea667e",
"baiduKey": "wTHbkkEweiFqZLKunMIjcrb2RcqNXkhc",
"options": { "options": {
"resizeEnable": true, "resizeEnable": true,
"center": [113.6401, 34.72468], "center": [113.6401, 34.72468],

View File

@@ -1,11 +1,11 @@
import { http } from "../utils/http"; import { http } from "../utils/http";
// 地图数据 // 地图数据
export const mapJson = (data?: object) => { export const mapJson = (params?: object) => {
return http.request("get", "/getMapInfo", data); return http.request("get", "/getMapInfo", { params });
}; };
// echarts数据 // echarts数据
export const echartsJson = (data?: object) => { export const echartsJson = (params?: object) => {
return http.request("get", "/getEchartsInfo", data); return http.request("get", "/getEchartsInfo", { params });
}; };

View File

@@ -1,5 +1,5 @@
import { http } from "../utils/http"; import { http } from "../utils/http";
export const getAsyncRoutes = (data?: object) => { export const getAsyncRoutes = (params?: object) => {
return http.request("get", "/getAsyncRoutes", data); return http.request("get", "/getAsyncRoutes", { params });
}; };

View File

@@ -13,5 +13,14 @@ export const getVerify = (): userType => {
// 登录 // 登录
export const getLogin = (data: object) => { export const getLogin = (data: object) => {
return http.request("post", "/login", data); return http.request("post", "/login", { data });
}; };
// 刷新token
export const refreshToken = (data: object) => {
return http.request("post", "/refreshToken", { data });
};
// export const searchVague = (data: object) => {
// return http.request("post", "/searchVague", { data });
// };

BIN
src/assets/avatars.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 2208059 */ font-family: "iconfont"; /* Project id 2208059 */
src: url("iconfont.woff2?t=1636197082361") format("woff2"), src: url("iconfont.woff2?t=1638023560828") format("woff2"),
url("iconfont.woff?t=1636197082361") format("woff"), url("iconfont.woff?t=1638023560828") format("woff"),
url("iconfont.ttf?t=1636197082361") format("truetype"); url("iconfont.ttf?t=1638023560828") format("truetype");
} }
.iconfont { .iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.team-icontabs::before {
content: "\e63e";
}
.team-iconlogo::before { .team-iconlogo::before {
content: "\e620"; content: "\e620";
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,13 @@
"css_prefix_text": "team-icon", "css_prefix_text": "team-icon",
"description": "pure-admin", "description": "pure-admin",
"glyphs": [ "glyphs": [
{
"icon_id": "20594647",
"name": "标签页",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{ {
"icon_id": "22129506", "icon_id": "22129506",
"name": "水能", "name": "水能",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5l-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"></path></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" color="#00000073" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5l-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"></path></g></svg>

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 452 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"></path></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" color="#00000073" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"></path></g></svg>

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -1,5 +1,79 @@
import { App } from "vue"; import { App, defineComponent } from "vue";
import icon from "./src/Icon.vue"; import icon from "./src/Icon.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { iconComponents } from "/@/plugins/element-plus";
/**
* find icon component
* @param icon icon图标
* @returns component
*/
export function findIconReg(icon: string) {
// fontawesome
const faReg = /^FA-/;
// iconfont
const iFReg = /^IF-/;
// remixicon
const riReg = /^RI-/;
// typeof icon === "function" 属于SVG
if (faReg.test(icon)) {
const text = icon.split(faReg)[1];
return findIcon(
text.slice(0, text.indexOf(" ")),
"FA",
text.slice(text.indexOf(" ") + 1, text.length)
);
} else if (iFReg.test(icon)) {
return findIcon(icon.split(iFReg)[1], "IF");
} else if (typeof icon === "function") {
return findIcon(icon, "SVG");
} else if (riReg.test(icon)) {
return findIcon(icon.split(riReg)[1], "RI");
} else {
return findIcon(icon, "EL");
}
}
// 支持fontawesome、iconfont、remixicon、element-plus/icons、自定义svg
export function findIcon(icon: String, type = "EL", property?: string) {
if (type === "FA") {
return defineComponent({
name: "FaIcon",
setup() {
return { icon, property };
},
components: { FontAwesomeIcon },
template: `<font-awesome-icon :icon="icon" v-bind:[property]="true" />`
});
} else if (type === "IF") {
return defineComponent({
name: "IfIcon",
data() {
return { icon: `iconfont ${icon}` };
},
template: `<i :class="icon" />`
});
} else if (type === "RI") {
return defineComponent({
name: "RIIcon",
data() {
return { icon: `ri-${icon}` };
},
template: `<i :class="icon" />`
});
} else if (type === "EL") {
const components = iconComponents.filter(
component => component.name === icon
);
if (components.length > 0) {
return components[0];
} else {
return null;
}
} else if (type === "SVG") {
return icon;
}
}
export const Icon = Object.assign(icon, { export const Icon = Object.assign(icon, {
install(app: App) { install(app: App) {

View File

@@ -1,6 +1,9 @@
import { App } from "vue"; import { App } from "vue";
import axios from "axios"; import axios from "axios";
import { loadEnv } from "@build/index";
let config: object = {}; let config: object = {};
const { VITE_PUBLIC_PATH } = loadEnv();
const setConfig = (cfg?: unknown) => { const setConfig = (cfg?: unknown) => {
config = Object.assign(config, cfg); config = Object.assign(config, cfg);
@@ -30,10 +33,7 @@ export const getServerConfig = async (app: App): Promise<undefined> => {
return axios({ return axios({
baseURL: "", baseURL: "",
method: "get", method: "get",
url: url: `${VITE_PUBLIC_PATH}serverConfig.json`
process.env.NODE_ENV === "production"
? "/manages/serverConfig.json"
: "/serverConfig.json"
}) })
.then(({ data: config }) => { .then(({ data: config }) => {
let $config = app.config.globalProperties.$config; let $config = app.config.globalProperties.$config;

View File

@@ -9,7 +9,7 @@ export const auth: Directive = {
const authRoles = value; const authRoles = value;
const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles); const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles);
if (!hasAuth) { if (!hasAuth) {
el.style.display = "none"; el.parentNode.removeChild(el);
} }
} else { } else {
throw new Error("need roles! Like v-auth=\"['admin','test']\""); throw new Error("need roles! Like v-auth=\"['admin','test']\"");

View File

@@ -74,8 +74,8 @@ const transitionMain = defineComponent({
:style="[ :style="[
hideTabs && layout ? 'padding-top: 48px;' : '', hideTabs && layout ? 'padding-top: 48px;' : '',
!hideTabs && layout ? 'padding-top: 85px;' : '', !hideTabs && layout ? 'padding-top: 85px;' : '',
hideTabs && !layout ? 'padding-top: 62px' : '', hideTabs && !layout ? 'padding-top: 48px' : '',
!hideTabs && !layout ? 'padding-top: 98px;' : '' !hideTabs && !layout ? 'padding-top: 85px;' : ''
]" ]"
> >
<router-view> <router-view>

View File

@@ -1,6 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import Notice from "./notice/index.vue";
import avatars from "/@/assets/avatars.jpg";
import { transformI18n } from "/@/plugins/i18n";
import Hamburger from "./sidebar/hamBurger.vue"; import Hamburger from "./sidebar/hamBurger.vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { storageSession } from "/@/utils/storage"; import { storageSession } from "/@/utils/storage";
@@ -17,13 +20,17 @@ const pureApp = useAppStoreHook();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
let usename = storageSession.getItem("info")?.username; let usename = storageSession.getItem("info")?.username;
const { locale, t } = useI18n(); const { locale } = useI18n();
watch( watch(
() => locale.value, () => locale.value,
() => { () => {
//@ts-ignore //@ts-ignore
document.title = t(unref(route.meta.title)); // 动态title document.title = transformI18n(
//@ts-ignore
unref(route.meta.title),
unref(route.meta.i18n)
); // 动态title
} }
); );
@@ -65,6 +72,8 @@ function translationEn() {
<Breadcrumb class="breadcrumb-container" /> <Breadcrumb class="breadcrumb-container" />
<div class="vertical-header-right"> <div class="vertical-header-right">
<!-- 通知 -->
<Notice />
<!-- 全屏 --> <!-- 全屏 -->
<screenfull v-show="!deviceDetection()" /> <screenfull v-show="!deviceDetection()" />
<!-- 国际化 --> <!-- 国际化 -->
@@ -98,16 +107,15 @@ function translationEn() {
<!-- 退出登陆 --> <!-- 退出登陆 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<img <img :src="avatars" />
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
/>
<p>{{ usename }}</p> <p>{{ usename }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item icon="el-icon-switch-button" @click="logout">{{ <el-dropdown-item @click="logout">
$t("message.hsLoginOut") <i class="ri-logout-circle-r-line"></i
}}</el-dropdown-item> >{{ $t("message.hsLoginOut") }}</el-dropdown-item
>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -151,6 +159,12 @@ function translationEn() {
color: #000000d9; color: #000000d9;
justify-content: flex-end; justify-content: flex-end;
:deep(.dropdown-badge) {
&:hover {
background: #f6f6f6;
}
}
.screen-full { .screen-full {
cursor: pointer; cursor: pointer;
@@ -239,7 +253,12 @@ function translationEn() {
} }
.logout { .logout {
max-width: 120px;
.el-dropdown-menu__item { .el-dropdown-menu__item {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
padding: 0 18px !important; padding: 0 18px !important;
} }

View File

@@ -0,0 +1,146 @@
export interface ListItem {
avatar: string;
title: string;
datetime: string;
type: string;
description: string;
status?: "" | "success" | "warning" | "info" | "danger";
extra?: string;
}
export interface TabItem {
key: string;
name: string;
list: ListItem[];
}
export const noticesData: TabItem[] = [
{
key: "1",
name: "通知",
list: [
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
title: "你收到了 12 份新周报",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
title: "你推荐的 前端高手 已通过第三轮面试",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
title: "这种模板可以区分多种通知类型",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
title:
"展示标题内容超过一行后的处理方式如果内容超过1行将自动截断并支持tooltip显示完整标题。",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
title: "左侧图标用于区分不同的类型",
datetime: "一年前",
description: "",
type: "1"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
title: "左侧图标用于区分不同的类型",
datetime: "一年前",
description: "",
type: "1"
}
]
},
{
key: "2",
name: "消息",
list: [
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
title: "李白 评论了你",
description: "长风破浪会有时,直挂云帆济沧海",
datetime: "一年前",
type: "2"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
title: "李白 回复了你",
description: "行路难,行路难,多歧路,今安在。",
datetime: "一年前",
type: "2"
},
{
avatar:
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
title: "标题",
description:
"请将鼠标移动到此处以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2超过2行的描述内容将被省略并且可以通过tooltip查看完整内容",
datetime: "一年前",
type: "2"
}
]
},
{
key: "3",
name: "代办",
list: [
{
avatar: "",
title: "任务名称",
description: "任务需要在 2021-11-16 20:00 前启动",
datetime: "",
extra: "未开始",
status: "info",
type: "3"
},
{
avatar: "",
title: "第三方紧急代码变更",
description:
"一拳提交于 2021-11-16需在 2021-11-18 前完成代码变更任务",
datetime: "",
extra: "马上到期",
status: "danger",
type: "3"
},
{
avatar: "",
title: "信息安全考试",
description: "指派小仙于 2021-12-12 前完成更新并发布",
datetime: "",
extra: "已耗时 8 天",
status: "warning",
type: "3"
},
{
avatar: "",
title: "vue-pure-admin 版本发布",
description: "vue-pure-admin 版本发布",
datetime: "",
extra: "进行中",
type: "3"
}
]
}
];

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref } from "vue";
import NoticeList from "./noticeList.vue";
import { noticesData } from "./data";
const activeName = ref(noticesData[0].name);
const notices = ref(noticesData);
let noticesNum = ref(0);
notices.value.forEach(notice => {
noticesNum.value += notice.list.length;
});
</script>
<template>
<el-dropdown trigger="click" placement="bottom-end">
<span class="dropdown-badge">
<el-badge :value="noticesNum" :max="99">
<el-icon class="header-notice-icon"><bell /></el-icon>
</el-badge>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-tabs v-model="activeName" class="dropdown-tabs">
<template v-for="item in notices" :key="item.key">
<el-tab-pane
:label="`${item.name}(${item.list.length})`"
:name="item.name"
>
<el-scrollbar max-height="330px">
<div class="noticeList-container">
<NoticeList :list="item.list" />
</div>
</el-scrollbar>
</el-tab-pane>
</template>
</el-tabs>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<style lang="scss" scoped>
.dropdown-badge {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
width: 60px;
cursor: pointer;
.header-notice-icon {
font-size: 18px;
}
}
.dropdown-tabs {
width: 336px;
background-color: #fff;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
border-radius: 4px;
:deep(.el-tabs__header) {
margin: 0;
}
:deep(.el-tabs__nav-scroll) {
display: flex;
justify-content: center;
}
:deep(.el-tabs__nav-wrap)::after {
height: 1px;
}
:deep(.noticeList-container) {
padding: 15px 24px 0 24px;
}
}
</style>

View File

@@ -0,0 +1,168 @@
<script setup lang="ts">
import { ListItem } from "./data";
import { ref, PropType, nextTick } from "vue";
const props = defineProps({
noticeItem: {
type: Object as PropType<ListItem>,
default: () => {}
}
});
const titleRef = ref(null);
const descriptionRef = ref(null);
const titleTooltip = ref(false);
const descriptionTooltip = ref(false);
function hoverTitle() {
nextTick(() => {
titleRef.value?.scrollWidth > titleRef.value?.clientWidth
? (titleTooltip.value = true)
: (titleTooltip.value = false);
});
}
function hoverDescription(event, description) {
// currentWidth 为文本在页面中所占的宽度创建标签加入到页面获取currentWidth ,最后在移除
let tempTag = document.createElement("span");
tempTag.innerText = description;
tempTag.className = "getDescriptionWidth";
document.querySelector("body").appendChild(tempTag);
let currentWidth = (
document.querySelector(".getDescriptionWidth") as HTMLSpanElement
).offsetWidth;
document.querySelector(".getDescriptionWidth").remove();
// cellWidth为容器的宽度
const cellWidth = event.target.offsetWidth;
// 当文本宽度大于容器宽度两倍时,代表文本显示超过两行
currentWidth > 2 * cellWidth
? (descriptionTooltip.value = true)
: (descriptionTooltip.value = false);
}
</script>
<template>
<div class="notice-container">
<el-avatar
v-if="props.noticeItem.avatar"
:size="30"
:src="props.noticeItem.avatar"
class="notice-container-avatar"
></el-avatar>
<div class="notice-container-text">
<div class="notice-text-title">
<el-tooltip
popper-class="notice-title-popper"
:disabled="!titleTooltip"
:content="props.noticeItem.title"
placement="top-start"
>
<div
ref="titleRef"
class="notice-title-content"
@mouseover="hoverTitle"
>
{{ props.noticeItem.title }}
</div>
</el-tooltip>
<el-tag
v-if="props.noticeItem?.extra"
:type="props.noticeItem?.status"
size="small"
class="notice-title-extra"
>{{ props.noticeItem?.extra }}
</el-tag>
</div>
<el-tooltip
popper-class="notice-title-popper"
:disabled="!descriptionTooltip"
:content="props.noticeItem.description"
placement="top-start"
>
<div
ref="descriptionRef"
class="notice-text-description"
@mouseover="hoverDescription($event, props.noticeItem.description)"
>
{{ props.noticeItem.description }}
</div>
</el-tooltip>
<div class="notice-text-datetime">
{{ props.noticeItem.datetime }}
</div>
</div>
</div>
</template>
<style>
.notice-title-popper {
max-width: 238px;
}
</style>
<style scoped lang="scss">
.notice-container {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
.notice-container-avatar {
margin-right: 16px;
background: #fff;
}
.notice-container-text {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
.notice-text-title {
display: flex;
margin-bottom: 8px;
font-weight: 400;
font-size: 14px;
line-height: 1.5715;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
.notice-title-content {
flex: 1;
width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.notice-title-extra {
float: right;
margin-top: -1.5px;
font-weight: 400;
}
}
.notice-text-description,
.notice-text-datetime {
font-size: 12px;
line-height: 1.5715;
color: rgba(0, 0, 0, 0.45);
}
.notice-text-description {
display: -webkit-box;
text-overflow: ellipsis;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.notice-text-datetime {
margin-top: 4px;
}
}
}
</style>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { PropType } from "vue";
import NoticeItem from "./noticeItem.vue";
import { ListItem } from "./data";
const props = defineProps({
list: {
type: Array as PropType<Array<ListItem>>,
default: () => []
}
});
</script>
<template>
<div v-if="props.list.length">
<NoticeItem
v-for="(item, index) in props.list"
:noticeItem="item"
:key="index"
></NoticeItem>
</div>
<el-empty v-else description="暂无数据"></el-empty>
</template>

View File

@@ -23,7 +23,7 @@ const { isFullscreen, toggle } = useFullscreen();
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-full { .screen-full {
width: 36px; width: 36px;
height: 62px; height: 48px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;

View File

@@ -10,6 +10,7 @@ import {
getCurrentInstance getCurrentInstance
} from "vue"; } from "vue";
import panel from "../panel/index.vue"; import panel from "../panel/index.vue";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { templateRef } from "@vueuse/core"; import { templateRef } from "@vueuse/core";
@@ -17,6 +18,7 @@ import { debounce } from "/@/utils/debounce";
import { themeColorsType } from "../../types"; import { themeColorsType } from "../../types";
import { useAppStoreHook } from "/@/store/modules/app"; import { useAppStoreHook } from "/@/store/modules/app";
import { storageLocal, storageSession } from "/@/utils/storage"; import { storageLocal, storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils"; import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils";
const router = useRouter(); const router = useRouter();
@@ -28,23 +30,23 @@ const instanceConfig =
getCurrentInstance().appContext.app.config.globalProperties.$config; getCurrentInstance().appContext.app.config.globalProperties.$config;
let themeColors = ref<Array<themeColorsType>>([ let themeColors = ref<Array<themeColorsType>>([
// 暗雅(默认) // 道奇蓝(默认)
{ rgb: "27, 42, 71", themeColor: "default" }, { rgb: "27, 42, 71", themeColor: "default" },
// // 亮白色
{ rgb: "255, 255, 255", themeColor: "light" }, { rgb: "255, 255, 255", themeColor: "light" },
// 薄暮 // 猩红色
{ rgb: "245, 34, 45", themeColor: "dusk" }, { rgb: "245, 34, 45", themeColor: "dusk" },
// 火山 // 橙红色
{ rgb: "250, 84, 28", themeColor: "volcano" }, { rgb: "250, 84, 28", themeColor: "volcano" },
// //
{ rgb: "250, 219, 20", themeColor: "yellow" }, { rgb: "250, 219, 20", themeColor: "yellow" },
// 明青 // 绿宝石
{ rgb: "19, 194, 194", themeColor: "mingQing" }, { rgb: "19, 194, 194", themeColor: "mingQing" },
// 极光绿 // 酸橙绿
{ rgb: "82, 196, 26", themeColor: "auroraGreen" }, { rgb: "82, 196, 26", themeColor: "auroraGreen" },
// 粉红 // 深粉色
{ rgb: "235, 47, 150", themeColor: "pink" }, { rgb: "235, 47, 150", themeColor: "pink" },
// 酱紫 // 深紫罗兰色
{ rgb: "114, 46, 209", themeColor: "saucePurple" } { rgb: "114, 46, 209", themeColor: "saucePurple" }
]); ]);
@@ -76,7 +78,8 @@ const logoVal = ref(storageLocal.getItem("logoVal") || "1");
const settings = reactive({ const settings = reactive({
greyVal: instance.sets.grey, greyVal: instance.sets.grey,
weakVal: instance.sets.weak, weakVal: instance.sets.weak,
tabsVal: instance.sets.hideTabs tabsVal: instance.sets.hideTabs,
multiTagsCache: instance.sets.multiTagsCache
}); });
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) { function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
@@ -92,7 +95,8 @@ const greyChange = (value): void => {
instance.sets = { instance.sets = {
grey: value, grey: value,
weak: instance.sets.weak, weak: instance.sets.weak,
hideTabs: instance.sets.hideTabs hideTabs: instance.sets.hideTabs,
multiTagsCache: instance.sets.multiTagsCache
}; };
}; };
@@ -106,7 +110,8 @@ const weekChange = (value): void => {
instance.sets = { instance.sets = {
grey: instance.sets.grey, grey: instance.sets.grey,
weak: value, weak: value,
hideTabs: instance.sets.hideTabs hideTabs: instance.sets.hideTabs,
multiTagsCache: instance.sets.multiTagsCache
}; };
}; };
@@ -115,11 +120,23 @@ const tagsChange = () => {
instance.sets = { instance.sets = {
grey: instance.sets.grey, grey: instance.sets.grey,
weak: instance.sets.weak, weak: instance.sets.weak,
hideTabs: showVal hideTabs: showVal,
multiTagsCache: instance.sets.multiTagsCache
}; };
emitter.emit("tagViewsChange", showVal); emitter.emit("tagViewsChange", showVal);
}; };
const multiTagsCacheChange = () => {
let multiTagsCache = settings.multiTagsCache;
instance.sets = {
grey: instance.sets.grey,
weak: instance.sets.weak,
hideTabs: instance.sets.hideTabs,
multiTagsCache: multiTagsCache
};
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
};
//初始化项目配置 //初始化项目配置
nextTick(() => { nextTick(() => {
settings.greyVal && settings.greyVal &&
@@ -135,6 +152,19 @@ function onReset() {
storageSession.clear(); storageSession.clear();
toggleClass(false, "html-grey", document.querySelector("html")); toggleClass(false, "html-grey", document.querySelector("html"));
toggleClass(false, "html-weakness", document.querySelector("html")); toggleClass(false, "html-weakness", document.querySelector("html"));
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
i18n: true,
showLink: true
}
}
]);
useMultiTagsStoreHook().multiTagsCacheChange(getConfig().MultiTagsCache);
router.push("/login"); router.push("/login");
} }
@@ -304,6 +334,18 @@ function setLayoutThemeColor(theme: string) {
> >
</el-switch> </el-switch>
</li> </li>
<li>
<span>标签页持久化</span>
<el-switch
v-model="settings.multiTagsCache"
inline-prompt
inactive-color="#a6a6a6"
active-text=""
inactive-text=""
@change="multiTagsCacheChange"
>
</el-switch>
</li>
<li> <li>
<span>标签风格</span> <span>标签风格</span>

View File

@@ -1,10 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { isEqual } from "lodash-es";
import { transformI18n } from "/@/plugins/i18n";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { useRoute, useRouter, RouteLocationMatched } from "vue-router"; import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
const levelList = ref([]);
const route = useRoute(); const route = useRoute();
const levelList = ref([]);
const router = useRouter(); const router = useRouter();
const routes = router.options.routes;
const multiTags = useMultiTagsStoreHook().multiTags;
const isDashboard = (route: RouteLocationMatched): boolean | string => { const isDashboard = (route: RouteLocationMatched): boolean | string => {
const name = route && (route.name as string); const name = route && (route.name as string);
@@ -15,19 +21,57 @@ const isDashboard = (route: RouteLocationMatched): boolean | string => {
}; };
const getBreadcrumb = (): void => { const getBreadcrumb = (): void => {
let matched = route.matched.filter(item => item.meta && item.meta.title); // 当前路由信息
let currentRoute;
if (Object.keys(route.query).length > 0) {
multiTags.forEach(item => {
if (isEqual(route.query, item?.query)) {
currentRoute = item;
}
});
} else {
currentRoute = findRouteByPath(router.currentRoute.value.path, multiTags);
}
// 当前路由的父级路径组成的数组
const parentRoutes = getParentPaths(router.currentRoute.value.path, routes);
// 存放组成面包屑的数组
let matched = [];
// 获取每个父级路径对应的路由信息
parentRoutes.forEach(path => {
if (path !== "/") {
matched.push(findRouteByPath(path, routes));
}
});
if (router.currentRoute.value.meta?.refreshRedirect) {
matched.unshift(
findRouteByPath(
router.currentRoute.value.meta.refreshRedirect as string,
routes
)
);
} else {
// 过滤与子级相同标题的父级路由
matched = matched.filter(item => {
return !item.redirect || (item.redirect && item.children.length !== 1);
});
}
if (currentRoute?.path !== "/welcome") {
matched.push(currentRoute);
}
const first = matched[0]; const first = matched[0];
if (!isDashboard(first)) { if (!isDashboard(first)) {
matched = [ matched = [
{ {
path: "/welcome", path: "/welcome",
parentPath: "/", parentPath: "/",
meta: { title: "message.hshome" } meta: { title: "message.hshome", i18n: true }
} as unknown as RouteLocationMatched } as unknown as RouteLocationMatched
].concat(matched); ].concat(matched);
} }
levelList.value = matched.filter( levelList.value = matched.filter(
item => item.meta && item.meta.title && item.meta.breadcrumb !== false item => item?.meta && item?.meta.title !== false
); );
}; };
@@ -38,6 +82,11 @@ watch(
() => getBreadcrumb() () => getBreadcrumb()
); );
watch(
() => route.query,
() => getBreadcrumb()
);
const handleLink = (item: RouteLocationMatched): any => { const handleLink = (item: RouteLocationMatched): any => {
const { redirect, path } = item; const { redirect, path } = item;
if (redirect) { if (redirect) {
@@ -55,10 +104,10 @@ const handleLink = (item: RouteLocationMatched): any => {
<span <span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect" class="no-redirect"
>{{ $t(item.meta.title) }}</span >{{ transformI18n(item.meta.title, item.meta.i18n) }}</span
> >
<a v-else @click.prevent="handleLink(item)"> <a v-else @click.prevent="handleLink(item)">
{{ $t(item.meta.title) }} {{ transformI18n(item.meta.title, item.meta.i18n) }}
</a> </a>
</el-breadcrumb-item> </el-breadcrumb-item>
</transition-group> </transition-group>

View File

@@ -9,8 +9,10 @@ import {
} from "vue"; } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import Notice from "../notice/index.vue";
import { templateRef } from "@vueuse/core"; import { templateRef } from "@vueuse/core";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
import avatars from "/@/assets/avatars.jpg";
import { algorithm } from "/@/utils/algorithm"; import { algorithm } from "/@/utils/algorithm";
import screenfull from "../screenfull/index.vue"; import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
@@ -27,7 +29,6 @@ const title =
getCurrentInstance().appContext.config.globalProperties.$config?.Title; getCurrentInstance().appContext.config.globalProperties.$config?.Title;
const menuRef = templateRef<ElRef | null>("menu", null); const menuRef = templateRef<ElRef | null>("menu", null);
const routeStore = usePermissionStoreHook();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const routers = useRouter().options.routes; const routers = useRouter().options.routes;
@@ -131,13 +132,15 @@ onMounted(() => {
@select="menuSelect" @select="menuSelect"
> >
<sidebar-item <sidebar-item
v-for="route in routeStore.wholeRoutes" v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path" :key="route.path"
:item="route" :item="route"
:base-path="route.path" :base-path="route.path"
/> />
</el-menu> </el-menu>
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 通知 -->
<Notice />
<!-- 全屏 --> <!-- 全屏 -->
<screenfull v-show="!deviceDetection()" /> <screenfull v-show="!deviceDetection()" />
<!-- 国际化 --> <!-- 国际化 -->
@@ -171,16 +174,15 @@ onMounted(() => {
<!-- 退出登陆 --> <!-- 退出登陆 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<img <img :src="avatars" />
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
/>
<p>{{ usename }}</p> <p>{{ usename }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item icon="el-icon-switch-button" @click="logout">{{ <el-dropdown-item @click="logout">
$t("message.hsLoginOut") <i class="ri-logout-circle-r-line"></i
}}</el-dropdown-item> >{{ $t("message.hsLoginOut") }}</el-dropdown-item
>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -221,7 +223,12 @@ onMounted(() => {
} }
.logout { .logout {
max-width: 120px;
.el-dropdown-menu__item { .el-dropdown-menu__item {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
padding: 0 18px !important; padding: 0 18px !important;
} }

View File

@@ -4,7 +4,8 @@ import { PropType, ref, nextTick, getCurrentInstance } from "vue";
import { childrenType } from "../../types"; import { childrenType } from "../../types";
import { useAppStoreHook } from "/@/store/modules/app"; import { useAppStoreHook } from "/@/store/modules/app";
import Icon from "/@/components/ReIcon/src/Icon.vue"; import Icon from "/@/components/ReIcon/src/Icon.vue";
import { transformI18n } from "/@/plugins/i18n";
import { findIconReg } from "/@/components/ReIcon";
const instance = getCurrentInstance().appContext.app.config.globalProperties; const instance = getCurrentInstance().appContext.app.config.globalProperties;
const menuMode = instance.$storage.layout?.layout === "vertical"; const menuMode = instance.$storage.layout?.layout === "vertical";
const pureApp = useAppStoreHook(); const pureApp = useAppStoreHook();
@@ -68,7 +69,12 @@ function hasOneShowingChild(
} }
function resolvePath(routePath) { function resolvePath(routePath) {
return path.resolve(props.basePath, routePath); const httpReg = /^http(s?):\/\//;
if (httpReg.test(routePath)) {
return props.basePath + "/" + routePath;
} else {
return path.resolve(props.basePath, routePath);
}
} }
</script> </script>
@@ -87,7 +93,10 @@ function resolvePath(routePath) {
<el-icon v-show="props.item.meta.icon"> <el-icon v-show="props.item.meta.icon">
<component <component
:is=" :is="
onlyOneChild.meta.icon || (props.item.meta && props.item.meta.icon) findIconReg(
onlyOneChild.meta.icon ||
(props.item.meta && props.item.meta.icon)
)
" "
></component> ></component>
</el-icon> </el-icon>
@@ -101,24 +110,33 @@ function resolvePath(routePath) {
overflow: 'hidden' overflow: 'hidden'
}" }"
> >
<span v-if="!menuMode">{{ $t(onlyOneChild.meta.title) }}</span> <span v-if="!menuMode">{{
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
}}</span>
<el-tooltip <el-tooltip
v-else v-else
placement="top" placement="top"
:offset="-10" :offset="-10"
:disabled="!onlyOneChild.showTooltip" :disabled="!onlyOneChild.showTooltip"
> >
<template #content> {{ $t(onlyOneChild.meta.title) }} </template> <template #content>
{{
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
}}
</template>
<span <span
ref="menuTextRef" ref="menuTextRef"
:style="{ :style="{
width: pureApp.sidebar.opened ? '125px' : '', width: pureApp.sidebar.opened ? '125px' : '',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis' textOverflow: 'ellipsis',
outline: 'none'
}" }"
@mouseover="hoverMenu(onlyOneChild)" @mouseover="hoverMenu(onlyOneChild)"
> >
{{ $t(onlyOneChild.meta.title) }} {{
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
}}
</span> </span>
</el-tooltip> </el-tooltip>
<Icon <Icon
@@ -139,16 +157,22 @@ function resolvePath(routePath) {
> >
<template #title> <template #title>
<el-icon v-show="props.item.meta.icon" :class="props.item.meta.icon"> <el-icon v-show="props.item.meta.icon" :class="props.item.meta.icon">
<component :is="props.item.meta && props.item.meta.icon"></component> <component
:is="findIconReg(props.item.meta && props.item.meta.icon)"
></component>
</el-icon> </el-icon>
<span v-if="!menuMode">{{ $t(props.item.meta.title) }}</span> <span v-if="!menuMode">{{
transformI18n(props.item.meta.title, props.item.meta.i18n)
}}</span>
<el-tooltip <el-tooltip
v-else v-else
placement="top" placement="top"
:offset="-10" :offset="-10"
:disabled="!pureApp.sidebar.opened || !props.item.showTooltip" :disabled="!pureApp.sidebar.opened || !props.item.showTooltip"
> >
<template #content> {{ $t(props.item.meta.title) }} </template> <template #content>
{{ transformI18n(props.item.meta.title, props.item.meta.i18n) }}
</template>
<div <div
ref="menuTextRef" ref="menuTextRef"
:style="{ :style="{
@@ -160,7 +184,7 @@ function resolvePath(routePath) {
@mouseover="hoverMenu(props.item)" @mouseover="hoverMenu(props.item)"
> >
<span style="overflow: hidden; text-overflow: ellipsis"> <span style="overflow: hidden; text-overflow: ellipsis">
{{ $t(props.item.meta.title) }} {{ transformI18n(props.item.meta.title, props.item.meta.i18n) }}
</span> </span>
</div> </div>
</el-tooltip> </el-tooltip>

View File

@@ -12,7 +12,6 @@ import { usePermissionStoreHook } from "/@/store/modules/permission";
const route = useRoute(); const route = useRoute();
const pureApp = useAppStoreHook(); const pureApp = useAppStoreHook();
const router = useRouter().options.routes; const router = useRouter().options.routes;
const routeStore = usePermissionStoreHook();
const showLogo = ref(storageLocal.getItem("logoVal") || "1"); const showLogo = ref(storageLocal.getItem("logoVal") || "1");
const isCollapse = computed(() => { const isCollapse = computed(() => {
return !pureApp.getSidebarStatus; return !pureApp.getSidebarStatus;
@@ -72,7 +71,7 @@ onBeforeMount(() => {
@select="menuSelect" @select="menuSelect"
> >
<sidebar-item <sidebar-item
v-for="route in routeStore.wholeRoutes" v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path" :key="route.path"
:item="route" :item="route"
class="outer-most" class="outer-most"

View File

@@ -0,0 +1,343 @@
@keyframes scheduleInWidth {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes scheduleOutWidth {
from {
width: 100%;
}
to {
width: 0;
}
}
@-webkit-keyframes rotate {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes rotate {
from {
-moz-transform: rotate(0deg);
}
to {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes rotate {
from {
-o-transform: rotate(0deg);
}
to {
-o-transform: rotate(360deg);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes close {
from {
transform: translate(-50%, -50%);
}
to {
transform: translate(0, -50%);
}
}
.tags-view {
width: 100%;
font-size: 14px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
background: #fff;
position: relative;
box-shadow: 0 0 1px #888;
.scroll-item {
border-radius: 3px 3px 0 0;
padding: 0 6px 0 6px;
box-shadow: 0 0 1px #888;
position: relative;
margin-right: 4px;
height: 28px;
display: inline-block;
line-height: 28px;
transition: all 0.4s;
cursor: pointer;
.el-icon-close {
font-size: 10px;
color: #1890ff;
cursor: pointer;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
transition: font-size 0.2s;
&:hover {
border-radius: 50%;
color: #fff;
background: #b4bccc;
font-size: 13px;
}
}
&.is-closable:not(:first-child) {
&:hover {
padding-right: 18px;
&:not(.is-active) {
.el-icon-close {
animation: close 200ms ease-in forwards;
}
}
}
}
}
a {
text-decoration: none;
color: #666;
padding: 0 4px 0 4px;
}
.scroll-container {
flex: 1;
overflow: hidden;
padding: 5px 0;
white-space: nowrap;
position: relative;
.tab {
position: relative;
float: left;
list-style: none;
overflow: visible;
white-space: nowrap;
transition: transform 0.5s ease-in-out;
.scroll-item {
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:nth-child(1) {
margin-left: 5px;
}
}
}
}
.right-button {
display: flex;
font-size: 16px;
li {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 38px;
border-right: 1px solid #ccc;
cursor: pointer;
}
}
/* 右键菜单 */
.contextmenu {
margin: 0;
background: #fff;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
color: #000000d9;
font-weight: normal;
font-size: 13px;
white-space: nowrap;
outline: 0;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
li {
width: 100%;
margin: 0;
padding: 7px 12px;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
background: #eee;
}
svg {
display: block;
margin-right: 0.5em;
}
}
}
}
.el-dropdown-menu {
padding: 0;
li {
width: 100%;
margin: 0;
padding: 0 12px;
cursor: pointer;
display: flex;
align-items: center;
svg {
display: block;
margin-right: 0.5em;
}
}
}
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
:deep(.el-dropdown-menu__item) i {
margin-right: 10px;
}
.el-dropdown-menu__item--divided::before {
margin: 0;
}
.el-dropdown-menu__item.is-disabled {
cursor: not-allowed;
}
.scroll-item.is-active {
background-color: #eaf4fe;
position: relative;
color: #fff;
&:not(:first-child) {
padding-right: 18px;
}
.el-icon-close {
transform: translate(0, -50%);
}
a {
color: #1890ff;
}
}
.ri-arrow-left-s-line {
width: 40px;
height: 38px;
line-height: 38px;
text-align: center;
font-size: 20px;
color: #00000073;
box-shadow: 5px 0 5px -6px #ccc;
&:hover {
cursor: w-resize;
}
}
.ri-arrow-right-s-line {
width: 40px;
height: 38px;
line-height: 38px;
text-align: center;
font-size: 20px;
border-right: 1px solid #ccc;
color: #00000073;
box-shadow: -5px 0 5px -6px #ccc;
&:hover {
cursor: e-resize;
}
}
/* 卡片模式下鼠标移入显示蓝色边框 */
.card-in {
color: #1890ff;
a {
color: #1890ff;
}
}
/* 卡片模式下鼠标移出隐藏蓝色边框 */
.card-out {
border: none;
color: #666;
a {
color: #666;
}
}
/* 灵动模式 */
.schedule-active {
width: 100%;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
}
/* 灵动模式下鼠标移入显示蓝色进度条 */
.schedule-in {
width: 100%;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
animation: scheduleInWidth 400ms ease-in;
}
/* 灵动模式下鼠标移出隐藏蓝色进度条 */
.schedule-out {
width: 0;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
animation: scheduleOutWidth 400ms ease-in;
}
/* 刷新按钮动画效果 */
.refresh-button {
-webkit-animation: rotate 600ms linear infinite;
-moz-animation: rotate 600ms linear infinite;
-o-animation: rotate 600ms linear infinite;
animation: rotate 600ms linear infinite;
}

View File

@@ -1,17 +1,3 @@
<script lang="ts">
let routerArrays: Array<RouteConfigs> = [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
showLink: true
}
}
];
</script>
<script setup lang="ts"> <script setup lang="ts">
import { import {
ref, ref,
@@ -23,14 +9,6 @@ import {
getCurrentInstance, getCurrentInstance,
ComputedRef ComputedRef
} from "vue"; } from "vue";
import { RouteConfigs, relativeStorageType, tagsViewsType } from "../../types";
import { emitter } from "/@/utils/mitt";
import { templateRef } from "@vueuse/core";
import { handleAliveRoute, delAliveRoutes } from "/@/router";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import close from "/@/assets/svg/close.svg"; import close from "/@/assets/svg/close.svg";
import refresh from "/@/assets/svg/refresh.svg"; import refresh from "/@/assets/svg/refresh.svg";
@@ -39,16 +17,172 @@ import closeLeft from "/@/assets/svg/close_left.svg";
import closeOther from "/@/assets/svg/close_other.svg"; import closeOther from "/@/assets/svg/close_other.svg";
import closeRight from "/@/assets/svg/close_right.svg"; import closeRight from "/@/assets/svg/close_right.svg";
let refreshButton = "refresh-button"; import { emitter } from "/@/utils/mitt";
const instance = getCurrentInstance(); import { isEqual, isEmpty } from "lodash-es";
import { transformI18n } from "/@/plugins/i18n";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { RouteConfigs, tagsViewsType } from "../../types";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
// 响应式storage
let relativeStorage: relativeStorageType;
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const translateX = ref<number>(0);
const activeIndex = ref<number>(-1);
let refreshButton = "refresh-button";
const instance = getCurrentInstance();
const pureSetting = useSettingStoreHook();
const showTags = ref(storageLocal.getItem("tagsVal") || false); const showTags = ref(storageLocal.getItem("tagsVal") || false);
const tabDom = templateRef<HTMLElement | null>("tabDom", null);
const containerDom = templateRef<HTMLElement | null>("containerDom", null); const containerDom = templateRef<HTMLElement | null>("containerDom", null);
const activeIndex = ref(-1); const scrollbarDom = templateRef<HTMLElement | null>("scrollbarDom", null);
let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => {
return useMultiTagsStoreHook()?.multiTags;
});
const linkIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "is-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "is-active";
} else {
return "";
}
}
};
});
const scheduleIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "schedule-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "schedule-active";
} else {
return "";
}
}
};
});
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return true;
} else {
return false;
}
} else {
if (isEqual(route?.query, item?.query)) {
return true;
} else {
return false;
}
}
};
});
const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => {
return item.path === route.path;
});
moveToView(index);
};
watch([route], () => {
activeIndex.value = -1;
dynamicTagView();
});
useResizeObserver(
scrollbarDom,
useDebounceFn(() => {
dynamicTagView();
}, 200)
);
const tabNavPadding = 10;
const moveToView = (index: number): void => {
if (!instance.refs["dynamic" + index]) {
return;
}
const tabItemEl = instance.refs["dynamic" + index];
const tabItemElOffsetLeft = (tabItemEl as HTMLElement).offsetLeft;
const tabItemOffsetWidth = (tabItemEl as HTMLElement).offsetWidth;
// 标签页导航栏可视长度(不包含溢出部分)
const scrollbarDomWidth = scrollbarDom.value
? scrollbarDom.value.offsetWidth
: 0;
// 已有标签页总长度(包含溢出部分)
const tabDomWidth = tabDom.value ? tabDom.value.offsetWidth : 0;
if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) {
translateX.value = 0;
} else if (tabItemElOffsetLeft < -translateX.value) {
// 标签在可视区域左侧
translateX.value = -tabItemElOffsetLeft + tabNavPadding;
} else if (
tabItemElOffsetLeft > -translateX.value &&
tabItemElOffsetLeft + tabItemOffsetWidth <
-translateX.value + scrollbarDomWidth
) {
// 标签在可视区域
translateX.value = Math.min(
0,
scrollbarDomWidth -
tabItemOffsetWidth -
tabItemElOffsetLeft -
tabNavPadding
);
} else {
// 标签在可视区域右侧
translateX.value = -(
tabItemElOffsetLeft -
(scrollbarDomWidth - tabNavPadding - tabItemOffsetWidth)
);
}
};
const handleScroll = (offset: number): void => {
const scrollbarDomWidth = scrollbarDom.value
? scrollbarDom.value?.offsetWidth
: 0;
const tabDomWidth = tabDom.value ? tabDom.value.offsetWidth : 0;
if (offset > 0) {
translateX.value = Math.min(0, translateX.value + offset);
} else {
if (scrollbarDomWidth < tabDomWidth) {
if (translateX.value >= -(tabDomWidth - scrollbarDomWidth)) {
translateX.value = Math.max(
translateX.value + offset,
scrollbarDomWidth - tabDomWidth
);
}
} else {
translateX.value = 0;
}
}
};
const tagsViews = ref<Array<tagsViewsType>>([ const tagsViews = ref<Array<tagsViewsType>>([
{ {
@@ -62,41 +196,38 @@ const tagsViews = ref<Array<tagsViewsType>>([
icon: close, icon: close,
text: "message.hscloseCurrentTab", text: "message.hscloseCurrentTab",
divided: false, divided: false,
disabled: routerArrays.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: closeLeft, icon: closeLeft,
text: "message.hscloseLeftTabs", text: "message.hscloseLeftTabs",
divided: true, divided: true,
disabled: routerArrays.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: closeRight, icon: closeRight,
text: "message.hscloseRightTabs", text: "message.hscloseRightTabs",
divided: false, divided: false,
disabled: routerArrays.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: closeOther, icon: closeOther,
text: "message.hscloseOtherTabs", text: "message.hscloseOtherTabs",
divided: true, divided: true,
disabled: routerArrays.length > 2 ? false : true, disabled: multiTags.value.length > 2 ? false : true,
show: true show: true
}, },
{ {
icon: closeAll, icon: closeAll,
text: "message.hscloseAllTabs", text: "message.hscloseAllTabs",
divided: false, divided: false,
disabled: routerArrays.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
} }
]); ]);
const dynamicTagList: ComputedRef<Array<RouteConfigs>> = computed(() => {
return relativeStorage.routesInStorage;
});
// 显示模式,默认灵动模式显示 // 显示模式,默认灵动模式显示
const showModel = ref(storageLocal.getItem("showModel") || "smart"); const showModel = ref(storageLocal.getItem("showModel") || "smart");
@@ -112,7 +243,7 @@ let buttonTop = ref(0);
let currentSelect = ref({}); let currentSelect = ref({});
function dynamicRouteTag(value: string, parentPath: string): void { function dynamicRouteTag(value: string, parentPath: string): void {
const hasValue = relativeStorage.routesInStorage.some((item: any) => { const hasValue = multiTags.value.some(item => {
return item.path === value; return item.path === value;
}); });
@@ -121,13 +252,12 @@ function dynamicRouteTag(value: string, parentPath: string): void {
arr.forEach((arrItem: any) => { arr.forEach((arrItem: any) => {
let pathConcat = parentPath + arrItem.path; let pathConcat = parentPath + arrItem.path;
if (arrItem.path === value || pathConcat === value) { if (arrItem.path === value || pathConcat === value) {
routerArrays.push({ useMultiTagsStoreHook().handleTags("push", {
path: value, path: value,
parentPath: `/${parentPath.split("/")[1]}`, parentPath: `/${parentPath.split("/")[1]}`,
meta: arrItem.meta, meta: arrItem.meta,
name: arrItem.name name: arrItem.name
}); });
relativeStorage.routesInStorage = routerArrays;
} else { } else {
if (arrItem.children && arrItem.children.length > 0) { if (arrItem.children && arrItem.children.length > 0) {
concatPath(arrItem.children, value, parentPath); concatPath(arrItem.children, value, parentPath);
@@ -142,9 +272,10 @@ function dynamicRouteTag(value: string, parentPath: string): void {
// 重新加载 // 重新加载
function onFresh() { function onFresh() {
toggleClass(true, refreshButton, document.querySelector(".rotate")); toggleClass(true, refreshButton, document.querySelector(".rotate"));
const { fullPath } = unref(route); const { fullPath, query } = unref(route);
router.replace({ router.replace({
path: "/redirect" + fullPath path: "/redirect" + fullPath,
query: query
}); });
setTimeout(() => { setTimeout(() => {
removeClass(document.querySelector(".rotate"), refreshButton); removeClass(document.querySelector(".rotate"), refreshButton);
@@ -154,28 +285,41 @@ function onFresh() {
function deleteDynamicTag(obj: any, current: any, tag?: string) { function deleteDynamicTag(obj: any, current: any, tag?: string) {
// 存放被删除的缓存路由 // 存放被删除的缓存路由
let delAliveRouteList = []; let delAliveRouteList = [];
let valueIndex: number = routerArrays.findIndex((item: any) => { let valueIndex: number = multiTags.value.findIndex((item: any) => {
return item.path === obj.path; if (item.query) {
if (item.path === obj.path) {
return item.query === obj.query;
}
} else {
return item.path === obj.path;
}
}); });
const spliceRoute = (start?: number, end?: number, other?: boolean): void => { const spliceRoute = (
startIndex?: number,
length?: number,
other?: boolean
): void => {
if (other) { if (other) {
relativeStorage.routesInStorage = [ useMultiTagsStoreHook().handleTags("equal", [
{ {
path: "/welcome", path: "/welcome",
parentPath: "/", parentPath: "/",
meta: { meta: {
title: "message.hshome", title: "message.hshome",
i18n: true,
icon: "el-icon-s-home", icon: "el-icon-s-home",
showLink: true showLink: true
} }
}, },
obj obj
]; ]);
routerArrays = relativeStorage.routesInStorage;
} else { } else {
delAliveRouteList = routerArrays.splice(start, end); // @ts-ignore
relativeStorage.routesInStorage = routerArrays; delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", {
startIndex,
length
});
} }
}; };
@@ -184,12 +328,12 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
} else if (tag === "left") { } else if (tag === "left") {
spliceRoute(1, valueIndex - 1); spliceRoute(1, valueIndex - 1);
} else if (tag === "right") { } else if (tag === "right") {
spliceRoute(valueIndex + 1, routerArrays.length); spliceRoute(valueIndex + 1, multiTags.value.length);
} else { } else {
// 从当前匹配到的路径中删除 // 从当前匹配到的路径中删除
spliceRoute(valueIndex, 1); spliceRoute(valueIndex, 1);
} }
let newRoute: any = routerArrays.slice(-1); let newRoute = useMultiTagsStoreHook().handleTags("slice");
if (current === route.path) { if (current === route.path) {
// 删除缓存路由 // 删除缓存路由
tag tag
@@ -199,19 +343,21 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (tag === "left") return; if (tag === "left") return;
nextTick(() => { nextTick(() => {
router.push({ router.push({
path: newRoute[0].path path: newRoute[0].path,
query: newRoute[0].query
}); });
}); });
} else { } else {
// 删除缓存路由 // 删除缓存路由
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]); tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
if (!routerArrays.length) return; if (!multiTags.value.length) return;
let isHasActiveTag = routerArrays.some(item => { let isHasActiveTag = multiTags.value.some(item => {
return item.path === route.path; return item.path === route.path;
}); });
!isHasActiveTag && !isHasActiveTag &&
router.push({ router.push({
path: newRoute[0].path path: newRoute[0].path,
query: newRoute[0].query
}); });
} }
} }
@@ -222,6 +368,19 @@ function deleteMenu(item, tag?: string) {
function onClickDrop(key, item, selectRoute?: RouteConfigs) { function onClickDrop(key, item, selectRoute?: RouteConfigs) {
if (item && item.disabled) return; if (item && item.disabled) return;
let selectTagRoute;
if (selectRoute) {
selectTagRoute = {
path: selectRoute.path,
meta: selectRoute.meta,
name: selectRoute.name,
query: selectRoute.query
};
} else {
selectTagRoute = { path: route.path, meta: route.meta };
}
// 当前路由信息 // 当前路由信息
switch (key) { switch (key) {
case 0: case 0:
@@ -230,61 +389,32 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
break; break;
case 1: case 1:
// 关闭当前标签页 // 关闭当前标签页
selectRoute deleteMenu(selectTagRoute);
? deleteMenu({
path: selectRoute.path,
meta: selectRoute.meta,
name: selectRoute.name
})
: deleteMenu({ path: route.path, meta: route.meta });
break; break;
case 2: case 2:
// 关闭左侧标签页 // 关闭左侧标签页
selectRoute deleteMenu(selectTagRoute, "left");
? deleteMenu(
{
path: selectRoute.path,
meta: selectRoute.meta
},
"left"
)
: deleteMenu({ path: route.path, meta: route.meta }, "left");
break; break;
case 3: case 3:
// 关闭右侧标签页 // 关闭右侧标签页
selectRoute deleteMenu(selectTagRoute, "right");
? deleteMenu(
{
path: selectRoute.path,
meta: selectRoute.meta
},
"right"
)
: deleteMenu({ path: route.path, meta: route.meta }, "right");
break; break;
case 4: case 4:
// 关闭其他标签页 // 关闭其他标签页
selectRoute deleteMenu(selectTagRoute, "other");
? deleteMenu(
{
path: selectRoute.path,
meta: selectRoute.meta
},
"other"
)
: deleteMenu({ path: route.path, meta: route.meta }, "other");
break; break;
case 5: case 5:
// 关闭全部标签页 // 关闭全部标签页
routerArrays.splice(1, routerArrays.length); useMultiTagsStoreHook().handleTags("splice", "", {
relativeStorage.routesInStorage = routerArrays; startIndex: 1,
length: multiTags.value.length
});
usePermissionStoreHook().clearAllCachePage(); usePermissionStoreHook().clearAllCachePage();
router.push("/welcome"); router.push("/welcome");
break; break;
} }
setTimeout(() => { setTimeout(() => {
showMenuModel(route.fullPath); showMenuModel(route.fullPath, route.query);
}); });
} }
@@ -310,18 +440,31 @@ function disabledMenus(value: boolean) {
} }
// 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 // 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页
function showMenuModel(currentPath: string, refresh = false) { function showMenuModel(
let allRoute = unref(relativeStorage.routesInStorage); currentPath: string,
let routeLength = unref(relativeStorage.routesInStorage).length; query: object = {},
// currentIndex为1时左侧的菜单是首页则不显示关闭左侧标签页 refresh = false
let currentIndex = allRoute.findIndex(v => v.path === currentPath); ) {
// 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页 let allRoute = multiTags.value;
let routeLength = multiTags.value.length;
let currentIndex = -1;
if (isEmpty(query)) {
currentIndex = allRoute.findIndex(v => v.path === currentPath);
} else {
currentIndex = allRoute.findIndex(v => isEqual(v.query, query));
}
showMenus(true); showMenus(true);
if (refresh) { if (refresh) {
tagsViews.value[0].show = true; tagsViews.value[0].show = true;
} }
/**
* currentIndex为1时左侧的菜单是首页则不显示关闭左侧标签页
* 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页
*/
if (currentIndex === 1 && routeLength !== 2) { if (currentIndex === 1 && routeLength !== 2) {
// 左侧的菜单是首页,右侧存在别的菜单 // 左侧的菜单是首页,右侧存在别的菜单
tagsViews.value[2].show = false; tagsViews.value[2].show = false;
@@ -360,10 +503,10 @@ function openMenu(tag, e) {
} else if (route.path !== tag.path) { } else if (route.path !== tag.path) {
// 右键菜单不匹配当前路由,隐藏刷新 // 右键菜单不匹配当前路由,隐藏刷新
tagsViews.value[0].show = false; tagsViews.value[0].show = false;
showMenuModel(tag.path); showMenuModel(tag.path, tag.query);
} else if ( } else if (
// eslint-disable-next-line no-dupe-else-if // eslint-disable-next-line no-dupe-else-if
relativeStorage.routesInStorage.length === 2 && multiTags.value.length === 2 &&
route.path !== tag.path route.path !== tag.path
) { ) {
showMenus(true); showMenus(true);
@@ -371,7 +514,7 @@ function openMenu(tag, e) {
tagsViews.value[4].show = false; tagsViews.value[4].show = false;
} else if (route.path === tag.path) { } else if (route.path === tag.path) {
// 右键当前激活的菜单 // 右键当前激活的菜单
showMenuModel(tag.path, true); showMenuModel(tag.path, tag.query, true);
} }
currentSelect.value = tag; currentSelect.value = tag;
@@ -385,7 +528,9 @@ function openMenu(tag, e) {
} else { } else {
buttonLeft.value = left; buttonLeft.value = left;
} }
buttonTop.value = e.clientY + 10; pureSetting.hiddenSideBar
? (buttonTop.value = e.clientY)
: (buttonTop.value = e.clientY - 40);
setTimeout(() => { setTimeout(() => {
visible.value = true; visible.value = true;
}, 10); }, 10);
@@ -393,7 +538,11 @@ function openMenu(tag, e) {
// 触发tags标签切换 // 触发tags标签切换
function tagOnClick(item) { function tagOnClick(item) {
showMenuModel(item.path); router.push({
path: item?.path,
query: item?.query
});
showMenuModel(item?.path, item?.query);
} }
// 鼠标移入 // 鼠标移入
@@ -437,8 +586,6 @@ watch(
onBeforeMount(() => { onBeforeMount(() => {
if (!instance) return; if (!instance) return;
relativeStorage = instance.appContext.app.config.globalProperties.$storage;
routerArrays = relativeStorage.routesInStorage ?? routerArrays;
// 根据当前路由初始化操作标签页的禁用状态 // 根据当前路由初始化操作标签页的禁用状态
showMenuModel(route.fullPath); showMenuModel(route.fullPath);
@@ -466,40 +613,51 @@ onBeforeMount(() => {
<template> <template>
<div ref="containerDom" class="tags-view" v-if="!showTags"> <div ref="containerDom" class="tags-view" v-if="!showTags">
<el-scrollbar wrap-class="scrollbar-wrapper" class="scroll-container"> <i class="ri-arrow-left-s-line" @click="handleScroll(200)"></i>
<div ref="scrollbarDom" class="scroll-container">
<div <div
v-for="(item, index) in dynamicTagList" class="tab"
:key="index" ref="tabDom"
:ref="'dynamic' + index" :style="{ transform: `translateX(${translateX}px)` }"
:class="[
'scroll-item is-closable',
$route.path === item.path ? 'is-active' : '',
$route.path === item.path && showModel === 'card' ? 'card-active' : ''
]"
@contextmenu.prevent="openMenu(item, $event)"
@mouseenter.prevent="onMouseenter(item, index)"
@mouseleave.prevent="onMouseleave(item, index)"
> >
<router-link :to="item.path" @click="tagOnClick(item)">{{
$t(item.meta.title)
}}</router-link>
<el-icon
v-if="
($route.path === item.path && index !== 0) ||
(index === activeIndex && index !== 0)
"
class="el-icon-close"
@click="deleteMenu(item)"
>
<CloseBold />
</el-icon>
<div <div
:ref="'schedule' + index" :ref="'dynamic' + index"
v-if="showModel !== 'card'" v-for="(item, index) in multiTags"
:class="[$route.path === item.path ? 'schedule-active' : '']" :key="index"
></div> :class="[
'scroll-item is-closable',
linkIsActive(item),
$route.path === item.path && showModel === 'card'
? 'card-active'
: ''
]"
@contextmenu.prevent="openMenu(item, $event)"
@mouseenter.prevent="onMouseenter(item, index)"
@mouseleave.prevent="onMouseleave(item, index)"
@click="tagOnClick(item)"
>
<router-link :to="item.path"
>{{ transformI18n(item.meta.title, item.meta.i18n) }}
</router-link>
<el-icon
v-if="
iconIsActive(item, index) ||
(index === activeIndex && index !== 0)
"
class="el-icon-close"
@click.stop="deleteMenu(item)"
>
<CloseBold />
</el-icon>
<div
:ref="'schedule' + index"
v-if="showModel !== 'card'"
:class="[scheduleIsActive(item)]"
></div>
</div>
</div> </div>
</el-scrollbar> </div>
<i class="ri-arrow-right-s-line" @click="handleScroll(-200)"></i>
<!-- 右键菜单按钮 --> <!-- 右键菜单按钮 -->
<transition name="el-zoom-in-top"> <transition name="el-zoom-in-top">
<ul <ul
@@ -560,277 +718,5 @@ onBeforeMount(() => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@keyframes scheduleInWidth { @import "./index.scss";
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes scheduleOutWidth {
from {
width: 100%;
}
to {
width: 0;
}
}
@-webkit-keyframes rotate {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes rotate {
from {
-moz-transform: rotate(0deg);
}
to {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes rotate {
from {
-o-transform: rotate(0deg);
}
to {
-o-transform: rotate(360deg);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.tags-view {
width: 100%;
font-size: 14px;
display: flex;
box-shadow: 0 0 1px #888;
.scroll-item {
border-radius: 3px 3px 0 0;
padding: 2px 6px;
display: inline-block;
position: relative;
margin-right: 4px;
height: 28px;
line-height: 25px;
transition: all 0.4s;
.el-icon-close {
font-size: 10px;
color: #1890ff;
cursor: pointer;
transform: fontsize3s;
&:hover {
border-radius: 50%;
color: #fff;
background: #b4bccc;
font-size: 14px;
}
}
&.is-closable:not(:first-child) {
&:hover {
padding-right: 8px;
}
}
}
a {
text-decoration: none;
color: #666;
padding: 0 4px 0 4px;
}
.scroll-container {
padding: 5px 0;
white-space: nowrap;
position: relative;
width: 100%;
background: #fff;
.scroll-item {
&:nth-child(1) {
margin-left: 5px;
}
}
.scrollbar-wrapper {
position: absolute;
height: 40px;
overflow-x: hidden !important;
}
}
// 右键菜单
.contextmenu {
margin: 0;
background: #fff;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
color: #000000d9;
font-weight: normal;
font-size: 13px;
white-space: nowrap;
outline: 0;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
li {
width: 100%;
margin: 0;
padding: 7px 12px;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
background: #eee;
}
svg {
display: block;
margin-right: 0.5em;
}
}
}
}
.right-button {
display: flex;
align-items: center;
background: #fff;
font-size: 16px;
li {
width: 40px;
height: 38px;
line-height: 38px;
text-align: center;
border-right: 1px solid #ccc;
cursor: pointer;
}
}
.el-dropdown-menu {
padding: 0;
li {
width: 100%;
margin: 0;
padding: 0 12px;
cursor: pointer;
display: flex;
align-items: center;
svg {
display: block;
margin-right: 0.5em;
}
}
}
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
:deep(.el-dropdown-menu__item) i {
margin-right: 10px;
}
.el-dropdown-menu__item--divided::before {
margin: 0;
}
.el-dropdown-menu__item.is-disabled {
cursor: not-allowed;
}
.is-active {
background-color: #eaf4fe;
position: relative;
color: #fff;
a {
color: #1890ff;
}
}
// 卡片模式
.card-active {
border: 1px solid #1890ff;
}
// 卡片模式下鼠标移入显示蓝色边框
.card-in {
border: 1px solid #1890ff;
color: #1890ff;
a {
color: #1890ff;
}
}
// 卡片模式下鼠标移出隐藏蓝色边框
.card-out {
border: none;
color: #666;
a {
color: #666;
}
}
// 灵动模式
.schedule-active {
width: 100%;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
}
// 灵动模式下鼠标移入显示蓝色进度条
.schedule-in {
width: 100%;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
animation: scheduleInWidth 400ms ease-in;
}
// 灵动模式下鼠标移出隐藏蓝色进度条
.schedule-out {
width: 0;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
animation: scheduleOutWidth 400ms ease-in;
}
// 刷新按钮动画效果
.refresh-button {
-webkit-animation: rotate 600ms linear infinite;
-moz-animation: rotate 600ms linear infinite;
-o-animation: rotate 600ms linear infinite;
animation: rotate 600ms linear infinite;
}
</style> </style>

View File

@@ -1,13 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
h, h,
ref,
unref,
reactive, reactive,
computed, computed,
onMounted, onMounted,
watchEffect,
onBeforeMount,
defineComponent, defineComponent,
getCurrentInstance getCurrentInstance
} from "vue"; } from "vue";
@@ -15,12 +11,13 @@ import { setType } from "./types";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { routerArrays } from "./types"; import { routerArrays } from "./types";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { useEventListener } from "@vueuse/core";
import backTop from "/@/assets/svg/back_top.svg"; import backTop from "/@/assets/svg/back_top.svg";
import { useAppStoreHook } from "/@/store/modules/app"; import { useAppStoreHook } from "/@/store/modules/app";
import fullScreen from "/@/assets/svg/full_screen.svg"; import fullScreen from "/@/assets/svg/full_screen.svg";
import exitScreen from "/@/assets/svg/exit_screen.svg"; import exitScreen from "/@/assets/svg/exit_screen.svg";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useSettingStoreHook } from "/@/store/modules/settings"; import { useSettingStoreHook } from "/@/store/modules/settings";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
import navbar from "./components/navbar.vue"; import navbar from "./components/navbar.vue";
import tag from "./components/tag/index.vue"; import tag from "./components/tag/index.vue";
@@ -29,19 +26,19 @@ import setting from "./components/setting/index.vue";
import Vertical from "./components/sidebar/vertical.vue"; import Vertical from "./components/sidebar/vertical.vue";
import Horizontal from "./components/sidebar/horizontal.vue"; import Horizontal from "./components/sidebar/horizontal.vue";
const instance = getCurrentInstance().appContext.app.config.globalProperties; const isMobile = deviceDetection();
const hiddenSideBar = ref(instance.$config?.HiddenSideBar);
const pureSetting = useSettingStoreHook(); const pureSetting = useSettingStoreHook();
const instance = getCurrentInstance().appContext.app.config.globalProperties;
// 清空缓存后从serverConfig.json读取默认配置并赋值到storage中 // 清空缓存后从serverConfig.json读取默认配置并赋值到storage中
const layout = computed(() => { const layout = computed(() => {
// 路由 // 路由
if ( if (
!instance.$storage.routesInStorage || useMultiTagsStore().multiTagsCache &&
instance.$storage.routesInStorage.length === 0 (!instance.$storage.tags || instance.$storage.tags.length === 0)
) { ) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties // eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.routesInStorage = routerArrays; instance.$storage.tags = routerArrays;
} }
// 国际化 // 国际化
if (!instance.$storage.locale) { if (!instance.$storage.locale) {
@@ -63,7 +60,8 @@ const layout = computed(() => {
instance.$storage.sets = { instance.$storage.sets = {
grey: instance.$config?.Grey ?? false, grey: instance.$config?.Grey ?? false,
weak: instance.$config?.Weak ?? false, weak: instance.$config?.Weak ?? false,
hideTabs: instance.$config?.HideTabs ?? false hideTabs: instance.$config?.HideTabs ?? false,
multiTagsCache: instance.$config?.MultiTagsCache ?? false
}; };
} }
return instance.$storage?.layout.layout; return instance.$storage?.layout.layout;
@@ -96,10 +94,6 @@ const set: setType = reactive({
}) })
}); });
const handleClickOutside = (params: boolean) => {
useAppStoreHook().closeSideBar({ withoutAnimation: params });
};
function setTheme(layoutModel: string) { function setTheme(layoutModel: string) {
window.document.body.setAttribute("layout", layoutModel); window.document.body.setAttribute("layout", layoutModel);
instance.$storage.layout = { instance.$storage.layout = {
@@ -108,51 +102,52 @@ function setTheme(layoutModel: string) {
}; };
} }
// 监听容器 function toggle(device: string, bool: boolean) {
emitter.on("resize", ({ detail }) => { useAppStoreHook().toggleDevice(device);
let { width } = detail; useAppStoreHook().toggleSideBar(bool, "resize");
width <= 670 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
});
watchEffect(() => {
if (set.device === "mobile" && !set.sidebar.opened) {
handleClickOutside(false);
}
});
const $_isMobile = () => {
const rect = document.body.getBoundingClientRect();
return rect.width - 1 < 992;
};
const $_resizeHandler = () => {
if (!document.hidden) {
const isMobile = $_isMobile();
useAppStoreHook().toggleDevice(isMobile ? "mobile" : "desktop");
if (isMobile) {
handleClickOutside(true);
}
}
};
function onFullScreen() {
unref(hiddenSideBar)
? (hiddenSideBar.value = false)
: (hiddenSideBar.value = true);
} }
onMounted(() => { // 判断是否可自动关闭菜单栏
const isMobile = $_isMobile(); let isAutoCloseSidebar = true;
if (isMobile) {
useAppStoreHook().toggleDevice("mobile"); // 监听容器
handleClickOutside(true); emitter.on("resize", ({ detail }) => {
if (isMobile) return;
let { width } = detail;
width <= 670 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
/** width app-wrapper类容器宽度
* 0 < width <= 760 隐藏侧边栏
* 760 < width <= 990 折叠侧边栏
* width > 990 展开侧边栏
*/
if (width > 0 && width <= 760) {
toggle("mobile", false);
isAutoCloseSidebar = true;
} else if (width > 760 && width <= 990) {
if (isAutoCloseSidebar) {
toggle("desktop", false);
isAutoCloseSidebar = false;
}
} else if (width > 990) {
if (!set.sidebar.isClickHamburger) {
toggle("desktop", true);
isAutoCloseSidebar = true;
}
} }
}); });
onBeforeMount(() => { onMounted(() => {
useEventListener("resize", $_resizeHandler); if (isMobile) {
toggle("mobile", false);
}
}); });
function onFullScreen() {
pureSetting.hiddenSideBar
? pureSetting.changeSetting({ key: "hiddenSideBar", value: false })
: pureSetting.changeSetting({ key: "hiddenSideBar", value: true });
}
const layoutHeader = defineComponent({ const layoutHeader = defineComponent({
render() { render() {
return h( return h(
@@ -167,10 +162,10 @@ const layoutHeader = defineComponent({
}, },
{ {
default: () => [ default: () => [
!hiddenSideBar.value && layout.value.includes("vertical") !pureSetting.hiddenSideBar && layout.value.includes("vertical")
? h(navbar) ? h(navbar)
: h("div"), : h("div"),
!hiddenSideBar.value && layout.value.includes("horizontal") !pureSetting.hiddenSideBar && layout.value.includes("horizontal")
? h(Horizontal) ? h(Horizontal)
: h("div"), : h("div"),
h( h(
@@ -183,7 +178,7 @@ const layoutHeader = defineComponent({
{ onClick: onFullScreen }, { onClick: onFullScreen },
{ {
default: () => [ default: () => [
!hiddenSideBar.value ? h(fullScreen) : h(exitScreen) !pureSetting.hiddenSideBar ? h(fullScreen) : h(exitScreen)
] ]
} }
) )
@@ -205,11 +200,18 @@ const layoutHeader = defineComponent({
set.sidebar.opened && set.sidebar.opened &&
layout.includes('vertical') layout.includes('vertical')
" "
class="drawer-bg" class="app-mask"
@click="handleClickOutside(false)" @click="useAppStoreHook().toggleSideBar()"
/> />
<Vertical v-show="!hiddenSideBar && layout.includes('vertical')" /> <Vertical
<div :class="['main-container', hiddenSideBar ? 'main-hidden' : '']"> v-show="!pureSetting.hiddenSideBar && layout.includes('vertical')"
/>
<div
:class="[
'main-container',
pureSetting.hiddenSideBar ? 'main-hidden' : ''
]"
>
<div v-if="set.fixedHeader"> <div v-if="set.fixedHeader">
<layout-header /> <layout-header />
<!-- 主体内容 --> <!-- 主体内容 -->
@@ -257,7 +259,7 @@ const layoutHeader = defineComponent({
margin-left: 0 !important; margin-left: 0 !important;
} }
.drawer-bg { .app-mask {
background: #000; background: #000;
opacity: 0.3; opacity: 0.3;
width: 100%; width: 100%;

View File

@@ -0,0 +1,15 @@
<template>
<router-view>
<template #default="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</transition>
</template>
</router-view>
</template>
<script lang="ts">
export default {
name: "layoutParentView"
};
</script>

View File

@@ -1,5 +1,4 @@
// 极光绿 /* 酸橙绿 */
$subMenuActiveText: #fff; $subMenuActiveText: #fff;
$menuBg: #0b1e15; $menuBg: #0b1e15;
$menuHover: #60ac80; $menuHover: #60ac80;

View File

@@ -1,24 +1,29 @@
/** /**
*此scss变量文件作为multipleScopeVars去编译时会自动移除!default以达到变量提升 * 道奇蓝(默认)
*同时此scss变量文件作为默认主题变量文件,被其他.scss通过 @import 时,必需 !default * 此scss变量文件作为multipleScopeVars去编译时会自动移除!default以达到变量提升
* 同时此scss变量文件作为默认主题变量文件被其他.scss通过 @import 时,必需 !default
*/ */
// 暗雅(默认) /* 菜单选中后字体样式 */
// 菜单选中后字体样式
$subMenuActiveText: #fff !default; $subMenuActiveText: #fff !default;
//菜单背景
/* 菜单背景 */
$menuBg: #001529 !default; $menuBg: #001529 !default;
// 鼠标覆盖到菜单时的背景
/* 鼠标覆盖到菜单时的背景 */
$menuHover: #4091f7 !default; $menuHover: #4091f7 !default;
// 子菜单背景
/* 子菜单背景 */
$subMenuBg: #0f0303 !default; $subMenuBg: #0f0303 !default;
// 有无子集的激活菜单背景
/* 有无子集的激活菜单背景 */
$subMenuActiveBg: #4091f7 !default; $subMenuActiveBg: #4091f7 !default;
$navTextColor: #fff !default; $navTextColor: #fff !default;
$menuText: rgba(254, 254, 254, 0.65) !default; $menuText: rgba(254, 254, 254, 0.65) !default;
// logo背景颜色
/* logo背景颜色 */
$sidebarLogo: #002140 !default; $sidebarLogo: #002140 !default;
// 鼠标覆盖到菜单时的字体颜色
/* 鼠标覆盖到菜单时的字体颜色 */
$menuTitleHover: #fff !default; $menuTitleHover: #fff !default;
$menuActiveBefore: #4091f7 !default; $menuActiveBefore: #4091f7 !default;

View File

@@ -1,5 +1,4 @@
// 薄暮 /* 猩红色 */
$subMenuActiveText: #fff; $subMenuActiveText: #fff;
$menuBg: #2a0608; $menuBg: #2a0608;
$menuHover: #e13c39; $menuHover: #e13c39;

View File

@@ -1,4 +1,4 @@
// 明亮 /* 亮白色 */
$subMenuActiveText: #409eff; $subMenuActiveText: #409eff;
$menuBg: #fff; $menuBg: #fff;
$menuHover: #e0ebf6; $menuHover: #e0ebf6;

View File

@@ -1,5 +1,4 @@
// 明青 /* 绿宝石 */
$subMenuActiveText: #fff; $subMenuActiveText: #fff;
$menuBg: #032121; $menuBg: #032121;
$menuHover: #59bfc1; $menuHover: #59bfc1;

View File

@@ -1,5 +1,4 @@
// 粉红 /* 深粉色 */
$subMenuActiveText: #fff; $subMenuActiveText: #fff;
$menuBg: #28081a; $menuBg: #28081a;
$menuHover: #d84493; $menuHover: #d84493;

View File

@@ -1,5 +1,4 @@
// 酱紫 /* 深紫罗兰色 */
$subMenuActiveText: #fff; $subMenuActiveText: #fff;
$menuBg: #130824; $menuBg: #130824;
$menuHover: #693ac9; $menuHover: #693ac9;

View File

@@ -1,5 +1,4 @@
// 火山 /* 橙红色 */
$subMenuActiveText: #fff; $subMenuActiveText: #fff;
$menuBg: #2b0e05; $menuBg: #2b0e05;
$menuHover: #e85f33; $menuHover: #e85f33;

View File

@@ -1,5 +1,4 @@
// 黄色 /* 金色 */
$subMenuActiveText: #d25f00; $subMenuActiveText: #d25f00;
$menuBg: #2b2503; $menuBg: #2b2503;
$menuHover: #f6da4d; $menuHover: #f6da4d;

View File

@@ -4,6 +4,7 @@ export const routerArrays: Array<RouteConfigs> = [
parentPath: "/", parentPath: "/",
meta: { meta: {
title: "message.hshome", title: "message.hshome",
i18n: true,
icon: "el-icon-s-home", icon: "el-icon-s-home",
showLink: true showLink: true
} }
@@ -13,17 +14,21 @@ export const routerArrays: Array<RouteConfigs> = [
export type RouteConfigs = { export type RouteConfigs = {
path?: string; path?: string;
parentPath?: string; parentPath?: string;
query?: object;
meta?: { meta?: {
title?: string; title?: string;
i18n?: boolean;
icon?: string; icon?: string;
showLink?: boolean; showLink?: boolean;
savedPosition?: boolean; savedPosition?: boolean;
authority?: Array<string>;
}; };
children?: RouteConfigs[];
name?: string; name?: string;
}; };
export type relativeStorageType = { export type multiTagsType = {
routesInStorage: Array<RouteConfigs>; tags: Array<RouteConfigs>;
}; };
export type tagsViewsType = { export type tagsViewsType = {
@@ -38,6 +43,7 @@ export interface setType {
sidebar: { sidebar: {
opened: boolean; opened: boolean;
withoutAnimation: boolean; withoutAnimation: boolean;
isClickHamburger: boolean;
}; };
device: string; device: string;
fixedHeader: boolean; fixedHeader: boolean;
@@ -58,6 +64,7 @@ export type childrenType = {
meta?: { meta?: {
icon?: string; icon?: string;
title?: string; title?: string;
i18n?: boolean;
extraIcon?: { extraIcon?: {
svg?: boolean; svg?: boolean;
name?: string; name?: string;
@@ -70,3 +77,9 @@ export type themeColorsType = {
rgb: string; rgb: string;
themeColor: string; themeColor: string;
}; };
export interface scrollbarDomType extends HTMLElement {
wrap?: {
offsetWidth: number;
};
}

View File

@@ -6,6 +6,7 @@ import { createApp, Directive } from "vue";
import { usI18n } from "../src/plugins/i18n"; import { usI18n } from "../src/plugins/i18n";
import { MotionPlugin } from "@vueuse/motion"; import { MotionPlugin } from "@vueuse/motion";
import { useTable } from "../src/plugins/vxe-table"; import { useTable } from "../src/plugins/vxe-table";
import { useFontawesome } from "../src/plugins/fontawesome";
import { useElementPlus } from "../src/plugins/element-plus"; import { useElementPlus } from "../src/plugins/element-plus";
import { injectResponsiveStorage } from "/@/utils/storage/responsive"; import { injectResponsiveStorage } from "/@/utils/storage/responsive";
@@ -33,7 +34,8 @@ getServerConfig(app).then(async config => {
.use(MotionPlugin) .use(MotionPlugin)
.use(useElementPlus) .use(useElementPlus)
.use(useTable) .use(useTable)
.use(usI18n); .use(usI18n)
.use(useFontawesome);
await router.isReady(); await router.isReady();
app.mount("#app"); app.mount("#app");
}); });

View File

@@ -23,7 +23,6 @@ import {
ElInput, ElInput,
ElForm, ElForm,
ElFormItem, ElFormItem,
ElLoading,
ElPopover, ElPopover,
ElPopper, ElPopper,
ElTooltip, ElTooltip,
@@ -36,26 +35,22 @@ import {
ElDescriptions, ElDescriptions,
ElDescriptionsItem, ElDescriptionsItem,
ElBacktop, ElBacktop,
ElSwitch ElSwitch,
ElBadge,
ElTabs,
ElTabPane,
ElAvatar,
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2,
// 指令
ElLoading,
ElInfiniteScroll
} from "element-plus"; } from "element-plus";
// https://element-plus.org/zh-CN/component/icon.html // Directives
import { const plugins = [ElLoading, ElInfiniteScroll];
Check,
Menu,
HomeFilled,
SetUp,
Edit,
Setting,
Lollipop,
Link,
Position,
Histogram,
RefreshRight,
ArrowDown,
Close,
CloseBold
} from "@element-plus/icons";
const components = [ const components = [
ElTag, ElTag,
@@ -94,8 +89,18 @@ const components = [
ElDescriptionsItem, ElDescriptionsItem,
ElBacktop, ElBacktop,
ElSwitch, ElSwitch,
ElBadge,
ElTabs,
ElTabPane,
ElAvatar,
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2
];
// icon // https://element-plus.org/zh-CN/component/icon.html
import {
Check, Check,
Menu, Menu,
HomeFilled, HomeFilled,
@@ -109,16 +114,40 @@ const components = [
RefreshRight, RefreshRight,
ArrowDown, ArrowDown,
Close, Close,
CloseBold CloseBold,
Bell
} from "@element-plus/icons-vue";
// Icon
export const iconComponents = [
Check,
Menu,
HomeFilled,
SetUp,
Edit,
Setting,
Lollipop,
Link,
Position,
Histogram,
RefreshRight,
ArrowDown,
Close,
CloseBold,
Bell
]; ];
const plugins = [ElLoading];
export function useElementPlus(app: App) { export function useElementPlus(app: App) {
// 注册组件
components.forEach((component: Component) => { components.forEach((component: Component) => {
app.component(component.name, component); app.component(component.name, component);
}); });
// 注册指令
plugins.forEach(plugin => { plugins.forEach(plugin => {
app.use(plugin); app.use(plugin);
}); });
// 注册图标
iconComponents.forEach((component: Component) => {
app.component(component.name, component);
});
} }

View File

@@ -0,0 +1,21 @@
/** 兼容fontawesome4和5版本
* 4版本: www.fontawesome.com.cn/faicons/
* 5版本https://fontawesome.com/v5.15/icons?d=gallery&p=2&m=free
* https://github.com/FortAwesome/vue-fontawesome
*/
import { App } from "vue";
import "font-awesome/css/font-awesome.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faUserSecret,
faCoffee,
faSpinner
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
// github.com/Remix-Design/RemixIcon/blob/master/README_CN.md#%E5%AE%89%E8%A3%85%E5%BC%95%E5%85%A5
import "remixicon/fonts/remixicon.css";
export function useFontawesome(app: App) {
library.add(faUserSecret, faCoffee, faSpinner);
app.component("font-awesome-icon", FontAwesomeIcon);
}

View File

@@ -42,6 +42,8 @@ export const menusConfig = {
permission: "权限管理", permission: "权限管理",
permissionPage: "页面权限", permissionPage: "页面权限",
permissionButton: "按钮权限", permissionButton: "按钮权限",
hstabs: "标签页操作",
hsMenuTree: "菜单树结构",
externalLink: "外链" externalLink: "外链"
} }
}, },
@@ -78,6 +80,8 @@ export const menusConfig = {
permission: "Permission Manage", permission: "Permission Manage",
permissionPage: "Page Permission", permissionPage: "Page Permission",
permissionButton: "Button Permission", permissionButton: "Button Permission",
hstabs: "Tabs Operate",
hsMenuTree: "Menu Tree",
externalLink: "External Link" externalLink: "External Link"
} }
} }

View File

@@ -13,3 +13,27 @@ export const i18n = createI18n({
export function usI18n(app: App) { export function usI18n(app: App) {
app.use(i18n); app.use(i18n);
} }
/**
* 国际化转换工具函数
* @param message message
* @param isI18n 如果true,获取对应的消息,否则返回本身
* @returns message
*/
export function transformI18n(message: string | object = "", isI18n = false) {
if (!message) {
return "";
}
// 处理存储动态路由的title,格式 {zh:"",en:""}
if (typeof message === "object") {
return message[i18n.global?.locale];
}
if (isI18n) {
//@ts-ignore
return i18n.global.tc.call(i18n.global, message);
} else {
return message;
}
}

View File

@@ -1,7 +1,6 @@
import "xe-utils"; import "xe-utils";
import { App } from "vue"; import { App } from "vue";
import { i18n } from "../i18n/index"; import { i18n } from "../i18n/index";
import "font-awesome/css/font-awesome.css";
import { import {
// 核心 // 核心
VXETable, VXETable,

View File

@@ -1,140 +1,27 @@
import { import { toRouteType } from "./types";
Router,
createRouter,
RouteComponent,
createWebHashHistory,
RouteRecordNormalized
} from "vue-router";
import { RouteConfigs } from "/@/layout/types";
import { split, uniqBy } from "lodash-es";
import { i18n } from "/@/plugins/i18n";
import { openLink } from "/@/utils/link"; import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress"; import NProgress from "/@/utils/progress";
import { useTimeoutFn } from "@vueuse/core"; import { constantRoutes } from "./modules";
import { storageSession, storageLocal } from "/@/utils/storage"; import { split, findIndex } from "lodash-es";
import { usePermissionStoreHook } from "/@/store/modules/permission"; import { transformI18n } from "/@/plugins/i18n";
// 静态路由
import homeRouter from "./modules/home";
import Layout from "/@/layout/index.vue";
import errorRouter from "./modules/error";
import editorRouter from "./modules/editor";
import nestedRouter from "./modules/nested";
import externalLink from "./modules/externalLink";
import remainingRouter from "./modules/remaining"; import remainingRouter from "./modules/remaining";
import flowChartRouter from "./modules/flowchart"; import { storageSession } from "/@/utils/storage";
import componentsRouter from "./modules/components"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
// 动态路由 import { usePermissionStoreHook } from "/@/store/modules/permission";
import { getAsyncRoutes } from "/@/api/routes"; import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
import {
// https://cn.vitejs.dev/guide/features.html#glob-import initRouter,
const modulesRoutes = import.meta.glob("/src/views/*/*/*.vue"); getHistoryMode,
getParentPaths,
const constantRoutes: Array<RouteComponent> = [ findRouteByPath,
homeRouter, handleAliveRoute
flowChartRouter, } from "./utils";
editorRouter,
componentsRouter,
nestedRouter,
externalLink,
errorRouter
];
// 按照路由中meta下的rank等级升序来排序路由
export const ascending = arr => {
return arr.sort((a: any, b: any) => {
return a?.meta?.rank - b?.meta?.rank;
});
};
// 将所有静态路由导出
export const constantRoutesArr: Array<RouteComponent> = ascending(
constantRoutes
).concat(...remainingRouter);
// 过滤meta中showLink为false的路由
export const filterTree = data => {
const newTree = data.filter(v => v.meta.showLink);
newTree.forEach(v => v.children && (v.children = filterTree(v.children)));
return newTree;
};
// 从路由中提取keepAlive为true的name组成数组此处本项目中并没有用到只是暴露个方法
export const getAliveRoute = () => {
const alivePageList = [];
const recursiveSearch = treeLists => {
if (!treeLists || !treeLists.length) {
return;
}
for (let i = 0; i < treeLists.length; i++) {
if (treeLists[i]?.meta?.keepAlive) alivePageList.push(treeLists[i].name);
recursiveSearch(treeLists[i].children);
}
};
recursiveSearch(router.options.routes);
return alivePageList;
};
// 批量删除缓存路由
export const delAliveRoutes = (delAliveRouteList: Array<RouteConfigs>) => {
delAliveRouteList.forEach(route => {
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: route?.name
});
});
};
// 处理缓存路由(添加、删除、刷新)
export const handleAliveRoute = (
matched: RouteRecordNormalized[],
mode?: string
) => {
switch (mode) {
case "add":
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
break;
case "delete":
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
break;
default:
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
useTimeoutFn(() => {
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
}, 100);
}
};
// 过滤后端传来的动态路由 重新生成规范路由
export const addAsyncRoutes = (arrRoutes: Array<RouteComponent>) => {
if (!arrRoutes || !arrRoutes.length) return;
arrRoutes.forEach((v: any) => {
if (v.redirect) {
v.component = Layout;
} else {
v.component = modulesRoutes[`/src/views${v.path}/index.vue`];
}
if (v.children) {
addAsyncRoutes(v.children);
}
});
return arrRoutes;
};
// 创建路由实例 // 创建路由实例
export const router: Router = createRouter({ export const router: Router = createRouter({
history: createWebHashHistory(), history: getHistoryMode(),
routes: filterTree(ascending(constantRoutes)).concat(...remainingRouter), routes: constantRoutes.concat(...remainingRouter),
strict: true,
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
return new Promise(resolve => { return new Promise(resolve => {
if (savedPosition) { if (savedPosition) {
@@ -150,53 +37,10 @@ export const router: Router = createRouter({
} }
}); });
// 初始化路由
export const initRouter = name => {
return new Promise(resolve => {
getAsyncRoutes({ name }).then(({ info }) => {
if (info.length === 0) {
usePermissionStoreHook().changeSetting(info);
} else {
addAsyncRoutes(info).map((v: any) => {
// 防止重复添加路由
if (
router.options.routes.findIndex(value => value.path === v.path) !==
-1
) {
return;
} else {
// 切记将路由push到routes后还需要使用addRoute这样路由才能正常跳转
router.options.routes.push(v);
// 最终路由进行升序
ascending(router.options.routes);
router.addRoute(v.name, v);
usePermissionStoreHook().changeSetting(info);
}
resolve(router);
});
}
router.addRoute({
path: "/:pathMatch(.*)",
redirect: "/error/404"
});
});
});
};
// 重置路由
export function resetRouter() {
router.getRoutes().forEach(route => {
const { name } = route;
if (name) {
router.hasRoute(name) && router.removeRoute(name);
}
});
}
// 路由白名单 // 路由白名单
const whiteList = ["/login"]; const whiteList = ["/login"];
router.beforeEach((to, _from, next) => { router.beforeEach((to: toRouteType, _from, next) => {
if (to.meta?.keepAlive) { if (to.meta?.keepAlive) {
const newMatched = to.matched; const newMatched = to.matched;
handleAliveRoute(newMatched, "add"); handleAliveRoute(newMatched, "add");
@@ -208,10 +52,15 @@ router.beforeEach((to, _from, next) => {
const name = storageSession.getItem("info"); const name = storageSession.getItem("info");
NProgress.start(); NProgress.start();
const externalLink = to?.redirectedFrom?.fullPath; const externalLink = to?.redirectedFrom?.fullPath;
// @ts-ignore if (!externalLink)
const { t } = i18n.global; to.matched.some(item => {
// @ts-ignore item.meta.title
if (!externalLink) to.meta.title ? (document.title = t(to.meta.title)) : ""; ? (document.title = transformI18n(
item.meta.title as string,
item.meta?.i18n as boolean
))
: "";
});
if (name) { if (name) {
if (_from?.name) { if (_from?.name) {
// 如果路由包含http 则是超链接 反之是普通路由 // 如果路由包含http 则是超链接 反之是普通路由
@@ -223,26 +72,74 @@ router.beforeEach((to, _from, next) => {
} }
} else { } else {
// 刷新 // 刷新
if (usePermissionStoreHook().wholeRoutes.length === 0) if (usePermissionStoreHook().wholeMenus.length === 0)
initRouter(name.username).then((router: Router) => { initRouter(name.username).then((router: Router) => {
router.push(to.path); if (!useMultiTagsStoreHook().getMultiTagsCache) {
// 刷新页面更新标签栏与页面路由匹配 const handTag = (
const localRoutes = storageLocal.getItem( path: string,
"responsive-routesInStorage" parentPath: string,
); name: RouteRecordName,
const optionsRoutes = router.options?.routes; meta: RouteMeta
const newLocalRoutes = []; ): void => {
optionsRoutes.forEach(ors => { useMultiTagsStoreHook().handleTags("push", {
localRoutes.forEach(lrs => { path,
if (ors.path === lrs.parentPath) { parentPath,
newLocalRoutes.push(lrs); name,
meta
});
};
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对静态路由)
if (to.meta?.realPath) {
const routes = router.options.routes;
const { refreshRedirect } = to.meta;
const { name, meta } = findRouteByPath(refreshRedirect, routes);
handTag(
refreshRedirect,
getParentPaths(refreshRedirect, routes)[1],
name,
meta
);
return router.push(refreshRedirect);
} else {
const { path } = to;
const index = findIndex(remainingRouter, v => {
return v.path == path;
});
const routes =
index === -1
? router.options.routes[0].children
: router.options.routes;
const route = findRouteByPath(path, routes);
const routePartent = getParentPaths(path, routes);
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对动态路由)
if (
path !== routes[0].path &&
route?.meta?.rank !== 0 &&
routePartent.length === 0
) {
const { name, meta } = findRouteByPath(
route?.meta?.refreshRedirect,
routes
);
handTag(
route.meta?.refreshRedirect,
getParentPaths(route.meta?.refreshRedirect, routes)[0],
name,
meta
);
return router.push(route.meta?.refreshRedirect);
} else {
handTag(
route.path,
routePartent[routePartent.length - 1],
route.name,
route.meta
);
return router.push(path);
} }
}); }
}); }
storageLocal.setItem( router.push(to.fullPath);
"responsive-routesInStorage",
uniqBy(newLocalRoutes, "path")
);
}); });
next(); next();
} }

View File

@@ -8,6 +8,7 @@ const componentsRouter = {
meta: { meta: {
icon: "Menu", icon: "Menu",
title: "message.hscomponents", title: "message.hscomponents",
i18n: true,
showLink: true, showLink: true,
rank: 4 rank: 4
}, },
@@ -18,7 +19,8 @@ const componentsRouter = {
component: () => import("/@/views/components/video/index.vue"), component: () => import("/@/views/components/video/index.vue"),
meta: { meta: {
title: "message.hsvideo", title: "message.hsvideo",
showLink: true showLink: true,
i18n: true
} }
}, },
{ {
@@ -29,6 +31,7 @@ const componentsRouter = {
title: "message.hsmap", title: "message.hsmap",
showLink: true, showLink: true,
keepAlive: true, keepAlive: true,
i18n: true,
transition: { transition: {
name: "fade" name: "fade"
} }
@@ -41,6 +44,7 @@ const componentsRouter = {
meta: { meta: {
title: "message.hsdraggable", title: "message.hsdraggable",
showLink: true, showLink: true,
i18n: true,
transition: { transition: {
enterTransition: "animate__zoomIn", enterTransition: "animate__zoomIn",
leaveTransition: "animate__zoomOut" leaveTransition: "animate__zoomOut"
@@ -55,6 +59,7 @@ const componentsRouter = {
meta: { meta: {
title: "message.hssplitPane", title: "message.hssplitPane",
showLink: true, showLink: true,
i18n: true,
extraIcon: { extraIcon: {
svg: true, svg: true,
name: "team-iconxinpinrenqiwang" name: "team-iconxinpinrenqiwang"
@@ -67,6 +72,7 @@ const componentsRouter = {
component: () => import("/@/views/components/button/index.vue"), component: () => import("/@/views/components/button/index.vue"),
meta: { meta: {
title: "message.hsbutton", title: "message.hsbutton",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -76,6 +82,7 @@ const componentsRouter = {
component: () => import("/@/views/components/cropping/index.vue"), component: () => import("/@/views/components/cropping/index.vue"),
meta: { meta: {
title: "message.hscropping", title: "message.hscropping",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -85,6 +92,7 @@ const componentsRouter = {
component: () => import("/@/views/components/count-to/index.vue"), component: () => import("/@/views/components/count-to/index.vue"),
meta: { meta: {
title: "message.hscountTo", title: "message.hscountTo",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -94,6 +102,7 @@ const componentsRouter = {
component: () => import("/@/views/components/selector/index.vue"), component: () => import("/@/views/components/selector/index.vue"),
meta: { meta: {
title: "message.hsselector", title: "message.hsselector",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -103,6 +112,7 @@ const componentsRouter = {
component: () => import("/@/views/components/seamless-scroll/index.vue"), component: () => import("/@/views/components/seamless-scroll/index.vue"),
meta: { meta: {
title: "message.hsseamless", title: "message.hsseamless",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -112,6 +122,7 @@ const componentsRouter = {
component: () => import("/@/views/components/contextmenu/index.vue"), component: () => import("/@/views/components/contextmenu/index.vue"),
meta: { meta: {
title: "message.hscontextmenu", title: "message.hscontextmenu",
i18n: true,
showLink: true showLink: true
} }
} }

View File

@@ -8,6 +8,7 @@ const editorRouter = {
meta: { meta: {
icon: "Edit", icon: "Edit",
title: "message.hseditor", title: "message.hseditor",
i18n: true,
showLink: true, showLink: true,
rank: 2 rank: 2
}, },
@@ -19,6 +20,7 @@ const editorRouter = {
meta: { meta: {
title: "message.hseditor", title: "message.hseditor",
showLink: true, showLink: true,
i18n: true,
keepAlive: true, keepAlive: true,
extraIcon: { extraIcon: {
svg: true, svg: true,

View File

@@ -9,6 +9,7 @@ const errorRouter = {
icon: "Position", icon: "Position",
title: "message.hserror", title: "message.hserror",
showLink: true, showLink: true,
i18n: true,
rank: 7 rank: 7
}, },
children: [ children: [
@@ -18,6 +19,7 @@ const errorRouter = {
component: () => import("/@/views/error/401.vue"), component: () => import("/@/views/error/401.vue"),
meta: { meta: {
title: "message.hsfourZeroOne", title: "message.hsfourZeroOne",
i18n: true,
showLink: true showLink: true
} }
}, },
@@ -27,6 +29,7 @@ const errorRouter = {
component: () => import("/@/views/error/404.vue"), component: () => import("/@/views/error/404.vue"),
meta: { meta: {
title: "message.hsfourZeroFour", title: "message.hsfourZeroFour",
i18n: true,
showLink: true showLink: true
} }
} }

View File

@@ -8,6 +8,7 @@ const externalLink = {
icon: "Link", icon: "Link",
title: "message.externalLink", title: "message.externalLink",
showLink: true, showLink: true,
i18n: true,
rank: 190 rank: 190
}, },
children: [ children: [
@@ -16,6 +17,7 @@ const externalLink = {
meta: { meta: {
title: "message.externalLink", title: "message.externalLink",
showLink: true, showLink: true,
i18n: true,
rank: 191 rank: 191
} }
} }

View File

@@ -9,6 +9,7 @@ const flowChartRouter = {
icon: "SetUp", icon: "SetUp",
title: "message.hsflowChart", title: "message.hsflowChart",
showLink: true, showLink: true,
i18n: true,
rank: 1 rank: 1
}, },
children: [ children: [
@@ -18,6 +19,7 @@ const flowChartRouter = {
component: () => import("/@/views/flow-chart/index.vue"), component: () => import("/@/views/flow-chart/index.vue"),
meta: { meta: {
title: "message.hsflowChart", title: "message.hsflowChart",
i18n: true,
showLink: true showLink: true
} }
} }

View File

@@ -7,7 +7,9 @@ const homeRouter = {
redirect: "/welcome", redirect: "/welcome",
meta: { meta: {
icon: "HomeFilled", icon: "HomeFilled",
title: "message.hshome",
showLink: true, showLink: true,
i18n: true,
rank: 0 rank: 0
}, },
children: [ children: [
@@ -17,6 +19,7 @@ const homeRouter = {
component: () => import("/@/views/welcome.vue"), component: () => import("/@/views/welcome.vue"),
meta: { meta: {
title: "message.hshome", title: "message.hshome",
i18n: true,
showLink: true showLink: true
} }
} }

View File

@@ -0,0 +1,39 @@
// 静态路由
import homeRouter from "./home";
import errorRouter from "./error";
import editorRouter from "./editor";
import nestedRouter from "./nested";
import menuTreeRouter from "./menuTree";
import externalLink from "./externalLink";
import flowChartRouter from "./flowchart";
import remainingRouter from "./remaining";
import componentsRouter from "./components";
import { RouteRecordRaw, RouteComponent } from "vue-router";
import {
ascending,
formatTwoStageRoutes,
formatFlatteningRoutes
} from "../utils";
// 原始静态路由(未做任何处理)
const routes = [
homeRouter,
errorRouter,
nestedRouter,
externalLink,
editorRouter,
menuTreeRouter,
flowChartRouter,
componentsRouter
];
// 导出处理后的静态路由(三级及以上的路由全部拍成二级)
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(ascending(routes))
);
// 用于渲染菜单,保持原始层级
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
...remainingRouter
);

View File

@@ -0,0 +1,29 @@
import Layout from "/@/layout/index.vue";
const menuTreeRouter = {
path: "/menuTree",
name: "reMenuTree",
component: Layout,
redirect: "/menuTree/index",
meta: {
icon: "RI-node-tree",
title: "message.hsMenuTree",
i18n: true,
showLink: true,
rank: 9
},
children: [
{
path: "/menuTree/index",
name: "reMenuTree",
component: () => import("/@/views/menu-tree/index.vue"),
meta: {
title: "message.hsMenuTree",
showLink: true,
i18n: true
}
}
]
};
export default menuTreeRouter;

View File

@@ -9,16 +9,18 @@ const nestedRouter = {
title: "message.hsmenus", title: "message.hsmenus",
icon: "Histogram", icon: "Histogram",
showLink: true, showLink: true,
i18n: true,
rank: 5 rank: 5
}, },
children: [ children: [
{ {
path: "/nested/menu1", path: "/nested/menu1",
component: () => import("/@/views/nested/menu1/index.vue"), component: () => import("/@/layout/routerView/parent.vue"),
name: "Menu1", name: "Menu1",
meta: { meta: {
title: "message.hsmenu1", title: "message.hsmenu1",
showLink: true, showLink: true,
i18n: true,
keepAlive: true keepAlive: true
}, },
redirect: "/nested/menu1/menu1-1", redirect: "/nested/menu1/menu1-1",
@@ -30,17 +32,19 @@ const nestedRouter = {
meta: { meta: {
title: "message.hsmenu1-1", title: "message.hsmenu1-1",
showLink: true, showLink: true,
i18n: true,
keepAlive: true keepAlive: true
} }
}, },
{ {
path: "/nested/menu1/menu1-2", path: "/nested/menu1/menu1-2",
component: () => import("/@/views/nested/menu1/menu1-2/index.vue"), component: () => import("/@/layout/routerView/parent.vue"),
name: "Menu1-2", name: "Menu1-2",
redirect: "/nested/menu1/menu1-2/menu1-2-1", redirect: "/nested/menu1/menu1-2/menu1-2-1",
meta: { meta: {
title: "message.hsmenu1-2", title: "message.hsmenu1-2",
showLink: true, showLink: true,
i18n: true,
keepAlive: true keepAlive: true
}, },
children: [ children: [
@@ -52,6 +56,7 @@ const nestedRouter = {
meta: { meta: {
title: "message.hsmenu1-2-1", title: "message.hsmenu1-2-1",
showLink: true, showLink: true,
i18n: true,
keepAlive: true keepAlive: true
} }
}, },
@@ -64,6 +69,7 @@ const nestedRouter = {
title: "message.hsmenu1-2-2", title: "message.hsmenu1-2-2",
showLink: true, showLink: true,
keepAlive: true, keepAlive: true,
i18n: true,
extraIcon: { extraIcon: {
svg: true, svg: true,
name: "team-iconxinpinrenqiwang" name: "team-iconxinpinrenqiwang"
@@ -79,6 +85,7 @@ const nestedRouter = {
meta: { meta: {
title: "message.hsmenu1-3", title: "message.hsmenu1-3",
showLink: true, showLink: true,
i18n: true,
keepAlive: true keepAlive: true
} }
} }
@@ -91,6 +98,7 @@ const nestedRouter = {
meta: { meta: {
title: "message.hsmenu2", title: "message.hsmenu2",
showLink: true, showLink: true,
i18n: true,
keepAlive: true keepAlive: true
} }
} }

View File

@@ -8,6 +8,7 @@ const remainingRouter = [
meta: { meta: {
title: "message.hslogin", title: "message.hslogin",
showLink: false, showLink: false,
i18n: true,
rank: 101 rank: 101
} }
}, },
@@ -18,6 +19,7 @@ const remainingRouter = [
meta: { meta: {
icon: "HomeFilled", icon: "HomeFilled",
title: "message.hshome", title: "message.hshome",
i18n: true,
showLink: false, showLink: false,
rank: 104 rank: 104
}, },

9
src/router/types.ts Normal file
View File

@@ -0,0 +1,9 @@
import { RouteLocationNormalized } from "vue-router";
export interface toRouteType extends RouteLocationNormalized {
meta: {
keepAlive: boolean;
refreshRedirect: string;
realPath: string;
};
}

289
src/router/utils.ts Normal file
View File

@@ -0,0 +1,289 @@
import {
RouterHistory,
RouteRecordRaw,
RouteComponent,
createWebHistory,
createWebHashHistory,
RouteRecordNormalized
} from "vue-router";
import { router } from "./index";
import { loadEnv } from "../../build";
import Layout from "/@/layout/index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { usePermissionStoreHook } from "/@/store/modules/permission";
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/*/*/*.vue");
// 动态路由
import { getAsyncRoutes } from "/@/api/routes";
// 按照路由中meta下的rank等级升序来排序路由
const ascending = (arr: any[]) => {
return arr.sort(
(a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta?.rank - b?.meta?.rank;
}
);
};
// 过滤meta中showLink为false的路由
const filterTree = (data: RouteComponent[]) => {
const newTree = data.filter(
(v: { meta: { showLink: boolean } }) => v.meta.showLink
);
newTree.forEach(
(v: { children }) => v.children && (v.children = filterTree(v.children))
);
return newTree;
};
// 批量删除缓存路由(keepalive)
const delAliveRoutes = (delAliveRouteList: Array<RouteConfigs>) => {
delAliveRouteList.forEach(route => {
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: route?.name
});
});
};
// 通过path获取父级路径
const getParentPaths = (path: string, routes: RouteRecordRaw[]) => {
// 深度遍历查找
function dfs(routes: RouteRecordRaw[], path: string, parents: string[]) {
for (let i = 0; i < routes.length; i++) {
const item = routes[i];
// 找到path则返回父级path
if (item.path === path) return parents;
// children不存在或为空则不递归
if (!item.children || !item.children.length) continue;
// 往下查找时将当前path入栈
parents.push(item.path);
if (dfs(item.children, path, parents).length) return parents;
// 深度遍历查找未找到时当前path 出栈
parents.pop();
}
// 未找到时返回空数组
return [];
}
return dfs(routes, path, []);
};
// 查找对应path的路由信息
const findRouteByPath = (path: string, routes: RouteRecordRaw[]) => {
let res = routes.find((item: { path: string }) => item.path == path);
if (res) {
return res;
} else {
for (let i = 0; i < routes.length; i++) {
if (
routes[i].children instanceof Array &&
routes[i].children.length > 0
) {
res = findRouteByPath(path, routes[i].children);
if (res) {
return res;
}
}
}
return null;
}
};
// 重置路由
const resetRouter = (): void => {
router.getRoutes().forEach(route => {
const { name } = route;
if (name) {
router.hasRoute(name) && router.removeRoute(name);
}
});
};
// 初始化路由
const initRouter = (name: string) => {
return new Promise(resolve => {
getAsyncRoutes({ name }).then(({ info }) => {
if (info.length === 0) {
usePermissionStoreHook().changeSetting(info);
} else {
formatFlatteningRoutes(addAsyncRoutes(info)).map(
(v: RouteRecordRaw) => {
// 防止重复添加路由
if (
router.options.routes[0].children.findIndex(
value => value.path === v.path
) !== -1
) {
return;
} else {
// 切记将路由push到routes后还需要使用addRoute这样路由才能正常跳转
router.options.routes[0].children.push(v);
// 最终路由进行升序
ascending(router.options.routes[0].children);
if (!router.hasRoute(v?.name)) router.addRoute(v);
}
resolve(router);
}
);
usePermissionStoreHook().changeSetting(info);
}
router.addRoute({
path: "/:pathMatch(.*)",
redirect: "/error/404"
});
});
});
};
/**
* 将多级嵌套路由处理成一维数组
* @param routesList 传入路由
* @returns 返回处理后的一维路由
*/
const formatFlatteningRoutes = (routesList: RouteRecordRaw[]) => {
if (routesList.length <= 0) return routesList;
for (let i = 0; i < routesList.length; i++) {
if (routesList[i].children) {
routesList = routesList
.slice(0, i + 1)
.concat(routesList[i].children, routesList.slice(i + 1));
}
}
return routesList;
};
/**
* 一维数组处理成多级嵌套数组三级及以上的路由全部拍成二级keep-alive 只支持到二级缓存)
* https://github.com/xiaoxian521/vue-pure-admin/issues/67
* @param routesList 处理后的一维路由菜单数组
* @returns 返回将一维数组重新处理成规定路由的格式
*/
const formatTwoStageRoutes = (routesList: RouteRecordRaw[]) => {
if (routesList.length <= 0) return routesList;
const newRoutesList: RouteRecordRaw[] = [];
routesList.forEach((v: RouteRecordRaw) => {
if (v.path === "/") {
newRoutesList.push({
component: v.component,
name: v.name,
path: v.path,
redirect: v.redirect,
meta: v.meta,
children: []
});
} else {
newRoutesList[0].children.push({ ...v });
}
});
return newRoutesList;
};
// 处理缓存路由(添加、删除、刷新)
const handleAliveRoute = (matched: RouteRecordNormalized[], mode?: string) => {
switch (mode) {
case "add":
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
break;
case "delete":
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
break;
default:
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
});
useTimeoutFn(() => {
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
});
}, 100);
}
};
// 过滤后端传来的动态路由 重新生成规范路由
const addAsyncRoutes = (arrRoutes: Array<RouteRecordRaw>) => {
if (!arrRoutes || !arrRoutes.length) return;
arrRoutes.forEach((v: RouteRecordRaw) => {
if (v.redirect) {
v.component = Layout;
} else {
if (v.meta.realPath) {
v.component = modulesRoutes[`/src/views${v.meta.realPath}/index.vue`];
} else {
v.component = modulesRoutes[`/src/views${v.path}/index.vue`];
}
}
if (v.children) {
addAsyncRoutes(v.children);
}
});
return arrRoutes;
};
// 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html
const getHistoryMode = (): RouterHistory => {
const routerHistory = loadEnv().VITE_ROUTER_HISTORY;
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
const historyMode = routerHistory.split(",");
const leftMode = historyMode[0];
const rightMode = historyMode[1];
// no param
if (historyMode.length === 1) {
if (leftMode === "hash") {
return createWebHashHistory("");
} else if (leftMode === "h5") {
return createWebHistory("");
}
} //has param
else if (historyMode.length === 2) {
if (leftMode === "hash") {
return createWebHashHistory(rightMode);
} else if (leftMode === "h5") {
return createWebHistory(rightMode);
}
}
};
// 是否有权限
const hasPermissions = (value: Array<string>): boolean => {
if (value && value instanceof Array && value.length > 0) {
const roles = usePermissionStoreHook().buttonAuth;
const permissionRoles = value;
const hasPermission = roles.some(role => {
return permissionRoles.includes(role);
});
if (!hasPermission) {
return false;
}
return true;
} else {
return false;
}
};
export {
ascending,
filterTree,
initRouter,
resetRouter,
hasPermissions,
getHistoryMode,
addAsyncRoutes,
delAliveRoutes,
getParentPaths,
findRouteByPath,
handleAliveRoute,
formatTwoStageRoutes,
formatFlatteningRoutes
};

View File

@@ -1,26 +1,19 @@
import { storageLocal } from "/@/utils/storage"; import { storageLocal } from "/@/utils/storage";
import { deviceDetection } from "/@/utils/deviceDetection"; import { deviceDetection } from "/@/utils/deviceDetection";
import { defineStore } from "pinia";
import { store } from "/@/store"; import { store } from "/@/store";
import { appType } from "./types";
import { defineStore } from "pinia";
import { getConfig } from "/@/config"; import { getConfig } from "/@/config";
interface AppState {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
};
layout: string;
device: string;
}
export const useAppStore = defineStore({ export const useAppStore = defineStore({
id: "pure-app", id: "pure-app",
state: (): AppState => ({ state: (): appType => ({
sidebar: { sidebar: {
opened: storageLocal.getItem("sidebarStatus") opened: storageLocal.getItem("sidebarStatus")
? !!+storageLocal.getItem("sidebarStatus") ? !!+storageLocal.getItem("sidebarStatus")
: true, : true,
withoutAnimation: false withoutAnimation: false,
isClickHamburger: false
}, },
// 这里的layout用于监听容器拖拉后恢复对应的导航模式 // 这里的layout用于监听容器拖拉后恢复对应的导航模式
layout: layout:
@@ -36,28 +29,27 @@ export const useAppStore = defineStore({
} }
}, },
actions: { actions: {
TOGGLE_SIDEBAR() { TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
this.sidebar.opened = !this.sidebar.opened; if (opened && resize) {
this.sidebar.withoutAnimation = false; this.sidebar.withoutAnimation = true;
if (this.sidebar.opened) { this.sidebar.opened = true;
storageLocal.setItem("sidebarStatus", 1); storageLocal.setItem("sidebarStatus", true);
} else { } else if (!opened && resize) {
storageLocal.setItem("sidebarStatus", 0); this.sidebar.withoutAnimation = true;
this.sidebar.opened = false;
storageLocal.setItem("sidebarStatus", false);
} else if (!opened && !resize) {
this.sidebar.withoutAnimation = false;
this.sidebar.opened = !this.sidebar.opened;
this.sidebar.isClickHamburger = !this.sidebar.opened;
storageLocal.setItem("sidebarStatus", this.sidebar.opened);
} }
}, },
CLOSE_SIDEBAR(withoutAnimation: boolean) {
storageLocal.setItem("sidebarStatus", 0);
this.sidebar.opened = false;
this.sidebar.withoutAnimation = withoutAnimation;
},
TOGGLE_DEVICE(device: string) { TOGGLE_DEVICE(device: string) {
this.device = device; this.device = device;
}, },
async toggleSideBar() { async toggleSideBar(opened?: boolean, resize?: string) {
await this.TOGGLE_SIDEBAR(); await this.TOGGLE_SIDEBAR(opened, resize);
},
closeSideBar(withoutAnimation) {
this.CLOSE_SIDEBAR(withoutAnimation);
}, },
toggleDevice(device) { toggleDevice(device) {
this.TOGGLE_DEVICE(device); this.TOGGLE_DEVICE(device);

View File

@@ -0,0 +1,104 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { isEqual } from "lodash-es";
import { storageLocal } from "/@/utils/storage";
import { multiType, positionType } from "./types";
export const useMultiTagsStore = defineStore({
id: "pure-multiTags",
state: () => ({
// 存储标签页信息(路由信息)
multiTags: storageLocal.getItem("responsive-sets").multiTagsCache
? storageLocal.getItem("responsive-tags")
: [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
i18n: true,
showLink: true
}
}
],
multiTagsCache: storageLocal.getItem("responsive-sets").multiTagsCache
}),
getters: {
getMultiTagsCache() {
return this.multiTagsCache;
}
},
actions: {
multiTagsCacheChange(multiTagsCache: boolean) {
this.multiTagsCache = multiTagsCache;
if (multiTagsCache) {
storageLocal.setItem("responsive-tags", this.multiTags);
} else {
storageLocal.removeItem("responsive-tags");
}
},
tagsCache(multiTags) {
this.getMultiTagsCache &&
storageLocal.setItem("responsive-tags", multiTags);
},
handleTags<T>(
mode: string,
value?: T | multiType,
position?: positionType
): T {
switch (mode) {
case "equal":
this.multiTags = value;
this.tagsCache(this.multiTags);
break;
case "push":
{
const tagVal = value as multiType;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {
return tag.path === tagVal?.path;
});
// 判断tag中的query键值是否相等
const tagQueryHasExits = this.multiTags.some(tag => {
return isEqual(tag.query, tagVal.query);
});
if (tagHasExits && tagQueryHasExits) return;
const meta = tagVal?.meta;
const dynamicLevel = meta?.dynamicLevel ?? -1;
if (dynamicLevel > 0) {
// dynamicLevel动态路由可打开的数量
const realPath = meta?.realPath ?? "";
// 获取到已经打开的动态路由数, 判断是否大于dynamicLevel
if (
this.multiTags.filter(e => e.meta?.realPath ?? "" === realPath)
.length >= dynamicLevel
) {
// 关闭第一个
const index = this.multiTags.findIndex(
item => item.meta?.realPath === realPath
);
index !== -1 && this.multiTags.splice(index, 1);
}
}
this.multiTags.push(value);
this.tagsCache(this.multiTags);
}
break;
case "splice":
this.multiTags.splice(position?.startIndex, position?.length);
this.tagsCache(this.multiTags);
return this.multiTags;
case "slice":
return this.multiTags.slice(-1);
}
}
}
});
export function useMultiTagsStoreHook() {
return useMultiTagsStore(store);
}

View File

@@ -1,28 +1,39 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "/@/store"; import { store } from "/@/store";
import { cacheType } from "./types"; import { cacheType } from "./types";
import { constantRoutesArr, ascending, filterTree } from "/@/router/index"; import { cloneDeep } from "lodash-es";
import { RouteConfigs } from "/@/layout/types";
import { constantMenus } from "/@/router/modules";
import { ascending, filterTree } from "/@/router/utils";
export const usePermissionStore = defineStore({ export const usePermissionStore = defineStore({
id: "pure-permission", id: "pure-permission",
state: () => ({ state: () => ({
// 静态路由 // 静态路由生成的菜单
constantRoutes: constantRoutesArr, constantMenus,
wholeRoutes: [], // 整体路由生成的菜单(静态、动态)
wholeMenus: [],
// 深拷贝一个菜单树,与导航菜单不突出
menusTree: [],
buttonAuth: [], buttonAuth: [],
// 缓存页面keepAlive // 缓存页面keepAlive
cachePageList: [] cachePageList: []
}), }),
actions: { actions: {
// 获取异步路由菜单
asyncActionRoutes(routes) { asyncActionRoutes(routes) {
if (this.wholeRoutes.length > 0) return; if (this.wholeMenus.length > 0) return;
this.wholeRoutes = filterTree( this.wholeMenus = filterTree(
ascending(this.constantRoutes.concat(routes)) ascending(this.constantMenus.concat(routes))
); );
const getButtonAuth = (arrRoutes: Array<string>) => { this.menusTree = cloneDeep(
filterTree(ascending(this.constantMenus.concat(routes)))
);
const getButtonAuth = (arrRoutes: Array<RouteConfigs>) => {
if (!arrRoutes || !arrRoutes.length) return; if (!arrRoutes || !arrRoutes.length) return;
arrRoutes.forEach((v: any) => { arrRoutes.forEach((v: RouteConfigs) => {
if (v.meta && v.meta.authority) { if (v.meta && v.meta.authority) {
this.buttonAuth.push(...v.meta.authority); this.buttonAuth.push(...v.meta.authority);
} }
@@ -32,7 +43,7 @@ export const usePermissionStore = defineStore({
}); });
}; };
getButtonAuth(this.wholeRoutes); getButtonAuth(this.wholeMenus);
}, },
async changeSetting(routes) { async changeSetting(routes) {
await this.asyncActionRoutes(routes); await this.asyncActionRoutes(routes);

View File

@@ -1,17 +1,14 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "/@/store"; import { store } from "/@/store";
import { setType } from "./types";
import { getConfig } from "/@/config"; import { getConfig } from "/@/config";
interface SettingState {
title: string;
fixedHeader: boolean;
}
export const useSettingStore = defineStore({ export const useSettingStore = defineStore({
id: "pure-setting", id: "pure-setting",
state: (): SettingState => ({ state: (): setType => ({
title: getConfig().Title, title: getConfig().Title,
fixedHeader: getConfig().FixedHeader fixedHeader: getConfig().FixedHeader,
hiddenSideBar: getConfig().HiddenSideBar
}), }),
getters: { getters: {
getTitle() { getTitle() {
@@ -19,6 +16,9 @@ export const useSettingStore = defineStore({
}, },
getFixedHeader() { getFixedHeader() {
return this.fixedHeader; return this.fixedHeader;
},
getHiddenSideBar() {
return this.HiddenSideBar;
} }
}, },
actions: { actions: {

View File

@@ -4,3 +4,38 @@ export type cacheType = {
mode: string; mode: string;
name?: RouteRecordName; name?: RouteRecordName;
}; };
export type positionType = {
startIndex?: number;
length?: number;
};
export type appType = {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
// 判断是否手动点击Hamburger
isClickHamburger: boolean;
};
layout: string;
device: string;
};
export type multiType = {
path: string;
parentPath: string;
name: string;
query: object;
meta: any;
};
export type setType = {
title: string;
fixedHeader: boolean;
hiddenSideBar: boolean;
};
export type userType = {
token: string;
name?: string;
};

84
src/store/modules/user.ts Normal file
View File

@@ -0,0 +1,84 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { userType } from "./types";
import { useRouter } from "vue-router";
import { getLogin, refreshToken } from "/@/api/user";
import { storageLocal, storageSession } from "/@/utils/storage";
import { getToken, setToken, removeToken } from "/@/utils/auth";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
const data = getToken();
let token = "";
let name = "";
if (data) {
const dataJson = JSON.parse(data);
if (dataJson) {
token = dataJson?.accessToken;
name = dataJson?.name ?? "admin";
}
}
export const useUserStore = defineStore({
id: "pure-user",
state: (): userType => ({
token,
name
}),
actions: {
SET_TOKEN(token) {
this.token = token;
},
SET_NAME(name) {
this.name = name;
},
// 登入
async loginByUsername(data) {
return new Promise<void>((resolve, reject) => {
getLogin(data)
.then(data => {
if (data) {
setToken(data);
resolve();
}
})
.catch(error => {
reject(error);
});
});
},
// 登出 清空缓存
logOut() {
this.token = "";
this.name = "";
removeToken();
storageLocal.clear();
storageSession.clear();
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
i18n: true,
showLink: true
}
}
]);
useRouter().push("/login");
},
// 刷新token
async refreshToken(data) {
return refreshToken(data).then(data => {
if (data) {
setToken(data);
return data;
}
});
}
}
});
export function useUserStoreHook() {
return useUserStore(store);
}

View File

@@ -1,5 +1,3 @@
// cover some element-plus styles
.el-breadcrumb__inner, .el-breadcrumb__inner,
.el-breadcrumb__inner a { .el-breadcrumb__inner a {
font-weight: 400 !important; font-weight: 400 !important;
@@ -15,7 +13,6 @@
display: none; display: none;
} }
// refine element ui upload
.upload-container { .upload-container {
.el-upload { .el-upload {
width: 100%; width: 100%;
@@ -27,17 +24,14 @@
} }
} }
// dropdown
.el-dropdown-menu { .el-dropdown-menu {
padding: 2px 0 2px 0 !important; padding: 2px 0 2px 0 !important;
} }
// to fix el-date-picker css style
.el-range-separator { .el-range-separator {
box-sizing: content-box; box-sizing: content-box;
} }
// el-tooltip的权重
.is-dark { .is-dark {
z-index: 99999 !important; z-index: 99999 !important;
} }

View File

@@ -70,7 +70,7 @@ ul {
display: none !important; display: none !important;
} }
// 灰色模式 /* 灰色模式 */
.html-grey { .html-grey {
filter: grayscale(100%); filter: grayscale(100%);
-webkit-filter: grayscale(100%); -webkit-filter: grayscale(100%);
@@ -79,7 +79,7 @@ ul {
-o-filter: grayscale(100%); -o-filter: grayscale(100%);
} }
// 色弱模式 /* 色弱模式 */
.html-weakness { .html-weakness {
filter: invert(80%); filter: invert(80%);
-webkit-filter: invert(80%); -webkit-filter: invert(80%);

View File

@@ -1,8 +1,7 @@
@import "../layout/theme/default-vars.scss"; @import "../layout/theme/default-vars.scss";
@mixin merge-style( @mixin merge-style(
// vertical模式下主体内容距离网页文档左侧的距离 /* vertical模式下主体内容距离网页文档左侧的距离 */ $sideBarWidth
$sideBarWidth
) { ) {
$menuActiveText: #7a80b4; $menuActiveText: #7a80b4;
@@ -40,7 +39,7 @@
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
z-index: 1000; z-index: 998;
width: calc(100% - 210px); width: calc(100% - 210px);
transition: width 0.28s; transition: width 0.28s;
} }
@@ -118,15 +117,21 @@
.el-menu-item, .el-menu-item,
.el-sub-menu__title { .el-sub-menu__title {
height: 50px;
color: $menuText; color: $menuText;
padding: 0 20px 0 40px; padding: 0 20px 0 40px;
&:hover { &:hover {
color: $menuTitleHover !important; color: $menuTitleHover !important;
} }
div,
span {
height: 50px;
line-height: 50px;
}
} }
// menu hover
.submenu-title-noDropdown, .submenu-title-noDropdown,
.el-sub-menu__title { .el-sub-menu__title {
&:hover { &:hover {
@@ -155,12 +160,12 @@
background-color: $subMenuBg !important; background-color: $subMenuBg !important;
} }
// 无子集的激活菜单背景 /* 无子集的激活菜单背景 */
.is-active.submenu-title-noDropdown.outer-most { .is-active.submenu-title-noDropdown.outer-most {
background: $subMenuActiveBg; background: $subMenuActiveBg;
} }
// 有子集的激活菜单背景 /* 有子集的激活菜单背景 */
.is-active.nest-menu { .is-active.nest-menu {
background: $subMenuActiveBg !important; background: $subMenuActiveBg !important;
} }
@@ -171,7 +176,7 @@
justify-content: space-around; justify-content: space-around;
background: $menuBg; background: $menuBg;
width: 100%; width: 100%;
height: 62px; height: 48px;
align-items: center; align-items: center;
.horizontal-header-left { .horizontal-header-left {
@@ -212,6 +217,15 @@
color: $subMenuActiveText; color: $subMenuActiveText;
justify-content: flex-end; justify-content: flex-end;
.dropdown-badge {
height: 48px;
color: $subMenuActiveText;
&:hover {
background: $menuHover;
}
}
.screen-full { .screen-full {
cursor: pointer; cursor: pointer;
@@ -221,7 +235,7 @@
} }
.globalization { .globalization {
height: 62px; height: 48px;
width: 40px; width: 40px;
padding: 11px; padding: 11px;
cursor: pointer; cursor: pointer;
@@ -234,7 +248,7 @@
.el-dropdown-link { .el-dropdown-link {
width: 100px; width: 100px;
height: 62px; height: 48px;
padding: 10px; padding: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -258,7 +272,7 @@
} }
.el-icon-setting { .el-icon-setting {
height: 62px; height: 48px;
width: 40px; width: 40px;
padding: 12px; padding: 12px;
display: flex; display: flex;
@@ -289,7 +303,8 @@
.submenu-title-noDropdown, .submenu-title-noDropdown,
.el-sub-menu__title { .el-sub-menu__title {
height: 60px; height: 48px;
line-height: 48px;
background: $menuBg; background: $menuBg;
} }
@@ -310,7 +325,7 @@
} }
} }
// vertical菜单折叠 /* vertical菜单折叠 */
.el-menu--vertical { .el-menu--vertical {
.el-menu--popup { .el-menu--popup {
background-color: $subMenuBg !important; background-color: $subMenuBg !important;
@@ -346,7 +361,7 @@
} }
} }
// 子菜单中还有子菜单 /* 子菜单中还有子菜单 */
.el-menu .el-sub-menu__title { .el-menu .el-sub-menu__title {
font-size: 12px; font-size: 12px;
min-width: $sideBarWidth !important; min-width: $sideBarWidth !important;
@@ -355,6 +370,8 @@
.el-menu-item, .el-menu-item,
.el-sub-menu__title { .el-sub-menu__title {
height: 50px;
line-height: 50px;
color: $menuText; color: $menuText;
background-color: $subMenuBg; background-color: $subMenuBg;
@@ -387,7 +404,7 @@
} }
} }
// horizontal菜单 /* horizontal菜单 */
.el-menu--horizontal { .el-menu--horizontal {
& > .el-sub-menu .el-sub-menu__icon-arrow { & > .el-sub-menu .el-sub-menu__icon-arrow {
position: static !important; position: static !important;
@@ -416,13 +433,13 @@
} }
} }
// 无子菜单时激活border-bottom /* 无子菜单时激活border-bottom */
.router-link-exact-active > .submenu-title-noDropdown { .router-link-exact-active > .submenu-title-noDropdown {
height: 60px; height: 60px;
border-bottom: 2px solid var(--el-menu-active-color); border-bottom: 2px solid var(--el-menu-active-color);
} }
// 子菜单中还有子菜单 /* 子菜单中还有子菜单 */
.el-menu .el-sub-menu__title { .el-menu .el-sub-menu__title {
font-size: 12px; font-size: 12px;
min-width: $sideBarWidth !important; min-width: $sideBarWidth !important;
@@ -433,12 +450,6 @@
} }
} }
& > .el-menu {
i {
margin-right: 16px;
}
}
.is-active > .el-sub-menu__title, .is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown { .is-active.submenu-title-noDropdown {
color: $subMenuActiveText !important; color: $subMenuActiveText !important;
@@ -455,7 +466,7 @@
} }
} }
// 有子集的激活菜单背景 /* 有子集的激活菜单背景 */
.is-active.nest-menu { .is-active.nest-menu {
background: $subMenuActiveBg !important; background: $subMenuActiveBg !important;
} }
@@ -475,13 +486,13 @@
min-width: $sideBarWidth !important; min-width: $sideBarWidth !important;
} }
// 有子菜单 /* 有子菜单 */
.el-menu--collapse .el-menu--collapse
.is-active.outer-most.el-sub-menu .is-active.outer-most.el-sub-menu
> .el-sub-menu__title::before { > .el-sub-menu__title::before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 5px; left: 0;
width: 3px; width: 3px;
height: 100%; height: 100%;
background-color: $menuActiveBefore; background-color: $menuActiveBefore;
@@ -493,11 +504,10 @@
transform: translateY(0); transform: translateY(0);
} }
// 无子菜单 /* 无子菜单 */
.el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before { .el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 5px;
width: 3px; width: 3px;
height: 100%; height: 100%;
background-color: $menuActiveBefore; background-color: $menuActiveBefore;
@@ -521,7 +531,7 @@
top: 50%; top: 50%;
} }
// 手机端 /* 手机端 */
.mobile { .mobile {
.fixed-header { .fixed-header {
width: 100% !important; width: 100% !important;
@@ -546,6 +556,7 @@
} }
} }
/* vertical菜单下hideSidebar去除动画 */
.withoutAnimation { .withoutAnimation {
.main-container, .main-container,
.sidebar-container { .sidebar-container {
@@ -557,6 +568,9 @@
body[layout="vertical"] { body[layout="vertical"] {
$sideBarWidth: 210px; $sideBarWidth: 210px;
@include merge-style($sideBarWidth); @include merge-style($sideBarWidth);
.el-menu--collapse {
width: 54px;
}
.sidebar-logo-container { .sidebar-logo-container {
background: $sidebarLogo; background: $sidebarLogo;
@@ -595,10 +609,8 @@ body[layout="vertical"] {
} }
} }
// 菜单折叠 /* 菜单折叠 */
.el-menu--collapse { .el-menu--collapse {
margin-left: -5px;
.el-sub-menu { .el-sub-menu {
& > .el-sub-menu__title { & > .el-sub-menu__title {
& > span { & > span {
@@ -611,6 +623,15 @@ body[layout="vertical"] {
} }
} }
/* 无子菜单 */
.el-menu-item [class^="el-icon"] {
right: 5px;
}
.el-sub-menu__title [class^="el-icon"] {
right: 2px;
}
.submenu-title-noDropdown { .submenu-title-noDropdown {
background: transparent !important; background: transparent !important;
} }

View File

@@ -1,5 +1,3 @@
// global transition css
/* fade */ /* fade */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {

43
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,43 @@
import Cookies from "js-cookie";
import { useUserStoreHook } from "/@/store/modules/user";
const TokenKey = "authorized-token";
type paramsMapType = {
name: string;
expires: number;
accessToken: string;
};
// 获取token
export function getToken() {
// 此处与TokenKey相同此写法解决初始化时Cookies中不存在TokenKey报错
return Cookies.get("authorized-token");
}
// 设置token以及过期时间cookies、sessionStorage各一份
// 后端需要将用户信息和token以及过期时间都返回给前端过期时间主要用于刷新token
export function setToken(data) {
const { accessToken, expires, name } = data;
// 提取关键信息进行存储
const paramsMap: paramsMapType = {
name,
expires: Date.now() + parseInt(expires),
accessToken
};
const dataString = JSON.stringify(paramsMap);
useUserStoreHook().SET_TOKEN(accessToken);
useUserStoreHook().SET_NAME(name);
expires > 0
? Cookies.set(TokenKey, dataString, {
expires: expires / 86400000
})
: Cookies.set(TokenKey, dataString);
sessionStorage.setItem(TokenKey, dataString);
}
// 删除token
export function removeToken() {
Cookies.remove(TokenKey);
sessionStorage.removeItem(TokenKey);
}

25
src/utils/http/README.md Normal file
View File

@@ -0,0 +1,25 @@
## 用法
### Get 请求
```
import { http } from "/@/utils/http";
// params传参
http.request('get', '/xxx', { params: param });
// url拼接传参
http.request('get', '/xxx?message=' + msg);
```
### Post 请求
```
import { http } from "/@/utils/http";
// params传参
http.request('get', '/xxx', { params: param });
// data传参
http.request('get', '/xxx', { data: param });
```

View File

@@ -1,32 +0,0 @@
import { AxiosRequestConfig } from "axios";
import { excludeProps } from "./utils";
/**
* 默认配置
*/
export const defaultConfig: AxiosRequestConfig = {
baseURL: "",
//10秒超时
timeout: 10000,
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
}
};
export function genConfig(config?: AxiosRequestConfig): AxiosRequestConfig {
if (!config) {
return defaultConfig;
}
const { headers } = config;
if (headers && typeof headers === "object") {
defaultConfig.headers = {
...defaultConfig.headers,
...headers
};
}
return { ...excludeProps(config!, "headers"), ...defaultConfig };
}
export const METHODS = ["post", "get", "put", "delete", "option", "patch"];

View File

@@ -1,236 +0,0 @@
import Axios, {
AxiosRequestConfig,
CancelTokenStatic,
AxiosInstance
} from "axios";
import NProgress from "../progress";
import { genConfig } from "./config";
import { transformConfigByMethod } from "./utils";
import {
cancelTokenType,
RequestMethods,
EnclosureHttpRequestConfig,
EnclosureHttpResoponse,
EnclosureHttpError
} from "./types.d";
class EnclosureHttp {
constructor() {
this.httpInterceptorsRequest();
this.httpInterceptorsResponse();
}
// 初始化配置对象
private static initConfig: EnclosureHttpRequestConfig = {};
// 保存当前Axios实例对象
private static axiosInstance: AxiosInstance = Axios.create(genConfig());
// 保存 EnclosureHttp实例
private static EnclosureHttpInstance: EnclosureHttp;
// axios取消对象
private CancelToken: CancelTokenStatic = Axios.CancelToken;
// 取消的凭证数组
private sourceTokenList: Array<cancelTokenType> = [];
// 记录当前这一次cancelToken的key
private currentCancelTokenKey = "";
public get cancelTokenList(): Array<cancelTokenType> {
return this.sourceTokenList;
}
// eslint-disable-next-line class-methods-use-this
public set cancelTokenList(value) {
throw new Error("cancelTokenList不允许赋值");
}
/**
* @description 私有构造不允许实例化
* @returns void 0
*/
// constructor() {}
/**
* @description 生成唯一取消key
* @param config axios配置
* @returns string
*/
// eslint-disable-next-line class-methods-use-this
private static genUniqueKey(config: EnclosureHttpRequestConfig): string {
return `${config.url}--${JSON.stringify(config.data)}`;
}
/**
* @description 取消重复请求
* @returns void 0
*/
private cancelRepeatRequest(): void {
const temp: { [key: string]: boolean } = {};
this.sourceTokenList = this.sourceTokenList.reduce<Array<cancelTokenType>>(
(res: Array<cancelTokenType>, cancelToken: cancelTokenType) => {
const { cancelKey, cancelExecutor } = cancelToken;
if (!temp[cancelKey]) {
temp[cancelKey] = true;
res.push(cancelToken);
} else {
cancelExecutor();
}
return res;
},
[]
);
}
/**
* @description 删除指定的CancelToken
* @returns void 0
*/
private deleteCancelTokenByCancelKey(cancelKey: string): void {
this.sourceTokenList =
this.sourceTokenList.length < 1
? this.sourceTokenList.filter(
cancelToken => cancelToken.cancelKey !== cancelKey
)
: [];
}
/**
* @description 拦截请求
* @returns void 0
*/
private httpInterceptorsRequest(): void {
EnclosureHttp.axiosInstance.interceptors.request.use(
(config: EnclosureHttpRequestConfig) => {
const $config = config;
NProgress.start(); // 每次切换页面时,调用进度条
const cancelKey = EnclosureHttp.genUniqueKey($config);
$config.cancelToken = new this.CancelToken(
(cancelExecutor: (cancel: any) => void) => {
this.sourceTokenList.push({ cancelKey, cancelExecutor });
}
);
this.cancelRepeatRequest();
this.currentCancelTokenKey = cancelKey;
// 优先判断post/get等方法是否传入回掉否则执行初始化设置等回掉
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback($config);
return $config;
}
if (EnclosureHttp.initConfig.beforeRequestCallback) {
EnclosureHttp.initConfig.beforeRequestCallback($config);
return $config;
}
return $config;
},
error => {
return Promise.reject(error);
}
);
}
/**
* @description 清空当前cancelTokenList
* @returns void 0
*/
public clearCancelTokenList(): void {
this.sourceTokenList.length = 0;
}
/**
* @description 拦截响应
* @returns void 0
*/
private httpInterceptorsResponse(): void {
const instance = EnclosureHttp.axiosInstance;
instance.interceptors.response.use(
(response: EnclosureHttpResoponse) => {
const $config = response.config;
// 请求每次成功一次就删除当前canceltoken标记
const cancelKey = EnclosureHttp.genUniqueKey($config);
this.deleteCancelTokenByCancelKey(cancelKey);
NProgress.done();
// 优先判断post/get等方法是否传入回掉否则执行初始化设置等回掉
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
return response.data;
}
if (EnclosureHttp.initConfig.beforeResponseCallback) {
EnclosureHttp.initConfig.beforeResponseCallback(response);
return response.data;
}
return response.data;
},
(error: EnclosureHttpError) => {
const $error = error;
// 判断当前的请求中是否在 取消token数组理存在如果存在则移除单次请求流程
if (this.currentCancelTokenKey) {
const haskey = this.sourceTokenList.filter(
cancelToken => cancelToken.cancelKey === this.currentCancelTokenKey
).length;
if (haskey) {
this.sourceTokenList = this.sourceTokenList.filter(
cancelToken =>
cancelToken.cancelKey !== this.currentCancelTokenKey
);
this.currentCancelTokenKey = "";
}
}
$error.isCancelRequest = Axios.isCancel($error);
NProgress.done();
// 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error);
}
);
}
public request<T>(
method: RequestMethods,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: EnclosureHttpRequestConfig
): Promise<T> {
const config = transformConfigByMethod(param, {
method,
url,
...axiosConfig
} as EnclosureHttpRequestConfig);
// 单独处理自定义请求/响应回掉
return new Promise((resolve, reject) => {
EnclosureHttp.axiosInstance
.request(config)
.then((response: undefined) => {
resolve(response);
})
.catch((error: any) => {
reject(error);
});
});
}
public post<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T> {
return this.request<T>("post", url, params, config);
}
public get<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T> {
return this.request<T>("get", url, params, config);
}
}
export default EnclosureHttp;

View File

@@ -1,2 +1,166 @@
import EnclosureHttp from "./core"; import Axios, { AxiosInstance, AxiosRequestConfig } from "axios";
export const http = new EnclosureHttp(); import {
resultType,
PureHttpError,
RequestMethods,
PureHttpResoponse,
PureHttpRequestConfig
} from "./types.d";
import qs from "qs";
import NProgress from "../progress";
// import { loadEnv } from "@build/index";
import { getToken } from "/@/utils/auth";
import { useUserStoreHook } from "/@/store/modules/user";
// 加载环境变量 VITE_PROXY_DOMAIN开发环境 VITE_PROXY_DOMAIN_REAL打包后的线上环境
// const { VITE_PROXY_DOMAIN, VITE_PROXY_DOMAIN_REAL } = loadEnv();
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
// baseURL:
// process.env.NODE_ENV === "production"
// ? VITE_PROXY_DOMAIN_REAL
// : VITE_PROXY_DOMAIN,
// 当前使用mock模拟请求将baseURL制空如果你的环境用到了http请求请删除下面的baseURL启用上面的baseURL并将11行、16行代码注释取消
baseURL: "",
timeout: 10000,
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
// 数组格式参数序列化
paramsSerializer: params => qs.stringify(params, { indices: false })
};
class PureHttp {
constructor() {
this.httpInterceptorsRequest();
this.httpInterceptorsResponse();
}
// 初始化配置对象
private static initConfig: PureHttpRequestConfig = {};
// 保存当前Axios实例对象
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
// 请求拦截
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
(config: PureHttpRequestConfig) => {
const $config = config;
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回掉否则执行初始化设置等回掉
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback($config);
return $config;
}
if (PureHttp.initConfig.beforeRequestCallback) {
PureHttp.initConfig.beforeRequestCallback($config);
return $config;
}
const token = getToken();
if (token) {
const data = JSON.parse(token);
const now = new Date().getTime();
const expired = parseInt(data.expires) - now <= 0;
if (expired) {
// token过期刷新
useUserStoreHook()
.refreshToken(data)
.then((res: resultType) => {
config.headers["Authorization"] = "Bearer " + res.accessToken;
return $config;
});
} else {
config.headers["Authorization"] = "Bearer " + data.accessToken;
return $config;
}
} else {
return $config;
}
},
error => {
return Promise.reject(error);
}
);
}
// 响应拦截
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResoponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回掉否则执行初始化设置等回掉
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
return response.data;
}
if (PureHttp.initConfig.beforeResponseCallback) {
PureHttp.initConfig.beforeResponseCallback(response);
return response.data;
}
return response.data;
},
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
NProgress.done();
// 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error);
}
);
}
// 通用请求工具函数
public request<T>(
method: RequestMethods,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: PureHttpRequestConfig
): Promise<T> {
const config = {
method,
url,
...param,
...axiosConfig
} as PureHttpRequestConfig;
// 单独处理自定义请求/响应回掉
return new Promise((resolve, reject) => {
PureHttp.axiosInstance
.request(config)
.then((response: undefined) => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
}
// 单独抽离的post工具函数
public post<T>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("post", url, params, config);
}
// 单独抽离的get工具函数
public get<T>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("get", url, params, config);
}
}
export const http = new PureHttp();

View File

@@ -1,50 +1,39 @@
import Axios, { import Axios, {
AxiosRequestConfig,
Canceler,
AxiosResponse,
Method, Method,
AxiosError AxiosError,
AxiosResponse,
AxiosRequestConfig
} from "axios"; } from "axios";
import { METHODS } from "./config"; export type resultType = {
accessToken?: string;
export type cancelTokenType = { cancelKey: string; cancelExecutor: Canceler }; };
export type RequestMethods = Extract< export type RequestMethods = Extract<
Method, Method,
"get" | "post" | "put" | "delete" | "patch" | "option" | "head" "get" | "post" | "put" | "delete" | "patch" | "option" | "head"
>; >;
export interface EnclosureHttpRequestConfig extends AxiosRequestConfig { export interface PureHttpError extends AxiosError {
beforeRequestCallback?: (request: EnclosureHttpRequestConfig) => void; // 请求发送之前
beforeResponseCallback?: (response: EnclosureHttpResoponse) => void; // 相应返回之前
}
export interface EnclosureHttpResoponse extends AxiosResponse {
config: EnclosureHttpRequestConfig;
}
export interface EnclosureHttpError extends AxiosError {
isCancelRequest?: boolean; isCancelRequest?: boolean;
} }
export default class EnclosureHttp { export interface PureHttpResoponse extends AxiosResponse {
cancelTokenList: Array<cancelTokenType>; config: PureHttpRequestConfig;
clearCancelTokenList(): void; }
export interface PureHttpRequestConfig extends AxiosRequestConfig {
beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
beforeResponseCallback?: (response: PureHttpResoponse) => void;
}
export default class PureHttp {
request<T>( request<T>(
method: RequestMethods, method: RequestMethods,
url: string, url: string,
param?: AxiosRequestConfig, param?: AxiosRequestConfig,
axiosConfig?: EnclosureHttpRequestConfig axiosConfig?: PureHttpRequestConfig
): Promise<T>;
post<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T>;
get<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T>; ): Promise<T>;
post<T>(url: string, params?: T, config?: PureHttpRequestConfig): Promise<T>;
get<T>(url: string, params?: T, config?: PureHttpRequestConfig): Promise<T>;
} }

View File

@@ -1,29 +0,0 @@
import { EnclosureHttpRequestConfig } from "./types.d";
export function excludeProps<T extends { [key: string]: any }>(
origin: T,
prop: string
): { [key: string]: T } {
return Object.keys(origin)
.filter(key => !prop.includes(key))
.reduce((res, key) => {
res[key] = origin[key];
return res;
}, {} as { [key: string]: T });
}
export function transformConfigByMethod(
params: any,
config: EnclosureHttpRequestConfig
): EnclosureHttpRequestConfig {
const { method } = config;
const props = ["delete", "get", "head", "options"].includes(
method!.toLocaleLowerCase()
)
? "params"
: "data";
return {
...config,
[props]: params
};
}

View File

@@ -7,7 +7,7 @@ NProgress.configure({
// 递增进度条的速度 // 递增进度条的速度
speed: 500, speed: 500,
// 是否显示加载ico // 是否显示加载ico
showSpinner: true, showSpinner: false,
// 自动递增间隔 // 自动递增间隔
trickleSpeed: 200, trickleSpeed: 200,
// 初始化时的最小百分比 // 初始化时的最小百分比

View File

@@ -1,53 +0,0 @@
import { loadEnv } from "@build/utils";
import { merge } from "lodash-es";
import tsCookies from "typescript-cookie/dist/src/compat";
class Cookies {
private static env = loadEnv();
constructor() {}
/**
* 存储 cookie 值
* @param name
* @param value
* @param cookieSetting
*/
set(name = "default", value = "", cookieSetting = {}) {
const currentCookieSetting = {
expires: 1
};
merge(currentCookieSetting, cookieSetting);
tsCookies.set(
`${Cookies.env.VITE_TITLE}-${Cookies.env.VITE_VERSION}-${name}`,
value,
currentCookieSetting
);
}
/**
* 拿到 cookie 值
* @param name
* @returns
*/
get(name = "default") {
return tsCookies.get(
`${Cookies.env.VITE_TITLE}-${Cookies.env.VITE_VERSION}-${name}`
);
}
/**
* 拿到 cookie 全部的值
* @returns
*/
getAll() {
return tsCookies.get();
}
/**
* 删除 cookie
* @param name
*/
remove(name = "default") {
tsCookies.remove(
`${Cookies.env.VITE_TITLE}-${Cookies.env.VITE_VERSION}-${name}`
);
}
}
export const cookies = new Cookies();

View File

@@ -1,93 +0,0 @@
import { loadEnv } from "@build/utils";
import { LocalStorage, LowSync } from "lowdb";
import { chain, cloneDeep } from "lodash-es";
import { storageLocal } from ".";
import { cookies } from "./cookie";
type Data = {
database: {};
sys: {};
};
/**
* db 数据存储,采用 LocalStorage存储
*/
class DB {
private db: LowSync<Data>;
private static env = loadEnv();
constructor() {
this.db = new LowSync<Data>(
new LocalStorage<Data>(`${DB.env.VITE_TITLE}-${DB.env.VITE_VERSION}`)
);
this.initialization();
// @ts-ignore
this.db.chain = chain(this.db.data);
}
private initialization() {
this.db.data = storageLocal.getItem(
`${DB.env.VITE_TITLE}-${DB.env.VITE_VERSION}`
) || { database: {}, sys: {} };
this.db.write();
}
/**
* 检查路径是否存在 不存在的话初始化
* @param param0
* @returns path
*/
pathInit({
dbName = "database",
path = "",
user = true,
validator = () => true,
defaultValue = ""
}): string {
const uuid = cookies.get("uuid") || "ghost-uuid";
const currentPath = `${dbName}.${user ? `user.${uuid}` : "public"}${
path ? `.${path}` : ""
}`;
// @ts-ignore
const value = this.db.chain.get(currentPath).value();
// @ts-ignore
if (!(value !== undefined && validator(value))) {
// @ts-ignore
this.db.chain.set(currentPath, defaultValue).value();
this.db.write();
}
return currentPath;
}
/**
*将数据存储到指定位置 | 路径不存在会自动初始化
*
* 效果类似于取值 dbName.path = value
* @param param0
*/
dbSet({ dbName = "database", path = "", value = "", user = false }): void {
const currentPath = this.pathInit({
dbName,
path,
user
});
// @ts-ignore
this.db.chain.set(currentPath, value).value();
this.db.write();
}
/**
* 获取数据
*
* 效果类似于取值 dbName.path || defaultValue
* @param param0
* @returns
*/
dbGet({
dbName = "database",
path = "",
defaultValue = "",
user = false
}): any {
// @ts-ignore
const values = this.db.chain
.get(this.pathInit({ dbName, path, user, defaultValue }))
.value();
return cloneDeep(values);
}
}
export const db = new DB();

View File

@@ -20,7 +20,7 @@ class sessionStorageProxy implements ProxyStorage {
// 取 // 取
public getItem(key: string): any { public getItem(key: string): any {
return JSON.parse(this.storage.getItem(key)) || null; return JSON.parse(this.storage.getItem(key));
} }
// 删 // 删

View File

@@ -3,44 +3,54 @@ import { App } from "vue";
import Storage from "responsive-storage"; import Storage from "responsive-storage";
export const injectResponsiveStorage = (app: App, config: ServerConfigs) => { export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
app.use(Storage, { const configObj = Object.assign(
// 默认显示首页tag {
routesInStorage: { // 国际化 默认中文zh
type: Array, locale: {
default: Storage.getData(undefined, "routesInStorage") ?? [ type: Object,
{ default: Storage.getData(undefined, "locale") ?? {
path: "/welcome", locale: config.Locale ?? "zh"
parentPath: "/", }
meta: { },
title: "message.hshome", // layout模式以及主题
icon: "el-icon-s-home", layout: {
showLink: true type: Object,
default: Storage.getData(undefined, "layout") ?? {
layout: config.Layout ?? "vertical",
theme: config.Theme ?? "default"
}
},
sets: {
type: Object,
default: Storage.getData(undefined, "sets") ?? {
grey: config.Grey ?? false,
weak: config.Weak ?? false,
hideTabs: config.HideTabs ?? false,
multiTagsCache: config.MultiTagsCache ?? false
}
}
},
config.MultiTagsCache
? {
// 默认显示首页tag
tags: {
type: Array,
default: Storage.getData(undefined, "tags") ?? [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
i18n: true,
icon: "HomeFilled",
showLink: true
}
}
]
} }
} }
] : {}
}, );
// 国际化 默认中文zh
locale: { app.use(Storage, configObj);
type: Object,
default: Storage.getData(undefined, "locale") ?? {
locale: config.Locale ?? "zh"
}
},
// layout模式以及主题
layout: {
type: Object,
default: Storage.getData(undefined, "layout") ?? {
layout: config.Layout ?? "vertical",
theme: config.Theme ?? "default"
}
},
sets: {
type: Object,
default: Storage.getData(undefined, "sets") ?? {
grey: config.Grey ?? false,
weak: config.Weak ?? false,
hideTabs: config.HideTabs ?? false
}
}
});
}; };

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