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
# 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
# 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" ] ]
# 开发环境代理
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: "^_",
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)
### 🎫 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)
### 🎫 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)
### 🎫 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.
## 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
- [vue-pure-admin](http://yiming_chang.gitee.io/manages)
Click to log in without password
<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://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dec0672a62e141f3b7f626c22ff6c7ef~tplv-k3u1fbpfcp-watermark.image">
<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">
<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://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/549c3184697f4d268a78c9833e5ec2ea~tplv-k3u1fbpfcp-watermark.awebp?">
</p>
### 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" />
## 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
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`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
## 配套视频教程
## 配套视频
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)
点击免密登录
<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://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dec0672a62e141f3b7f626c22ff6c7ef~tplv-k3u1fbpfcp-watermark.image">
<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">
<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://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/549c3184697f4d268a78c9833e5ec2ea~tplv-k3u1fbpfcp-watermark.awebp?">
</p>
### 使用 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 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)) {
let realName = envConf[envName].replace(/\\n/g, "\n");
@@ -9,13 +18,6 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
if (envName === "VITE_PORT") {
realName = Number(realName);
}
if (envName === "VITE_PROXY" && realName) {
try {
realName = JSON.parse(realName.replace(/'/g, '"'));
} catch (error) {
realName = "";
}
}
ret[envName] = realName;
if (typeof realName === "string") {
process.env[envName] = realName;
@@ -25,8 +27,15 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
}
return ret;
};
// 跨域代理重写
const regExps = (value: string, reg: string): string => {
return value.replace(new RegExp(reg, "g"), "");
};
// 环境变量
const loadEnv = (): ViteEnv => {
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: {
icon: "Setting",
title: "message.hssysManagement",
i18n: true,
showLink: true,
rank: 6
},
@@ -18,6 +19,7 @@ const systemRouter = {
name: "user",
meta: {
title: "message.hsBaseinfo",
i18n: true,
showLink: true
}
},
@@ -26,7 +28,9 @@ const systemRouter = {
name: "dict",
meta: {
title: "message.hsDict",
showLink: true
i18n: true,
showLink: true,
keepAlive: true
}
}
]
@@ -39,6 +43,7 @@ const permissionRouter = {
meta: {
title: "message.permission",
icon: "Lollipop",
i18n: true,
showLink: true,
rank: 3
},
@@ -48,6 +53,7 @@ const permissionRouter = {
name: "permissionPage",
meta: {
title: "message.permissionPage",
i18n: true,
showLink: true
}
},
@@ -56,6 +62,7 @@ const permissionRouter = {
name: "permissionButton",
meta: {
title: "message.permissionButton",
i18n: true,
showLink: true,
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页面中
function setDifAuthority(authority, routes) {
routes.children[1].meta.authority = [authority];
@@ -77,12 +120,16 @@ export default [
if (query.name === "admin") {
return {
code: 0,
info: [systemRouter, setDifAuthority("v-admin", permissionRouter)]
info: [
tabsRouter,
systemRouter,
setDifAuthority("v-admin", permissionRouter)
]
};
} else {
return {
code: 0,
info: [setDifAuthority("v-test", permissionRouter)]
info: [tabsRouter, setDifAuthority("v-test", permissionRouter)]
};
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "vue-pure-admin",
"version": "2.6.0",
"version": "2.7.0",
"private": true,
"engines": {
"node": ">= 16",
@@ -29,7 +29,10 @@
],
"dependencies": {
"@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/extension": "0.7.1",
"@vueuse/core": "^6.7.1",
@@ -40,9 +43,10 @@
"cropperjs": "^1.5.11",
"dayjs": "^1.10.7",
"echarts": "^5.2.1",
"element-plus": "1.2.0-beta.3",
"element-plus": "1.2.0-beta.6",
"element-resize-detector": "^1.2.3",
"font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",
"lodash-es": "^4.17.21",
"lowdb": "^3.0.0",
"mitt": "^3.0.0",
@@ -51,19 +55,21 @@
"path": "^0.12.7",
"path-to-regexp": "^6.2.0",
"pinia": "^2.0.0-rc.14",
"qs": "^6.10.1",
"remixicon": "^2.5.0",
"resize-observer-polyfill": "^1.5.1",
"responsive-storage": "^1.0.11",
"sortablejs": "1.13.0",
"typescript-cookie": "^1.0.0",
"v-contextmenu": "3.0.0",
"vue": "^3.2.21",
"vue": "^3.2.24",
"vue-i18n": "^9.2.0-beta.3",
"vue-json-pretty": "^2.0.2",
"vue-router": "^4.0.11",
"vue-router": "^4.0.12",
"vue-types": "^4.1.0",
"vuedraggable": "4.1.0",
"vxe-table": "4.0.30",
"wangeditor": "4.7.7",
"wangeditor": "^4.7.9",
"xe-ajax": "4.0.5",
"xe-utils": "3.4.0",
"xgplayer": "2.28.0"
@@ -72,17 +78,20 @@
"@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
"@types/mockjs": "1.0.3",
"@types/node": "14.14.14",
"@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"@vitejs/plugin-vue": "^1.9.4",
"@vitejs/plugin-vue-jsx": "^1.2.0",
"@vue/compiler-sfc": "^3.2.21",
"@vitejs/plugin-legacy": "^1.6.4",
"@vitejs/plugin-vue": "^1.10.2",
"@vitejs/plugin-vue-jsx": "^1.3.1",
"@vue/compiler-sfc": "^3.2.24",
"@vue/eslint-config-prettier": "6.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",
"babel-plugin-transform-remove-console": "6.9.4",
"chalk": "2.4.2",
@@ -97,15 +106,15 @@
"prettier": "2.3.2",
"pretty-quick": "3.1.1",
"rimraf": "3.0.2",
"sass": "^1.43.4",
"sass": "^1.45.0",
"sass-loader": "^12.3.0",
"stylelint": "13.13.1",
"stylelint-config-prettier": "8.0.2",
"stylelint-config-standard": "22.0.0",
"stylelint-order": "4.1.0",
"typescript": "4.4.2",
"unplugin-element-plus": "^0.1.0",
"vite": "latest",
"unplugin-element-plus": "^0.1.3",
"vite": "2.6.14",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-style-import": "^1.2.1",
"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",
"FixedHeader": true,
"HiddenSideBar": false,
"MultiTagsCache": false,
"KeepAlive": true,
"Locale": "zh",
"Layout": "vertical",
@@ -12,7 +13,6 @@
"HideTabs": false,
"MapConfigure": {
"amapKey": "97b3248d1553172e81f168cf94ea667e",
"baiduKey": "wTHbkkEweiFqZLKunMIjcrb2RcqNXkhc",
"options": {
"resizeEnable": true,
"center": [113.6401, 34.72468],

View File

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

View File

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

View File

@@ -13,5 +13,14 @@ export const getVerify = (): userType => {
// 登录
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-family: "iconfont"; /* Project id 2208059 */
src: url("iconfont.woff2?t=1636197082361") format("woff2"),
url("iconfont.woff?t=1636197082361") format("woff"),
url("iconfont.ttf?t=1636197082361") format("truetype");
src: url("iconfont.woff2?t=1638023560828") format("woff2"),
url("iconfont.woff?t=1638023560828") format("woff"),
url("iconfont.ttf?t=1638023560828") format("truetype");
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.team-icontabs::before {
content: "\e63e";
}
.team-iconlogo::before {
content: "\e620";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,13 @@
"css_prefix_text": "team-icon",
"description": "pure-admin",
"glyphs": [
{
"icon_id": "20594647",
"name": "标签页",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "22129506",
"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 { 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, {
install(app: App) {

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
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 { useRouter, useRoute } from "vue-router";
import { storageSession } from "/@/utils/storage";
@@ -17,13 +20,17 @@ const pureApp = useAppStoreHook();
const router = useRouter();
const route = useRoute();
let usename = storageSession.getItem("info")?.username;
const { locale, t } = useI18n();
const { locale } = useI18n();
watch(
() => locale.value,
() => {
//@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" />
<div class="vertical-header-right">
<!-- 通知 -->
<Notice />
<!-- 全屏 -->
<screenfull v-show="!deviceDetection()" />
<!-- 国际化 -->
@@ -98,16 +107,15 @@ function translationEn() {
<!-- 退出登陆 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
/>
<img :src="avatars" />
<p>{{ usename }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item icon="el-icon-switch-button" @click="logout">{{
$t("message.hsLoginOut")
}}</el-dropdown-item>
<el-dropdown-item @click="logout">
<i class="ri-logout-circle-r-line"></i
>{{ $t("message.hsLoginOut") }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -151,6 +159,12 @@ function translationEn() {
color: #000000d9;
justify-content: flex-end;
:deep(.dropdown-badge) {
&:hover {
background: #f6f6f6;
}
}
.screen-full {
cursor: pointer;
@@ -239,7 +253,12 @@ function translationEn() {
}
.logout {
max-width: 120px;
.el-dropdown-menu__item {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
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>
.screen-full {
width: 36px;
height: 62px;
height: 48px;
display: flex;
align-items: center;
justify-content: space-around;

View File

@@ -10,6 +10,7 @@ import {
getCurrentInstance
} from "vue";
import panel from "../panel/index.vue";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import { emitter } from "/@/utils/mitt";
import { templateRef } from "@vueuse/core";
@@ -17,6 +18,7 @@ import { debounce } from "/@/utils/debounce";
import { themeColorsType } from "../../types";
import { useAppStoreHook } from "/@/store/modules/app";
import { storageLocal, storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils";
const router = useRouter();
@@ -28,23 +30,23 @@ const instanceConfig =
getCurrentInstance().appContext.app.config.globalProperties.$config;
let themeColors = ref<Array<themeColorsType>>([
// 暗雅(默认)
// 道奇蓝(默认)
{ rgb: "27, 42, 71", themeColor: "default" },
//
// 亮白色
{ rgb: "255, 255, 255", themeColor: "light" },
// 薄暮
// 猩红色
{ rgb: "245, 34, 45", themeColor: "dusk" },
// 火山
// 橙红色
{ rgb: "250, 84, 28", themeColor: "volcano" },
//
//
{ rgb: "250, 219, 20", themeColor: "yellow" },
// 明青
// 绿宝石
{ rgb: "19, 194, 194", themeColor: "mingQing" },
// 极光绿
// 酸橙绿
{ rgb: "82, 196, 26", themeColor: "auroraGreen" },
// 粉红
// 深粉色
{ rgb: "235, 47, 150", themeColor: "pink" },
// 酱紫
// 深紫罗兰色
{ rgb: "114, 46, 209", themeColor: "saucePurple" }
]);
@@ -76,7 +78,8 @@ const logoVal = ref(storageLocal.getItem("logoVal") || "1");
const settings = reactive({
greyVal: instance.sets.grey,
weakVal: instance.sets.weak,
tabsVal: instance.sets.hideTabs
tabsVal: instance.sets.hideTabs,
multiTagsCache: instance.sets.multiTagsCache
});
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
@@ -92,7 +95,8 @@ const greyChange = (value): void => {
instance.sets = {
grey: value,
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 = {
grey: instance.sets.grey,
weak: value,
hideTabs: instance.sets.hideTabs
hideTabs: instance.sets.hideTabs,
multiTagsCache: instance.sets.multiTagsCache
};
};
@@ -115,11 +120,23 @@ const tagsChange = () => {
instance.sets = {
grey: instance.sets.grey,
weak: instance.sets.weak,
hideTabs: showVal
hideTabs: showVal,
multiTagsCache: instance.sets.multiTagsCache
};
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(() => {
settings.greyVal &&
@@ -135,6 +152,19 @@ function onReset() {
storageSession.clear();
toggleClass(false, "html-grey", 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");
}
@@ -304,6 +334,18 @@ function setLayoutThemeColor(theme: string) {
>
</el-switch>
</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>
<span>标签风格</span>

View File

@@ -1,10 +1,16 @@
<script setup lang="ts">
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";
const levelList = ref([]);
const route = useRoute();
const levelList = ref([]);
const router = useRouter();
const routes = router.options.routes;
const multiTags = useMultiTagsStoreHook().multiTags;
const isDashboard = (route: RouteLocationMatched): boolean | string => {
const name = route && (route.name as string);
@@ -15,19 +21,57 @@ const isDashboard = (route: RouteLocationMatched): boolean | string => {
};
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];
if (!isDashboard(first)) {
matched = [
{
path: "/welcome",
parentPath: "/",
meta: { title: "message.hshome" }
meta: { title: "message.hshome", i18n: true }
} as unknown as RouteLocationMatched
].concat(matched);
}
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()
);
watch(
() => route.query,
() => getBreadcrumb()
);
const handleLink = (item: RouteLocationMatched): any => {
const { redirect, path } = item;
if (redirect) {
@@ -55,10 +104,10 @@ const handleLink = (item: RouteLocationMatched): any => {
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>{{ $t(item.meta.title) }}</span
>{{ transformI18n(item.meta.title, item.meta.i18n) }}</span
>
<a v-else @click.prevent="handleLink(item)">
{{ $t(item.meta.title) }}
{{ transformI18n(item.meta.title, item.meta.i18n) }}
</a>
</el-breadcrumb-item>
</transition-group>

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import { usePermissionStoreHook } from "/@/store/modules/permission";
const route = useRoute();
const pureApp = useAppStoreHook();
const router = useRouter().options.routes;
const routeStore = usePermissionStoreHook();
const showLogo = ref(storageLocal.getItem("logoVal") || "1");
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
@@ -72,7 +71,7 @@ onBeforeMount(() => {
@select="menuSelect"
>
<sidebar-item
v-for="route in routeStore.wholeRoutes"
v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path"
:item="route"
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">
import {
ref,
@@ -23,14 +9,6 @@ import {
getCurrentInstance,
ComputedRef
} 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 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 closeRight from "/@/assets/svg/close_right.svg";
let refreshButton = "refresh-button";
const instance = getCurrentInstance();
import { emitter } from "/@/utils/mitt";
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 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 tabDom = templateRef<HTMLElement | null>("tabDom", 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>>([
{
@@ -62,41 +196,38 @@ const tagsViews = ref<Array<tagsViewsType>>([
icon: close,
text: "message.hscloseCurrentTab",
divided: false,
disabled: routerArrays.length > 1 ? false : true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeLeft,
text: "message.hscloseLeftTabs",
divided: true,
disabled: routerArrays.length > 1 ? false : true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeRight,
text: "message.hscloseRightTabs",
divided: false,
disabled: routerArrays.length > 1 ? false : true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeOther,
text: "message.hscloseOtherTabs",
divided: true,
disabled: routerArrays.length > 2 ? false : true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: closeAll,
text: "message.hscloseAllTabs",
divided: false,
disabled: routerArrays.length > 1 ? false : true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
}
]);
const dynamicTagList: ComputedRef<Array<RouteConfigs>> = computed(() => {
return relativeStorage.routesInStorage;
});
// 显示模式,默认灵动模式显示
const showModel = ref(storageLocal.getItem("showModel") || "smart");
@@ -112,7 +243,7 @@ let buttonTop = ref(0);
let currentSelect = ref({});
function dynamicRouteTag(value: string, parentPath: string): void {
const hasValue = relativeStorage.routesInStorage.some((item: any) => {
const hasValue = multiTags.value.some(item => {
return item.path === value;
});
@@ -121,13 +252,12 @@ function dynamicRouteTag(value: string, parentPath: string): void {
arr.forEach((arrItem: any) => {
let pathConcat = parentPath + arrItem.path;
if (arrItem.path === value || pathConcat === value) {
routerArrays.push({
useMultiTagsStoreHook().handleTags("push", {
path: value,
parentPath: `/${parentPath.split("/")[1]}`,
meta: arrItem.meta,
name: arrItem.name
});
relativeStorage.routesInStorage = routerArrays;
} else {
if (arrItem.children && arrItem.children.length > 0) {
concatPath(arrItem.children, value, parentPath);
@@ -142,9 +272,10 @@ function dynamicRouteTag(value: string, parentPath: string): void {
// 重新加载
function onFresh() {
toggleClass(true, refreshButton, document.querySelector(".rotate"));
const { fullPath } = unref(route);
const { fullPath, query } = unref(route);
router.replace({
path: "/redirect" + fullPath
path: "/redirect" + fullPath,
query: query
});
setTimeout(() => {
removeClass(document.querySelector(".rotate"), refreshButton);
@@ -154,28 +285,41 @@ function onFresh() {
function deleteDynamicTag(obj: any, current: any, tag?: string) {
// 存放被删除的缓存路由
let delAliveRouteList = [];
let valueIndex: number = routerArrays.findIndex((item: any) => {
return item.path === obj.path;
let valueIndex: number = multiTags.value.findIndex((item: any) => {
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) {
relativeStorage.routesInStorage = [
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
i18n: true,
icon: "el-icon-s-home",
showLink: true
}
},
obj
];
routerArrays = relativeStorage.routesInStorage;
]);
} else {
delAliveRouteList = routerArrays.splice(start, end);
relativeStorage.routesInStorage = routerArrays;
// @ts-ignore
delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", {
startIndex,
length
});
}
};
@@ -184,12 +328,12 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
} else if (tag === "left") {
spliceRoute(1, valueIndex - 1);
} else if (tag === "right") {
spliceRoute(valueIndex + 1, routerArrays.length);
spliceRoute(valueIndex + 1, multiTags.value.length);
} else {
// 从当前匹配到的路径中删除
spliceRoute(valueIndex, 1);
}
let newRoute: any = routerArrays.slice(-1);
let newRoute = useMultiTagsStoreHook().handleTags("slice");
if (current === route.path) {
// 删除缓存路由
tag
@@ -199,19 +343,21 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (tag === "left") return;
nextTick(() => {
router.push({
path: newRoute[0].path
path: newRoute[0].path,
query: newRoute[0].query
});
});
} else {
// 删除缓存路由
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
if (!routerArrays.length) return;
let isHasActiveTag = routerArrays.some(item => {
if (!multiTags.value.length) return;
let isHasActiveTag = multiTags.value.some(item => {
return item.path === route.path;
});
!isHasActiveTag &&
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) {
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) {
case 0:
@@ -230,61 +389,32 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
break;
case 1:
// 关闭当前标签页
selectRoute
? deleteMenu({
path: selectRoute.path,
meta: selectRoute.meta,
name: selectRoute.name
})
: deleteMenu({ path: route.path, meta: route.meta });
deleteMenu(selectTagRoute);
break;
case 2:
// 关闭左侧标签页
selectRoute
? deleteMenu(
{
path: selectRoute.path,
meta: selectRoute.meta
},
"left"
)
: deleteMenu({ path: route.path, meta: route.meta }, "left");
deleteMenu(selectTagRoute, "left");
break;
case 3:
// 关闭右侧标签页
selectRoute
? deleteMenu(
{
path: selectRoute.path,
meta: selectRoute.meta
},
"right"
)
: deleteMenu({ path: route.path, meta: route.meta }, "right");
deleteMenu(selectTagRoute, "right");
break;
case 4:
// 关闭其他标签页
selectRoute
? deleteMenu(
{
path: selectRoute.path,
meta: selectRoute.meta
},
"other"
)
: deleteMenu({ path: route.path, meta: route.meta }, "other");
deleteMenu(selectTagRoute, "other");
break;
case 5:
// 关闭全部标签页
routerArrays.splice(1, routerArrays.length);
relativeStorage.routesInStorage = routerArrays;
useMultiTagsStoreHook().handleTags("splice", "", {
startIndex: 1,
length: multiTags.value.length
});
usePermissionStoreHook().clearAllCachePage();
router.push("/welcome");
break;
}
setTimeout(() => {
showMenuModel(route.fullPath);
showMenuModel(route.fullPath, route.query);
});
}
@@ -310,18 +440,31 @@ function disabledMenus(value: boolean) {
}
// 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页
function showMenuModel(currentPath: string, refresh = false) {
let allRoute = unref(relativeStorage.routesInStorage);
let routeLength = unref(relativeStorage.routesInStorage).length;
// currentIndex为1时左侧的菜单是首页则不显示关闭左侧标签页
let currentIndex = allRoute.findIndex(v => v.path === currentPath);
// 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页
function showMenuModel(
currentPath: string,
query: object = {},
refresh = false
) {
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);
if (refresh) {
tagsViews.value[0].show = true;
}
/**
* currentIndex为1时左侧的菜单是首页则不显示关闭左侧标签页
* 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页
*/
if (currentIndex === 1 && routeLength !== 2) {
// 左侧的菜单是首页,右侧存在别的菜单
tagsViews.value[2].show = false;
@@ -360,10 +503,10 @@ function openMenu(tag, e) {
} else if (route.path !== tag.path) {
// 右键菜单不匹配当前路由,隐藏刷新
tagsViews.value[0].show = false;
showMenuModel(tag.path);
showMenuModel(tag.path, tag.query);
} else if (
// eslint-disable-next-line no-dupe-else-if
relativeStorage.routesInStorage.length === 2 &&
multiTags.value.length === 2 &&
route.path !== tag.path
) {
showMenus(true);
@@ -371,7 +514,7 @@ function openMenu(tag, e) {
tagsViews.value[4].show = false;
} else if (route.path === tag.path) {
// 右键当前激活的菜单
showMenuModel(tag.path, true);
showMenuModel(tag.path, tag.query, true);
}
currentSelect.value = tag;
@@ -385,7 +528,9 @@ function openMenu(tag, e) {
} else {
buttonLeft.value = left;
}
buttonTop.value = e.clientY + 10;
pureSetting.hiddenSideBar
? (buttonTop.value = e.clientY)
: (buttonTop.value = e.clientY - 40);
setTimeout(() => {
visible.value = true;
}, 10);
@@ -393,7 +538,11 @@ function openMenu(tag, e) {
// 触发tags标签切换
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(() => {
if (!instance) return;
relativeStorage = instance.appContext.app.config.globalProperties.$storage;
routerArrays = relativeStorage.routesInStorage ?? routerArrays;
// 根据当前路由初始化操作标签页的禁用状态
showMenuModel(route.fullPath);
@@ -466,40 +613,51 @@ onBeforeMount(() => {
<template>
<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
v-for="(item, index) in dynamicTagList"
:key="index"
:ref="'dynamic' + index"
: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)"
class="tab"
ref="tabDom"
:style="{ transform: `translateX(${translateX}px)` }"
>
<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
:ref="'schedule' + index"
v-if="showModel !== 'card'"
:class="[$route.path === item.path ? 'schedule-active' : '']"
></div>
:ref="'dynamic' + index"
v-for="(item, index) in multiTags"
:key="index"
: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>
</el-scrollbar>
</div>
<i class="ri-arrow-right-s-line" @click="handleScroll(-200)"></i>
<!-- 右键菜单按钮 -->
<transition name="el-zoom-in-top">
<ul
@@ -560,277 +718,5 @@ onBeforeMount(() => {
</template>
<style lang="scss" scoped>
@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);
}
}
.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;
}
@import "./index.scss";
</style>

View File

@@ -1,13 +1,9 @@
<script setup lang="ts">
import {
h,
ref,
unref,
reactive,
computed,
onMounted,
watchEffect,
onBeforeMount,
defineComponent,
getCurrentInstance
} from "vue";
@@ -15,12 +11,13 @@ import { setType } from "./types";
import { useI18n } from "vue-i18n";
import { routerArrays } from "./types";
import { emitter } from "/@/utils/mitt";
import { useEventListener } from "@vueuse/core";
import backTop from "/@/assets/svg/back_top.svg";
import { useAppStoreHook } from "/@/store/modules/app";
import fullScreen from "/@/assets/svg/full_screen.svg";
import exitScreen from "/@/assets/svg/exit_screen.svg";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
import navbar from "./components/navbar.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 Horizontal from "./components/sidebar/horizontal.vue";
const instance = getCurrentInstance().appContext.app.config.globalProperties;
const hiddenSideBar = ref(instance.$config?.HiddenSideBar);
const isMobile = deviceDetection();
const pureSetting = useSettingStoreHook();
const instance = getCurrentInstance().appContext.app.config.globalProperties;
// 清空缓存后从serverConfig.json读取默认配置并赋值到storage中
const layout = computed(() => {
// 路由
if (
!instance.$storage.routesInStorage ||
instance.$storage.routesInStorage.length === 0
useMultiTagsStore().multiTagsCache &&
(!instance.$storage.tags || instance.$storage.tags.length === 0)
) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.routesInStorage = routerArrays;
instance.$storage.tags = routerArrays;
}
// 国际化
if (!instance.$storage.locale) {
@@ -63,7 +60,8 @@ const layout = computed(() => {
instance.$storage.sets = {
grey: instance.$config?.Grey ?? 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;
@@ -96,10 +94,6 @@ const set: setType = reactive({
})
});
const handleClickOutside = (params: boolean) => {
useAppStoreHook().closeSideBar({ withoutAnimation: params });
};
function setTheme(layoutModel: string) {
window.document.body.setAttribute("layout", layoutModel);
instance.$storage.layout = {
@@ -108,51 +102,52 @@ function setTheme(layoutModel: string) {
};
}
// 监听容器
emitter.on("resize", ({ detail }) => {
let { width } = detail;
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);
function toggle(device: string, bool: boolean) {
useAppStoreHook().toggleDevice(device);
useAppStoreHook().toggleSideBar(bool, "resize");
}
onMounted(() => {
const isMobile = $_isMobile();
if (isMobile) {
useAppStoreHook().toggleDevice("mobile");
handleClickOutside(true);
// 判断是否可自动关闭菜单栏
let isAutoCloseSidebar = 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(() => {
useEventListener("resize", $_resizeHandler);
onMounted(() => {
if (isMobile) {
toggle("mobile", false);
}
});
function onFullScreen() {
pureSetting.hiddenSideBar
? pureSetting.changeSetting({ key: "hiddenSideBar", value: false })
: pureSetting.changeSetting({ key: "hiddenSideBar", value: true });
}
const layoutHeader = defineComponent({
render() {
return h(
@@ -167,10 +162,10 @@ const layoutHeader = defineComponent({
},
{
default: () => [
!hiddenSideBar.value && layout.value.includes("vertical")
!pureSetting.hiddenSideBar && layout.value.includes("vertical")
? h(navbar)
: h("div"),
!hiddenSideBar.value && layout.value.includes("horizontal")
!pureSetting.hiddenSideBar && layout.value.includes("horizontal")
? h(Horizontal)
: h("div"),
h(
@@ -183,7 +178,7 @@ const layoutHeader = defineComponent({
{ onClick: onFullScreen },
{
default: () => [
!hiddenSideBar.value ? h(fullScreen) : h(exitScreen)
!pureSetting.hiddenSideBar ? h(fullScreen) : h(exitScreen)
]
}
)
@@ -205,11 +200,18 @@ const layoutHeader = defineComponent({
set.sidebar.opened &&
layout.includes('vertical')
"
class="drawer-bg"
@click="handleClickOutside(false)"
class="app-mask"
@click="useAppStoreHook().toggleSideBar()"
/>
<Vertical v-show="!hiddenSideBar && layout.includes('vertical')" />
<div :class="['main-container', hiddenSideBar ? 'main-hidden' : '']">
<Vertical
v-show="!pureSetting.hiddenSideBar && layout.includes('vertical')"
/>
<div
:class="[
'main-container',
pureSetting.hiddenSideBar ? 'main-hidden' : ''
]"
>
<div v-if="set.fixedHeader">
<layout-header />
<!-- 主体内容 -->
@@ -257,7 +259,7 @@ const layoutHeader = defineComponent({
margin-left: 0 !important;
}
.drawer-bg {
.app-mask {
background: #000;
opacity: 0.3;
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;
$menuBg: #0b1e15;
$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;
//菜单背景
/* 菜单背景 */
$menuBg: #001529 !default;
// 鼠标覆盖到菜单时的背景
/* 鼠标覆盖到菜单时的背景 */
$menuHover: #4091f7 !default;
// 子菜单背景
/* 子菜单背景 */
$subMenuBg: #0f0303 !default;
// 有无子集的激活菜单背景
/* 有无子集的激活菜单背景 */
$subMenuActiveBg: #4091f7 !default;
$navTextColor: #fff !default;
$menuText: rgba(254, 254, 254, 0.65) !default;
// logo背景颜色
/* logo背景颜色 */
$sidebarLogo: #002140 !default;
// 鼠标覆盖到菜单时的字体颜色
/* 鼠标覆盖到菜单时的字体颜色 */
$menuTitleHover: #fff !default;
$menuActiveBefore: #4091f7 !default;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ export const routerArrays: Array<RouteConfigs> = [
parentPath: "/",
meta: {
title: "message.hshome",
i18n: true,
icon: "el-icon-s-home",
showLink: true
}
@@ -13,17 +14,21 @@ export const routerArrays: Array<RouteConfigs> = [
export type RouteConfigs = {
path?: string;
parentPath?: string;
query?: object;
meta?: {
title?: string;
i18n?: boolean;
icon?: string;
showLink?: boolean;
savedPosition?: boolean;
authority?: Array<string>;
};
children?: RouteConfigs[];
name?: string;
};
export type relativeStorageType = {
routesInStorage: Array<RouteConfigs>;
export type multiTagsType = {
tags: Array<RouteConfigs>;
};
export type tagsViewsType = {
@@ -38,6 +43,7 @@ export interface setType {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
isClickHamburger: boolean;
};
device: string;
fixedHeader: boolean;
@@ -58,6 +64,7 @@ export type childrenType = {
meta?: {
icon?: string;
title?: string;
i18n?: boolean;
extraIcon?: {
svg?: boolean;
name?: string;
@@ -70,3 +77,9 @@ export type themeColorsType = {
rgb: 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 { MotionPlugin } from "@vueuse/motion";
import { useTable } from "../src/plugins/vxe-table";
import { useFontawesome } from "../src/plugins/fontawesome";
import { useElementPlus } from "../src/plugins/element-plus";
import { injectResponsiveStorage } from "/@/utils/storage/responsive";
@@ -33,7 +34,8 @@ getServerConfig(app).then(async config => {
.use(MotionPlugin)
.use(useElementPlus)
.use(useTable)
.use(usI18n);
.use(usI18n)
.use(useFontawesome);
await router.isReady();
app.mount("#app");
});

View File

@@ -23,7 +23,6 @@ import {
ElInput,
ElForm,
ElFormItem,
ElLoading,
ElPopover,
ElPopper,
ElTooltip,
@@ -36,26 +35,22 @@ import {
ElDescriptions,
ElDescriptionsItem,
ElBacktop,
ElSwitch
ElSwitch,
ElBadge,
ElTabs,
ElTabPane,
ElAvatar,
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2,
// 指令
ElLoading,
ElInfiniteScroll
} from "element-plus";
// https://element-plus.org/zh-CN/component/icon.html
import {
Check,
Menu,
HomeFilled,
SetUp,
Edit,
Setting,
Lollipop,
Link,
Position,
Histogram,
RefreshRight,
ArrowDown,
Close,
CloseBold
} from "@element-plus/icons";
// Directives
const plugins = [ElLoading, ElInfiniteScroll];
const components = [
ElTag,
@@ -94,8 +89,18 @@ const components = [
ElDescriptionsItem,
ElBacktop,
ElSwitch,
ElBadge,
ElTabs,
ElTabPane,
ElAvatar,
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2
];
// icon
// https://element-plus.org/zh-CN/component/icon.html
import {
Check,
Menu,
HomeFilled,
@@ -109,16 +114,40 @@ const components = [
RefreshRight,
ArrowDown,
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) {
// 注册组件
components.forEach((component: Component) => {
app.component(component.name, component);
});
// 注册指令
plugins.forEach(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: "权限管理",
permissionPage: "页面权限",
permissionButton: "按钮权限",
hstabs: "标签页操作",
hsMenuTree: "菜单树结构",
externalLink: "外链"
}
},
@@ -78,6 +80,8 @@ export const menusConfig = {
permission: "Permission Manage",
permissionPage: "Page Permission",
permissionButton: "Button Permission",
hstabs: "Tabs Operate",
hsMenuTree: "Menu Tree",
externalLink: "External Link"
}
}

View File

@@ -13,3 +13,27 @@ export const i18n = createI18n({
export function usI18n(app: App) {
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 { App } from "vue";
import { i18n } from "../i18n/index";
import "font-awesome/css/font-awesome.css";
import {
// 核心
VXETable,

View File

@@ -1,140 +1,27 @@
import {
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 { toRouteType } from "./types";
import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress";
import { useTimeoutFn } from "@vueuse/core";
import { storageSession, storageLocal } from "/@/utils/storage";
import { usePermissionStoreHook } from "/@/store/modules/permission";
// 静态路由
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 { constantRoutes } from "./modules";
import { split, findIndex } from "lodash-es";
import { transformI18n } from "/@/plugins/i18n";
import remainingRouter from "./modules/remaining";
import flowChartRouter from "./modules/flowchart";
import componentsRouter from "./modules/components";
// 动态路由
import { getAsyncRoutes } from "/@/api/routes";
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/*/*/*.vue");
const constantRoutes: Array<RouteComponent> = [
homeRouter,
flowChartRouter,
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;
};
import { storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
import {
initRouter,
getHistoryMode,
getParentPaths,
findRouteByPath,
handleAliveRoute
} from "./utils";
// 创建路由实例
export const router: Router = createRouter({
history: createWebHashHistory(),
routes: filterTree(ascending(constantRoutes)).concat(...remainingRouter),
history: getHistoryMode(),
routes: constantRoutes.concat(...remainingRouter),
strict: true,
scrollBehavior(to, from, savedPosition) {
return new Promise(resolve => {
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"];
router.beforeEach((to, _from, next) => {
router.beforeEach((to: toRouteType, _from, next) => {
if (to.meta?.keepAlive) {
const newMatched = to.matched;
handleAliveRoute(newMatched, "add");
@@ -208,10 +52,15 @@ router.beforeEach((to, _from, next) => {
const name = storageSession.getItem("info");
NProgress.start();
const externalLink = to?.redirectedFrom?.fullPath;
// @ts-ignore
const { t } = i18n.global;
// @ts-ignore
if (!externalLink) to.meta.title ? (document.title = t(to.meta.title)) : "";
if (!externalLink)
to.matched.some(item => {
item.meta.title
? (document.title = transformI18n(
item.meta.title as string,
item.meta?.i18n as boolean
))
: "";
});
if (name) {
if (_from?.name) {
// 如果路由包含http 则是超链接 反之是普通路由
@@ -223,26 +72,74 @@ router.beforeEach((to, _from, next) => {
}
} else {
// 刷新
if (usePermissionStoreHook().wholeRoutes.length === 0)
if (usePermissionStoreHook().wholeMenus.length === 0)
initRouter(name.username).then((router: Router) => {
router.push(to.path);
// 刷新页面更新标签栏与页面路由匹配
const localRoutes = storageLocal.getItem(
"responsive-routesInStorage"
);
const optionsRoutes = router.options?.routes;
const newLocalRoutes = [];
optionsRoutes.forEach(ors => {
localRoutes.forEach(lrs => {
if (ors.path === lrs.parentPath) {
newLocalRoutes.push(lrs);
if (!useMultiTagsStoreHook().getMultiTagsCache) {
const handTag = (
path: string,
parentPath: string,
name: RouteRecordName,
meta: RouteMeta
): void => {
useMultiTagsStoreHook().handleTags("push", {
path,
parentPath,
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(
"responsive-routesInStorage",
uniqBy(newLocalRoutes, "path")
);
}
}
router.push(to.fullPath);
});
next();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ const remainingRouter = [
meta: {
title: "message.hslogin",
showLink: false,
i18n: true,
rank: 101
}
},
@@ -18,6 +19,7 @@ const remainingRouter = [
meta: {
icon: "HomeFilled",
title: "message.hshome",
i18n: true,
showLink: false,
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 { deviceDetection } from "/@/utils/deviceDetection";
import { defineStore } from "pinia";
import { store } from "/@/store";
import { appType } from "./types";
import { defineStore } from "pinia";
import { getConfig } from "/@/config";
interface AppState {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
};
layout: string;
device: string;
}
export const useAppStore = defineStore({
id: "pure-app",
state: (): AppState => ({
state: (): appType => ({
sidebar: {
opened: storageLocal.getItem("sidebarStatus")
? !!+storageLocal.getItem("sidebarStatus")
: true,
withoutAnimation: false
withoutAnimation: false,
isClickHamburger: false
},
// 这里的layout用于监听容器拖拉后恢复对应的导航模式
layout:
@@ -36,28 +29,27 @@ export const useAppStore = defineStore({
}
},
actions: {
TOGGLE_SIDEBAR() {
this.sidebar.opened = !this.sidebar.opened;
this.sidebar.withoutAnimation = false;
if (this.sidebar.opened) {
storageLocal.setItem("sidebarStatus", 1);
} else {
storageLocal.setItem("sidebarStatus", 0);
TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
if (opened && resize) {
this.sidebar.withoutAnimation = true;
this.sidebar.opened = true;
storageLocal.setItem("sidebarStatus", true);
} else if (!opened && resize) {
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) {
this.device = device;
},
async toggleSideBar() {
await this.TOGGLE_SIDEBAR();
},
closeSideBar(withoutAnimation) {
this.CLOSE_SIDEBAR(withoutAnimation);
async toggleSideBar(opened?: boolean, resize?: string) {
await this.TOGGLE_SIDEBAR(opened, resize);
},
toggleDevice(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 { store } from "/@/store";
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({
id: "pure-permission",
state: () => ({
// 静态路由
constantRoutes: constantRoutesArr,
wholeRoutes: [],
// 静态路由生成的菜单
constantMenus,
// 整体路由生成的菜单(静态、动态)
wholeMenus: [],
// 深拷贝一个菜单树,与导航菜单不突出
menusTree: [],
buttonAuth: [],
// 缓存页面keepAlive
cachePageList: []
}),
actions: {
// 获取异步路由菜单
asyncActionRoutes(routes) {
if (this.wholeRoutes.length > 0) return;
this.wholeRoutes = filterTree(
ascending(this.constantRoutes.concat(routes))
if (this.wholeMenus.length > 0) return;
this.wholeMenus = filterTree(
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;
arrRoutes.forEach((v: any) => {
arrRoutes.forEach((v: RouteConfigs) => {
if (v.meta && 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) {
await this.asyncActionRoutes(routes);

View File

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

View File

@@ -4,3 +4,38 @@ export type cacheType = {
mode: string;
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 a {
font-weight: 400 !important;
@@ -15,7 +13,6 @@
display: none;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
@@ -27,17 +24,14 @@
}
}
// dropdown
.el-dropdown-menu {
padding: 2px 0 2px 0 !important;
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
// el-tooltip的权重
.is-dark {
z-index: 99999 !important;
}

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
// global transition css
/* fade */
.fade-enter-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";
export const http = new EnclosureHttp();
import Axios, { AxiosInstance, AxiosRequestConfig } from "axios";
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, {
AxiosRequestConfig,
Canceler,
AxiosResponse,
Method,
AxiosError
AxiosError,
AxiosResponse,
AxiosRequestConfig
} from "axios";
import { METHODS } from "./config";
export type cancelTokenType = { cancelKey: string; cancelExecutor: Canceler };
export type resultType = {
accessToken?: string;
};
export type RequestMethods = Extract<
Method,
"get" | "post" | "put" | "delete" | "patch" | "option" | "head"
>;
export interface EnclosureHttpRequestConfig extends AxiosRequestConfig {
beforeRequestCallback?: (request: EnclosureHttpRequestConfig) => void; // 请求发送之前
beforeResponseCallback?: (response: EnclosureHttpResoponse) => void; // 相应返回之前
}
export interface EnclosureHttpResoponse extends AxiosResponse {
config: EnclosureHttpRequestConfig;
}
export interface EnclosureHttpError extends AxiosError {
export interface PureHttpError extends AxiosError {
isCancelRequest?: boolean;
}
export default class EnclosureHttp {
cancelTokenList: Array<cancelTokenType>;
clearCancelTokenList(): void;
export interface PureHttpResoponse extends AxiosResponse {
config: PureHttpRequestConfig;
}
export interface PureHttpRequestConfig extends AxiosRequestConfig {
beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
beforeResponseCallback?: (response: PureHttpResoponse) => void;
}
export default class PureHttp {
request<T>(
method: RequestMethods,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: EnclosureHttpRequestConfig
): Promise<T>;
post<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T>;
get<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
axiosConfig?: PureHttpRequestConfig
): 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,
// 是否显示加载ico
showSpinner: true,
showSpinner: false,
// 自动递增间隔
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 {
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";
export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
app.use(Storage, {
// 默认显示首页tag
routesInStorage: {
type: Array,
default: Storage.getData(undefined, "routesInStorage") ?? [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "message.hshome",
icon: "el-icon-s-home",
showLink: true
const configObj = Object.assign(
{
// 国际化 默认中文zh
locale: {
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,
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: {
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
}
}
});
: {}
);
app.use(Storage, configObj);
};

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