Compare commits

..

23 Commits

Author SHA1 Message Date
Soybean
1d14269557 optimize(typings): update component typings 2025-10-28 18:37:24 +08:00
Soybean
5cfb606042 chore(projects): release v2.0.0-beta.2 2025-10-27 18:50:16 +08:00
Soybean
e471e9140e fix(layout): fix getSiderWidth 2025-10-27 18:49:54 +08:00
Soybean
b2c919b673 feat(global-tab): add support for switching tabs with right mouse button click 2025-10-27 17:51:56 +08:00
Soybean
8dc17e62f1 fix(scripts): update command to use 'npm-check-updates' instead of 'ncu' 2025-10-27 17:42:04 +08:00
Soybean
60115572a8 Merge branch 'v2.0' 2025-10-25 15:13:46 +08:00
Soybean
56ad0df974 chore(projects): release v2.0.0-beta.1 2025-10-25 15:06:55 +08:00
Soybean
6cbf5705bb chore(deps): update deps 2025-10-25 15:01:41 +08:00
Soybean
87a66a4236 optimize(projects): optimize radius settings 2025-10-25 14:52:10 +08:00
CyberShen123
3549c4dbd5 feat(projects): support set global redius 2025-10-25 14:47:04 +08:00
CyberShen123
24c6df528b feat(projects): support set global redius 2025-10-25 14:47:04 +08:00
whyang
dac5075be9 fix(styles): show light color scrollbar while dark mode is on 2025-10-25 14:42:18 +08:00
wenyuan
a8d1e5d266 feat(projects): support closing tabs with middle mouse button click 2025-10-25 14:34:33 +08:00
wenyuan
345aa2932f feat(styles): add text-autospace property to improve text layout 2025-10-25 14:25:20 +08:00
wenyuan
017440c1e6 docs(projects): add contribution leaderboard 2025-10-21 21:29:31 +08:00
恕瑞玛的皇帝
e18d39724a docs(projects): add github trendshift info. 2025-09-27 21:17:09 +08:00
Soybean
fd087f59fa fix(packages): axios: fix json response. fixed #815 2025-09-17 17:03:30 +08:00
Soybean
9fa951aa06 chore(deps): update deps 2025-09-17 14:23:06 +08:00
Soybean
12b25e0d58 fix(types): fix proxy types 2025-08-28 00:26:29 +08:00
Soybean
e33f944a74 chore(deps): update deps 2025-08-28 00:26:12 +08:00
Azir-11
8d7f91dccf chore(other): update the ESLint validation configuration to support more file types. 2025-08-13 10:10:10 +08:00
Soybean
33ade53904 chore(readme): remove DartNode sponsorship badge from README files 2025-08-11 10:46:15 +08:00
Soybean
923eb98a5c fix(readme): update GitHub stars and forks links for gitee 2025-07-20 01:12:19 +08:00
70 changed files with 1824 additions and 1427 deletions

2
.gitignore vendored
View File

@@ -33,5 +33,3 @@ package-lock.json
yarn.lock yarn.lock
.VSCodeCounter .VSCodeCounter
.temp

View File

@@ -1,6 +1,152 @@
# Changelog # Changelog
## [v2.0.0-beta.2](https://github.com/soybeanjs/soybean-admin/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2025-10-27)
###    🚀 Features
- **global-tab**: add support for switching tabs with right mouse button click &nbsp;-&nbsp; by @soybeanjs [<samp>(b2c91)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b2c919b6)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **layout**: fix getSiderWidth &nbsp;-&nbsp; by @soybeanjs [<samp>(e471e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e471e914)
- **packages**: axios: fix json response. fixed #815 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/815 [<samp>(fd087)</samp>](https://github.com/soybeanjs/soybean-admin/commit/fd087f59)
- **readme**: update GitHub stars and forks links for gitee &nbsp;-&nbsp; by @soybeanjs [<samp>(923eb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/923eb98a)
- **scripts**: update command to use 'npm-check-updates' instead of 'ncu' &nbsp;-&nbsp; by @soybeanjs [<samp>(8dc17)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8dc17e62)
- **types**: fix proxy types &nbsp;-&nbsp; by @soybeanjs [<samp>(12b25)</samp>](https://github.com/soybeanjs/soybean-admin/commit/12b25e0d)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**:
- add github trendshift info. &nbsp;-&nbsp; by **恕瑞玛的皇帝** [<samp>(e18d3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e18d3972)
- add contribution leaderboard &nbsp;-&nbsp; by @wenyuanw [<samp>(01744)</samp>](https://github.com/soybeanjs/soybean-admin/commit/017440c1)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**:
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(e33f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e33f944a)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(9fa95)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9fa951aa)
- **other**:
- update the ESLint validation configuration to support more file types. &nbsp;-&nbsp; by **Azir-11** [<samp>(8d7f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8d7f91dc)
- **readme**:
- remove DartNode sponsorship badge from README files &nbsp;-&nbsp; by @soybeanjs [<samp>(33ade)</samp>](https://github.com/soybeanjs/soybean-admin/commit/33ade539)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![wenyuanw](https://github.com/wenyuanw.png?size=48)](https://github.com/wenyuanw)&nbsp;&nbsp;
[恕瑞玛的皇帝](mailto:2075125282@qq.com)
## [v2.0.0-beta.1](https://github.com/soybeanjs/soybean-admin/compare/v1.3.15...v2.0.0-beta.1) (2025-10-25)
### &nbsp;&nbsp;&nbsp;🚨 Breaking Changes
- **hooks**: refactor useTable and enhance type definitions &nbsp;-&nbsp; by @soybeanjs [<samp>(8cc51)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8cc5177)
- **projects**: optimize layout mode, split horizontal mix component into two layouts, and rename the component. &nbsp;-&nbsp; by **Azir** [<samp>(b6ac3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b6ac310)
- **request**: remove cancelRequest method and related logic from request instances &nbsp;-&nbsp; by @soybeanjs [<samp>(b4e12)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b4e1253)
### &nbsp;&nbsp;&nbsp;🚀 Features
- **components**:
- add the IconTooltip component. &nbsp;-&nbsp; by **Azir-11** [<samp>(a55b4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a55b4dc)
- replace NTooltip with IconTooltip and optimize the layout of related components. &nbsp;-&nbsp; by **Azir-11** [<samp>(40057)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4005763)
- **hooks**:
- add scrollX computation for total table width in useNaiveTable &nbsp;-&nbsp; by @Lruihao [<samp>(358e1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/358e129)
- **packages**:
- materials support slider-tab. closed #823 &nbsp;-&nbsp; by @CyberShen in https://github.com/soybeanjs/soybean-admin/issues/823 [<samp>(61fa4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/61fa4b7)
- **projects**:
- refactor theme drawer with tabbed layout for better UX. &nbsp;-&nbsp; by **Azir** [<samp>(8ba71)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8ba71a0)
- Add current time display option for watermark &nbsp;-&nbsp; by @wenyuanw in https://github.com/soybeanjs/soybean-admin/issues/772 [<samp>(f238f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f238fcb)
- add 'vertical-hybrid-header-first' layout mode &nbsp;-&nbsp; by @wenyuanw [<samp>(b4e5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b4e5c6d)
- add prompt information for scrolling mode and tab bar caching. &nbsp;-&nbsp; by **Azir-11** [<samp>(29a2a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/29a2a5c)
- support theme preset function. &nbsp;-&nbsp; by **Azir-11** [<samp>(257f1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/257f118)
- modify the default value of the reset cache policy to 'refresh'. &nbsp;-&nbsp; by **Azir-11** [<samp>(3c0a5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3c0a528)
- optimize tabs cache cleaning strategy. close #820. &nbsp;-&nbsp; by **Azir-11** in https://github.com/soybeanjs/soybean-admin/issues/820 [<samp>(ef7ac)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ef7acc6)
- support closing tabs with middle mouse button click &nbsp;-&nbsp; by @wenyuanw [<samp>(a8d1e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a8d1e5d)
- support set global redius &nbsp;-&nbsp; by **CyberShen123** [<samp>(24c6d)</samp>](https://github.com/soybeanjs/soybean-admin/commit/24c6df5)
- support set global redius &nbsp;-&nbsp; by **CyberShen123** [<samp>(3549c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3549c4d)
- **styles**:
- add text-autospace property to improve text layout &nbsp;-&nbsp; by @wenyuanw [<samp>(345aa)</samp>](https://github.com/soybeanjs/soybean-admin/commit/345aa29)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **hooks**:
- correct chart rendering logic in useEcharts &nbsp;-&nbsp; by @soybeanjs [<samp>(8a7cd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8a7cd59)
- **packages**:
- fix the parsing logic for stored data to ensure correct return of boolean values &nbsp;-&nbsp; by @Lruihao [<samp>(9ea56)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9ea56c9)
- axios: fix json response. fixed #815 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/815 [<samp>(5be86)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5be864a)
- **projects**:
- Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 &nbsp;-&nbsp; by @xiaobao0505 in https://github.com/soybeanjs/soybean-admin/issues/780 [<samp>(41191)</samp>](https://github.com/soybeanjs/soybean-admin/commit/41191d5)
- **styles**:
- show light color scrollbar while dark mode is on &nbsp;-&nbsp; by **whyang** [<samp>(dac50)</samp>](https://github.com/soybeanjs/soybean-admin/commit/dac5075)
- **table**:
- add type annotations for records in useTable hook &nbsp;-&nbsp; by @soybeanjs [<samp>(32b8f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/32b8f99)
- **types**:
- fix proxy types &nbsp;-&nbsp; by @soybeanjs [<samp>(3d72f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3d72f95)
- fix ts type error &nbsp;-&nbsp; by @soybeanjs [<samp>(d5a3a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d5a3a25)
### &nbsp;&nbsp;&nbsp;🛠 Optimizations
- **hooks**:
- optimize useEcharts &nbsp;-&nbsp; by @soybeanjs [<samp>(936b8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/936b834)
- **packages**:
- remove ofetch package &nbsp;-&nbsp; by @soybeanjs [<samp>(abaaa)</samp>](https://github.com/soybeanjs/soybean-admin/commit/abaaa4a)
- **projects**:
- improve theme drawer responsive width for mobile devices &nbsp;-&nbsp; by @wenyuanw [<samp>(8439a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8439a60)
- improve robustness of second-level menu key logic &nbsp;-&nbsp; by @wenyuanw [<samp>(8b8a2)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8b8a208)
- optimize theme drawer width &nbsp;-&nbsp; by @soybeanjs [<samp>(81468)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8146858)
- optimize api type file &nbsp;-&nbsp; by @soybeanjs [<samp>(3a343)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3a343ee)
- optimize radius settings &nbsp;-&nbsp; by @soybeanjs [<samp>(87a66)</samp>](https://github.com/soybeanjs/soybean-admin/commit/87a66a4)
- **request**:
- enhance request options and response handling with generic types &nbsp;-&nbsp; by @soybeanjs [<samp>(50a5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/50a5cba)
### &nbsp;&nbsp;&nbsp;💅 Refactors
- **hooks**:
- optimize useContext and update useMixMenuContext &nbsp;-&nbsp; by @soybeanjs [<samp>(c9651)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c965140)
- streamline column visibility handling in useTable and table components &nbsp;-&nbsp; by @soybeanjs [<samp>(ee434)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ee43414)
- remove useSignal hook and update exports &nbsp;-&nbsp; by @soybeanjs [<samp>(87adc)</samp>](https://github.com/soybeanjs/soybean-admin/commit/87adc35)
- **menu**:
- optimize the margin on the menu &nbsp;-&nbsp; by **NicholasLD** [<samp>(d7311)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d731111)
- **projects**:
- remove unnecessary logic in onRouteSwitchWhenLoggedIn &nbsp;-&nbsp; by @wenyuanw [<samp>(d6c81)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d6c8142)
- **request**:
- unify response transformation methods and deprecate transformBackendResponse &nbsp;-&nbsp; by @soybeanjs [<samp>(f83ee)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f83eefb)
- **types**:
- move Auth and Route namespaces to separate files and clean up api.d.ts &nbsp;-&nbsp; by **Azir** [<samp>(d37ce)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d37ce04)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: add github trendshift info. &nbsp;-&nbsp; by **恕瑞玛的皇帝** [<samp>(2a0c9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2a0c9f1)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**:
- update NodeJS and pnpm version requirements in package.json and documentation &nbsp;-&nbsp; by **Junior25306** [<samp>(a5c4b)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a5c4b4e)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(5cb1c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5cb1ceb)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(aeb63)</samp>](https://github.com/soybeanjs/soybean-admin/commit/aeb6369)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(e89b8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e89b86c)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(c962f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c962f7b)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(12135)</samp>](https://github.com/soybeanjs/soybean-admin/commit/1213531)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(b041f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b041fdd)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(d567c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d567c05)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(6cbf5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/6cbf570)
- **other**:
- update the ESLint validation configuration to support more file types. &nbsp;-&nbsp; by **Azir-11** [<samp>(be8f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/be8f915)
- **packages**:
- update Vite version to 7 in package.json and documentation. &nbsp;-&nbsp; by **Azir** [<samp>(03dd6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/03dd64c)
- add picomatch to fix scripts &nbsp;-&nbsp; by @soybeanjs [<samp>(805c3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/805c338)
- **vscode**:
- remove unused vue.server.hybridMode setting from .vscode/settings.json &nbsp;-&nbsp; by @soybeanjs [<samp>(13319)</samp>](https://github.com/soybeanjs/soybean-admin/commit/133196f)
### &nbsp;&nbsp;&nbsp;🎨 Styles
- **projects**: format code. &nbsp;-&nbsp; by **Azir-11** [<samp>(100e0)</samp>](https://github.com/soybeanjs/soybean-admin/commit/100e0ea)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![wenyuanw](https://github.com/wenyuanw.png?size=48)](https://github.com/wenyuanw)&nbsp;&nbsp;[![CyberShen](https://github.com/CyberShen.png?size=48)](https://github.com/CyberShen)&nbsp;&nbsp;[![Lruihao](https://github.com/Lruihao.png?size=48)](https://github.com/Lruihao)&nbsp;&nbsp;[![xiaobao0505](https://github.com/xiaobao0505.png?size=48)](https://github.com/xiaobao0505)&nbsp;&nbsp;
[CyberShen123](mailto:s.lijun@qq.com),&nbsp;[whyang](mailto:whyang9701@gmail.com),&nbsp;[HongxuanG](mailto:1359774872@qq.com),&nbsp;[Azir-11](mailto:2075125282@qq.com),&nbsp;[NicholasLD](mailto:878639947@qq.com),&nbsp;[Junior25306](mailto:dayu429@qq.com)
## [v1.3.15](https://github.com/soybeanjs/soybean-admin/compare/v1.3.14...v1.3.15) (2025-06-24) ## [v1.3.15](https://github.com/soybeanjs/soybean-admin/compare/v1.3.14...v1.3.15) (2025-06-24)
### &nbsp;&nbsp;&nbsp;🚀 Features ### &nbsp;&nbsp;&nbsp;🚀 Features

View File

@@ -7,11 +7,10 @@
--- ---
[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) [![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
[![github stars](https://img.shields.io/github/stars/soybeanjs/soybean-admin)](https://github.com/soybeanjs/soybean-admin) [![github stars](https://img.shields.io/github/stars/honghuangdc/soybean-admin)](https://github.com/soybeanjs/soybean-admin)
[![github forks](https://img.shields.io/github/forks/soybeanjs/soybean-admin)](https://github.com/soybeanjs/soybean-admin) [![github forks](https://img.shields.io/github/forks/honghuangdc/soybean-admin)](https://github.com/soybeanjs/soybean-admin)
[![gitee stars](https://gitee.com/honghuangdc/soybean-admin/badge/star.svg)](https://gitee.com/honghuangdc/soybean-admin) [![gitee stars](https://gitee.com/honghuangdc/soybean-admin/badge/star.svg)](https://gitee.com/honghuangdc/soybean-admin)
[![gitcode star](https://gitcode.com/soybeanjs/soybean-admin/star/badge.svg)](https://gitcode.com/soybeanjs/soybean-admin) [![gitcode star](https://gitcode.com/soybeanjs/soybean-admin/star/badge.svg)](https://gitcode.com/soybeanjs/soybean-admin)
[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
<div style="display: flex; gap: 12px; align-items: center;"> <div style="display: flex; gap: 12px; align-items: center;">
<a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -181,6 +180,14 @@ Thanks the following people for their contributions. If you want to contribute t
<img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" /> <img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" />
</a> </a>
---
Here are the most active contributors from the past year. Thank you all for your support, which has enabled the project's continued development.
<a href="https://openomy.com/soybeanjs/soybean-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://www.openomy.com/svg?repo=soybeanjs/soybean-admin&chart=list&latestMonth=12" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
## Communication ## Communication
`SoybeanAdmin` is a completely open source and free project, helping developers to develop medium and large-scale management systems more conveniently. It also provides WeChat and QQ communication groups. If you have any questions, please feel free to ask in the group. `SoybeanAdmin` is a completely open source and free project, helping developers to develop medium and large-scale management systems more conveniently. It also provides WeChat and QQ communication groups. If you have any questions, please feel free to ask in the group.

View File

@@ -7,11 +7,10 @@
--- ---
[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) [![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
[![github stars](https://img.shields.io/github/stars/soybeanjs/soybean-admin)](https://github.com/soybeanjs/soybean-admin) [![github stars](https://img.shields.io/github/stars/honghuangdc/soybean-admin)](https://github.com/soybeanjs/soybean-admin)
[![github forks](https://img.shields.io/github/forks/soybeanjs/soybean-admin)](https://github.com/soybeanjs/soybean-admin) [![github forks](https://img.shields.io/github/forks/honghuangdc/soybean-admin)](https://github.com/soybeanjs/soybean-admin)
[![gitee stars](https://gitee.com/honghuangdc/soybean-admin/badge/star.svg)](https://gitee.com/honghuangdc/soybean-admin) [![gitee stars](https://gitee.com/honghuangdc/soybean-admin/badge/star.svg)](https://gitee.com/honghuangdc/soybean-admin)
[![gitcode star](https://gitcode.com/soybeanjs/soybean-admin/star/badge.svg)](https://gitcode.com/soybeanjs/soybean-admin) [![gitcode star](https://gitcode.com/soybeanjs/soybean-admin/star/badge.svg)](https://gitcode.com/soybeanjs/soybean-admin)
[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
<div style="display: flex; gap: 12px; align-items: center;"> <div style="display: flex; gap: 12px; align-items: center;">
<a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -209,6 +208,14 @@ pnpm build
<img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" /> <img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" />
</a> </a>
---
以下是近一年中活跃度较高的贡献者,感谢各位的支持,让项目得以持续发展。
<a href="https://openomy.com/soybeanjs/soybean-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://www.openomy.com/svg?repo=soybeanjs/soybean-admin&chart=list&latestMonth=12" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
## 交流 ## 交流
`SoybeanAdmin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供微信和 QQ 交流群,使用问题欢迎在群内提问。 `SoybeanAdmin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供微信和 QQ 交流群,使用问题欢迎在群内提问。

View File

@@ -2,7 +2,7 @@ import type { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx'; import vueJsx from '@vitejs/plugin-vue-jsx';
import progress from 'vite-plugin-progress'; import progress from 'vite-plugin-progress';
import elegantRouter from 'elegant-router/vite'; import { setupElegantRouter } from './router';
import { setupUnocss } from './unocss'; import { setupUnocss } from './unocss';
import { setupUnplugin } from './unplugin'; import { setupUnplugin } from './unplugin';
import { setupHtmlPlugin } from './html'; import { setupHtmlPlugin } from './html';
@@ -13,7 +13,7 @@ export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
vue(), vue(),
vueJsx(), vueJsx(),
setupDevtoolsPlugin(viteEnv), setupDevtoolsPlugin(viteEnv),
elegantRouter(), setupElegantRouter(),
setupUnocss(viteEnv), setupUnocss(viteEnv),
...setupUnplugin(viteEnv), ...setupUnplugin(viteEnv),
progress(), progress(),

41
build/plugins/router.ts Normal file
View File

@@ -0,0 +1,41 @@
import type { RouteMeta } from 'vue-router';
import ElegantVueRouter from '@elegant-router/vue/vite';
import type { RouteKey } from '@elegant-router/types';
export function setupElegantRouter() {
return ElegantVueRouter({
layouts: {
base: 'src/layouts/base-layout/index.vue',
blank: 'src/layouts/blank-layout/index.vue'
},
routePathTransformer(routeName, routePath) {
const key = routeName as RouteKey;
if (key === 'login') {
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
const moduleReg = modules.join('|');
return `/login/:module(${moduleReg})?`;
}
return routePath;
},
onRouteMetaGen(routeName) {
const key = routeName as RouteKey;
const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
const meta: Partial<RouteMeta> = {
title: key,
i18nKey: `route.${key}` as App.I18n.I18nKey
};
if (constantRoutes.includes(key)) {
meta.constant = true;
}
return meta;
}
});
}

View File

@@ -1,37 +0,0 @@
import type { RouteMeta } from 'vue-router';
import { defineConfig } from 'elegant-router';
import type { RouteKey } from '@elegant-router/types';
export default defineConfig({
pageDir: ['src/views'],
layouts: {
base: 'src/layouts/base-layout/index.vue',
blank: 'src/layouts/blank-layout/index.vue'
},
getRoutePath: node => {
if (node.name === 'Login') {
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
const moduleReg = modules.join('|');
return `/login/:module(${moduleReg})?`;
}
return node.path;
},
getRouteMeta: node => {
const constantRoutes: RouteKey[] = ['Login', '403', '404', '500'];
const name = node.name as RouteKey;
const meta: Partial<RouteMeta> = {
title: name
};
if (constantRoutes.includes(name)) {
meta.constant = true;
}
return meta;
}
});

View File

@@ -1,7 +1,7 @@
{ {
"name": "soybean-admin", "name": "soybean-admin",
"type": "module", "type": "module",
"version": "1.3.15", "version": "2.0.0-beta.2",
"description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。", "description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
"author": { "author": {
"name": "Soybean", "name": "Soybean",
@@ -54,7 +54,7 @@
"@sa/hooks": "workspace:*", "@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*", "@sa/materials": "workspace:*",
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"@vueuse/core": "13.9.0", "@vueuse/core": "14.0.0",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"dayjs": "1.11.18", "dayjs": "1.11.18",
"defu": "6.1.4", "defu": "6.1.4",
@@ -67,41 +67,40 @@
"vue": "3.5.22", "vue": "3.5.22",
"vue-draggable-plus": "0.6.0", "vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.12", "vue-i18n": "11.1.12",
"vue-router": "4.5.1" "vue-router": "4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@elegant-router/vue": "0.3.8", "@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.395", "@iconify/json": "2.2.400",
"@sa/scripts": "workspace:*", "@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*", "@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1", "@soybeanjs/eslint-config": "1.7.1",
"@types/node": "24.7.2", "@types/node": "24.9.1",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.5.3", "@unocss/eslint-config": "66.5.4",
"@unocss/preset-icons": "66.5.3", "@unocss/preset-icons": "66.5.4",
"@unocss/preset-uno": "66.5.3", "@unocss/preset-uno": "66.5.4",
"@unocss/transformer-directives": "66.5.3", "@unocss/transformer-directives": "66.5.4",
"@unocss/transformer-variant-group": "66.5.3", "@unocss/transformer-variant-group": "66.5.4",
"@unocss/vite": "66.5.3", "@unocss/vite": "66.5.4",
"@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "5.1.1", "@vitejs/plugin-vue-jsx": "5.1.1",
"consola": "3.4.2", "consola": "3.4.2",
"elegant-router": "1.0.4", "eslint": "9.38.0",
"eslint": "9.37.0", "eslint-plugin-vue": "10.5.1",
"eslint-plugin-vue": "10.5.0",
"kolorist": "1.8.0", "kolorist": "1.8.0",
"sass": "1.93.2", "sass": "1.93.2",
"simple-git-hooks": "2.13.1", "simple-git-hooks": "2.13.1",
"tsx": "4.20.6", "tsx": "4.20.6",
"typescript": "5.9.3", "typescript": "5.9.3",
"unplugin-icons": "22.4.2", "unplugin-icons": "22.5.0",
"unplugin-vue-components": "29.1.0", "unplugin-vue-components": "30.0.0",
"vite": "7.1.9", "vite": "7.1.12",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "8.0.2", "vite-plugin-vue-devtools": "8.0.3",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.1" "vue-tsc": "3.1.2"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify", "commit-msg": "pnpm sa git-commit-verify",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/alova", "name": "@sa/alova",
"version": "1.3.15", "version": "2.0.0-beta.2",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./fetch": "./src/fetch.ts", "./fetch": "./src/fetch.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/axios", "name": "@sa/axios",
"version": "1.3.15", "version": "2.0.0-beta.2",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/color", "name": "@sa/color",
"version": "1.3.15", "version": "2.0.0-beta.2",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/hooks", "name": "@sa/hooks",
"version": "1.3.15", "version": "2.0.0-beta.2",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/materials", "name": "@sa/materials",
"version": "1.3.15", "version": "2.0.0-beta.2",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/scripts", "name": "@sa/scripts",
"version": "1.3.15", "version": "2.0.0-beta.2",
"bin": { "bin": {
"sa": "./bin.ts" "sa": "./bin.ts"
}, },
@@ -15,13 +15,13 @@
"devDependencies": { "devDependencies": {
"@soybeanjs/changelog": "0.3.25", "@soybeanjs/changelog": "0.3.25",
"bumpp": "10.3.1", "bumpp": "10.3.1",
"c12": "3.3.0", "c12": "3.3.1",
"cac": "6.7.14", "cac": "6.7.14",
"consola": "3.4.2", "consola": "3.4.2",
"enquirer": "2.4.1", "enquirer": "2.4.1",
"execa": "9.6.0", "execa": "9.6.0",
"kolorist": "1.8.0", "kolorist": "1.8.0",
"npm-check-updates": "19.0.0", "npm-check-updates": "19.1.1",
"picomatch": "4.0.3", "picomatch": "4.0.3",
"rimraf": "6.0.1" "rimraf": "6.0.1"
} }

View File

@@ -1,5 +1,5 @@
import { execCommand } from '../shared'; import { execCommand } from '../shared';
export async function updatePkg(args: string[] = ['--deep', '-u']) { export async function updatePkg(args: string[] = ['--deep', '-u']) {
execCommand('npx', ['ncu', ...args], { stdio: 'inherit' }); execCommand('npx', ['npm-check-updates', ...args], { stdio: 'inherit' });
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/uno-preset", "name": "@sa/uno-preset",
"version": "1.3.15", "version": "2.0.0-beta.2",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/utils", "name": "@sa/utils",
"version": "1.3.15", "version": "2.0.0-beta.2",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

1511
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext(); const { secondLevelMenus, childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext();
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue')); const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
@@ -77,9 +77,9 @@ const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-h
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first'); const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
const siderWidth = computed(() => getSiderWidth()); const siderWidth = computed(() => getSiderAndCollapsedWidth(false));
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth()); const siderCollapsedWidth = computed(() => getSiderAndCollapsedWidth(true));
function getSiderAndCollapsedWidth(isCollapsed: boolean) { function getSiderAndCollapsedWidth(isCollapsed: boolean) {
const { const {
@@ -104,7 +104,7 @@ function getSiderAndCollapsedWidth(isCollapsed: boolean) {
const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value; const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
let finalWidth = isMixMode ? mixWidth : width; let finalWidth = isMixMode ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) { if (isVerticalMix.value && appStore.mixSiderFixed && secondLevelMenus.value.length) {
finalWidth += mixChildMenuWidth; finalWidth += mixChildMenuWidth;
} }
@@ -114,14 +114,6 @@ function getSiderAndCollapsedWidth(isCollapsed: boolean) {
return finalWidth; return finalWidth;
} }
function getSiderWidth() {
return getSiderAndCollapsedWidth(false);
}
function getSiderCollapsedWidth() {
return getSiderAndCollapsedWidth(true);
}
</script> </script>
<template> <template>

View File

@@ -26,6 +26,8 @@ const tabRef = ref<HTMLElement>();
const isPCFlag = isPC(); const isPCFlag = isPC();
const TAB_DATA_ID = 'data-tab-id'; const TAB_DATA_ID = 'data-tab-id';
const MIDDLE_MOUSE_BUTTON = 1;
const RIGHT_MOUSE_BUTTON = 2;
type TabNamedNodeMap = NamedNodeMap & { type TabNamedNodeMap = NamedNodeMap & {
[TAB_DATA_ID]: Attr; [TAB_DATA_ID]: Attr;
@@ -84,6 +86,26 @@ function handleCloseTab(tab: App.Global.Tab) {
tabStore.removeTab(tab.id); tabStore.removeTab(tab.id);
} }
function handleMousedown(e: MouseEvent, tab: App.Global.Tab) {
const isMiddleClick = e.button === MIDDLE_MOUSE_BUTTON;
if (!isMiddleClick || !themeStore.tab.closeTabByMiddleClick) {
return;
}
if (tabStore.isTabRetain(tab.id)) {
return;
}
e.preventDefault();
handleCloseTab(tab);
}
function switchTab(e: MouseEvent, tab: App.Global.Tab) {
if ([MIDDLE_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON].includes(e.button)) return;
tabStore.switchRouteByTab(tab);
}
async function refresh() { async function refresh() {
appStore.reloadPage(500); appStore.reloadPage(500);
} }
@@ -182,7 +204,8 @@ init();
:active="tab.id === tabStore.activeTabId" :active="tab.id === tabStore.activeTabId"
:active-color="themeStore.themeColor" :active-color="themeStore.themeColor"
:closable="!tabStore.isTabRetain(tab.id)" :closable="!tabStore.isTabRetain(tab.id)"
@pointerdown="tabStore.switchRouteByTab(tab)" @pointerdown="switchTab($event, tab)"
@mousedown="handleMousedown($event, tab)"
@close="handleCloseTab(tab)" @close="handleCloseTab(tab)"
@contextmenu="handleContextMenu($event, tab.id)" @contextmenu="handleContextMenu($event, tab.id)"
> >

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ThemeSchema from './modules/theme-schema.vue'; import ThemeSchema from './modules/theme-schema.vue';
import ThemeColor from './modules/theme-color.vue'; import ThemeColor from './modules/theme-color.vue';
import ThemeRadius from './modules/theme-radius.vue';
defineOptions({ defineOptions({
name: 'AppearanceSettings' name: 'AppearanceSettings'
@@ -11,6 +12,7 @@ defineOptions({
<div class="flex-col-stretch gap-16px"> <div class="flex-col-stretch gap-16px">
<ThemeSchema /> <ThemeSchema />
<ThemeColor /> <ThemeColor />
<ThemeRadius />
</div> </div>
</template> </template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../../../components/setting-item.vue';
defineOptions({
name: 'ThemeRadius'
});
const themeStore = useThemeStore();
</script>
<template>
<NDivider>{{ $t('theme.appearance.themeRadius.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.appearance.themeRadius.title')">
<NInputNumber v-model:value="themeStore.themeRadius" size="small" :step="1" :min="0" :max="16" class="w-120px" />
</SettingItem>
</TransitionGroup>
</template>
<style scoped></style>

View File

@@ -35,6 +35,12 @@ const themeStore = useThemeStore();
class="w-120px" class="w-120px"
/> />
</SettingItem> </SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5" :label="$t('theme.layout.tab.closeByMiddleClick')">
<template #suffix>
<IconTooltip :desc="$t('theme.layout.tab.closeByMiddleClickTip')" />
</template>
<NSwitch v-model:value="themeStore.tab.closeTabByMiddleClick" />
</SettingItem>
</TransitionGroup> </TransitionGroup>
</template> </template>

View File

@@ -14,6 +14,7 @@ type ThemePreset = Pick<
| 'colourWeakness' | 'colourWeakness'
| 'recommendColor' | 'recommendColor'
| 'themeColor' | 'themeColor'
| 'themeRadius'
| 'otherColor' | 'otherColor'
| 'isInfoFollowPrimary' | 'isInfoFollowPrimary'
| 'layout' | 'layout'

View File

@@ -83,6 +83,9 @@ const local: App.I18n.Schema = {
error: 'Error', error: 'Error',
followPrimary: 'Follow Primary' followPrimary: 'Follow Primary'
}, },
themeRadius: {
title: 'Theme Radius'
},
recommendColor: 'Apply Recommended Color Algorithm', recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to', recommendColorDesc: 'The recommended color algorithm refers to',
preset: { preset: {
@@ -138,7 +141,9 @@ const local: App.I18n.Schema = {
slider: 'Slider', slider: 'Slider',
chrome: 'Chrome', chrome: 'Chrome',
button: 'Button' button: 'Button'
} },
closeByMiddleClick: 'Close Tab by Middle Click',
closeByMiddleClickTip: 'Enable closing tabs by clicking with the middle mouse button'
}, },
header: { header: {
title: 'Header Settings', title: 'Header Settings',

View File

@@ -83,6 +83,9 @@ const local: App.I18n.Schema = {
error: '错误色', error: '错误色',
followPrimary: '跟随主色' followPrimary: '跟随主色'
}, },
themeRadius: {
title: '主题圆角'
},
recommendColor: '应用推荐算法的颜色', recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照', recommendColorDesc: '推荐颜色的算法参照',
preset: { preset: {
@@ -135,7 +138,9 @@ const local: App.I18n.Schema = {
slider: '滑块风格', slider: '滑块风格',
chrome: '谷歌风格', chrome: '谷歌风格',
button: '按钮风格' button: '按钮风格'
} },
closeByMiddleClick: '鼠标中键关闭标签页',
closeByMiddleClickTip: '启用后可以使用鼠标中键点击标签页进行关闭'
}, },
header: { header: {
title: '头部设置', title: '头部设置',

View File

@@ -1,27 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
/* oxlint-disable */
// biome-ignore lint: disable
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { RouteFileKey, RouteLayoutKey, RawRouteComponent } from "@elegant-router/types";
export const layouts: Record<RouteLayoutKey, RawRouteComponent> = {
base: () => import("@/layouts/base-layout/index.vue"),
blank: () => import("@/layouts/blank-layout/index.vue"),
};
export const views: Record<RouteFileKey, RawRouteComponent> = {
403: () => import("@/views/(builtin)/403/index.vue"),
404: () => import("@/views/(builtin)/404/index.vue"),
500: () => import("@/views/(builtin)/500/index.vue"),
Home: () => import("@/views/home/index.vue"),
IframeUrl: () => import("@/views/(builtin)/iframe/[url].vue"),
Login: () => import("@/views/(builtin)/login/index.vue"),
ManageMenu: () => import("@/views/manage/menu/index.vue"),
ManageRole: () => import("@/views/manage/role/index.vue"),
ManageRoute: () => import("@/views/manage/route/index.vue"),
ManageUser: () => import("@/views/manage/user/index.vue"),
Wip: () => import("@/views/(builtin)/wip/index.vue"),
};

View File

@@ -1,130 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// biome-ignore lint: disable
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { AutoRouterRoute } from '@elegant-router/types';
export const routes: AutoRouterRoute[] = [
{
name: 'Root',
path: '/',
redirect: '/home',
meta: {
title: "Root"
},
},
{
name: 'NotFound',
path: '/:pathMatch(.*)*',
layout: 'base',
component: '404',
meta: {
title: "NotFound"
},
},
{
name: '403',
path: '/403',
layout: 'base',
component: '403',
meta: {
title: "403",
constant: true
},
},
{
name: '404',
path: '/404',
layout: 'base',
component: '404',
meta: {
title: "404",
constant: true
},
},
{
name: '500',
path: '/500',
layout: 'base',
component: '500',
meta: {
title: "500",
constant: true
},
},
{
name: 'Home',
path: '/home',
layout: 'base',
component: 'Home',
meta: {
title: "Home"
},
},
{
name: 'IframeUrl',
path: '/iframe/:url',
layout: 'base',
component: 'IframeUrl',
meta: {
title: "IframeUrl"
}
},
{
name: 'Login',
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
layout: 'base',
component: 'Login',
meta: {
title: "Login",
constant: true
},
},
{
name: 'ManageMenu',
path: '/manage/menu',
layout: 'base',
component: 'ManageMenu',
meta: {
title: "ManageMenu"
},
},
{
name: 'ManageRole',
path: '/manage/role',
layout: 'base',
component: 'ManageRole',
meta: {
title: "ManageRole"
},
},
{
name: 'ManageRoute',
path: '/manage/route',
layout: 'base',
component: 'ManageRoute',
meta: {
title: "ManageRoute"
}
},
{
name: 'ManageUser',
path: '/manage/user',
layout: 'base',
component: 'ManageUser',
meta: {
title: "ManageUser"
},
},
{
name: 'Wip',
path: '/wip',
layout: 'base',
component: 'Wip',
meta: {
title: "Wip"
}
}
];

View File

@@ -1,28 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
/* oxlint-disable */
// biome-ignore lint: disable
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { RouteKey, RoutePathMap } from '@elegant-router/types';
const routePathMap: RoutePathMap = {
"Root": "/",
"NotFound": "/:pathMatch(.*)*",
"403": "/403",
"404": "/404",
"500": "/500",
"Home": "/home",
"IframeUrl": "/iframe/:url",
"Login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"ManageMenu": "/manage/menu",
"ManageRole": "/manage/role",
"ManageRoute": "/manage/route",
"ManageUser": "/manage/user",
"Wip": "/wip",
};
export function getRoutePath(key: RouteKey) {
return routePathMap[key];
}

View File

@@ -1,70 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
/* oxlint-disable */
// biome-ignore lint: disable
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { RouteRecordRaw } from 'vue-router';
import type {
AutoRouterRedirect,
AutoRouterRoute,
AutoRouterSingleView,
RawRouteComponent,
RouteFileKey,
RouteLayoutKey
} from '@elegant-router/types';
export function transformToVueRoutes(
routes: AutoRouterRoute[],
layouts: Record<RouteLayoutKey, RawRouteComponent>,
views: Record<RouteFileKey, RawRouteComponent>
) {
const { redirects, groupedRoutes } = getFormattedRoutes(routes);
const vueRoutes: RouteRecordRaw[] = [...redirects];
groupedRoutes.forEach((items, layout) => {
const layoutRoute: RouteRecordRaw = {
path: `/${layout}-layout`,
component: layouts[layout],
children: items.map(item => {
const { layout: _, component, ...rest } = item;
return {
component: views[component],
...rest
};
})
};
vueRoutes.push(layoutRoute);
});
return vueRoutes;
}
function getFormattedRoutes(routes: AutoRouterRoute[]) {
const groupedRoutes = new Map<RouteLayoutKey, AutoRouterSingleView[]>();
const redirects: AutoRouterRedirect[] = [];
routes.forEach(route => {
if (isAutoRouterRedirect(route)) {
redirects.push(route);
return;
}
const items = groupedRoutes.get(route.layout) || [];
items.push(route);
groupedRoutes.set(route.layout, items);
});
return {
redirects,
groupedRoutes
};
}
function isAutoRouterRedirect(route: AutoRouterRoute): route is AutoRouterRedirect {
return 'redirect' in route;
}

View File

@@ -0,0 +1,24 @@
/* eslint-disable */
/* prettier-ignore */
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { RouteComponent } from "vue-router";
import type { LastLevelRouteKey, RouteLayout } from "@elegant-router/types";
import BaseLayout from "@/layouts/base-layout/index.vue";
import BlankLayout from "@/layouts/blank-layout/index.vue";
export const layouts: Record<RouteLayout, RouteComponent | (() => Promise<RouteComponent>)> = {
base: BaseLayout,
blank: BlankLayout,
};
export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<RouteComponent>)> = {
403: () => import("@/views/_builtin/403/index.vue"),
404: () => import("@/views/_builtin/404/index.vue"),
500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
home: () => import("@/views/home/index.vue"),
};

View File

@@ -0,0 +1,78 @@
/* eslint-disable */
/* prettier-ignore */
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { GeneratedRoute } from '@elegant-router/types';
export const generatedRoutes: GeneratedRoute[] = [
{
name: '403',
path: '/403',
component: 'layout.blank$view.403',
meta: {
title: '403',
i18nKey: 'route.403',
constant: true,
hideInMenu: true
}
},
{
name: '404',
path: '/404',
component: 'layout.blank$view.404',
meta: {
title: '404',
i18nKey: 'route.404',
constant: true,
hideInMenu: true
}
},
{
name: '500',
path: '/500',
component: 'layout.blank$view.500',
meta: {
title: '500',
i18nKey: 'route.500',
constant: true,
hideInMenu: true
}
},
{
name: 'home',
path: '/home',
component: 'layout.base$view.home',
meta: {
title: 'home',
i18nKey: 'route.home',
icon: 'mdi:monitor-dashboard',
order: 1
}
},
{
name: 'iframe-page',
path: '/iframe-page/:url',
component: 'layout.base$view.iframe-page',
props: true,
meta: {
title: 'iframe-page',
i18nKey: 'route.iframe-page',
constant: true,
hideInMenu: true,
keepAlive: true
}
},
{
name: 'login',
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
component: 'layout.blank$view.login',
props: true,
meta: {
title: 'login',
i18nKey: 'route.login',
constant: true,
hideInMenu: true
}
}
];

View File

@@ -0,0 +1,192 @@
/* eslint-disable */
/* prettier-ignore */
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
import type { RouteRecordRaw, RouteComponent } from 'vue-router';
import type { ElegantConstRoute } from '@elegant-router/vue';
import type { RouteMap, RouteKey, RoutePath } from '@elegant-router/types';
/**
* transform elegant const routes to vue routes
* @param routes elegant const routes
* @param layouts layout components
* @param views view components
*/
export function transformElegantRoutesToVueRoutes(
routes: ElegantConstRoute[],
layouts: Record<string, RouteComponent | (() => Promise<RouteComponent>)>,
views: Record<string, RouteComponent | (() => Promise<RouteComponent>)>
) {
return routes.flatMap(route => transformElegantRouteToVueRoute(route, layouts, views));
}
/**
* transform elegant route to vue route
* @param route elegant const route
* @param layouts layout components
* @param views view components
*/
function transformElegantRouteToVueRoute(
route: ElegantConstRoute,
layouts: Record<string, RouteComponent | (() => Promise<RouteComponent>)>,
views: Record<string, RouteComponent | (() => Promise<RouteComponent>)>
) {
const LAYOUT_PREFIX = 'layout.';
const VIEW_PREFIX = 'view.';
const ROUTE_DEGREE_SPLITTER = '_';
const FIRST_LEVEL_ROUTE_COMPONENT_SPLIT = '$';
function isLayout(component: string) {
return component.startsWith(LAYOUT_PREFIX);
}
function getLayoutName(component: string) {
const layout = component.replace(LAYOUT_PREFIX, '');
if(!layouts[layout]) {
throw new Error(`Layout component "${layout}" not found`);
}
return layout;
}
function isView(component: string) {
return component.startsWith(VIEW_PREFIX);
}
function getViewName(component: string) {
const view = component.replace(VIEW_PREFIX, '');
if(!views[view]) {
throw new Error(`View component "${view}" not found`);
}
return view;
}
function isFirstLevelRoute(item: ElegantConstRoute) {
return !item.name.includes(ROUTE_DEGREE_SPLITTER);
}
function isSingleLevelRoute(item: ElegantConstRoute) {
return isFirstLevelRoute(item) && !item.children?.length;
}
function getSingleLevelRouteComponent(component: string) {
const [layout, view] = component.split(FIRST_LEVEL_ROUTE_COMPONENT_SPLIT);
return {
layout: getLayoutName(layout),
view: getViewName(view)
};
}
const vueRoutes: RouteRecordRaw[] = [];
// add props: true to route
if (route.path.includes(':') && !route.props) {
route.props = true;
}
const { name, path, component, children, ...rest } = route;
const vueRoute = { name, path, ...rest } as RouteRecordRaw;
try {
if (component) {
if (isSingleLevelRoute(route)) {
const { layout, view } = getSingleLevelRouteComponent(component);
const singleLevelRoute: RouteRecordRaw = {
path,
component: layouts[layout],
meta: {
title: route.meta?.title || ''
},
children: [
{
name,
path: '',
component: views[view],
...rest
} as RouteRecordRaw
]
};
return [singleLevelRoute];
}
if (isLayout(component)) {
const layoutName = getLayoutName(component);
vueRoute.component = layouts[layoutName];
}
if (isView(component)) {
const viewName = getViewName(component);
vueRoute.component = views[viewName];
}
}
} catch (error: any) {
console.error(`Error transforming route "${route.name}": ${error.toString()}`);
return [];
}
// add redirect to child
if (children?.length && !vueRoute.redirect) {
vueRoute.redirect = {
name: children[0].name
};
}
if (children?.length) {
const childRoutes = children.flatMap(child => transformElegantRouteToVueRoute(child, layouts, views));
if(isFirstLevelRoute(route)) {
vueRoute.children = childRoutes;
} else {
vueRoutes.push(...childRoutes);
}
}
vueRoutes.unshift(vueRoute);
return vueRoutes;
}
/**
* map of route name and route path
*/
const routeMap: RouteMap = {
"root": "/",
"not-found": "/:pathMatch(.*)*",
"403": "/403",
"404": "/404",
"500": "/500",
"home": "/home",
"iframe-page": "/iframe-page/:url",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
};
/**
* get route path by route name
* @param name route name
*/
export function getRoutePath<T extends RouteKey>(name: T) {
return routeMap[name];
}
/**
* get route name by route path
* @param path route path
*/
export function getRouteName(path: RoutePath) {
const routeEntries = Object.entries(routeMap) as [RouteKey, RoutePath][];
const routeName: RouteKey | null = routeEntries.find(([, routePath]) => routePath === path)?.[0] || null;
return routeName;
}

View File

@@ -1,6 +1,12 @@
import type { App } from 'vue'; import type { App } from 'vue';
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory } from 'vue-router'; import {
import type { RouterHistory } from 'vue-router'; type RouterHistory,
createMemoryHistory,
createRouter,
createWebHashHistory,
createWebHistory
} from 'vue-router';
import { createBuiltinVueRoutes } from './routes/builtin';
import { createRouterGuard } from './guard'; import { createRouterGuard } from './guard';
const { VITE_ROUTER_HISTORY_MODE = 'history', VITE_BASE_URL } = import.meta.env; const { VITE_ROUTER_HISTORY_MODE = 'history', VITE_BASE_URL } = import.meta.env;
@@ -13,7 +19,7 @@ const historyCreatorMap: Record<Env.RouterHistoryMode, (base?: string) => Router
export const router = createRouter({ export const router = createRouter({
history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL), history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL),
routes: [] routes: createBuiltinVueRoutes()
}); });
/** Setup Vue Router */ /** Setup Vue Router */

View File

@@ -0,0 +1,31 @@
import type { CustomRoute } from '@elegant-router/types';
import { layouts, views } from '../elegant/imports';
import { getRoutePath, transformElegantRoutesToVueRoutes } from '../elegant/transform';
export const ROOT_ROUTE: CustomRoute = {
name: 'root',
path: '/',
redirect: getRoutePath(import.meta.env.VITE_ROUTE_HOME) || '/home',
meta: {
title: 'root',
constant: true
}
};
const NOT_FOUND_ROUTE: CustomRoute = {
name: 'not-found',
path: '/:pathMatch(.*)*',
component: 'layout.blank$view.404',
meta: {
title: 'not-found',
constant: true
}
};
/** builtin routes, it must be constant and setup in vue-router */
const builtinRoutes: CustomRoute[] = [ROOT_ROUTE, NOT_FOUND_ROUTE];
/** create builtin vue routes */
export function createBuiltinVueRoutes() {
return transformElegantRoutesToVueRoutes(builtinRoutes, layouts, views);
}

View File

@@ -1,39 +1,40 @@
import type { AutoRouterRedirect, AutoRouterRoute } from '@elegant-router/types'; import type { CustomRoute, ElegantConstRoute, ElegantRoute } from '@elegant-router/types';
import { routes } from '../_generated/routes'; import { generatedRoutes } from '../elegant/routes';
import { layouts, views } from '../_generated/imports'; import { layouts, views } from '../elegant/imports';
import { transformToVueRoutes } from '../_generated/transformer'; import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
/**
* custom routes
*
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/
const customRoutes: CustomRoute[] = [];
/** create routes when the auth route mode is static */ /** create routes when the auth route mode is static */
export function createStaticRoutes() { export function createStaticRoutes() {
const constantRoutes: AutoRouterRoute[] = []; const constantRoutes: ElegantRoute[] = [];
const authRoutes: AutoRouterRoute[] = [];
let rootRoute: AutoRouterRedirect | undefined; const authRoutes: ElegantRoute[] = [];
routes.forEach(item => { [...customRoutes, ...generatedRoutes].forEach(item => {
if (item.meta?.constant) { if (item.meta?.constant) {
constantRoutes.push(item); constantRoutes.push(item);
} else { } else {
authRoutes.push(item); authRoutes.push(item);
} }
if (item.name === 'Root') {
rootRoute = item as AutoRouterRedirect;
}
}); });
return { return {
constantRoutes, constantRoutes,
authRoutes, authRoutes
rootRoute: rootRoute as AutoRouterRedirect
}; };
} }
/** /**
* Get auth vue routes * Get auth vue routes
* *
* @param authRoutes Elegant routes * @param routes Elegant routes
*/ */
export function getAuthVueRoutes(authRoutes: AutoRouterRoute[]) { export function getAuthVueRoutes(routes: ElegantConstRoute[]) {
return transformToVueRoutes(authRoutes, layouts, views); return transformElegantRoutesToVueRoutes(routes, layouts, views);
} }

View File

@@ -53,7 +53,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
}); });
/** Naive theme */ /** Naive theme */
const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value.recommendColor)); const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value));
/** /**
* Settings json * Settings json

View File

@@ -236,22 +236,23 @@ function getNaiveThemeColors(colors: App.Theme.ThemeColor, recommended = false)
/** /**
* Get naive theme * Get naive theme
* *
* @param colors Theme colors * @param settings Theme settings object.
* @param [recommended=false] Use recommended color. Default is `false` * @param settings.recommendColor Whether to use recommended color palette.
* @param settings.themeRadius Border radius to use in the theme (in px).
*/ */
export function getNaiveTheme(colors: App.Theme.ThemeColor, recommended = false) { export function getNaiveTheme(colors: App.Theme.ThemeColor, settings: App.Theme.ThemeSetting) {
const { primary: colorLoading } = colors; const { primary: colorLoading } = colors;
const theme: GlobalThemeOverrides = { const theme: GlobalThemeOverrides = {
common: { common: {
...getNaiveThemeColors(colors, recommended), ...getNaiveThemeColors(colors, settings.recommendColor),
borderRadius: '6px' borderRadius: `${settings.themeRadius}px`
}, },
LoadingBar: { LoadingBar: {
colorLoading colorLoading
}, },
Tag: { Tag: {
borderRadius: '6px' borderRadius: `${settings.themeRadius}px`
} }
}; };

View File

@@ -6,6 +6,7 @@ html,
body, body,
#app { #app {
height: 100%; height: 100%;
text-autospace: normal;
} }
html { html {

View File

@@ -1,7 +1,14 @@
@mixin scrollbar($size: 7px, $color: rgba(0, 0, 0, 0.5)) { @mixin scrollbar($size: 7px, $color: rgba(0, 0, 0, 0.5), $dark-color: rgba(255, 255, 255, 0.5)) {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: $color transparent; scrollbar-color: $color transparent;
// Dark theme override
.dark & {
// guide safari use light color scrollbar
color-scheme: dark;
scrollbar-color: $dark-color transparent;
}
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: $color; background-color: $color;
border-radius: $size; border-radius: $size;

View File

@@ -14,6 +14,7 @@
"warning": "#d4bb9d", "warning": "#d4bb9d",
"error": "#c49a9a" "error": "#c49a9a"
}, },
"themeRadius": 6,
"isInfoFollowPrimary": true, "isInfoFollowPrimary": true,
"layout": { "layout": {
"mode": "vertical-mix", "mode": "vertical-mix",

View File

@@ -14,6 +14,7 @@
"warning": "#faad14", "warning": "#faad14",
"error": "#f5222d" "error": "#f5222d"
}, },
"themeRadius": 6,
"isInfoFollowPrimary": true, "isInfoFollowPrimary": true,
"layout": { "layout": {
"mode": "vertical", "mode": "vertical",

View File

@@ -14,6 +14,7 @@
"warning": "#faad14", "warning": "#faad14",
"error": "#f5222d" "error": "#f5222d"
}, },
"themeRadius": 6,
"isInfoFollowPrimary": true, "isInfoFollowPrimary": true,
"layout": { "layout": {
"mode": "vertical", "mode": "vertical",

View File

@@ -14,6 +14,7 @@
"warning": "#faad14", "warning": "#faad14",
"error": "#f5222d" "error": "#f5222d"
}, },
"themeRadius": 6,
"isInfoFollowPrimary": true, "isInfoFollowPrimary": true,
"layout": { "layout": {
"mode": "vertical", "mode": "vertical",

View File

@@ -5,6 +5,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
colourWeakness: false, colourWeakness: false,
recommendColor: false, recommendColor: false,
themeColor: '#646cff', themeColor: '#646cff',
themeRadius: 6,
otherColor: { otherColor: {
info: '#2080f0', info: '#2080f0',
success: '#52c41a', success: '#52c41a',
@@ -37,7 +38,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
visible: true, visible: true,
cache: true, cache: true,
height: 44, height: 44,
mode: 'chrome' mode: 'chrome',
closeTabByMiddleClick: false
}, },
fixedHeaderAndTab: true, fixedHeaderAndTab: true,
sider: { sider: {

View File

@@ -5,38 +5,46 @@
*/ */
declare namespace Api { declare namespace Api {
namespace Common { namespace Common {
interface CreatedUpdatedDTO { /** common params of paginating */
/** 创建人 */ interface PaginatingCommonParams {
createdBy: string; /** current page number */
/** 创建时间 */ current: number;
createdTime: string; /** page size */
/** 更新人 */ size: number;
updatedBy: string; /** total count */
/** 更新时间 */
updatedTime: string;
}
interface CommonRecordDTO extends CreatedUpdatedDTO {
/** 主键 */
id: number;
/**
* 启用状态 Enable status
*
* - "1": 启用
* - "2": 禁用
*/
status: Union.EnableStatus | null;
}
interface PaginationQueryDTO {
page: number;
pageSize: number;
sort: string;
}
interface PaginationDTO extends Pick<PaginationQueryDTO, 'page' | 'pageSize'> {
/** 总条数 */
total: number; total: number;
} }
/** common params of paginating query list data */
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
records: T[];
}
/** common search params of table */
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
/**
* enable status
*
* - "1": enabled
* - "2": disabled
*/
type EnableStatus = '1' | '2';
/** common record */
type CommonRecord<T = any> = {
/** record id */
id: number;
/** record creator */
createBy: string;
/** record create time */
createTime: string;
/** record updater */
updateBy: string;
/** record update time */
updateTime: string;
/** record status */
status: EnableStatus | null;
} & T;
} }
} }

View File

@@ -1,42 +0,0 @@
declare namespace Api {
namespace Menu {
type RouteKey = import('@elegant-router/types').RouteKey;
interface Menu extends Common.CommonRecordDTO {
/** 菜单名称 */
name: string;
/** 菜单类型 */
menuType: Union.MenuType;
/** 菜单描述 */
description?: string | null;
/** 菜单图标 */
icon?: string | null;
/** 菜单图标类型 */
iconType?: string | null;
/** 父级菜单ID */
parentId?: number | null;
/** 菜单排序 */
order?: number | null;
/** 是否隐藏 */
isHidden?: boolean | null;
/** iframe 页面URL */
iframeUrl?: string | null;
/** 链接 */
href?: string | null;
/** 路由名称 */
routeName?: RouteKey | null;
/** 路由查询参数 */
routeQueries?: Record<string, string | number | boolean> | null;
/** 路由路径参数 */
routeParams?: Record<string, string | number | boolean> | null;
}
interface MenuListDTO extends Common.PaginationDTO {
list: Menu[];
}
interface MenuTreeDTO extends Menu {
children?: MenuTreeDTO[];
}
}
}

View File

@@ -1,34 +1,19 @@
declare namespace Api { declare namespace Api {
/**
* namespace Route
*
* backend api module: "route"
*/
namespace Route { namespace Route {
interface RouteDTO extends Common.CommonRecordDTO { type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute;
/** 路由路径 */
path: string; interface MenuRoute extends ElegantConstRoute {
/** 路由名称 */ id: string;
name: string;
/** 路由布局 */
layout: string;
/** 路由组件 */
component: string;
/** 重定向路径 */
redirect?: string | null;
/** 路由图标 */
icon?: string | null;
/** 路由图标类型 */
iconType?: string | null;
/** 是否缓存 */
isCache?: boolean | null;
/** 是否支持多开 */
isMultiple?: boolean | null;
/** 固定索引 */
fixedIndex?: number | null;
/** 是否需要权限 */
requireAuth?: boolean | null;
/** 路由属性 */
props?: Record<string, string | number | boolean> | null;
} }
interface RouteListDTO extends Common.PaginationDTO { interface UserRoute {
list: RouteDTO[]; routes: MenuRoute[];
home: import('@elegant-router/types').LastLevelRouteKey;
} }
} }
} }

View File

@@ -1,31 +0,0 @@
declare namespace Api {
namespace Union {
/**
* 是否 Yes or No
*
* - "Y": yes
* - "N": no
*/
type YesOrNo = 'Y' | 'N';
/**
* 启用状态 Enable status
*
* - "1": 启用
* - "2": 禁用
*/
type EnableStatus = '1' | '2';
/**
* 菜单类型
*
* - "directory": 目录
* - "menu": 菜单
* - "iframe": iframe 页面iframe 嵌入页面)
* - "link": 链接(跳转外部链接)
* - "page": 页面(不展示在菜单中,可用于作为菜单类型的子菜单展示在面包屑中)
* - "other": 其他(作用自行定义)
*/
type MenuType = 'page' | 'directory' | 'menu' | 'iframe' | 'link' | 'other';
}
}

101
src/typings/app.d.ts vendored
View File

@@ -16,6 +16,8 @@ declare namespace App {
recommendColor: boolean; recommendColor: boolean;
/** Theme color */ /** Theme color */
themeColor: string; themeColor: string;
/** Theme radius */
themeRadius: number;
/** Other color */ /** Other color */
otherColor: OtherColor; otherColor: OtherColor;
/** Whether info color is followed by the primary color */ /** Whether info color is followed by the primary color */
@@ -69,6 +71,8 @@ declare namespace App {
height: number; height: number;
/** Tab mode */ /** Tab mode */
mode: UnionKey.ThemeTabMode; mode: UnionKey.ThemeTabMode;
/** Whether to close tab by middle click */
closeTabByMiddleClick: boolean;
}; };
/** Fixed header and tab */ /** Fixed header and tab */
fixedHeaderAndTab: boolean; fixedHeaderAndTab: boolean;
@@ -179,9 +183,9 @@ declare namespace App {
type VNode = import('vue').VNode; type VNode = import('vue').VNode;
type RouteLocationNormalizedLoaded = import('vue-router').RouteLocationNormalizedLoaded; type RouteLocationNormalizedLoaded = import('vue-router').RouteLocationNormalizedLoaded;
type RouteKey = import('@elegant-router/types').RouteKey; type RouteKey = import('@elegant-router/types').RouteKey;
type RoutePathMap = import('@elegant-router/types').RoutePathMap; type RouteMap = import('@elegant-router/types').RouteMap;
type RoutePath = import('@elegant-router/types').RoutePath; type RoutePath = import('@elegant-router/types').RoutePath;
type RouteFileKey = import('@elegant-router/types').RouteFileKey; type LastLevelRouteKey = import('@elegant-router/types').LastLevelRouteKey;
/** The router push options */ /** The router push options */
type RouterPushOptions = { type RouterPushOptions = {
@@ -199,25 +203,77 @@ declare namespace App {
showMenu?: boolean; showMenu?: boolean;
} }
type MenuType = 'page' | 'directory' | 'menu' | 'iframe' | 'link' | 'button' | 'other';
/** The global menu */ /** The global menu */
interface Menu { type Menu = {
id: number; /**
name: string; * The menu key
menuType: MenuType; *
description?: string | null; * Equal to the route key
icon?: string | null; */
iconType?: string | null; key: string;
parentId?: number | null; /** The menu label */
order?: number | null; label: string;
isHidden?: boolean | null; /** The menu i18n key */
iframeUrl?: string | null; i18nKey?: I18n.I18nKey | null;
href?: string | null; /** The route key */
routeName?: RouteKey | null; routeKey: RouteKey;
routeQueries?: Record<string, string | number | boolean> | null; /** The route path */
routeParams?: Record<string, string | number | boolean> | null; routePath: RoutePath;
} /** The menu icon */
icon?: () => VNode;
/** The menu children */
children?: Menu[];
};
type Breadcrumb = Omit<Menu, 'children'> & {
options?: Breadcrumb[];
};
/** Tab route */
type TabRoute = Pick<RouteLocationNormalizedLoaded, 'name' | 'path' | 'meta'> &
Partial<Pick<RouteLocationNormalizedLoaded, 'fullPath' | 'query' | 'matched'>>;
/** The global tab */
type Tab = {
/** The tab id */
id: string;
/** The tab label */
label: string;
/**
* The new tab label
*
* If set, the tab label will be replaced by this value
*/
newLabel?: string;
/**
* The old tab label
*
* when reset the tab label, the tab label will be replaced by this value
*/
oldLabel?: string;
/** The tab route key */
routeKey: LastLevelRouteKey;
/** The tab route path */
routePath: RouteMap[LastLevelRouteKey];
/** The tab route full path */
fullPath: string;
/** The tab fixed index */
fixedIndex?: number | null;
/**
* Tab icon
*
* Iconify icon
*/
icon?: string;
/**
* Tab local icon
*
* Local icon
*/
localIcon?: string;
/** I18n key */
i18nKey?: I18n.I18nKey | null;
};
/** Form rule */ /** Form rule */
type FormRule = import('naive-ui').FormItemRule; type FormRule = import('naive-ui').FormItemRule;
@@ -325,6 +381,9 @@ declare namespace App {
} & Record<Theme.ThemeColorKey, string>; } & Record<Theme.ThemeColorKey, string>;
recommendColor: string; recommendColor: string;
recommendColorDesc: string; recommendColorDesc: string;
themeRadius: {
title: string;
};
preset: { preset: {
title: string; title: string;
apply: string; apply: string;
@@ -348,6 +407,8 @@ declare namespace App {
cacheTip: string; cacheTip: string;
height: string; height: string;
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>; mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
closeByMiddleClick: string;
closeByMiddleClickTip: string;
}; };
header: { header: {
title: string; title: string;

View File

@@ -1,8 +1,12 @@
/* eslint-disable */ /* eslint-disable */
// @ts-nocheck // @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable import { GlobalComponents } from 'vue'
export {} export {}
/* prettier-ignore */ /* prettier-ignore */
@@ -66,7 +70,6 @@ declare module 'vue' {
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab'] NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs'] NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing'] NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip'] NTooltip: typeof import('naive-ui')['NTooltip']
NWatermark: typeof import('naive-ui')['NWatermark'] NWatermark: typeof import('naive-ui')['NWatermark']
@@ -83,3 +86,79 @@ declare module 'vue' {
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
} }
} }
// For TSX support
declare global {
const AppProvider: typeof import('./../components/common/app-provider.vue')['default']
const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
const CountTo: typeof import('./../components/custom/count-to.vue')['default']
const DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
const ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
const FullScreen: typeof import('./../components/common/full-screen.vue')['default']
const IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
const IconLocalBanner: typeof import('~icons/local/banner')['default']
const IconLocalLogo: typeof import('~icons/local/logo')['default']
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
const IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
const IconUilSearch: typeof import('~icons/uil/search')['default']
const LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
const LookForward: typeof import('./../components/custom/look-forward.vue')['default']
const MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
const NAlert: typeof import('naive-ui')['NAlert']
const NBadge: typeof import('naive-ui')['NBadge']
const NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
const NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
const NButton: typeof import('naive-ui')['NButton']
const NCard: typeof import('naive-ui')['NCard']
const NCheckbox: typeof import('naive-ui')['NCheckbox']
const NColorPicker: typeof import('naive-ui')['NColorPicker']
const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
const NDivider: typeof import('naive-ui')['NDivider']
const NDrawer: typeof import('naive-ui')['NDrawer']
const NDrawerContent: typeof import('naive-ui')['NDrawerContent']
const NDropdown: typeof import('naive-ui')['NDropdown']
const NEmpty: typeof import('naive-ui')['NEmpty']
const NForm: typeof import('naive-ui')['NForm']
const NFormItem: typeof import('naive-ui')['NFormItem']
const NGi: typeof import('naive-ui')['NGi']
const NGrid: typeof import('naive-ui')['NGrid']
const NInput: typeof import('naive-ui')['NInput']
const NInputGroup: typeof import('naive-ui')['NInputGroup']
const NInputNumber: typeof import('naive-ui')['NInputNumber']
const NList: typeof import('naive-ui')['NList']
const NListItem: typeof import('naive-ui')['NListItem']
const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
const NMenu: typeof import('naive-ui')['NMenu']
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
const NModal: typeof import('naive-ui')['NModal']
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
const NPopover: typeof import('naive-ui')['NPopover']
const NScrollbar: typeof import('naive-ui')['NScrollbar']
const NSelect: typeof import('naive-ui')['NSelect']
const NSpace: typeof import('naive-ui')['NSpace']
const NStatistic: typeof import('naive-ui')['NStatistic']
const NSwitch: typeof import('naive-ui')['NSwitch']
const NTab: typeof import('naive-ui')['NTab']
const NTabs: typeof import('naive-ui')['NTabs']
const NThing: typeof import('naive-ui')['NThing']
const NTooltip: typeof import('naive-ui')['NTooltip']
const NWatermark: typeof import('naive-ui')['NWatermark']
const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
const ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
const SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
const SvgIcon: typeof import('./../components/custom/svg-icon.vue')['default']
const SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
const TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
}

View File

@@ -1,100 +1,240 @@
/* eslint-disable */ /* eslint-disable */
/* prettier-ignore */ /* prettier-ignore */
/* oxlint-disable */
// biome-ignore lint: disable
// Generated by elegant-router // Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router // Read more: https://github.com/soybeanjs/elegant-router
declare module "@elegant-router/types" { declare module "@elegant-router/types" {
type RouteRecordSingleView = import("vue-router").RouteRecordSingleView; type ElegantConstRoute = import('@elegant-router/vue').ElegantConstRoute;
type RouteRecordRedirect = import("vue-router").RouteRecordRedirect;
type RouteComponent = import("vue-router").RouteComponent;
type Lazy<T> = () => Promise<T>;
export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>;
/** /**
* route layout key * route layout
*/ */
export type RouteLayoutKey = "base" | "blank"; export type RouteLayout = "base" | "blank";
/** /**
* route path map * route map
*/ */
export type RoutePathMap = { export type RouteMap = {
"Root": "/"; "root": "/";
"NotFound": "/:pathMatch(.*)*"; "not-found": "/:pathMatch(.*)*";
"403": "/403"; "403": "/403";
"404": "/404"; "404": "/404";
"500": "/500"; "500": "/500";
"Home": "/home"; "home": "/home";
"IframeUrl": "/iframe/:url"; "iframe-page": "/iframe-page/:url";
"Login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"ManageMenu": "/manage/menu";
"ManageRole": "/manage/role";
"ManageRoute": "/manage/route";
"ManageUser": "/manage/user";
"Wip": "/wip";
}; };
/** /**
* route key * route key
*/ */
export type RouteKey = keyof RoutePathMap; export type RouteKey = keyof RouteMap;
/** /**
* route path * route path
*/ */
export type RoutePath = RoutePathMap[RouteKey]; export type RoutePath = RouteMap[RouteKey];
/** /**
* root route key * custom route key
*/ */
export type RootRouteKey = 'Root'; export type CustomRouteKey = Extract<
RouteKey,
| "root"
| "not-found"
>;
/** /**
* not found route key * the generated route key
*/ */
export type NotFoundRouteKey = 'NotFound'; export type GeneratedRouteKey = Exclude<RouteKey, CustomRouteKey>;
/** /**
* builtin route key * the first level route key, which contain the layout of the route
*/ */
export type BuiltinRouteKey = RootRouteKey | NotFoundRouteKey; export type FirstLevelRouteKey = Extract<
RouteKey,
| "403"
| "404"
| "500"
| "home"
| "iframe-page"
| "login"
>;
/** /**
* reuse route key * the custom first level route key
*/ */
export type ReuseRouteKey = never; export type CustomFirstLevelRouteKey = Extract<
CustomRouteKey,
| "root"
| "not-found"
>;
/** /**
* the route file key, which has it's own file * the last level route key, which has the page file
*/ */
export type RouteFileKey = Exclude<RouteKey, BuiltinRouteKey | ReuseRouteKey>; export type LastLevelRouteKey = Extract<
RouteKey,
| "403"
| "404"
| "500"
| "iframe-page"
| "login"
| "home"
>;
/** /**
* mapped name and path * the custom last level route key
*/ */
type MappedNamePath = { export type CustomLastLevelRouteKey = Extract<
[K in RouteKey]: { name: K; path: RoutePathMap[K] }; CustomRouteKey,
}[RouteKey]; | "root"
| "not-found"
>;
/** /**
* auto router single view * the single level route key
*/ */
export type AutoRouterSingleView = Omit<RouteRecordSingleView, 'component' | 'name' | 'path'> & { export type SingleLevelRouteKey = FirstLevelRouteKey & LastLevelRouteKey;
component: RouteFileKey;
layout: RouteLayoutKey;
} & MappedNamePath;
/** /**
* auto router redirect * the custom single level route key
*/ */
export type AutoRouterRedirect = Omit<RouteRecordRedirect, 'children' | 'name' | 'path'> & MappedNamePath; export type CustomSingleLevelRouteKey = CustomFirstLevelRouteKey & CustomLastLevelRouteKey;
/** /**
* auto router route * the first level route key, but not the single level
*/ */
export type AutoRouterRoute = AutoRouterSingleView | AutoRouterRedirect; export type FirstLevelRouteNotSingleKey = Exclude<FirstLevelRouteKey, SingleLevelRouteKey>;
/**
* the custom first level route key, but not the single level
*/
export type CustomFirstLevelRouteNotSingleKey = Exclude<CustomFirstLevelRouteKey, CustomSingleLevelRouteKey>;
/**
* the center level route key
*/
export type CenterLevelRouteKey = Exclude<GeneratedRouteKey, FirstLevelRouteKey | LastLevelRouteKey>;
/**
* the custom center level route key
*/
export type CustomCenterLevelRouteKey = Exclude<CustomRouteKey, CustomFirstLevelRouteKey | CustomLastLevelRouteKey>;
/**
* the center level route key
*/
type GetChildRouteKey<K extends RouteKey, T extends RouteKey = RouteKey> = T extends `${K}_${infer R}`
? R extends `${string}_${string}`
? never
: T
: never;
/**
* the single level route
*/
type SingleLevelRoute<K extends SingleLevelRouteKey = SingleLevelRouteKey> = K extends string
? Omit<ElegantConstRoute, 'children'> & {
name: K;
path: RouteMap[K];
component: `layout.${RouteLayout}$view.${K}`;
}
: never;
/**
* the last level route
*/
type LastLevelRoute<K extends GeneratedRouteKey> = K extends LastLevelRouteKey
? Omit<ElegantConstRoute, 'children'> & {
name: K;
path: RouteMap[K];
component: `view.${K}`;
}
: never;
/**
* the center level route
*/
type CenterLevelRoute<K extends GeneratedRouteKey> = K extends CenterLevelRouteKey
? Omit<ElegantConstRoute, 'component'> & {
name: K;
path: RouteMap[K];
children: (CenterLevelRoute<GetChildRouteKey<K>> | LastLevelRoute<GetChildRouteKey<K>>)[];
}
: never;
/**
* the multi level route
*/
type MultiLevelRoute<K extends FirstLevelRouteNotSingleKey = FirstLevelRouteNotSingleKey> = K extends string
? ElegantConstRoute & {
name: K;
path: RouteMap[K];
component: `layout.${RouteLayout}`;
children: (CenterLevelRoute<GetChildRouteKey<K>> | LastLevelRoute<GetChildRouteKey<K>>)[];
}
: never;
/**
* the custom first level route
*/
type CustomSingleLevelRoute<K extends CustomFirstLevelRouteKey = CustomFirstLevelRouteKey> = K extends string
? Omit<ElegantConstRoute, 'children'> & {
name: K;
path: RouteMap[K];
component?: `layout.${RouteLayout}$view.${LastLevelRouteKey}`;
}
: never;
/**
* the custom last level route
*/
type CustomLastLevelRoute<K extends CustomRouteKey> = K extends CustomLastLevelRouteKey
? Omit<ElegantConstRoute, 'children'> & {
name: K;
path: RouteMap[K];
component?: `view.${LastLevelRouteKey}`;
}
: never;
/**
* the custom center level route
*/
type CustomCenterLevelRoute<K extends CustomRouteKey> = K extends CustomCenterLevelRouteKey
? Omit<ElegantConstRoute, 'component'> & {
name: K;
path: RouteMap[K];
children: (CustomCenterLevelRoute<GetChildRouteKey<K>> | CustomLastLevelRoute<GetChildRouteKey<K>>)[];
}
: never;
/**
* the custom multi level route
*/
type CustomMultiLevelRoute<K extends CustomFirstLevelRouteNotSingleKey = CustomFirstLevelRouteNotSingleKey> =
K extends string
? ElegantConstRoute & {
name: K;
path: RouteMap[K];
component: `layout.${RouteLayout}`;
children: (CustomCenterLevelRoute<GetChildRouteKey<K>> | CustomLastLevelRoute<GetChildRouteKey<K>>)[];
}
: never;
/**
* the custom route
*/
type CustomRoute = CustomSingleLevelRoute | CustomMultiLevelRoute;
/**
* the generated route
*/
type GeneratedRoute = SingleLevelRoute | MultiLevelRoute;
/**
* the elegant route
*/
type ElegantRoute = GeneratedRoute | CustomRoute;
} }

View File

@@ -1,7 +1,7 @@
export {}; import 'vue-router';
declare module 'vue-router' { declare module 'vue-router' {
export interface RouteMeta { interface RouteMeta {
/** /**
* Title of the route * Title of the route
* *

View File

@@ -1,40 +0,0 @@
/* eslint-disable */
// @ts-nocheck
/* prettier-ignore */
/* oxlint-disable */
// biome-ignore lint: disable
// Generated by elegant-router
// Read more: https://github.com/soybeanjs/elegant-router
export {}
declare module "vue-router" {
type RouteNamedMap = import("vue-router/auto-routes").RouteNamedMap;
export interface TypesConfig {
RouteNamedMap: RouteNamedMap;
}
}
declare module "vue-router/auto-routes" {
import type { RouteParamsRawGeneric, RouteParamsGeneric, RouteMeta, RouteRecordInfo, ParamValue, ParamValueZeroOrOne } from "vue-router";
/**
* route named map
*/
export interface RouteNamedMap {
"Root": RouteRecordInfo<"Root", "/", Record<never, never>, Record<never, never>>;
"NotFound": RouteRecordInfo<"NotFound", "/:pathMatch(.*)*", Record<never, never>, Record<never, never>>;
"403": RouteRecordInfo<"403", "/403", Record<never, never>, Record<never, never>>;
"404": RouteRecordInfo<"404", "/404", Record<never, never>, Record<never, never>>;
"500": RouteRecordInfo<"500", "/500", Record<never, never>, Record<never, never>>;
"Home": RouteRecordInfo<"Home", "/home", Record<never, never>, Record<never, never>>;
"IframeUrl": RouteRecordInfo<"IframeUrl", "/iframe/:url", { url: ParamValue<true> }, { url: ParamValue<false> }>;
"Login": RouteRecordInfo<"Login", "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", { module?: ParamValueZeroOrOne<true> }, { module?: ParamValueZeroOrOne<false> }>;
"ManageMenu": RouteRecordInfo<"ManageMenu", "/manage/menu", Record<never, never>, Record<never, never>>;
"ManageRole": RouteRecordInfo<"ManageRole", "/manage/role", Record<never, never>, Record<never, never>>;
"ManageRoute": RouteRecordInfo<"ManageRoute", "/manage/route", Record<never, never>, Record<never, never>>;
"ManageUser": RouteRecordInfo<"ManageUser", "/manage/user", Record<never, never>, Record<never, never>>;
"Wip": RouteRecordInfo<"Wip", "/wip", Record<never, never>, Record<never, never>>
}
}

View File

@@ -1,5 +0,0 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core'; import { createReusableTemplate } from '@vueuse/core';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales'; import { $t } from '@/locales';
defineOptions({ defineOptions({
@@ -72,6 +73,8 @@ interface GradientBgProps {
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>(); const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>();
const themeStore = useThemeStore();
function getGradientColor(color: CardData['color']) { function getGradientColor(color: CardData['color']) {
return `linear-gradient(to bottom right, ${color.start}, ${color.end})`; return `linear-gradient(to bottom right, ${color.start}, ${color.end})`;
} }
@@ -81,7 +84,10 @@ function getGradientColor(color: CardData['color']) {
<NCard :bordered="false" size="small" class="card-wrapper"> <NCard :bordered="false" size="small" class="card-wrapper">
<!-- define component start: GradientBg --> <!-- define component start: GradientBg -->
<DefineGradientBg v-slot="{ $slots, gradientColor }"> <DefineGradientBg v-slot="{ $slots, gradientColor }">
<div class="rd-8px px-16px pb-4px pt-8px text-white" :style="{ backgroundImage: gradientColor }"> <div
class="px-16px pb-4px pt-8px text-white"
:style="{ backgroundImage: gradientColor, borderRadius: themeStore.themeRadius + 'px' }"
>
<component :is="$slots.default" /> <component :is="$slots.default" />
</div> </div>
</DefineGradientBg> </DefineGradientBg>

View File

@@ -1,5 +0,0 @@
<script setup lang="ts"></script>
<template>
<div>ManageMenu</div>
</template>

View File

@@ -1,5 +0,0 @@
<script setup lang="ts"></script>
<template>
<div>ManageRole</div>
</template>

View File

@@ -1,5 +0,0 @@
<script setup lang="ts"></script>
<template>
<div>ManageRoute</div>
</template>

View File

@@ -1,5 +0,0 @@
<script setup lang="ts"></script>
<template>
<div>ManageUser</div>
</template>