Compare commits

...

90 Commits

Author SHA1 Message Date
Soybean
7710bcdb0a optimize(projects): optimize code 2024-08-11 23:35:45 +08:00
Ohh
b36d914e48 feat(projects): add plugin typeit (#591) 2024-08-11 23:30:12 +08:00
Soybean
bbacff0581 optimize(projects): optimize plugin gantt 2024-08-06 10:21:46 +08:00
Ohh
f86bca063a feat(projects): add plugin gantt preview (#578) 2024-08-05 10:26:32 +08:00
Soybean
48e005bcaf Merge branch 'main' into example 2024-08-01 16:06:59 +08:00
Soybean
e6aa25e9f8 chore(projects): release v1.3.4 2024-08-01 16:02:56 +08:00
Soybean
968370b1aa chore(projects): update deps & fix vue-router type 2024-08-01 02:18:22 +08:00
Soybean
cb1d44525d optimize(projects): optimize code 2024-07-31 23:11:50 +08:00
Soybean
939c512f02 refactor(projects)!: don't reset active menu of vertical-mix layout when it is mixSiderFixed 2024-07-31 23:11:16 +08:00
Soybean
d825b6e260 feat(projects): add watermark override theme settings 2024-07-30 21:50:22 +08:00
Soybean
05b0147b47 Merge branch 'main' into example 2024-07-30 18:04:14 +08:00
Soybean
3f23993a83 chore(projects): release v1.3.3 2024-07-30 18:03:26 +08:00
Soybean
ebc838c3e0 docs(projects): update CHANGELOG 2024-07-30 18:03:06 +08:00
Soybean
5646a50ddf fix(projects): fix watermark settings 2024-07-30 17:53:17 +08:00
Soybean
e7cd34ec37 chore(projects): Merge branch 'main' into example 2024-07-30 17:26:42 +08:00
Soybean
217a3bb259 chore(projects): release v1.3.2 2024-07-30 17:18:16 +08:00
Soybean
d37591dd7a docs(projects): update CHANGELOG 2024-07-30 17:17:54 +08:00
Soybean
cf67d55cbf optimize(projects): optimize response code comparison 2024-07-30 15:12:45 +08:00
Soybean
98b75c22c3 optimize(projects): remove home NAlert closable 2024-07-30 14:57:53 +08:00
Soybean
993e9caf79 chore(deps): update deps 2024-07-30 14:55:45 +08:00
Soybean
f26d0a61eb optimize(projects): add type WatermarkProps 2024-07-30 14:41:30 +08:00
Azir
eed617f9eb optimize(types): remove useless types. 2024-07-30 09:33:35 +08:00
paynezhuang
ea8aa6c4e6 feat(projects): add full screen watermark. close#571 (#573) 2024-07-29 23:04:26 +08:00
青菜白玉汤
4dde4c22b1 feat(projects): add color fading mode.close #567 (#569) 2024-07-28 23:00:22 +08:00
Ohh
a9dce21878 feat(projects): add plugin pdf preview (#568) 2024-07-27 03:06:11 +08:00
Soybean
59f07d8ac4 fix(projects): fix vertical-mix menu selected 2024-07-23 15:52:01 +08:00
Soybean
24320ab899 Merge branch 'main' into example 2024-07-22 19:05:11 +08:00
Soybean
e8378318c6 chore(projects): release v1.3.1 2024-07-22 19:04:20 +08:00
Soybean
840273474f fix(projects): fix vertical-menu will not render when the layout is from mobile 2024-07-22 19:03:01 +08:00
Soybean
4e55b0e9bd fix(projects): fix HorizontalMixMenu inverted. fixed #563 2024-07-22 19:02:56 +08:00
Azir
5377002932 style(other): modify the Chinese name of the grayscale mode 2024-07-22 14:39:04 +08:00
青菜白玉汤
2f1f2c2b2a fix(projects): fix the issue of abnormal width of the sidebar in the top menu mix and reverse mode (#562)
(cherry picked from commit c469512bd4)
2024-07-22 13:34:35 +08:00
青菜白玉汤
c469512bd4 fix(projects): fix the issue of abnormal width of the sidebar in the top menu mix and reverse mode (#562) 2024-07-22 11:39:19 +08:00
Soybean
3a970a090d fix(components): fix component name and iconify icon name conflict 2024-07-22 01:12:00 +08:00
Soybean
4a7e9c2669 chore(projects): merge branch main into example 2024-07-22 00:57:11 +08:00
Soybean
d6f3effce0 chore(deps): update deps 2024-07-22 00:35:53 +08:00
Soybean
fa4367a7b9 optimize(projects): optimize page code: plugin_excel 2024-07-22 00:34:13 +08:00
Soybean
613c836e0f docs(projects): update CHANGELOG 2024-07-22 00:28:40 +08:00
Soybean
f73f70c51b chore(projects): release v1.3.0 2024-07-22 00:26:38 +08:00
Soybean
a0b76daeff docs(projects): update CHANGELOG 2024-07-22 00:26:01 +08:00
Soybean
f6bd6b8603 chore(deps): update deps 2024-07-22 00:21:58 +08:00
Soybean
087e532613 refactor(projects)!: refactor global menu & support reversed-horizontal-mix-menu. close #365 2024-07-22 00:19:12 +08:00
Ohh
c425822b3a feat(projects): add plugin excel export (#561) 2024-07-21 20:54:18 +08:00
Soybean
00f41dd25e refactor(projects): change css vars mount to root 2024-07-20 04:05:24 +08:00
Soybean
1d1b148c8f refactor(projects): combine theme tokens and theme settings. close #379 2024-07-20 04:00:32 +08:00
Soybean
958d0baf3f feat(packages): @sa/scripts: add ignore pattern list for command gitCommitVerify. close #504 2024-07-20 03:17:23 +08:00
Soybean
0206969e98 chore(projects): add script czh 2024-07-20 03:07:46 +08:00
Soybean
527fd79df2 feat(packages): @sa/axios: replace CancelTokenSource by AbortController. close #530, close #532 2024-07-20 03:04:23 +08:00
142vip.cn
06971f3935 feat(packages): @sa/scripts: command gitCommit support chinese (#548) 2024-07-20 02:45:31 +08:00
青菜白玉汤
f4eeb2ed63 fix(hooks): prevent program freezing when pagesize returns 0 (#545) 2024-07-20 02:45:25 +08:00
青菜白玉汤
793b16e9ba feat(projects): make branch main tiny & modify request retry times to 0 2024-07-20 02:44:32 +08:00
Soybean
447bda2b48 chore(projects): merge branch main into example 2024-07-20 02:35:42 +08:00
Soybean
cf9fc7d31f chore(projects): release v1.2.8 2024-07-20 02:27:55 +08:00
Soybean
58fc09623c docs(projects): update CHANGELOG 2024-07-20 02:27:35 +08:00
Soybean
276ea7fa2f fix(types): fix data type of useHookTable 2024-07-20 02:23:25 +08:00
Soybean
96c1044ffb fix(packages): @sa/hooks: fix searchParams of useHookTable. fixed #552 2024-07-20 02:18:58 +08:00
Soybean
a91335d74b refactor(projects): replace cloneDeep of lodash-es with klona 2024-07-20 02:14:20 +08:00
dodu2014
3e2a993db8 fix(types): Fixed the reference type error (#551) 2024-07-20 02:00:59 +08:00
Soybean
cf0192ad71 chore(deps): update deps 2024-07-20 02:00:21 +08:00
dodu2014
c0303a8637 fix(types): Fixed the reference type error (#551) 2024-07-19 18:12:29 +08:00
Ohh
c7ab61bbd4 feat(projects): add plugin pinyin-pro (#557) 2024-07-19 16:20:36 +08:00
Soybean
ec6a497452 fix(projects): fix page manage-role default-expanded-names 2024-07-19 10:19:33 +08:00
paynezhuang
074077dfaa feat(projects): Manage search area using collapsed components , close #537 (#549) 2024-07-15 14:46:04 +08:00
Soybean
fcf866783c chore(projects): merge branch main into example 2024-07-12 10:28:10 +08:00
Soybean
1c9e36c47b chore(projects): merge branch main into example 2024-06-21 10:15:20 +08:00
Soybean
d3210edc6a chore(projects): merge branch main into example 2024-06-15 01:13:36 +08:00
Soybean
799ebcc714 Merge branch 'main' into example 2024-06-14 00:05:12 +08:00
Soybean
4a8b9032cd chore(projects): merge branch main into example 2024-06-13 23:30:33 +08:00
Soybean
8fea803fca Merge branch 'main' into example 2024-06-12 11:10:08 +08:00
Soybean
599d0f10e8 Merge branch 'main' into example 2024-06-07 11:55:56 +08:00
Soybean
781f88d227 Merge branch 'main' into example 2024-06-06 23:33:41 +08:00
Soybean
5739346484 chore(projects): merge main to example 2024-06-06 10:36:31 +08:00
Soybean
d194f0b1b6 chore(projects): merge main to example 2024-06-04 20:24:34 +08:00
Soybean
19bea283c9 chore(projects): merge main to example 2024-06-02 01:30:02 +08:00
Soybean
53a8e5678a chore(projects): merge main to example 2024-05-24 02:28:43 +08:00
Soybean
10b231918a chore(projects): merge main to example 2024-05-20 01:24:46 +08:00
Soybean
81d51a9630 fix(projects): fix baidu map request protocol 2024-05-08 23:19:49 +08:00
Soybean
fbebef3acb fix(projects): fix page: plugin_video style 2024-05-08 23:01:38 +08:00
Soybean
12d7bf94ee optimize(projects): optimize code 2024-05-08 22:57:22 +08:00
Soybean
0478154db8 chore(projects): update pnpm-lock.yaml 2024-05-08 22:39:16 +08:00
Soybean
994ef2b9b1 merge main to example (#433)
* docs(projects): update CHANGELOG

* fix(styles): fix useTable type
2024-05-08 22:37:18 +08:00
paynezhuang
ef32eecfdb feat(projects): add page plugin_barcode (#432) 2024-05-08 22:35:12 +08:00
Soybean
83ae0bad0f docs(projects): update CHANGELOG (#431) 2024-05-07 22:02:11 +08:00
Soybean
9a9e2d144a chore(projects): lock deps version 2024-05-07 21:59:23 +08:00
lisong
a2110a98cf feat(projects): add page plugin_editor、plugin_icon、plugin_map、plugin_print、plugin_swiper and plugin_video (#430) 2024-05-07 21:56:23 +08:00
Soybean
daf6600c21 feat(projects): add page plugin_charts_echarts 2024-05-07 19:51:54 +08:00
Soybean
a9b3a961af Merge branch 'main' into example 2024-05-07 19:18:27 +08:00
Soybean
3a35d62e6f Merge branch 'main' into example 2024-05-07 18:22:22 +08:00
Soybean
aaf28a42c2 optimize(projects): optimize plugin_copy 2024-05-07 18:20:23 +08:00
恕瑞玛的皇帝
019f80faef feat(projects): 添加插件示例:剪贴板 2024-05-07 17:24:16 +08:00
95 changed files with 6987 additions and 2912 deletions

View File

@@ -1,6 +1,159 @@
# Changelog
## [v1.3.4](https://github.com/honghuangdc/soybean-admin/compare/v1.3.3...v1.3.4) (2024-08-01)
###    🚨 Breaking Changes
- **projects**: don't reset active menu of vertical-mix layout when it is mixSiderFixed &nbsp;-&nbsp; by @honghuangdc [<samp>(939c5)</samp>](https://github.com/honghuangdc/soybean-admin/commit/939c512)
### &nbsp;&nbsp;&nbsp;🛠 Optimizations
- **projects**: optimize code &nbsp;-&nbsp; by @honghuangdc [<samp>(cb1d4)</samp>](https://github.com/honghuangdc/soybean-admin/commit/cb1d445)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **projects**: update deps & fix vue-router type &nbsp;-&nbsp; by @honghuangdc [<samp>(96837)</samp>](https://github.com/honghuangdc/soybean-admin/commit/968370b)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;
## [v1.3.3](https://github.com/honghuangdc/soybean-admin/compare/v1.3.2...v1.3.3) (2024-07-30)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **projects**: fix watermark settings &nbsp;-&nbsp; by @honghuangdc [<samp>(5646a)</samp>](https://github.com/honghuangdc/soybean-admin/commit/5646a50)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: update CHANGELOG &nbsp;-&nbsp; by @honghuangdc [<samp>(ebc83)</samp>](https://github.com/honghuangdc/soybean-admin/commit/ebc838c)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;
## [v1.3.2](https://github.com/honghuangdc/soybean-admin/compare/v1.3.1...v1.3.2) (2024-07-30)
### &nbsp;&nbsp;&nbsp;🚀 Features
- **projects**:
- add color fading mode.close #567 &nbsp;-&nbsp; by @Azir-11 in https://github.com/honghuangdc/soybean-admin/issues/569 and https://github.com/honghuangdc/soybean-admin/issues/567 [<samp>(4dde4)</samp>](https://github.com/honghuangdc/soybean-admin/commit/4dde4c2)
- add full screen watermark. close#571 &nbsp;-&nbsp; by @paynezhuang in https://github.com/honghuangdc/soybean-admin/issues/573 and https://github.com/honghuangdc/soybean-admin/issues/571 [<samp>(ea8aa)</samp>](https://github.com/honghuangdc/soybean-admin/commit/ea8aa6c)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **projects**: fix vertical-mix menu selected &nbsp;-&nbsp; by @honghuangdc [<samp>(59f07)</samp>](https://github.com/honghuangdc/soybean-admin/commit/59f07d8)
### &nbsp;&nbsp;&nbsp;🛠 Optimizations
- **projects**:
- add type WatermarkProps &nbsp;-&nbsp; by @honghuangdc [<samp>(f26d0)</samp>](https://github.com/honghuangdc/soybean-admin/commit/f26d0a6)
- remove home NAlert closable &nbsp;-&nbsp; by @honghuangdc [<samp>(98b75)</samp>](https://github.com/honghuangdc/soybean-admin/commit/98b75c2)
- optimize response code comparison &nbsp;-&nbsp; by @honghuangdc [<samp>(cf67d)</samp>](https://github.com/honghuangdc/soybean-admin/commit/cf67d55)
- **types**:
- remove useless types. &nbsp;-&nbsp; by **Azir** [<samp>(eed61)</samp>](https://github.com/honghuangdc/soybean-admin/commit/eed617f)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: update CHANGELOG &nbsp;-&nbsp; by @honghuangdc [<samp>(d3759)</samp>](https://github.com/honghuangdc/soybean-admin/commit/d37591d)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**: update deps &nbsp;-&nbsp; by @honghuangdc [<samp>(993e9)</samp>](https://github.com/honghuangdc/soybean-admin/commit/993e9ca)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;[![paynezhuang](https://github.com/paynezhuang.png?size=48)](https://github.com/paynezhuang)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;
[Azir](mailto:2075125282@qq.com),&nbsp;
## [v1.3.1](https://github.com/honghuangdc/soybean-admin/compare/v1.3.0...v1.3.1) (2024-07-22)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **projects**:
- fix the issue of abnormal width of the sidebar in the top menu mix and reverse mode &nbsp;-&nbsp; by @Azir-11 in https://github.com/honghuangdc/soybean-admin/issues/562 [<samp>(c4695)</samp>](https://github.com/honghuangdc/soybean-admin/commit/c469512)
- fix HorizontalMixMenu inverted. fixed #563 &nbsp;-&nbsp; by @honghuangdc in https://github.com/honghuangdc/soybean-admin/issues/563 [<samp>(4e55b)</samp>](https://github.com/honghuangdc/soybean-admin/commit/4e55b0e)
- fix vertical-menu will not render when the layout is from mobile &nbsp;-&nbsp; by @honghuangdc [<samp>(84027)</samp>](https://github.com/honghuangdc/soybean-admin/commit/8402734)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: update CHANGELOG &nbsp;-&nbsp; by @honghuangdc [<samp>(613c8)</samp>](https://github.com/honghuangdc/soybean-admin/commit/613c836)
### &nbsp;&nbsp;&nbsp;🎨 Styles
- **other**: modify the Chinese name of the grayscale mode &nbsp;-&nbsp; by **Azir** [<samp>(53770)</samp>](https://github.com/honghuangdc/soybean-admin/commit/5377002)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;
[Azir](mailto:2075125282@qq.com),&nbsp;
## [v1.3.0](https://github.com/soybeanjs/soybean-admin/compare/v1.2.8...v1.3.0) (2024-07-22)
### &nbsp;&nbsp;&nbsp;🚨 Breaking Changes
- **projects**: refactor global menu & support `reversed-horizontal-mix-menu`. close #365 &nbsp;-&nbsp; by @honghuangdc in https://github.com/soybeanjs/soybean-admin/issues/365 [<samp>(087e5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/087e532)
### &nbsp;&nbsp;&nbsp;🚀 Features
- **packages**:
- `@sa/scripts`: command `gitCommit` support chinese &nbsp;-&nbsp; by @mmdapl in https://github.com/soybeanjs/soybean-admin/issues/548 [<samp>(06971)</samp>](https://github.com/soybeanjs/soybean-admin/commit/06971f3)
- @sa/axios: replace CancelTokenSource by AbortController. close #530, close #532 &nbsp;-&nbsp; by @honghuangdc in https://github.com/soybeanjs/soybean-admin/issues/530 and https://github.com/soybeanjs/soybean-admin/issues/532 [<samp>(527fd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/527fd79)
- @sa/scripts: add ignore pattern list for command `gitCommitVerify`. close #504 &nbsp;-&nbsp; by @honghuangdc in https://github.com/soybeanjs/soybean-admin/issues/504 [<samp>(958d0)</samp>](https://github.com/soybeanjs/soybean-admin/commit/958d0ba)
- **projects**:
- make branch `main` tiny & modify request retry times to 0 &nbsp;-&nbsp; by @Azir-11 [<samp>(793b1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/793b16e)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **hooks**: prevent program freezing when pagesize returns 0 &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/545 [<samp>(f4eeb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f4eeb2e)
### &nbsp;&nbsp;&nbsp;💅 Refactors
- **projects**:
- combine `theme tokens` and `theme settings`. close #379 &nbsp;-&nbsp; by @honghuangdc in https://github.com/soybeanjs/soybean-admin/issues/379 [<samp>(1d1b1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/1d1b148)
- change css vars mount to root &nbsp;-&nbsp; by @honghuangdc [<samp>(00f41)</samp>](https://github.com/soybeanjs/soybean-admin/commit/00f41dd)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: update CHANGELOG &nbsp;-&nbsp; by @honghuangdc [<samp>(a0b76)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a0b76da)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**: update deps &nbsp;-&nbsp; by @honghuangdc [<samp>(f6bd6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f6bd6b8)
- **projects**: add script `czh` &nbsp;-&nbsp; by @honghuangdc [<samp>(02069)</samp>](https://github.com/soybeanjs/soybean-admin/commit/0206969)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;[![mmdapl](https://github.com/mmdapl.png?size=48)](https://github.com/mmdapl)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;
## [v1.2.8](https://github.com/soybeanjs/soybean-admin/compare/v1.2.7...v1.2.8) (2024-07-20)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **packages**:
- @sa/hooks: fix searchParams of useHookTable. fixed #552 &nbsp;-&nbsp; by @honghuangdc in https://github.com/soybeanjs/soybean-admin/issues/552 [<samp>(96c10)</samp>](https://github.com/soybeanjs/soybean-admin/commit/96c1044)
- **types**:
- Fixed the reference type error &nbsp;-&nbsp; by **dodu2014** in https://github.com/soybeanjs/soybean-admin/issues/551 [<samp>(3e2a9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3e2a993)
- fix data type of useHookTable &nbsp;-&nbsp; by @honghuangdc [<samp>(276ea)</samp>](https://github.com/soybeanjs/soybean-admin/commit/276ea7f)
### &nbsp;&nbsp;&nbsp;💅 Refactors
- **projects**: replace `cloneDeep` of `lodash-es` with `klona` &nbsp;-&nbsp; by @honghuangdc [<samp>(a9133)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a91335d)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: update CHANGELOG &nbsp;-&nbsp; by @honghuangdc [<samp>(58fc0)</samp>](https://github.com/soybeanjs/soybean-admin/commit/58fc096)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**: update deps &nbsp;-&nbsp; by @honghuangdc [<samp>(cf019)</samp>](https://github.com/soybeanjs/soybean-admin/commit/cf0192a)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;
[dodu2014](mailto:dodu@live.cn)
## [v1.2.7](https://github.com/honghuangdc/soybean-admin/compare/v1.2.6...v1.2.7) (2024-07-12)
### &nbsp;&nbsp;&nbsp;🛠 Optimizations

View File

@@ -1,5 +1,182 @@
# 更新日志
## [v1.3.2](https://github.com/honghuangdc/soybean-admin/compare/v1.3.1...v1.3.2) (2024-07-30)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **项目**:
- 添加颜色渐变模式。关闭 #567 &nbsp;-&nbsp;@Azir-11 在 https://github.com/honghuangdc/soybean-admin/issues/569 和 https://github.com/honghuangdc/soybean-admin/issues/567 [<samp>(4dde4)</samp>](https://github.com/honghuangdc/soybean-admin/commit/4dde4c2)
- 添加全屏水印。关闭 #571 &nbsp;-&nbsp;@paynezhuang 在 https://github.com/honghuangdc/soybean-admin/issues/573 和 https://github.com/honghuangdc/soybean-admin/issues/571 [<samp>(ea8aa)</samp>](https://github.com/honghuangdc/soybean-admin/commit/ea8aa6c)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **项目**: 修复 vertical-mix 菜单选中问题 &nbsp;-&nbsp;@honghuangdc [<samp>(59f07)</samp>](https://github.com/honghuangdc/soybean-admin/commit/59f07d8)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **项目**:
- 添加类型 WatermarkProps &nbsp;-&nbsp;@honghuangdc [<samp>(f26d0)</samp>](https://github.com/honghuangdc/soybean-admin/commit/f26d0a6)
- 移除 home NAlert 可关闭 &nbsp;-&nbsp;@honghuangdc [<samp>(98b75)</samp>](https://github.com/honghuangdc/soybean-admin/commit/98b75c2)
- 优化响应代码比较 &nbsp;-&nbsp;@honghuangdc [<samp>(cf67d)</samp>](https://github.com/honghuangdc/soybean-admin/commit/cf67d55)
- **类型**:
- 移除无用类型。 &nbsp;-&nbsp;**Azir** [<samp>(eed61)</samp>](https://github.com/honghuangdc/soybean-admin/commit/eed617f)
### &nbsp;&nbsp;&nbsp;📖 文档
- **项目**: 更新更新日志 &nbsp;-&nbsp;@honghuangdc [<samp>(d3759)</samp>](https://github.com/honghuangdc/soybean-admin/commit/d37591d)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **依赖**: 更新依赖 &nbsp;-&nbsp;@honghuangdc [<samp>(993e9)</samp>](https://github.com/honghuangdc/soybean-admin/commit/993e9ca)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;[![paynezhuang](https://github.com/paynezhuang.png?size=48)](https://github.com/paynezhuang)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;
[Azir](mailto:2075125282@qq.com),&nbsp;
## [v1.3.1](https://github.com/honghuangdc/soybean-admin/compare/v1.3.0...v1.3.1) (2024-07-22)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **项目**:
- 修复顶部菜单混合和反向模式下侧边栏宽度异常的问题 &nbsp;-&nbsp;@Azir-11 在 https://github.com/honghuangdc/soybean-admin/issues/562 修复 [<samp>(c4695)</samp>](https://github.com/honghuangdc/soybean-admin/commit/c469512)
- 修复水平混合菜单反转问题。修复 #563 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/honghuangdc/soybean-admin/issues/563 修复 [<samp>(4e55b)</samp>](https://github.com/honghuangdc/soybean-admin/commit/4e55b0e)
- 修复当布局来自移动端时垂直菜单不渲染的问题 &nbsp;-&nbsp;@honghuangdc 修复 [<samp>(84027)</samp>](https://github.com/honghuangdc/soybean-admin/commit/8402734)
### &nbsp;&nbsp;&nbsp;📖 文档
- **项目**: 更新更新日志 &nbsp;-&nbsp;@honghuangdc 更新 [<samp>(613c8)</samp>](https://github.com/honghuangdc/soybean-admin/commit/613c836)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **其他**: 修改灰度模式的中文名称 &nbsp;-&nbsp;**Azir** 修改 [<samp>(53770)</samp>](https://github.com/honghuangdc/soybean-admin/commit/5377002)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;
[Azir](mailto:2075125282@qq.com),&nbsp;
## [v1.3.0](https://github.com/soybeanjs/soybean-admin/compare/v1.2.8...v1.3.0) (2024-07-22)
### &nbsp;&nbsp;&nbsp;🚨 破坏性变更
- **项目**: 重构全局菜单 & 支持 `reversed-horizontal-mix-menu`。关闭 #365 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/soybeanjs/soybean-admin/issues/365 提出 [<samp>(087e5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/087e532)
### &nbsp;&nbsp;&nbsp;🚀 功能
- **包**:
- `@sa/scripts`: 命令 `gitCommit` 支持中文 &nbsp;-&nbsp;@mmdapl 在 https://github.com/soybeanjs/soybean-admin/issues/548 提出 [<samp>(06971)</samp>](https://github.com/soybeanjs/soybean-admin/commit/06971f3)
- @sa/axios: 用 AbortController 替换 CancelTokenSource。关闭 #530, 关闭 #532 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/soybeanjs/soybean-admin/issues/530 和 https://github.com/soybeanjs/soybean-admin/issues/532 提出 [<samp>(527fd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/527fd79)
- @sa/scripts: 为命令 `gitCommitVerify` 添加忽略模式列表。关闭 #504 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/soybeanjs/soybean-admin/issues/504 提出 [<samp>(958d0)</samp>](https://github.com/soybeanjs/soybean-admin/commit/958d0ba)
- **项目**:
- 使分支 `main` 更精简 & 修改请求重试次数为 0 &nbsp;-&nbsp;@Azir-11 提出 [<samp>(793b1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/793b16e)
### &nbsp;&nbsp;&nbsp;🐞 修复
- **钩子**: 当 pagesize 返回 0 时防止程序冻结 &nbsp;-&nbsp;@Azir-11 在 https://github.com/soybeanjs/soybean-admin/issues/545 提出 [<samp>(f4eeb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f4eeb2e)
### &nbsp;&nbsp;&nbsp;💅 重构
- **项目**:
- 合并 `theme tokens``theme settings`。关闭 #379 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/soybeanjs/soybean-admin/issues/379 提出 [<samp>(1d1b1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/1d1b148)
- 将 css 变量挂载到 root &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(00f41)</samp>](https://github.com/soybeanjs/soybean-admin/commit/00f41dd)
### &nbsp;&nbsp;&nbsp;📖 文档
- **项目**: 更新更新日志 &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(a0b76)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a0b76da)
### &nbsp;&nbsp;&nbsp;🏡 杂务
- **依赖**: 更新依赖 &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(f6bd6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f6bd6b8)
- **项目**: 添加脚本 `czh` &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(02069)</samp>](https://github.com/soybeanjs/soybean-admin/commit/0206969)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;[![mmdapl](https://github.com/mmdapl.png?size=48)](https://github.com/mmdapl)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;
## [v1.2.8](https://github.com/soybeanjs/soybean-admin/compare/v1.2.7...v1.2.8) (2024-07-20)
### &nbsp;&nbsp;&nbsp;🐞 修复
- **包**:
- @sa/hooks: 修复 useHookTable 的 searchParams。修复了 #552 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/soybeanjs/soybean-admin/issues/552 提出 [<samp>(96c10)</samp>](https://github.com/soybeanjs/soybean-admin/commit/96c1044)
- **类型**:
- 修复了引用类型错误 &nbsp;-&nbsp;**dodu2014** 在 https://github.com/soybeanjs/soybean-admin/issues/551 提出 [<samp>(3e2a9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3e2a993)
- 修复 useHookTable 的数据类型 &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(276ea)</samp>](https://github.com/soybeanjs/soybean-admin/commit/276ea7f)
### &nbsp;&nbsp;&nbsp;💅 重构
- **项目**: 用 `klona` 替换 `lodash-es``cloneDeep` &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(a9133)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a91335d)
### &nbsp;&nbsp;&nbsp;📖 文档
- **项目**: 更新更新日志 &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(58fc0)</samp>](https://github.com/soybeanjs/soybean-admin/commit/58fc096)
### &nbsp;&nbsp;&nbsp;🏡 杂务
- **依赖**: 更新依赖 &nbsp;-&nbsp;@honghuangdc 提出 [<samp>(cf019)</samp>](https://github.com/soybeanjs/soybean-admin/commit/cf0192a)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;
[dodu2014](mailto:dodu@live.cn)
## [v1.2.7](https://github.com/honghuangdc/soybean-admin/compare/v1.2.6...v1.2.7) (2024-07-12)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **项目**: 支持自定义菜单图标大小 &nbsp;-&nbsp;@wynn-w 在 https://github.com/honghuangdc/soybean-admin/issues/534 提出 [<samp>(e035e)</samp>](https://github.com/honghuangdc/soybean-admin/commit/e035eab)
### &nbsp;&nbsp;&nbsp;🏡 杂务
- **依赖**:
- 更新依赖 &nbsp;-&nbsp;@honghuangdc [<samp>(72ede)</samp>](https://github.com/honghuangdc/soybean-admin/commit/72ede8b)
- 更新依赖 &nbsp;-&nbsp;@honghuangdc [<samp>(be13c)</samp>](https://github.com/honghuangdc/soybean-admin/commit/be13ca2)
- 更新依赖 &nbsp;-&nbsp;@honghuangdc [<samp>(752ec)</samp>](https://github.com/honghuangdc/soybean-admin/commit/752ec1e)
- **项目**:
- 修复已弃用的配置配置 &nbsp;-&nbsp;@paynezhuang 在 https://github.com/honghuangdc/soybean-admin/issues/524 提出 [<samp>(0d20e)</samp>](https://github.com/honghuangdc/soybean-admin/commit/0d20e4c)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;[![wynn-w](https://github.com/wynn-w.png?size=48)](https://github.com/wynn-w)&nbsp;&nbsp;[![paynezhuang](https://github.com/paynezhuang.png?size=48)](https://github.com/paynezhuang)&nbsp;&nbsp;
## [v1.2.6](https://github.com/honghuangdc/soybean-admin/compare/v1.2.5...v1.2.6) (2024-06-21)
### &nbsp;&nbsp;&nbsp;🐞 修复
- **项目**:
- 请求模态标题使用 i18n。修复 #507 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/honghuangdc/soybean-admin/issues/507 提出 [<samp>(f7de3)</samp>](https://github.com/honghuangdc/soybean-admin/commit/f7de3fd)
-`useTable` 添加 `getDataByPage`。修复 #499 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/honghuangdc/soybean-admin/issues/499 提出 [<samp>(425c6)</samp>](https://github.com/honghuangdc/soybean-admin/commit/425c69a)
- 修复登录重定向到 routeHome 时,动态路由的 routeHome 与静态路由不同的问题。修复 #511 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/honghuangdc/soybean-admin/issues/511 提出 [<samp>(49f60)</samp>](https://github.com/honghuangdc/soybean-admin/commit/49f60b2)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **项目**: 优化 `getRouteQueryOfLoginRoute` &nbsp;-&nbsp;@honghuangdc [<samp>(693f7)</samp>](https://github.com/honghuangdc/soybean-admin/commit/693f704)
### &nbsp;&nbsp;&nbsp;📖 文档
- **项目**:
- 更新更新日志 &nbsp;-&nbsp;@honghuangdc [<samp>(5c67d)</samp>](https://github.com/honghuangdc/soybean-admin/commit/5c67d06)
- 更新自述文件 &nbsp;-&nbsp;@honghuangdc [<samp>(1e67a)</samp>](https://github.com/honghuangdc/soybean-admin/commit/1e67ae8)
### &nbsp;&nbsp;&nbsp;🏡 杂务
- **依赖**:
- 更新依赖。关闭 #510 &nbsp;-&nbsp;@honghuangdc 在 https://github.com/honghuangdc/soybean-admin/issues/510 提出 [<samp>(53143)</samp>](https://github.com/honghuangdc/soybean-admin/commit/531432d)
- 更新依赖 &nbsp;-&nbsp;@honghuangdc [<samp>(c7f6f)</samp>](https://github.com/honghuangdc/soybean-admin/commit/c7f6f2a)
### &nbsp;&nbsp;&nbsp;🤖 CI
- **项目**:
- 添加 github 问题模板 &nbsp;-&nbsp;@honghuangdc [<samp>(b5027)</samp>](https://github.com/honghuangdc/soybean-admin/commit/b5027c8)
- 更新 github 问题模板 &nbsp;-&nbsp;@honghuangdc [<samp>(ff1d5)</samp>](https://github.com/honghuangdc/soybean-admin/commit/ff1d504)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![honghuangdc](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)&nbsp;&nbsp;
## [v1.2.5](https://github.com/soybeanjs/soybean-admin/compare/v1.2.4...v1.2.5) (2024-06-15)

View File

@@ -1,7 +1,7 @@
{
"name": "soybean-admin",
"type": "module",
"version": "1.2.7",
"version": "1.3.4",
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
"author": {
"name": "Soybean",
@@ -35,6 +35,7 @@
"build:test": "vite build --mode test",
"cleanup": "sa cleanup",
"commit": "sa git-commit",
"czh": "sa git-commit -l=zh-cn",
"dev": "vite --mode test",
"dev:prod": "vite --mode prod",
"gen-route": "sa gen-route",
@@ -46,6 +47,8 @@
"update-pkg": "sa update-pkg"
},
"dependencies": {
"@antv/data-set": "0.11.8",
"@antv/g2": "5.2.1",
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@sa/axios": "workspace:*",
@@ -55,50 +58,63 @@
"@sa/utils": "workspace:*",
"@vueuse/core": "10.11.0",
"clipboard": "2.0.11",
"dayjs": "1.11.11",
"dayjs": "1.11.12",
"dhtmlx-gantt": "8.0.9",
"dompurify": "3.1.6",
"echarts": "5.5.1",
"lodash-es": "4.17.21",
"naive-ui": "2.38.2",
"jsbarcode": "3.11.6",
"naive-ui": "2.39.0",
"nprogress": "0.2.0",
"pinia": "2.1.7",
"pinia": "2.2.0",
"pinyin-pro": "3.23.1",
"print-js": "1.6.0",
"swiper": "11.1.5",
"tailwind-merge": "2.4.0",
"vue": "3.4.31",
"typeit": "8.8.4",
"vditor": "3.10.4",
"vue": "3.4.35",
"vue-draggable-plus": "0.5.2",
"vue-i18n": "9.13.1",
"vue-router": "4.4.0"
"vue-pdf-embed": "2.1.0",
"vue-router": "4.4.1",
"wangeditor": "4.7.15",
"xgplayer": "3.0.19",
"xlsx": "0.18.5"
},
"devDependencies": {
"@elegant-router/vue": "0.3.7",
"@iconify/json": "2.2.226",
"@amap/amap-jsapi-types": "0.0.15",
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.232",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.3.7",
"@types/lodash-es": "4.17.12",
"@types/node": "20.14.10",
"@soybeanjs/eslint-config": "1.4.0",
"@types/bmapgl": "0.0.7",
"@types/dompurify": "3.0.5",
"@types/node": "22.0.1",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "0.61.3",
"@unocss/preset-icons": "0.61.3",
"@unocss/preset-uno": "0.61.3",
"@unocss/transformer-directives": "0.61.3",
"@unocss/transformer-variant-group": "0.61.3",
"@unocss/vite": "0.61.3",
"@vitejs/plugin-vue": "5.0.5",
"@unocss/eslint-config": "0.61.9",
"@unocss/preset-icons": "0.61.9",
"@unocss/preset-uno": "0.61.9",
"@unocss/transformer-directives": "0.61.9",
"@unocss/transformer-variant-group": "0.61.9",
"@unocss/vite": "0.61.9",
"@vitejs/plugin-vue": "5.1.1",
"@vitejs/plugin-vue-jsx": "4.0.0",
"eslint": "9.6.0",
"eslint": "9.8.0",
"eslint-plugin-vue": "9.27.0",
"lint-staged": "15.2.7",
"sass": "1.77.7",
"sass": "1.77.8",
"simple-git-hooks": "2.11.1",
"tsx": "4.16.2",
"typescript": "5.5.3",
"unplugin-icons": "0.19.0",
"unplugin-vue-components": "0.27.2",
"vite": "5.3.3",
"tsx": "4.16.3",
"typescript": "5.5.4",
"unplugin-icons": "0.19.1",
"unplugin-vue-components": "0.27.3",
"vite": "5.3.5",
"vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.3.5",
"vite-plugin-vue-devtools": "7.3.7",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.0.26"
"vue-tsc": "2.0.29"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/axios",
"version": "1.2.7",
"version": "1.3.4",
"exports": {
".": "./src/index.ts"
},
@@ -12,7 +12,7 @@
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.7.2",
"axios-retry": "4.4.1",
"axios-retry": "4.4.2",
"qs": "6.12.3"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
import axios, { AxiosError } from 'axios';
import type { AxiosResponse, CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import axiosRetry from 'axios-retry';
import { nanoid } from '@sa/utils';
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
@@ -22,7 +22,7 @@ function createCommonRequest<ResponseData = any>(
const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf);
const cancelTokenSourceMap = new Map<string, CancelTokenSource>();
const abortControllerMap = new Map<string, AbortController>();
// config axios retry
const retryOptions = createRetryOptions(axiosConf);
@@ -35,10 +35,12 @@ function createCommonRequest<ResponseData = any>(
const requestId = nanoid();
config.headers.set(REQUEST_ID_KEY, requestId);
// config cancel token
const cancelTokenSource = axios.CancelToken.source();
config.cancelToken = cancelTokenSource.token;
cancelTokenSourceMap.set(requestId, cancelTokenSource);
// config abort controller
if (!config.signal) {
const abortController = new AbortController();
config.signal = abortController.signal;
abortControllerMap.set(requestId, abortController);
}
// handle config by hook
const handledConfig = opts.onRequest?.(config) || config;
@@ -79,18 +81,18 @@ function createCommonRequest<ResponseData = any>(
);
function cancelRequest(requestId: string) {
const cancelTokenSource = cancelTokenSourceMap.get(requestId);
if (cancelTokenSource) {
cancelTokenSource.cancel();
cancelTokenSourceMap.delete(requestId);
const abortController = abortControllerMap.get(requestId);
if (abortController) {
abortController.abort();
abortControllerMap.delete(requestId);
}
}
function cancelAllRequest() {
cancelTokenSourceMap.forEach(cancelTokenSource => {
cancelTokenSource.cancel();
abortControllerMap.forEach(abortController => {
abortController.abort();
});
cancelTokenSourceMap.clear();
abortControllerMap.clear();
}
return {

View File

@@ -20,7 +20,7 @@ export function createDefaultOptions<ResponseData = any>(options?: Partial<Reque
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
const retryConfig: IAxiosRetryConfig = {
retries: 3
retries: 0
};
Object.assign(retryConfig, config);

View File

@@ -69,7 +69,19 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
};
export interface RequestInstanceCommon<T> {
/**
* cancel the request by request id
*
* if the request provide abort controller sign from config, it will not collect in the abort controller map
*
* @param requestId
*/
cancelRequest: (requestId: string) => void;
/**
* cancel all request
*
* if the request provide abort controller sign from config, it will not collect in the abort controller map
*/
cancelAllRequest: () => void;
/** you can set custom state in the request instance */
state: T;

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/color",
"version": "1.2.7",
"version": "1.3.4",
"exports": {
".": "./src/index.ts"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/hooks",
"version": "1.2.7",
"version": "1.3.4",
"exports": {
".": "./src/index.ts"
},
@@ -10,6 +10,7 @@
}
},
"dependencies": {
"@sa/axios": "workspace:*"
"@sa/axios": "workspace:*",
"@sa/utils": "workspace:*"
}
}

View File

@@ -1,5 +1,6 @@
import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue';
import { jsonClone } from '@sa/utils';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
@@ -65,11 +66,11 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
const allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<T[]> = ref([]);
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
@@ -129,7 +130,7 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, apiParams);
Object.assign(searchParams, jsonClone(apiParams));
}
if (immediate) {

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/materials",
"version": "1.2.7",
"version": "1.3.4",
"exports": {
".": "./src/index.ts"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/fetch",
"version": "1.2.7",
"version": "1.3.4",
"exports": {
".": "./src/index.ts"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/scripts",
"version": "1.2.7",
"version": "1.3.4",
"bin": {
"sa": "./bin.ts"
},
@@ -21,7 +21,7 @@
"enquirer": "2.4.1",
"execa": "9.3.0",
"kolorist": "1.8.0",
"npm-check-updates": "16.14.20",
"npm-check-updates": "17.0.0",
"rimraf": "6.0.1"
}
}

View File

@@ -1,9 +1,9 @@
import path from 'node:path';
import { readFileSync } from 'node:fs';
import { prompt } from 'enquirer';
import { bgRed, green, red, yellow } from 'kolorist';
import { execCommand } from '../shared';
import type { CliOption } from '../types';
import { locales } from '../locales';
import type { Lang } from '../locales';
interface PromptObject {
types: string;
@@ -14,13 +14,11 @@ interface PromptObject {
/**
* Git commit with Conventional Commits standard
*
* @param gitCommitTypes
* @param gitCommitScopes
* @param lang
*/
export async function gitCommit(
gitCommitTypes: CliOption['gitCommitTypes'],
gitCommitScopes: CliOption['gitCommitScopes']
) {
export async function gitCommit(lang: Lang = 'en-us') {
const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
const typesChoices = gitCommitTypes.map(([value, msg]) => {
const nameWithSuffix = `${value}:`;
@@ -41,19 +39,19 @@ export async function gitCommit(
{
name: 'types',
type: 'select',
message: 'Please select a type',
message: gitCommitMessages.types,
choices: typesChoices
},
{
name: 'scopes',
type: 'select',
message: 'Please select a scope',
message: gitCommitMessages.scopes,
choices: scopesChoices
},
{
name: 'description',
type: 'text',
message: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
message: gitCommitMessages.description
}
]);
@@ -67,20 +65,20 @@ export async function gitCommit(
}
/** Git commit message verify */
export async function gitCommitVerify() {
export async function gitCommitVerify(lang: Lang = 'en-us', ignores: RegExp[] = []) {
const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
if (ignores.some(regExp => regExp.test(commitMsg))) return;
const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
if (!REG_EXP.test(commitMsg)) {
throw new Error(
`${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
)}`
);
const errorMsg = locales[lang].gitCommitVerify;
throw new Error(errorMsg);
}
}

View File

@@ -12,34 +12,18 @@ const defaultOptions: CliOption = {
'**/node_modules',
'!node_modules/**'
],
gitCommitTypes: [
['feat', 'A new feature'],
['fix', 'A bug fix'],
['docs', 'Documentation only changes'],
['style', 'Changes that do not affect the meaning of the code'],
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
['perf', 'A code change that improves performance'],
['optimize', 'A code change that optimizes code quality'],
['test', 'Adding missing tests or correcting existing tests'],
['build', 'Changes that affect the build system or external dependencies'],
['ci', 'Changes to our CI configuration files and scripts'],
['chore', "Other changes that don't modify src or test files"],
['revert', 'Reverts a previous commit']
],
gitCommitScopes: [
['projects', 'project'],
['packages', 'packages'],
['components', 'components'],
['hooks', 'hook functions'],
['utils', 'utils functions'],
['types', 'TS declaration'],
['styles', 'style'],
['deps', 'project dependencies'],
['release', 'release project'],
['other', 'other changes']
],
ncuCommandArgs: ['--deep', '-u'],
changelogOptions: {}
changelogOptions: {},
gitCommitVerifyIgnores: [
/^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m,
/^(Merge tag (.*?))(?:\r?\n)*$/m,
/^(R|r)evert (.*)/,
/^(amend|fixup|squash)!/,
/^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
/^Merge remote-tracking branch(\s*)(.*)/,
/^Automatic merge(.*)/,
/^Auto-merged (.*?) into (.*)/
]
};
export async function loadCliOptions(overrides?: Partial<CliOption>, cwd = process.cwd()) {

View File

@@ -3,6 +3,7 @@ import { blue, lightGreen } from 'kolorist';
import { version } from '../package.json';
import { cleanup, genChangelog, generateRoute, gitCommit, gitCommitVerify, release, updatePkg } from './commands';
import { loadCliOptions } from './config';
import type { Lang } from './locales';
type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'changelog' | 'release' | 'gen-route';
@@ -18,13 +19,19 @@ interface CommandArg {
/** Generate changelog by total tags */
total?: boolean;
/**
* The glob pattern of dirs to cleanup
* The glob pattern of dirs to clean up
*
* If not set, it will use the default value
*
* Multiple values use "," to separate them
*/
cleanupDir?: string;
/**
* display lang of cli
*
* @default 'en-us'
*/
lang?: Lang;
}
export async function setupCli() {
@@ -44,6 +51,7 @@ export async function setupCli() {
'-c, --cleanupDir <dir>',
'The glob pattern of dirs to cleanup, If not set, it will use the default value, Multiple values use "," to separate them'
)
.option('-l, --lang <lang>', 'display lang of cli', { default: 'en-us', type: [String] })
.help();
const commands: CommandWithAction<CommandArg> = {
@@ -61,14 +69,14 @@ export async function setupCli() {
},
'git-commit': {
desc: 'git commit, generate commit message which match Conventional Commits standard',
action: async () => {
await gitCommit(cliOptions.gitCommitTypes, cliOptions.gitCommitScopes);
action: async args => {
await gitCommit(args?.lang);
}
},
'git-commit-verify': {
desc: 'verify git commit message, make sure it match Conventional Commits standard',
action: async () => {
await gitCommitVerify();
action: async args => {
await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores);
}
},
changelog: {

View File

@@ -0,0 +1,78 @@
import { bgRed, green, red, yellow } from 'kolorist';
export type Lang = 'zh-cn' | 'en-us';
export const locales = {
'zh-cn': {
gitCommitMessages: {
types: '请选择提交类型',
scopes: '请选择提交范围',
description: `请输入描述信息(${yellow('!')}开头表示破坏性改动`
},
gitCommitTypes: [
['feat', '新功能'],
['fix', '修复Bug'],
['docs', '只更新文档'],
['style', '修改代码风格,不影响代码含义的变更'],
['refactor', '代码重构,既不修复 bug 也不添加功能的代码变更'],
['perf', '可提高性能的代码更改'],
['optimize', '优化代码质量的代码更改'],
['test', '添加缺失的测试或更正现有测'],
['build', '影响构建系统或外部依赖项的更改'],
['ci', '对 CI 配置文件和脚本的更改'],
['chore', '没有修改src或测试文件的其他变更'],
['revert', '还原先前的提交']
] as [string, string][],
gitCommitScopes: [
['projects', '项目'],
['packages', '包'],
['components', '组件'],
['hooks', '钩子函数'],
['utils', '工具函数'],
['types', 'TS类型声明'],
['styles', '代码风格'],
['deps', '项目依赖'],
['release', '发布项目新版本'],
['other', '其他的变更']
] as [string, string][],
gitCommitVerify: `${bgRed(' 错误 ')} ${red('git 提交信息必须符合 Conventional Commits 标准!')}\n\n${green(
'推荐使用命令 `pnpm commit` 生成符合 Conventional Commits 标准的提交信息。\n获取有关 Conventional Commits 的更多信息,请访问此链接: https://conventionalcommits.org'
)}`
},
'en-us': {
gitCommitMessages: {
types: 'Please select a type',
scopes: 'Please select a scope',
description: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
},
gitCommitTypes: [
['feat', 'A new feature'],
['fix', 'A bug fix'],
['docs', 'Documentation only changes'],
['style', 'Changes that do not affect the meaning of the code'],
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
['perf', 'A code change that improves performance'],
['optimize', 'A code change that optimizes code quality'],
['test', 'Adding missing tests or correcting existing tests'],
['build', 'Changes that affect the build system or external dependencies'],
['ci', 'Changes to our CI configuration files and scripts'],
['chore', "Other changes that don't modify src or test files"],
['revert', 'Reverts a previous commit']
] as [string, string][],
gitCommitScopes: [
['projects', 'project'],
['packages', 'packages'],
['components', 'components'],
['hooks', 'hook functions'],
['utils', 'utils functions'],
['types', 'TS declaration'],
['styles', 'style'],
['deps', 'project dependencies'],
['release', 'release project'],
['other', 'other changes']
] as [string, string][],
gitCommitVerify: `${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
)}`
}
} satisfies Record<Lang, Record<string, unknown>>;

View File

@@ -14,10 +14,6 @@ export interface CliOption {
* ```
*/
cleanupDirs: string[];
/** Git commit types */
gitCommitTypes: [string, string][];
/** Git commit scopes */
gitCommitScopes: [string, string][];
/**
* Npm-check-updates command args
*
@@ -30,4 +26,6 @@ export interface CliOption {
* @link https://github.com/soybeanjs/changelog
*/
changelogOptions: Partial<ChangelogOption>;
/** The ignore pattern list of git commit verify */
gitCommitVerifyIgnores: RegExp[];
}

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/uno-preset",
"version": "1.2.7",
"version": "1.3.4",
"exports": {
".": "./src/index.ts"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/utils",
"version": "1.2.7",
"version": "1.3.4",
"exports": {
".": "./src/index.ts"
},
@@ -12,6 +12,7 @@
"dependencies": {
"colord": "2.9.3",
"crypto-js": "4.2.0",
"klona": "2.0.6",
"localforage": "1.10.0",
"nanoid": "5.0.7"
},

View File

@@ -1,3 +1,4 @@
export * from './crypto';
export * from './storage';
export * from './nanoid';
export * from './klona';

View File

@@ -0,0 +1,3 @@
import { klona as jsonClone } from 'klona/json';
export { jsonClone };

5189
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { NConfigProvider, darkTheme } from 'naive-ui';
import type { WatermarkProps } from 'naive-ui';
import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme';
import { naiveDateLocales, naiveLocales } from './locales/naive';
@@ -21,6 +22,22 @@ const naiveLocale = computed(() => {
const naiveDateLocale = computed(() => {
return naiveDateLocales[appStore.locale];
});
const watermarkProps = computed<WatermarkProps>(() => {
return {
content: themeStore.watermark?.text || 'SoybeanAdmin',
cross: true,
fullscreen: true,
fontSize: 16,
lineHeight: 16,
width: 384,
height: 384,
xOffset: 12,
yOffset: 60,
rotate: -15,
zIndex: 9999
};
});
</script>
<template>
@@ -33,6 +50,7 @@ const naiveDateLocale = computed(() => {
>
<AppProvider>
<RouterView class="bg-layout" />
<NWatermark v-if="themeStore.watermark?.visible" v-bind="watermarkProps" />
</AppProvider>
</NConfigProvider>
</template>

View File

@@ -9,7 +9,7 @@ defineProps<Props>();
</script>
<template>
<div class="bg-container text-base_text transition-300" :class="{ 'bg-inverted text-#1f1f1f': inverted }">
<div class="bg-container text-base-text transition-300" :class="{ 'bg-inverted text-#1f1f1f': inverted }">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,78 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
defineOptions({ name: 'CustomIconSelect' });
interface Props {
/** Selected icon */
value: string;
/** List of icons */
icons: string[];
/** Icon for when nothing is selected */
emptyIcon?: string;
}
const props = withDefaults(defineProps<Props>(), {
emptyIcon: 'mdi:apps'
});
interface Emits {
(e: 'update:value', val: string): void;
}
const emit = defineEmits<Emits>();
const modelValue = computed({
get() {
return props.value;
},
set(val: string) {
emit('update:value', val);
}
});
const selectedIcon = computed(() => modelValue.value || props.emptyIcon);
const searchValue = ref('');
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
function handleChange(iconItem: string) {
modelValue.value = iconItem;
}
</script>
<template>
<NPopover placement="bottom-end" trigger="click">
<template #trigger>
<NInput v-model:value="modelValue" readonly placeholder="点击选择图标">
<template #suffix>
<SvgIcon :icon="selectedIcon" class="p-5px text-30px" />
</template>
</NInput>
</template>
<template #header>
<NInput v-model:value="searchValue" placeholder="搜索图标"></NInput>
</template>
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
<span v-for="iconItem in iconsList" :key="iconItem" @click="handleChange(iconItem)">
<SvgIcon
:icon="iconItem"
class="m-2px cursor-pointer border-1px border-#d9d9d9 p-5px text-30px"
:class="{ 'border-primary': modelValue === iconItem }"
/>
</span>
</div>
<NEmpty v-else class="w-306px" description="你什么也找不到" />
</NPopover>
</template>
<style lang="scss" scoped>
:deep(.n-input-wrapper) {
padding-right: 0;
}
:deep(.n-input__suffix) {
border: 1px solid #d9d9d9;
}
</style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import WebSiteLink from './web-site-link.vue';
defineOptions({ name: 'GithubLink' });
interface Props {
/** github link */
link: string;
}
defineProps<Props>();
</script>
<template>
<WebSiteLink label="github地址" :link="link" />
</template>
<style scoped></style>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
defineOptions({ name: 'WebSiteLink' });
interface Props {
/** Web site name */
label: string;
/** Web site link */
link: string;
}
defineProps<Props>();
</script>
<template>
<p>
<span>{{ label }}</span>
<a class="text-blue-500" :href="link" target="#">
{{ link }}
</a>
</p>
</template>
<style scoped></style>

View File

@@ -1,5 +1,9 @@
import { transformRecordToOption } from '@/utils/common';
export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
light: 'theme.themeSchema.light',
dark: 'theme.themeSchema.dark',

8
src/constants/map-sdk.ts Normal file
View File

@@ -0,0 +1,8 @@
/** baidu map sdk url */
export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`;
/** Amap sdk url */
export const AMAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';
/** tencent sdk url */
export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7';

View File

@@ -30,17 +30,30 @@ export function useRouterPush(inSetup = true) {
name: key
};
if (query) {
if (Object.keys(query || {}).length) {
routeLocation.query = query;
}
if (params) {
if (Object.keys(params || {}).length) {
routeLocation.params = params;
}
return routerPush(routeLocation);
}
function routerPushByKeyWithMetaQuery(key: RouteKey) {
const allRoutes = router.getRoutes();
const meta = allRoutes.find(item => item.name === key)?.meta || null;
const query: Record<string, string> = {};
meta?.query?.forEach(item => {
query[item.key] = item.value;
});
return routerPushByKey(key, { query });
}
async function toHome() {
return routerPushByKey('root');
}
@@ -95,6 +108,7 @@ export function useRouterPush(inSetup = true) {
routerPush,
routerBack,
routerPushByKey,
routerPushByKeyWithMetaQuery,
toLogin,
toggleLoginModule,
redirectFromLogin

View File

@@ -1,7 +1,7 @@
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
import type { Ref } from 'vue';
import type { PaginationProps } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { jsonClone } from '@sa/utils';
import { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
@@ -40,17 +40,20 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
const pageSize = size <= 0 ? 10 : size;
const recordsWithIndex = records.map((item, index) => {
return {
...item,
index: (current - 1) * size + index + 1
index: (current - 1) * pageSize + index + 1
};
});
return {
data: recordsWithIndex,
pageNum: current,
pageSize: size,
pageSize,
total
};
},
@@ -225,7 +228,7 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
function handleEdit(id: T['id']) {
operateType.value = 'edit';
const findItem = data.value.find(item => item.id === id) || null;
editingData.value = cloneDeep(findItem);
editingData.value = jsonClone(findItem);
openDrawer();
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, defineAsyncComponent } from 'vue';
import { AdminLayout, LAYOUT_SCROLL_EL_ID } from '@sa/materials';
import type { LayoutMode } from '@sa/materials';
import { useAppStore } from '@/store/modules/app';
@@ -18,7 +18,9 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { menus } = setupMixMenuContext();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
const layoutMode = computed(() => {
const vertical: LayoutMode = 'vertical';
@@ -26,30 +28,34 @@ const layoutMode = computed(() => {
return themeStore.layout.mode.includes(vertical) ? vertical : horizontal;
});
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: {
showLogo: false,
showMenu: false,
showMenuToggler: true
},
'vertical-mix': {
showLogo: false,
showMenu: false,
showMenuToggler: false
},
horizontal: {
showLogo: true,
showMenu: true,
showMenuToggler: false
},
'horizontal-mix': {
showLogo: true,
showMenu: true,
showMenuToggler: false
}
};
const headerProps = computed(() => {
const { mode, reverseHorizontalMix } = themeStore.layout;
const headerProps = computed(() => headerPropsConfig[themeStore.layout.mode]);
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: {
showLogo: false,
showMenu: false,
showMenuToggler: true
},
'vertical-mix': {
showLogo: false,
showMenu: false,
showMenuToggler: false
},
horizontal: {
showLogo: true,
showMenu: true,
showMenuToggler: false
},
'horizontal-mix': {
showLogo: true,
showMenu: true,
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
}
};
return headerPropsConfig[mode];
});
const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
@@ -62,11 +68,16 @@ const siderWidth = computed(() => getSiderWidth());
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
function getSiderWidth() {
const { reverseHorizontalMix } = themeStore.layout;
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
}
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && menus.value.length) {
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
}
@@ -74,11 +85,16 @@ function getSiderWidth() {
}
function getSiderCollapsedWidth() {
const { reverseHorizontalMix } = themeStore.layout;
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
}
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix.value && appStore.mixSiderFixed && menus.value.length) {
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
}
@@ -116,6 +132,7 @@ function getSiderCollapsedWidth() {
<template #sider>
<GlobalSider />
</template>
<GlobalMenu />
<GlobalContent />
<ThemeDrawer />
<template #footer>

View File

@@ -26,10 +26,30 @@ function useMixMenu() {
setActiveFirstLevelMenuKey(firstLevelRouteName);
}
const menus = computed(
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const childLevelMenus = computed<App.Global.Menu[]>(
() => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const isActiveFirstLevelMenuHasChildren = computed(() => {
if (!activeFirstLevelMenuKey.value) {
return false;
}
const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
return Boolean(findItem?.children?.length);
});
watch(
() => route.name,
() => {
@@ -39,9 +59,12 @@ function useMixMenu() {
);
return {
allMenus,
firstLevelMenus,
childLevelMenus,
isActiveFirstLevelMenuHasChildren,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey,
menus
getActiveFirstLevelMenuKey
};
}

View File

@@ -68,6 +68,7 @@ function handleDropdown(key: DropdownKey) {
if (key === 'logout') {
logout();
} else {
// If your other options are jumps from other routes, they will be directly supported here
routerPushByKey(key);
}
}

View File

@@ -1,14 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import HorizontalMenu from '../global-menu/base-menu.vue';
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
import GlobalLogo from '../global-logo/index.vue';
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
import GlobalSearch from '../global-search/index.vue';
import { useMixMenuContext } from '../../context';
import ThemeButton from './components/theme-button.vue';
import UserAvatar from './components/user-avatar.vue';
@@ -29,29 +26,15 @@ defineProps<Props>();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { isFullscreen, toggle } = useFullscreen();
const { menus } = useMixMenuContext();
const headerMenus = computed(() => {
if (themeStore.layout.mode === 'horizontal') {
return routeStore.menus;
}
if (themeStore.layout.mode === 'horizontal-mix') {
return menus.value;
}
return [];
});
</script>
<template>
<DarkModeContainer class="h-full flex-y-center px-12px shadow-header">
<GlobalLogo v-if="showLogo" class="h-full" :style="{ width: themeStore.sider.width + 'px' }" />
<HorizontalMenu v-if="showMenu" mode="horizontal" :menus="headerMenus" class="px-12px" />
<MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
<div v-if="showMenu" :id="GLOBAL_HEADER_MENU_ID" class="h-full flex-y-center flex-1-hidden"></div>
<div v-else class="h-full flex-y-center flex-1-hidden">
<MenuToggler v-if="showMenuToggler" :collapsed="appStore.siderCollapse" @click="appStore.toggleSiderCollapse" />
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
</div>
<div class="h-full flex-y-center justify-end">

View File

@@ -1,96 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { MentionOption, MenuProps } from 'naive-ui';
import { SimpleScrollbar } from '@sa/materials';
import type { RouteKey } from '@elegant-router/types';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
defineOptions({
name: 'BaseMenu'
});
interface Props {
darkTheme?: boolean;
mode?: MenuProps['mode'];
menus: App.Global.Menu[];
}
const props = withDefaults(defineProps<Props>(), {
mode: 'vertical'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKey } = useRouterPush();
const naiveMenus = computed(() => props.menus as unknown as MentionOption[]);
const isHorizontal = computed(() => props.mode === 'horizontal');
const siderCollapse = computed(() => themeStore.layout.mode === 'vertical' && appStore.siderCollapse);
const headerHeight = computed(() => `${themeStore.header.height}px`);
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (isHorizontal.value || siderCollapse.value || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
function handleClickMenu(key: RouteKey) {
const query = routeStore.getRouteQueryOfMetaByKey(key);
routerPushByKey(key, { query });
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
:mode="mode"
:value="selectedKey"
:collapsed="siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:options="naiveMenus"
:inverted="darkTheme"
:indent="18"
responsive
@update:value="handleClickMenu"
/>
</SimpleScrollbar>
</template>
<style scoped>
:deep(.n-menu--horizontal) {
--n-item-height: v-bind(headerHeight) !important;
}
</style>

View File

@@ -3,31 +3,29 @@ import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core';
import { SimpleScrollbar } from '@sa/materials';
import { transformColorWithOpacity } from '@sa/color';
import { useAppStore } from '@/store/modules/app';
import { useRouteStore } from '@/store/modules/route';
import { useThemeStore } from '@/store/modules/theme';
defineOptions({
name: 'FirstLevelMenu'
});
interface Props {
menus: App.Global.Menu[];
activeMenuKey?: string;
inverted?: boolean;
siderCollapse?: boolean;
darkMode?: boolean;
themeColor: string;
}
defineProps<Props>();
const props = defineProps<Props>();
interface Emits {
(e: 'select', menu: App.Global.Menu): boolean;
(e: 'toggleSiderCollapse'): void;
}
const emit = defineEmits<Emits>();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
interface MixMenuItemProps {
/** Menu item label */
label: App.Global.Menu['label'];
@@ -36,12 +34,12 @@ interface MixMenuItemProps {
/** Active menu item */
active: boolean;
/** Mini size */
isMini: boolean;
isMini?: boolean;
}
const [DefineMixMenuItem, MixMenuItem] = createReusableTemplate<MixMenuItemProps>();
const selectedBgColor = computed(() => {
const { darkMode, themeColor } = themeStore;
const { darkMode, themeColor } = props;
const light = transformColorWithOpacity(themeColor, 0.1, '#ffffff');
const dark = transformColorWithOpacity(themeColor, 0.3, '#000000');
@@ -52,6 +50,10 @@ const selectedBgColor = computed(() => {
function handleClickMixMenu(menu: App.Global.Menu) {
emit('select', menu);
}
function toggleSiderCollapse() {
emit('toggleSiderCollapse');
}
</script>
<template>
@@ -80,21 +82,21 @@ function handleClickMixMenu(menu: App.Global.Menu) {
<slot></slot>
<SimpleScrollbar>
<MixMenuItem
v-for="menu in routeStore.menus"
v-for="menu in menus"
:key="menu.key"
:label="menu.label"
:icon="menu.icon"
:active="menu.key === activeMenuKey"
:is-mini="appStore.siderCollapse"
:is-mini="siderCollapse"
@click="handleClickMixMenu(menu)"
/>
</SimpleScrollbar>
<MenuToggler
arrow-icon
:collapsed="appStore.siderCollapse"
:collapsed="siderCollapse"
:z-index="99"
:class="{ 'text-white:88 !hover:text-white': inverted }"
@click="appStore.toggleSiderCollapse"
@click="toggleSiderCollapse"
/>
</div>
</template>

View File

@@ -1,28 +0,0 @@
<script setup lang="ts">
import { useRouterPush } from '@/hooks/common/router';
import { useMixMenuContext } from '../../context';
import FirstLevelMenu from './first-level-menu.vue';
defineOptions({
name: 'HorizontalMixMenu'
});
const { activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
const { routerPushByKey } = useRouterPush();
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKey(menu.routeKey);
}
}
</script>
<template>
<FirstLevelMenu :active-menu-key="activeFirstLevelMenuKey" @select="handleSelectMixMenu">
<slot></slot>
</FirstLevelMenu>
</template>
<style scoped></style>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import VerticalMenu from './modules/vertical-menu.vue';
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
import HorizontalMenu from './modules/horizontal-menu.vue';
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
defineOptions({
name: 'GlobalMenu'
});
const appStore = useAppStore();
const themeStore = useThemeStore();
const activeMenu = computed(() => {
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
vertical: VerticalMenu,
'vertical-mix': VerticalMixMenu,
horizontal: HorizontalMenu,
'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
};
return menuMap[themeStore.layout.mode];
});
const reRenderVertical = computed(() => themeStore.layout.mode === 'vertical' && appStore.isMobile);
</script>
<template>
<component :is="activeMenu" :key="reRenderVertical" />
</template>
<style scoped></style>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
defineOptions({
name: 'HorizontalMenu'
});
const route = useRoute();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="selectedKey"
:options="routeStore.menus"
:indent="18"
responsive
@update:value="routerPushByKeyWithMetaQuery"
/>
</Teleport>
</template>
<style scoped></style>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import FirstLevelMenu from '../components/first-level-menu.vue';
import { useMixMenuContext } from '../../../context';
defineOptions({
name: 'HorizontalMixMenu'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
}
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="selectedKey"
:options="childLevelMenus"
:indent="18"
responsive
@update:value="routerPushByKeyWithMetaQuery"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<FirstLevelMenu
:menus="allMenus"
:active-menu-key="activeFirstLevelMenuKey"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
/>
</Teleport>
</template>
<style scoped></style>

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMixMenuContext } from '../../../context';
defineOptions({
name: 'ReversedHorizontalMixMenu'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const {
firstLevelMenus,
childLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
isActiveFirstLevelMenuHasChildren
} = useMixMenuContext();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
function handleSelectMixMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="activeFirstLevelMenuKey"
:options="firstLevelMenus"
:indent="18"
responsive
@update:value="handleSelectMixMenu"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:options="childLevelMenus"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</Teleport>
</template>
<style scoped></style>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
defineOptions({
name: 'VerticalMenu'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:options="routeStore.menus"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</Teleport>
</template>
<style scoped></style>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useMixMenuContext } from '../../../context';
import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue';
defineOptions({
name: 'VerticalMenuMix'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const {
allMenus,
childLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey
//
} = useMixMenuContext();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (menu.children?.length) {
setDrawerVisible(true);
} else {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
}
function handleResetActiveMenu() {
setDrawerVisible(false);
if (!appStore.mixSiderFixed) {
getActiveFirstLevelMenuKey();
}
}
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu
:menus="allMenus"
:active-menu-key="activeFirstLevelMenuKey"
:inverted="inverted"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
>
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
:inverted="inverted"
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
<PinToggler
:pin="appStore.mixSiderFixed"
:class="{ 'text-white:88 !hover:text-white': inverted }"
@click="appStore.toggleMixSiderFixed"
/>
</header>
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:options="childLevelMenus"
:collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</DarkModeContainer>
</div>
</div>
</Teleport>
</template>
<style scoped></style>

View File

@@ -1,72 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useBoolean } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import { useMixMenuContext } from '../../context';
import FirstLevelMenu from './first-level-menu.vue';
import BaseMenu from './base-menu.vue';
defineOptions({
name: 'VerticalMixMenu'
});
const appStore = useAppStore();
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const { menus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey, getActiveFirstLevelMenuKey } = useMixMenuContext();
const siderInverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasMenus = computed(() => menus.value.length > 0);
const showDrawer = computed(() => hasMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (menu.children?.length) {
setDrawerVisible(true);
} else {
routerPushByKey(menu.routeKey);
}
}
function handleResetActiveMenu() {
getActiveFirstLevelMenuKey();
setDrawerVisible(false);
}
</script>
<template>
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu :active-menu-key="activeFirstLevelMenuKey" :inverted="siderInverted" @select="handleSelectMixMenu">
<slot></slot>
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
:inverted="siderInverted"
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
<PinToggler
:pin="appStore.mixSiderFixed"
:class="{ 'text-white:88 !hover:text-white': siderInverted }"
@click="appStore.toggleMixSiderFixed"
/>
</header>
<BaseMenu :dark-theme="siderInverted" :menus="menus" />
</DarkModeContainer>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -2,11 +2,8 @@
import { computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import GlobalLogo from '../global-logo/index.vue';
import VerticalMenu from '../global-menu/base-menu.vue';
import VerticalMixMenu from '../global-menu/vertical-mix-menu.vue';
import HorizontalMixMenu from '../global-menu/horizontal-mix-menu.vue';
defineOptions({
name: 'GlobalSider'
@@ -14,12 +11,12 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
</script>
<template>
@@ -29,11 +26,7 @@ const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
:show-title="!appStore.siderCollapse"
:style="{ height: themeStore.header.height + 'px' }"
/>
<VerticalMixMenu v-if="isVerticalMix">
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
</VerticalMixMenu>
<HorizontalMixMenu v-else-if="isHorizontalMix" />
<VerticalMenu v-else :dark-theme="darkMenu" :menus="routeStore.menus" />
<div :id="GLOBAL_SIDER_MENU_ID" :class="menuWrapperClass"></div>
</DarkModeContainer>
</template>

View File

@@ -14,7 +14,7 @@ defineProps<Props>();
<template>
<div class="w-full flex-y-center justify-between">
<div>
<span class="pr-8px text-base_text">{{ label }}</span>
<span class="pr-8px text-base-text">{{ label }}</span>
<slot name="suffix"></slot>
</div>
<slot></slot>

View File

@@ -25,6 +25,10 @@ function handleGrayscaleChange(value: boolean) {
themeStore.setGrayscale(value);
}
function handleColourWeaknessChange(value: boolean) {
themeStore.setColourWeakness(value);
}
const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layout.mode.includes('vertical'));
</script>
@@ -53,6 +57,9 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
<SettingItem :label="$t('theme.grayscale')">
<NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" />
</SettingItem>
<SettingItem :label="$t('theme.colourWeakness')">
<NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" />
</SettingItem>
</div>
</template>

View File

@@ -3,6 +3,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import LayoutModeCard from '../components/layout-mode-card.vue';
import SettingItem from '../components/setting-item.vue';
defineOptions({
name: 'LayoutMode'
@@ -10,6 +11,10 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
function handleReverseHorizontalMixChange(value: boolean) {
themeStore.setLayoutReverseHorizontalMix(value);
}
</script>
<template>
@@ -44,6 +49,13 @@ const themeStore = useThemeStore();
</div>
</template>
</LayoutModeCard>
<SettingItem
v-if="themeStore.layout.mode === 'horizontal-mix'"
:label="$t('theme.layoutMode.reverseHorizontalMix')"
class="mt-16px"
>
<NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />
</SettingItem>
</template>
<style scoped>

View File

@@ -101,6 +101,19 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
>
<NSwitch v-model:value="themeStore.footer.right" />
</SettingItem>
<SettingItem v-if="themeStore.watermark" key="8" :label="$t('theme.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark?.visible" key="8-1" :label="$t('theme.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize
type="text"
size="small"
class="w-120px"
placeholder="SoybeanAdmin"
/>
</SettingItem>
</TransitionGroup>
</template>

View File

@@ -23,6 +23,7 @@ const local: App.I18n.Schema = {
deleteSuccess: 'Delete Success',
confirmDelete: 'Are you sure you want to delete?',
edit: 'Edit',
warning: 'Warning',
error: 'Error',
index: 'Index',
keywordSearch: 'Please enter keyword',
@@ -64,12 +65,14 @@ const local: App.I18n.Schema = {
auto: 'Follow System'
},
grayscale: 'Grayscale',
colourWeakness: 'Colour Weakness',
layoutMode: {
title: 'Layout Mode',
vertical: 'Vertical Menu Mode',
horizontal: 'Horizontal Menu Mode',
'vertical-mix': 'Vertical Mix Menu Mode',
'horizontal-mix': 'Horizontal Mix menu Mode'
'horizontal-mix': 'Horizontal Mix menu Mode',
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
},
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to',
@@ -132,6 +135,10 @@ const local: App.I18n.Schema = {
height: 'Footer Height',
right: 'Right Footer'
},
watermark: {
visible: 'Watermark Full Screen Visible',
text: 'Watermark Text'
},
themeDrawerTitle: 'Theme Configuration',
pageFunTitle: 'Page Function',
configOperation: {
@@ -182,7 +189,25 @@ const local: App.I18n.Schema = {
exception: 'Exception',
exception_403: '403',
exception_404: '404',
exception_500: '500'
exception_500: '500',
plugin: 'Plugin',
plugin_copy: 'Copy',
plugin_charts: 'Charts',
plugin_charts_echarts: 'ECharts',
plugin_editor: 'Editor',
plugin_editor_quill: 'Quill',
plugin_editor_markdown: 'Markdown',
plugin_icon: 'Icon',
plugin_map: 'Map',
plugin_print: 'Print',
plugin_swiper: 'Swiper',
plugin_video: 'Video',
plugin_barcode: 'Barcode',
plugin_pinyin: 'pinyin',
plugin_excel: 'Excel',
plugin_pdf: 'PDF preview',
plugin_gantt: 'Gantt Chart',
plugin_typeit: 'Typeit'
},
page: {
login: {
@@ -245,6 +270,8 @@ const local: App.I18n.Schema = {
devDep: 'Development Dependency'
},
home: {
branchDesc:
'For the convenience of everyone in developing and updating the merge, we have streamlined the code of the main branch, only retaining the homepage menu, and the rest of the content has been moved to the example branch for maintenance. The preview address displays the content of the example branch.',
greeting: 'Good morning, {userName}, today is another day full of vitality!',
weatherDesc: 'Today is cloudy to clear, 20℃ - 25℃!',
projectCount: 'Project Count',

View File

@@ -23,6 +23,7 @@ const local: App.I18n.Schema = {
deleteSuccess: '删除成功',
confirmDelete: '确认删除吗?',
edit: '编辑',
warning: '警告',
error: '错误',
index: '序号',
keywordSearch: '请输入关键词搜索',
@@ -63,13 +64,15 @@ const local: App.I18n.Schema = {
dark: '暗黑模式',
auto: '跟随系统'
},
grayscale: '灰模式',
grayscale: '灰模式',
colourWeakness: '色弱模式',
layoutMode: {
title: '布局模式',
vertical: '左侧菜单模式',
'vertical-mix': '左侧菜单混合模式',
horizontal: '顶部菜单模式',
'horizontal-mix': '顶部菜单混合模式'
'horizontal-mix': '顶部菜单混合模式',
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
},
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照',
@@ -132,6 +135,10 @@ const local: App.I18n.Schema = {
height: '底部高度',
right: '底部局右'
},
watermark: {
visible: '显示全屏水印',
text: '水印文本'
},
themeDrawerTitle: '主题配置',
pageFunTitle: '页面功能',
configOperation: {
@@ -182,7 +189,25 @@ const local: App.I18n.Schema = {
exception: '异常页',
exception_403: '403',
exception_404: '404',
exception_500: '500'
exception_500: '500',
plugin: '插件示例',
plugin_copy: '剪贴板',
plugin_charts: '图表',
plugin_charts_echarts: 'ECharts',
plugin_editor: '编辑器',
plugin_editor_quill: '富文本编辑器',
plugin_editor_markdown: 'MD 编辑器',
plugin_icon: '图标',
plugin_map: '地图',
plugin_print: '打印',
plugin_swiper: 'Swiper',
plugin_video: '视频',
plugin_barcode: '条形码',
plugin_pinyin: '拼音',
plugin_excel: 'Excel',
plugin_pdf: 'PDF 预览',
plugin_gantt: '甘特图',
plugin_typeit: '打字机'
},
page: {
login: {
@@ -245,6 +270,8 @@ const local: App.I18n.Schema = {
devDep: '开发依赖'
},
home: {
branchDesc:
'为了方便大家开发和更新合并我们对main分支的代码进行了精简只保留了首页菜单其余内容已移至example分支进行维护。预览地址显示的内容即为example分支的内容。',
greeting: '早安,{userName}, 今天又是充满活力的一天!',
weatherDesc: '今日多云转晴20℃ - 25℃!',
projectCount: '项目数',

View File

@@ -1,3 +1,6 @@
import 'virtual:svg-icons-register';
import 'uno.css';
import '../styles/css/global.css';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';

View File

@@ -36,5 +36,20 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
manage_user: () => import("@/views/manage/user/index.vue"),
"multi-menu_first_child": () => import("@/views/multi-menu/first_child/index.vue"),
"multi-menu_second_child_home": () => import("@/views/multi-menu/second_child_home/index.vue"),
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
plugin_charts_echarts: () => import("@/views/plugin/charts/echarts/index.vue"),
plugin_copy: () => import("@/views/plugin/copy/index.vue"),
plugin_editor_markdown: () => import("@/views/plugin/editor/markdown/index.vue"),
plugin_editor_quill: () => import("@/views/plugin/editor/quill/index.vue"),
plugin_excel: () => import("@/views/plugin/excel/index.vue"),
plugin_gantt: () => import("@/views/plugin/gantt/index.vue"),
plugin_icon: () => import("@/views/plugin/icon/index.vue"),
plugin_map: () => import("@/views/plugin/map/index.vue"),
plugin_pdf: () => import("@/views/plugin/pdf/index.vue"),
plugin_pinyin: () => import("@/views/plugin/pinyin/index.vue"),
plugin_print: () => import("@/views/plugin/print/index.vue"),
plugin_swiper: () => import("@/views/plugin/swiper/index.vue"),
plugin_typeit: () => import("@/views/plugin/typeit/index.vue"),
plugin_video: () => import("@/views/plugin/video/index.vue"),
"user-center": () => import("@/views/user-center/index.vue"),
};

View File

@@ -330,6 +330,192 @@ export const generatedRoutes: GeneratedRoute[] = [
}
]
},
{
name: 'plugin',
path: '/plugin',
component: 'layout.base',
meta: {
title: '插件示例',
i18nKey: 'route.plugin',
order: 7,
icon: 'clarity:plugin-line'
},
children: [
{
name: 'plugin_barcode',
path: '/plugin/barcode',
component: 'view.plugin_barcode',
meta: {
title: 'plugin_barcode',
i18nKey: 'route.plugin_barcode',
icon: 'ic:round-barcode'
}
},
{
name: 'plugin_charts',
path: '/plugin/charts',
meta: {
title: 'plugin_charts',
i18nKey: 'route.plugin_charts',
icon: 'mdi:chart-areaspline'
},
children: [
{
name: 'plugin_charts_echarts',
path: '/plugin/charts/echarts',
component: 'view.plugin_charts_echarts',
meta: {
title: 'plugin_charts_echarts',
i18nKey: 'route.plugin_charts_echarts',
icon: 'simple-icons:apacheecharts'
}
}
]
},
{
name: 'plugin_copy',
path: '/plugin/copy',
component: 'view.plugin_copy',
meta: {
title: 'plugin_copy',
i18nKey: 'route.plugin_copy',
icon: 'mdi:clipboard-outline'
}
},
{
name: 'plugin_editor',
path: '/plugin/editor',
meta: {
title: 'plugin_editor',
i18nKey: 'route.plugin_editor',
icon: 'icon-park-outline:editor'
},
children: [
{
name: 'plugin_editor_markdown',
path: '/plugin/editor/markdown',
component: 'view.plugin_editor_markdown',
meta: {
title: 'plugin_editor_markdown',
i18nKey: 'route.plugin_editor_markdown',
icon: 'ri:markdown-line'
}
},
{
name: 'plugin_editor_quill',
path: '/plugin/editor/quill',
component: 'view.plugin_editor_quill',
meta: {
title: 'plugin_editor_quill',
i18nKey: 'route.plugin_editor_quill',
icon: 'mdi:file-document-edit-outline'
}
}
]
},
{
name: 'plugin_excel',
path: '/plugin/excel',
component: 'view.plugin_excel',
meta: {
title: 'plugin_excel',
i18nKey: 'route.plugin_excel',
icon: 'ri:file-excel-2-line',
keepAlive: true
}
},
{
name: 'plugin_gantt',
path: '/plugin/gantt',
component: 'view.plugin_gantt',
meta: {
title: 'plugin_gantt',
i18nKey: 'route.plugin_gantt',
icon: 'ant-design:bar-chart-outlined'
}
},
{
name: 'plugin_icon',
path: '/plugin/icon',
component: 'view.plugin_icon',
meta: {
title: 'plugin_icon',
i18nKey: 'route.plugin_icon',
localIcon: 'custom-icon'
}
},
{
name: 'plugin_map',
path: '/plugin/map',
component: 'view.plugin_map',
meta: {
title: 'plugin_map',
i18nKey: 'route.plugin_map',
icon: 'mdi:map'
}
},
{
name: 'plugin_pdf',
path: '/plugin/pdf',
component: 'view.plugin_pdf',
meta: {
title: 'plugin_pdf',
i18nKey: 'route.plugin_pdf',
icon: 'uiw:file-pdf'
}
},
{
name: 'plugin_pinyin',
path: '/plugin/pinyin',
component: 'view.plugin_pinyin',
meta: {
title: 'plugin_pinyin',
i18nKey: 'route.plugin_pinyin',
icon: 'entypo-social:google-hangouts'
}
},
{
name: 'plugin_print',
path: '/plugin/print',
component: 'view.plugin_print',
meta: {
title: 'plugin_print',
i18nKey: 'route.plugin_print',
icon: 'mdi:printer'
}
},
{
name: 'plugin_swiper',
path: '/plugin/swiper',
component: 'view.plugin_swiper',
meta: {
title: 'plugin_swiper',
i18nKey: 'route.plugin_swiper',
icon: 'simple-icons:swiper'
}
},
{
name: 'plugin_typeit',
path: '/plugin/typeit',
component: 'view.plugin_typeit',
meta: {
title: 'plugin_typeit',
i18nKey: 'route.plugin_typeit',
icon: 'mdi:typewriter'
}
},
{
name: 'plugin_video',
path: '/plugin/video',
component: 'view.plugin_video',
meta: {
title: 'plugin_video',
i18nKey: 'route.plugin_video',
icon: 'mdi:video'
}
}
]
},
{
name: 'user-center',
path: '/user-center',

View File

@@ -97,10 +97,13 @@ function transformElegantRouteToVueRoute(
if (component) {
if (isSingleLevelRoute(route)) {
const { layout, view } = getSingleLevelRouteComponent(component);
const singleLevelRoute: RouteRecordRaw = {
path,
component: layouts[layout],
meta: {
title: route.meta?.title || ''
},
children: [
{
name,
@@ -110,36 +113,35 @@ function transformElegantRouteToVueRoute(
} 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));
@@ -201,6 +203,24 @@ const routeMap: RouteMap = {
"multi-menu_second": "/multi-menu/second",
"multi-menu_second_child": "/multi-menu/second/child",
"multi-menu_second_child_home": "/multi-menu/second/child/home",
"plugin": "/plugin",
"plugin_barcode": "/plugin/barcode",
"plugin_charts": "/plugin/charts",
"plugin_charts_echarts": "/plugin/charts/echarts",
"plugin_copy": "/plugin/copy",
"plugin_editor": "/plugin/editor",
"plugin_editor_markdown": "/plugin/editor/markdown",
"plugin_editor_quill": "/plugin/editor/quill",
"plugin_excel": "/plugin/excel",
"plugin_gantt": "/plugin/gantt",
"plugin_icon": "/plugin/icon",
"plugin_map": "/plugin/map",
"plugin_pdf": "/plugin/pdf",
"plugin_pinyin": "/plugin/pinyin",
"plugin_print": "/plugin/print",
"plugin_swiper": "/plugin/swiper",
"plugin_typeit": "/plugin/typeit",
"plugin_video": "/plugin/video",
"user-center": "/user-center"
};

View File

@@ -35,6 +35,7 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
},
async onBackendFail(response, instance) {
const authStore = useAuthStore();
const responseCode = String(response.data.code);
function handleLogout() {
authStore.resetStore();
@@ -49,14 +50,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
if (logoutCodes.includes(response.data.code)) {
if (logoutCodes.includes(responseCode)) {
handleLogout();
return null;
}
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(response.data.code) && !request.state.errMsgStack?.includes(response.data.msg)) {
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
// prevent the user from refreshing the page
@@ -82,7 +83,7 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
// when the backend response code is in `expiredTokenCodes`, it means the token is expired, and refresh token
// the api `refreshToken` can not return error code in `expiredTokenCodes`, otherwise it will be a dead loop, should return `logoutCodes` or `modalLogoutCodes`
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
if (expiredTokenCodes.includes(response.data.code) && !request.state.isRefreshingToken) {
if (expiredTokenCodes.includes(responseCode) && !request.state.isRefreshingToken) {
request.state.isRefreshingToken = true;
const refreshConfig = await handleRefreshToken(response.config);
@@ -108,7 +109,7 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
// get backend error message and code
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.msg || message;
backendErrorCode = error.response?.data?.code || '';
backendErrorCode = String(error.response?.data?.code || '');
}
// the error message is displayed in the modal

View File

@@ -354,34 +354,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
return getSelectedMenuKeyPathByKey(selectedKey, menus.value);
}
/**
* Get route meta by key
*
* @param key Route key
*/
function getRouteMetaByKey(key: string) {
const allRoutes = router.getRoutes();
return allRoutes.find(route => route.name === key)?.meta || null;
}
/**
* Get route query of meta by key
*
* @param key
*/
function getRouteQueryOfMetaByKey(key: string) {
const meta = getRouteMetaByKey(key);
const query: Record<string, string> = {};
meta?.query?.forEach(item => {
query[item.key] = item.value;
});
return query;
}
return {
resetStore,
routeHome,
@@ -398,7 +370,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
isInitAuthRoute,
setIsInitAuthRoute,
getIsAuthRouteExist,
getSelectedMenuKeyPath,
getRouteQueryOfMetaByKey
getSelectedMenuKeyPath
};
});

View File

@@ -6,12 +6,12 @@ import { getPaletteColorByNumber } from '@sa/color';
import { SetupStoreId } from '@/enum';
import { localStg } from '@/utils/storage';
import {
addThemeVarsToHtml,
addThemeVarsToGlobal,
createThemeToken,
getNaiveTheme,
initThemeSettings,
toggleCssDarkMode,
toggleGrayscaleMode
toggleAuxiliaryColorModes,
toggleCssDarkMode
} from './shared';
/** Theme store */
@@ -33,6 +33,9 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
/** grayscale mode */
const grayscaleMode = computed(() => settings.value.grayscale);
/** colourWeakness mode */
const colourWeaknessMode = computed(() => settings.value.colourWeakness);
/** Theme colors */
const themeColors = computed(() => {
const { themeColor, otherColor, isInfoFollowPrimary } = settings.value;
@@ -79,6 +82,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
settings.value.grayscale = isGrayscale;
}
/**
* Set colourWeakness value
*
* @param isColourWeakness
*/
function setColourWeakness(isColourWeakness: boolean) {
settings.value.colourWeakness = isColourWeakness;
}
/** Toggle theme scheme */
function toggleThemeScheme() {
const themeSchemes: UnionKey.ThemeScheme[] = ['light', 'dark', 'auto'];
@@ -123,10 +135,22 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
settings.value.layout.mode = mode;
}
/** Setup theme vars to html */
function setupThemeVarsToHtml() {
const { themeTokens, darkThemeTokens } = createThemeToken(themeColors.value, settings.value.recommendColor);
addThemeVarsToHtml(themeTokens, darkThemeTokens);
/** Setup theme vars to global */
function setupThemeVarsToGlobal() {
const { themeTokens, darkThemeTokens } = createThemeToken(
themeColors.value,
settings.value.tokens,
settings.value.recommendColor
);
addThemeVarsToGlobal(themeTokens, darkThemeTokens);
}
/**
* Set layout reverse horizontal mix
*
* @param reverse Reverse horizontal mix
*/
function setLayoutReverseHorizontalMix(reverse: boolean) {
settings.value.layout.reverseHorizontalMix = reverse;
}
/** Cache theme settings */
@@ -155,9 +179,9 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
);
watch(
grayscaleMode,
[grayscaleMode, colourWeaknessMode],
val => {
toggleGrayscaleMode(val);
toggleAuxiliaryColorModes(val[0], val[1]);
},
{ immediate: true }
);
@@ -166,7 +190,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
watch(
themeColors,
val => {
setupThemeVarsToHtml();
setupThemeVarsToGlobal();
localStg.set('themeColor', val.primary);
},
{ immediate: true }
@@ -185,10 +209,12 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
naiveTheme,
settingsJson,
setGrayscale,
setColourWeakness,
resetStore,
setThemeScheme,
toggleThemeScheme,
updateThemeColors,
setThemeLayout
setThemeLayout,
setLayoutReverseHorizontalMix
};
});

View File

@@ -30,39 +30,40 @@ export function initThemeSettings() {
}
/**
* Create theme token
* create theme token css vars value by theme settings
*
* @param colors Theme colors
* @param tokens Theme setting tokens
* @param [recommended=false] Use recommended color. Default is `false`
*/
export function createThemeToken(colors: App.Theme.ThemeColor, recommended = false) {
export function createThemeToken(
colors: App.Theme.ThemeColor,
tokens?: App.Theme.ThemeSetting['tokens'],
recommended = false
) {
const paletteColors = createThemePaletteColors(colors, recommended);
const themeTokens: App.Theme.ThemeToken = {
const { light, dark } = tokens || themeSettings.tokens;
const themeTokens: App.Theme.ThemeTokenCSSVars = {
colors: {
...paletteColors,
nprogress: paletteColors.primary,
container: 'rgb(255, 255, 255)',
layout: 'rgb(247, 250, 252)',
inverted: 'rgb(0, 20, 40)',
base_text: 'rgb(31, 31, 31)'
...light.colors
},
boxShadow: {
header: '0 1px 2px rgb(0, 21, 41, 0.08)',
sider: '2px 0 8px 0 rgb(29, 35, 41, 0.05)',
tab: '0 1px 2px rgb(0, 21, 41, 0.08)'
...light.boxShadow
}
};
const darkThemeTokens: App.Theme.ThemeToken = {
const darkThemeTokens: App.Theme.ThemeTokenCSSVars = {
colors: {
...themeTokens.colors,
container: 'rgb(28, 28, 28)',
layout: 'rgb(18, 18, 18)',
base_text: 'rgb(224, 224, 224)'
...dark?.colors
},
boxShadow: {
...themeTokens.boxShadow
...themeTokens.boxShadow,
...dark?.boxShadow
}
};
@@ -132,16 +133,16 @@ function getCssVarByTokens(tokens: App.Theme.BaseToken) {
}
/**
* Add theme vars to html
* Add theme vars to global
*
* @param tokens
*/
export function addThemeVarsToHtml(tokens: App.Theme.BaseToken, darkTokens: App.Theme.BaseToken) {
export function addThemeVarsToGlobal(tokens: App.Theme.BaseToken, darkTokens: App.Theme.BaseToken) {
const cssVarStr = getCssVarByTokens(tokens);
const darkCssVarStr = getCssVarByTokens(darkTokens);
const css = `
html {
:root {
${cssVarStr}
}
`;
@@ -179,20 +180,16 @@ export function toggleCssDarkMode(darkMode = false) {
}
/**
* Toggle grayscale mode
* Toggle auxiliary color modes
*
* @param grayscaleMode Is grayscale mode
* @param grayscaleMode
* @param colourWeakness
*/
export function toggleGrayscaleMode(grayscaleMode = false) {
const GRAYSCALE_CLASS = 'grayscale';
const { add, remove } = toggleHtmlClass(GRAYSCALE_CLASS);
if (grayscaleMode) {
add();
} else {
remove();
}
export function toggleAuxiliaryColorModes(grayscaleMode = false, colourWeakness = false) {
const htmlElement = document.documentElement;
htmlElement.style.filter = [grayscaleMode ? 'grayscale(100%)' : '', colourWeakness ? 'invert(80%)' : '']
.filter(Boolean)
.join(' ');
}
type NaiveColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active';

View File

@@ -1,5 +1,5 @@
import type { PiniaPluginContext } from 'pinia';
import { cloneDeep } from 'lodash-es';
import { jsonClone } from '@sa/utils';
import { SetupStoreId } from '@/enum';
/**
@@ -13,7 +13,7 @@ export function resetSetupStore(context: PiniaPluginContext) {
if (setupSyntaxIds.includes(context.store.$id)) {
const { $state } = context.store;
const defaultStore = cloneDeep($state);
const defaultStore = jsonClone($state);
context.store.$reset = () => {
context.store.$patch(defaultStore);

View File

@@ -11,7 +11,3 @@ body,
html {
overflow-x: hidden;
}
html.grayscale {
filter: grayscale(100%);
}

View File

@@ -2,6 +2,7 @@
export const themeSettings: App.Theme.ThemeSetting = {
themeScheme: 'light',
grayscale: false,
colourWeakness: false,
recommendColor: false,
themeColor: '#646cff',
otherColor: {
@@ -13,7 +14,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
isInfoFollowPrimary: true,
layout: {
mode: 'vertical',
scrollMode: 'content'
scrollMode: 'content',
reverseHorizontalMix: false
},
page: {
animate: true,
@@ -46,6 +48,32 @@ export const themeSettings: App.Theme.ThemeSetting = {
fixed: false,
height: 48,
right: true
},
watermark: {
visible: false,
text: 'SoybeanAdmin'
},
tokens: {
light: {
colors: {
container: 'rgb(255, 255, 255)',
layout: 'rgb(247, 250, 252)',
inverted: 'rgb(0, 20, 40)',
'base-text': 'rgb(31, 31, 31)'
},
boxShadow: {
header: '0 1px 2px rgb(0, 21, 41, 0.08)',
sider: '2px 0 8px 0 rgb(29, 35, 41, 0.05)',
tab: '0 1px 2px rgb(0, 21, 41, 0.08)'
}
},
dark: {
colors: {
container: 'rgb(28, 28, 28)',
layout: 'rgb(18, 18, 18)',
'base-text': 'rgb(224, 224, 224)'
}
}
}
};
@@ -54,4 +82,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
*
* If publish new version, use `overrideThemeSettings` to override certain theme settings
*/
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {};
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {
watermark: {
visible: false,
text: 'SoybeanAdmin'
}
};

View File

@@ -18,14 +18,14 @@ function createColorPaletteVars() {
const colorPaletteVars = createColorPaletteVars();
/** Theme vars */
export const themeVars: App.Theme.ThemeToken = {
export const themeVars: App.Theme.ThemeTokenCSSVars = {
colors: {
...colorPaletteVars,
nprogress: 'rgb(var(--nprogress-color))',
container: 'rgb(var(--container-bg-color))',
layout: 'rgb(var(--layout-bg-color))',
inverted: 'rgb(var(--inverted-bg-color))',
base_text: 'rgb(var(--base-text-color))'
'base-text': 'rgb(var(--base-text-color))'
},
boxShadow: {
header: 'var(--header-box-shadow)',

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

@@ -4,22 +4,14 @@ declare namespace App {
namespace Theme {
type ColorPaletteNumber = import('@sa/color').ColorPaletteNumber;
/** Theme token */
type ThemeToken = {
colors: ThemeTokenColor;
boxShadow: {
header: string;
sider: string;
tab: string;
};
};
/** Theme setting */
interface ThemeSetting {
/** Theme scheme */
themeScheme: UnionKey.ThemeScheme;
/** grayscale mode */
grayscale: boolean;
/** colour weakness mode */
colourWeakness: boolean;
/** Whether to recommend color */
recommendColor: boolean;
/** Theme color */
@@ -34,6 +26,12 @@ declare namespace App {
mode: UnionKey.ThemeLayoutMode;
/** Scroll mode */
scrollMode: UnionKey.ThemeScrollMode;
/**
* Whether to reverse the horizontal mix
*
* if true, the vertical child level menus in left and horizontal first level menus in top
*/
reverseHorizontalMix?: boolean;
};
/** Page */
page: {
@@ -97,6 +95,20 @@ declare namespace App {
/** Whether float the footer to the right when the layout is 'horizontal-mix' */
right: boolean;
};
/** Watermark */
watermark?: {
/** Whether to show the watermark */
visible: boolean;
/** Watermark text */
text: string;
};
/** define some theme settings tokens, will transform to css variables */
tokens: {
light: ThemeSettingToken;
dark?: {
[K in keyof ThemeSettingToken]?: Partial<ThemeSettingToken[K]>;
};
};
}
interface OtherColor {
@@ -118,14 +130,33 @@ declare namespace App {
type BaseToken = Record<string, Record<string, string>>;
interface ThemeTokenColor extends ThemePaletteColor {
nprogress: string;
interface ThemeSettingTokenColor {
/** the progress bar color, if not set, will use the primary color */
nprogress?: string;
container: string;
layout: string;
inverted: string;
base_text: string;
[key: string]: string;
'base-text': string;
}
interface ThemeSettingTokenBoxShadow {
header: string;
sider: string;
tab: string;
}
interface ThemeSettingToken {
colors: ThemeSettingTokenColor;
boxShadow: ThemeSettingTokenBoxShadow;
}
type ThemeTokenColor = ThemePaletteColor & ThemeSettingTokenColor;
/** Theme token CSS variables */
type ThemeTokenCSSVars = {
colors: ThemeTokenColor & { [key: string]: string };
boxShadow: ThemeSettingTokenBoxShadow & { [key: string]: string };
};
}
/** Global namespace */
@@ -148,7 +179,7 @@ declare namespace App {
}
/** The global menu */
interface Menu {
type Menu = {
/**
* The menu key
*
@@ -167,7 +198,7 @@ declare namespace App {
icon?: () => VNode;
/** The menu children */
children?: Menu[];
}
};
type Breadcrumb = Omit<Menu, 'children'> & {
options?: Breadcrumb[];
@@ -273,6 +304,7 @@ declare namespace App {
deleteSuccess: string;
confirmDelete: string;
edit: string;
warning: string;
error: string;
index: string;
keywordSearch: string;
@@ -309,7 +341,8 @@ declare namespace App {
theme: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
grayscale: string;
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string>;
colourWeakness: string;
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
recommendColor: string;
recommendColorDesc: string;
themeColor: {
@@ -349,6 +382,10 @@ declare namespace App {
height: string;
right: string;
};
watermark: {
visible: string;
text: string;
};
themeDrawerTitle: string;
pageFunTitle: string;
configOperation: {
@@ -420,6 +457,7 @@ declare namespace App {
devDep: string;
};
home: {
branchDesc: string;
greeting: string;
weatherDesc: string;
projectCount: string;

View File

@@ -11,12 +11,16 @@ declare module 'vue' {
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
CountTo: typeof import('./../components/custom/count-to.vue')['default']
CustomIconSelect: typeof import('./../components/custom/custom-icon-select.vue')['default']
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
GithubLink: typeof import('./../components/custom/github-link.vue')['default']
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
'IconCharm:download': typeof import('~icons/charm/download')['default']
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['default']
@@ -25,8 +29,12 @@ declare module 'vue' {
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalActivity: typeof import('~icons/local/activity')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalCast: typeof import('~icons/local/cast')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
'IconMaterialSymbolsLight:rotate90DegreesCcwOutlineRounded': typeof import('~icons/material-symbols-light/rotate90-degrees-ccw-outline-rounded')['default']
'IconMdi:printer': typeof import('~icons/mdi/printer')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
@@ -42,6 +50,8 @@ declare module 'vue' {
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
@@ -68,21 +78,25 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPagination: typeof import('naive-ui')['NPagination']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace']
NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
@@ -94,5 +108,6 @@ declare module 'vue' {
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
WebSiteLink: typeof import('./../components/custom/web-site-link.vue')['default']
}
}

View File

@@ -57,6 +57,24 @@ declare module "@elegant-router/types" {
"multi-menu_second": "/multi-menu/second";
"multi-menu_second_child": "/multi-menu/second/child";
"multi-menu_second_child_home": "/multi-menu/second/child/home";
"plugin": "/plugin";
"plugin_barcode": "/plugin/barcode";
"plugin_charts": "/plugin/charts";
"plugin_charts_echarts": "/plugin/charts/echarts";
"plugin_copy": "/plugin/copy";
"plugin_editor": "/plugin/editor";
"plugin_editor_markdown": "/plugin/editor/markdown";
"plugin_editor_quill": "/plugin/editor/quill";
"plugin_excel": "/plugin/excel";
"plugin_gantt": "/plugin/gantt";
"plugin_icon": "/plugin/icon";
"plugin_map": "/plugin/map";
"plugin_pdf": "/plugin/pdf";
"plugin_pinyin": "/plugin/pinyin";
"plugin_print": "/plugin/print";
"plugin_swiper": "/plugin/swiper";
"plugin_typeit": "/plugin/typeit";
"plugin_video": "/plugin/video";
"user-center": "/user-center";
};
@@ -72,7 +90,7 @@ declare module "@elegant-router/types" {
/**
* custom route key
*/
*/
export type CustomRouteKey = Extract<
RouteKey,
| "root"
@@ -93,7 +111,7 @@ declare module "@elegant-router/types" {
/**
* the generated route key
*/
*/
export type GeneratedRouteKey = Exclude<RouteKey, CustomRouteKey>;
/**
@@ -111,6 +129,7 @@ declare module "@elegant-router/types" {
| "login"
| "manage"
| "multi-menu"
| "plugin"
| "user-center"
>;
@@ -151,6 +170,21 @@ declare module "@elegant-router/types" {
| "manage_user"
| "multi-menu_first_child"
| "multi-menu_second_child_home"
| "plugin_barcode"
| "plugin_charts_echarts"
| "plugin_copy"
| "plugin_editor_markdown"
| "plugin_editor_quill"
| "plugin_excel"
| "plugin_gantt"
| "plugin_icon"
| "plugin_map"
| "plugin_pdf"
| "plugin_pinyin"
| "plugin_print"
| "plugin_swiper"
| "plugin_typeit"
| "plugin_video"
| "user-center"
>;

20
src/typings/package.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
/// <reference types="@amap/amap-jsapi-types" />
/// <reference types="bmapgl" />
declare namespace BMap {
class Map extends BMapGL.Map {}
class Point extends BMapGL.Point {}
}
declare const TMap: any;
interface Window {
/**
* make baidu map request under https protocol
*
* - 0: http
* - 1: https
* - 2: https
*/
HOST_TYPE: '0' | '1' | '2';
}

View File

@@ -20,7 +20,7 @@ declare namespace UnionKey {
* - vertical: the vertical menu in left
* - horizontal: the horizontal menu in top
* - vertical-mix: two vertical mixed menus in left
* - horizontal-mix: the vertical menu in left and horizontal menu in top
* - horizontal-mix: the vertical first level menus in left and horizontal child level menus in top
*/
type ThemeLayoutMode = 'vertical' | 'horizontal' | 'vertical-mix' | 'horizontal-mix';

View File

@@ -37,7 +37,7 @@ const { columns, columnChecks, data, loading, pagination, getData, getDataByPage
align: 'center',
width: 80,
render: row => {
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
const tagMap: Record<Api.SystemManage.MenuType, NaiveUI.ThemeColor> = {
1: 'default',
2: 'primary'
};

View File

@@ -26,41 +26,45 @@ function search() {
</script>
<template>
<NCard :title="$t('common.search')" :bordered="false" size="small" class="card-wrapper">
<NForm :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleName')" path="roleName" class="pr-24px">
<NInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleCode')" path="roleCode" class="pr-24px">
<NInput v-model:value="model.roleCode" :placeholder="$t('page.manage.role.form.roleCode')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleStatus')" path="status" class="pr-24px">
<NSelect
v-model:value="model.status"
:placeholder="$t('page.manage.role.form.roleStatus')"
:options="translateOptions(enableStatusOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse :default-expanded-names="['role-search']">
<NCollapseItem :title="$t('common.search')" name="role-search">
<NForm :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleName')" path="roleName" class="pr-24px">
<NInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleCode')" path="roleCode" class="pr-24px">
<NInput v-model:value="model.roleCode" :placeholder="$t('page.manage.role.form.roleCode')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.role.roleStatus')" path="status" class="pr-24px">
<NSelect
v-model:value="model.status"
:placeholder="$t('page.manage.role.form.roleStatus')"
:options="translateOptions(enableStatusOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>

View File

@@ -43,55 +43,69 @@ async function search() {
</script>
<template>
<NCard :title="$t('common.search')" :bordered="false" size="small" class="card-wrapper">
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userName')" path="userName" class="pr-24px">
<NInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userGender')" path="userGender" class="pr-24px">
<NSelect
v-model:value="model.userGender"
:placeholder="$t('page.manage.user.form.userGender')"
:options="translateOptions(userGenderOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.nickName')" path="nickName" class="pr-24px">
<NInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userPhone')" path="userPhone" class="pr-24px">
<NInput v-model:value="model.userPhone" :placeholder="$t('page.manage.user.form.userPhone')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userEmail')" path="userEmail" class="pr-24px">
<NInput v-model:value="model.userEmail" :placeholder="$t('page.manage.user.form.userEmail')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userStatus')" path="userStatus" class="pr-24px">
<NSelect
v-model:value="model.status"
:placeholder="$t('page.manage.user.form.userStatus')"
:options="translateOptions(enableStatusOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 m:12" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userName')" path="userName" class="pr-24px">
<NInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
:label="$t('page.manage.user.userGender')"
path="userGender"
class="pr-24px"
>
<NSelect
v-model:value="model.userGender"
:placeholder="$t('page.manage.user.form.userGender')"
:options="translateOptions(userGenderOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.nickName')" path="nickName" class="pr-24px">
<NInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userPhone')" path="userPhone" class="pr-24px">
<NInput v-model:value="model.userPhone" :placeholder="$t('page.manage.user.form.userPhone')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userEmail')" path="userEmail" class="pr-24px">
<NInput v-model:value="model.userEmail" :placeholder="$t('page.manage.user.form.userEmail')" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
:label="$t('page.manage.user.userStatus')"
path="userStatus"
class="pr-24px"
>
<NSelect
v-model:value="model.status"
:placeholder="$t('page.manage.user.form.userStatus')"
:options="translateOptions(enableStatusOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 m:12" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>

View File

@@ -0,0 +1,114 @@
<script setup lang="ts">
import JsBarcode from 'jsbarcode';
import type { Options } from 'jsbarcode';
import { onMounted } from 'vue';
const text = 'Soybean';
interface CodeConfig {
id: string;
title: string;
text: string;
options: Options;
}
const codes: CodeConfig[] = [
{
id: 'code39',
title: 'CODE 39 正常尺寸',
text: 'Hello',
options: { format: 'code39' }
},
{
id: 'code128',
title: 'CODE 128 正常尺寸',
text,
options: {}
},
{
id: 'ean-13',
title: 'ENA-13 商品条形码',
text: '1234567890128',
options: { format: 'ean13' }
},
{
id: 'upc-a',
title: 'UPC-A 商品条形码',
text: '123456789012',
options: { format: 'upc' }
},
{
id: 'barcode',
title: '不一样的高度,不一样的颜色',
text: 'Hello',
options: {
height: 30,
lineColor: '#9ca3af'
}
},
{
id: 'barcode1',
title: '加个背景色',
text,
options: {
background: '#9ca3af',
lineColor: '#ffffff'
}
},
{
id: 'barcode2',
title: '字体好大',
text,
options: {
fontSize: 40
}
},
{
id: 'barcode3',
title: '粗狂的条码,文字离远点',
text: 'Hi',
options: {
textMargin: 30,
width: 4
}
},
{
id: 'barcode4',
title: '字体跑上面来,还是粗体',
text,
options: {
textPosition: 'top',
fontOptions: 'bold'
}
}
];
function generateBarcode() {
codes.forEach(code => {
JsBarcode(`#${code.id}`, code.text, code.options);
});
}
onMounted(() => {
generateBarcode();
});
</script>
<template>
<div class="overflow-hidden">
<NCard title="条形码" :bordered="false" class="h-full card-wrapper" content-class="overflow-hidden">
<NScrollbar class="h-full">
<NGrid cols="1 s:2 l:3" :x-gap="12" :y-gap="24" responsive="screen" item-responsive>
<NGi v-for="item in codes" :key="item.id">
<div class="flex-col-center">
<h3>{{ item.title }}</h3>
<svg :id="item.id" class="h-130px" />
</div>
</NGi>
</NGrid>
</NScrollbar>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,706 @@
import { graphic } from 'echarts';
import type { ScatterSeriesOption } from 'echarts/charts';
import type { SingleAxisComponentOption, TitleComponentOption } from 'echarts/components';
import type { ECOption } from '@/hooks/common/echarts';
export const pieOptions: ECOption = {
legend: {},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true }
}
},
series: [
{
name: 'Nightingale Chart',
type: 'pie',
radius: [50, 150],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8
},
data: [
{ value: 40, name: 'rose 1' },
{ value: 38, name: 'rose 2' },
{ value: 32, name: 'rose 3' },
{ value: 30, name: 'rose 4' },
{ value: 28, name: 'rose 5' },
{ value: 26, name: 'rose 6' },
{ value: 22, name: 'rose 7' },
{ value: 18, name: 'rose 8' }
]
}
]
};
export const lineOptions: ECOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
title: {
text: 'Stacked Line'
},
legend: {
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
color: '#37a2da',
name: 'Email',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#37a2da'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [120, 132, 101, 134, 90, 230, 210]
},
{
color: '#9fe6b8',
name: 'Union Ads',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#9fe6b8'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [220, 182, 191, 234, 290, 330, 310]
},
{
color: '#fedb5c',
name: 'Video Ads',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#fedb5c'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [150, 232, 201, 154, 190, 330, 410]
},
{
color: '#fb7293',
name: 'Direct',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#fb7293'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [320, 332, 301, 334, 390, 330, 320]
},
{
color: '#e7bcf3',
name: 'Search Engine',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#e7bcf3'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
};
export const barOptions: ECOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
color: '#8378ea',
showBackground: true,
barGap: 100,
itemStyle: {
borderRadius: [40, 40, 0, 0]
},
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)'
}
}
]
};
export function getPictorialBarOption(): ECOption {
const category: string[] = [];
let dottedBase = Number(new Date());
const lineData: number[] = [];
const barData: number[] = [];
for (let i = 0; i < 20; i += 1) {
const date = new Date((dottedBase += 3600 * 24 * 1000));
category.push([date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-'));
const b = Math.random() * 200;
const d = Math.random() * 200;
barData.push(b);
lineData.push(d + b);
}
const options: ECOption = {
backgroundColor: '#0f375f',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['line', 'bar'],
textStyle: {
color: '#ccc'
}
},
xAxis: {
data: category,
axisLine: {
lineStyle: {
color: '#ccc'
}
}
},
yAxis: {
splitLine: { show: false },
axisLine: {
lineStyle: {
color: '#ccc'
}
}
},
series: [
{
name: 'line',
type: 'line',
smooth: true,
showAllSymbol: true,
symbol: 'emptyCircle',
symbolSize: 15,
data: lineData
},
{
name: 'bar',
type: 'bar',
barWidth: 10,
itemStyle: {
borderRadius: 5,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#14c8d4' },
{ offset: 1, color: '#43eec6' }
])
},
data: barData
},
{
name: 'line',
type: 'bar',
barGap: '-100%',
barWidth: 10,
itemStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(20,200,212,0.5)' },
{ offset: 0.2, color: 'rgba(20,200,212,0.2)' },
{ offset: 1, color: 'rgba(20,200,212,0)' }
])
},
z: -12,
data: lineData
},
{
name: 'dotted',
type: 'pictorialBar',
symbol: 'rect',
itemStyle: {
color: '#0f375f'
},
symbolRepeat: true,
symbolSize: [12, 4],
symbolMargin: 1,
z: -10,
data: lineData
}
]
};
return options;
}
export function getScatterOption() {
// prettier-ignore
const hours = ['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a','10a','11a', '12p', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p'];
// prettier-ignore
const days = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
// prettier-ignore
const data: [number, number, number][] = [[0,0,5],[0,1,1],[0,2,0],[0,3,0],[0,4,0],[0,5,0],[0,6,0],[0,7,0],[0,8,0],[0,9,0],[0,10,0],[0,11,2],[0,12,4],[0,13,1],[0,14,1],[0,15,3],[0,16,4],[0,17,6],[0,18,4],[0,19,4],[0,20,3],[0,21,3],[0,22,2],[0,23,5],[1,0,7],[1,1,0],[1,2,0],[1,3,0],[1,4,0],[1,5,0],[1,6,0],[1,7,0],[1,8,0],[1,9,0],[1,10,5],[1,11,2],[1,12,2],[1,13,6],[1,14,9],[1,15,11],[1,16,6],[1,17,7],[1,18,8],[1,19,12],[1,20,5],[1,21,5],[1,22,7],[1,23,2],[2,0,1],[2,1,1],[2,2,0],[2,3,0],[2,4,0],[2,5,0],[2,6,0],[2,7,0],[2,8,0],[2,9,0],[2,10,3],[2,11,2],[2,12,1],[2,13,9],[2,14,8],[2,15,10],[2,16,6],[2,17,5],[2,18,5],[2,19,5],[2,20,7],[2,21,4],[2,22,2],[2,23,4],[3,0,7],[3,1,3],[3,2,0],[3,3,0],[3,4,0],[3,5,0],[3,6,0],[3,7,0],[3,8,1],[3,9,0],[3,10,5],[3,11,4],[3,12,7],[3,13,14],[3,14,13],[3,15,12],[3,16,9],[3,17,5],[3,18,5],[3,19,10],[3,20,6],[3,21,4],[3,22,4],[3,23,1],[4,0,1],[4,1,3],[4,2,0],[4,3,0],[4,4,0],[4,5,1],[4,6,0],[4,7,0],[4,8,0],[4,9,2],[4,10,4],[4,11,4],[4,12,2],[4,13,4],[4,14,4],[4,15,14],[4,16,12],[4,17,1],[4,18,8],[4,19,5],[4,20,3],[4,21,7],[4,22,3],[4,23,0],[5,0,2],[5,1,1],[5,2,0],[5,3,3],[5,4,0],[5,5,0],[5,6,0],[5,7,0],[5,8,2],[5,9,0],[5,10,4],[5,11,1],[5,12,5],[5,13,10],[5,14,5],[5,15,7],[5,16,11],[5,17,6],[5,18,0],[5,19,5],[5,20,3],[5,21,4],[5,22,2],[5,23,0],[6,0,1],[6,1,0],[6,2,0],[6,3,0],[6,4,0],[6,5,0],[6,6,0],[6,7,0],[6,8,0],[6,9,0],[6,10,1],[6,11,0],[6,12,2],[6,13,1],[6,14,3],[6,15,4],[6,16,0],[6,17,0],[6,18,0],[6,19,0],[6,20,1],[6,21,2],[6,22,2],[6,23,6]];
const title: TitleComponentOption[] = [];
const singleAxis: SingleAxisComponentOption[] = [];
const series: ScatterSeriesOption[] = [];
days.forEach((day, idx) => {
title.push({
textBaseline: 'middle',
top: `${((idx + 0.5) * 100) / 7}%`,
text: day
});
singleAxis.push({
left: 150,
type: 'category',
boundaryGap: false,
data: hours,
top: `${(idx * 100) / 7 + 5}%`,
height: `${100 / 7 - 10}%`,
axisLabel: {
interval: 2
}
});
series.push({
singleAxisIndex: idx,
coordinateSystem: 'singleAxis',
type: 'scatter',
data: [],
symbolSize(dataItem) {
return dataItem[1] * 4;
}
});
});
data.forEach(dataItem => {
(series as any)[dataItem[0]].data.push([dataItem[1], dataItem[2]]);
});
const option: ECOption = {
tooltip: {
position: 'top'
},
title,
singleAxis,
series: series as any
};
return option;
}
export const radarOptions: ECOption = {
title: {
text: 'Multiple Radar'
},
tooltip: {
trigger: 'axis'
},
legend: {
left: 'center',
data: ['A Software', 'A Phone', 'Another Phone', 'Precipitation', 'Evaporation']
},
radar: [
{
indicator: [
{ name: 'Brand', max: 100 },
{ name: 'Content', max: 100 },
{ name: 'Usability', max: 100 },
{ name: 'Function', max: 100 }
],
center: ['25%', '40%'],
radius: 80
},
{
indicator: [
{ name: 'Look', max: 100 },
{ name: 'Photo', max: 100 },
{ name: 'System', max: 100 },
{ name: 'Performance', max: 100 },
{ name: 'Screen', max: 100 }
],
radius: 80,
center: ['50%', '60%']
},
{
indicator: (() => {
const res = [];
for (let i = 1; i <= 12; i += 1) {
res.push({ name: `${i}`, max: 100 });
}
return res;
})(),
center: ['75%', '40%'],
radius: 80
}
],
series: [
{
type: 'radar',
tooltip: {
trigger: 'item'
},
areaStyle: {},
data: [
{
value: [60, 73, 85, 40],
name: 'A Software'
}
]
},
{
type: 'radar',
radarIndex: 1,
areaStyle: {},
data: [
{
value: [85, 90, 90, 95, 95],
name: 'A Phone'
},
{
value: [95, 80, 95, 90, 93],
name: 'Another Phone'
}
]
},
{
type: 'radar',
radarIndex: 2,
areaStyle: {},
data: [
{
name: 'Precipitation',
value: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 75.6, 82.2, 48.7, 18.8, 6.0, 2.3]
},
{
name: 'Evaporation',
value: [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 35.6, 62.2, 32.6, 20.0, 6.4, 3.3]
}
]
}
]
};
export const gaugeOptions: ECOption = {
series: [
{
name: 'hour',
type: 'gauge',
startAngle: 90,
endAngle: -270,
min: 0,
max: 12,
splitNumber: 12,
clockwise: true,
axisLine: {
lineStyle: {
width: 15,
color: [[1, 'rgba(0,0,0,0.7)']],
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 15
}
},
splitLine: {
lineStyle: {
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 3,
shadowOffsetX: 1,
shadowOffsetY: 2
}
},
axisLabel: {
fontSize: 50,
distance: 25,
formatter(value) {
if (value === 0) {
return '';
}
return `${value}`;
}
},
anchor: {
show: true,
icon: 'path://M532.8,70.8C532.8,70.8,532.8,70.8,532.8,70.8L532.8,70.8C532.7,70.8,532.8,70.8,532.8,70.8z M456.1,49.6c-2.2-6.2-8.1-10.6-15-10.6h-37.5v10.6h37.5l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3v0h-22.5c-1.5,0.1-3,0.4-4.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.8c-0.6,1.7-0.9,3.4-0.9,5.3v16h10.6v-16l0,0l0,0c0-2.7,2.1-5,4.7-5.3h10.3l10.4,21.2h11.8l-10.4-21.2h0c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3C457,53,456.7,51.2,456.1,49.6z M388.9,92.1h11.3L381,39h-3.6h-11.3L346.8,92v0h11.3l3.9-10.7h7.3h7.7l3.9-10.6h-7.7h-7.3l7.7-21.2v0L388.9,92.1z M301,38.9h-10.6v53.1H301V70.8h28.4l3.7-10.6H301V38.9zM333.2,38.9v10.6v10.7v31.9h10.6V38.9H333.2z M249.5,81.4L249.5,81.4L249.5,81.4c-2.9,0-5.3-2.4-5.3-5.3h0V54.9h0l0,0c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.9-10.6h-37.5c-1.9,0-3.6,0.3-5.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.7c-0.6,1.7-0.9,3.5-0.9,5.3l0,0v21.3c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.5,0.9,5.3,0.9h33.6l3.9-10.6H249.5z M176.8,38.9v10.6h49.6l3.9-10.6H176.8z M192.7,81.4L192.7,81.4L192.7,81.4c-2.9,0-5.3-2.4-5.3-5.3l0,0v-5.3h38.9l3.9-10.6h-53.4v10.6v5.3l0,0c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.4,0.9,5.3,0.9h23.4h10.2l3.9-10.6l0,0H192.7z M460.1,38.9v10.6h21.4v42.5h10.6V49.6h17.5l3.8-10.6H460.1z M541.6,68.2c-0.2,0.1-0.4,0.3-0.7,0.4C541.1,68.4,541.4,68.3,541.6,68.2L541.6,68.2z M554.3,60.2h-21.6v0l0,0c-2.9,0-5.3-2.4-5.3-5.3c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.8-10.6h-37.5l0,0c-6.9,0-12.8,4.4-15,10.6c-0.6,1.7-0.9,3.5-0.9,5.3c0,1.9,0.3,3.7,0.9,5.3c2.2,6.2,8.1,10.6,15,10.6h21.6l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3l0,0h-37.5v10.6h37.5c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3c0-1.9-0.3-3.7-0.9-5.3C567.2,64.6,561.3,60.2,554.3,60.2z',
showAbove: false,
offsetCenter: [0, '-35%'],
size: 120,
keepAspect: true,
itemStyle: {
color: '#707177'
}
},
pointer: {
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
width: 12,
length: '55%',
offsetCenter: [0, '8%'],
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
detail: {
show: false
},
title: {
offsetCenter: [0, '30%']
},
data: [
{
value: 0
}
]
},
{
name: 'minute',
type: 'gauge',
startAngle: 90,
endAngle: -270,
min: 0,
max: 60,
clockwise: true,
axisLine: {
show: false
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
pointer: {
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
width: 8,
length: '70%',
offsetCenter: [0, '8%'],
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
anchor: {
show: true,
size: 20,
showAbove: false,
itemStyle: {
borderWidth: 15,
borderColor: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
detail: {
show: false
},
title: {
offsetCenter: ['0%', '-40%']
},
data: [
{
value: 0
}
]
},
{
name: 'second',
type: 'gauge',
startAngle: 90,
endAngle: -270,
min: 0,
max: 60,
animationEasingUpdate: 'bounceOut',
clockwise: true,
axisLine: {
show: false
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
pointer: {
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
width: 4,
length: '85%',
offsetCenter: [0, '8%'],
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
anchor: {
show: true,
size: 15,
showAbove: true,
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
detail: {
show: false
},
title: {
offsetCenter: ['0%', '-40%']
},
data: [
{
value: 0
}
]
}
]
};

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { onUnmounted } from 'vue';
import { useEcharts } from '@/hooks/common/echarts';
import {
barOptions,
gaugeOptions,
getPictorialBarOption,
getScatterOption,
lineOptions,
pieOptions,
radarOptions
} from './data';
const { domRef: pieRef } = useEcharts(() => pieOptions, { onRender() {} });
const { domRef: lineRef } = useEcharts(() => lineOptions, { onRender() {} });
const { domRef: barRef } = useEcharts(() => barOptions, { onRender() {} });
const { domRef: pictorialBarRef } = useEcharts(() => getPictorialBarOption(), { onRender() {} });
const { domRef: radarRef } = useEcharts(() => radarOptions, { onRender() {} });
const { domRef: scatterRef } = useEcharts(() => getScatterOption(), { onRender() {} });
const { domRef: gaugeRef, setOptions: setGaugeOptions } = useEcharts(() => gaugeOptions, { onRender() {} });
let intervalId: NodeJS.Timeout;
function initGaugeChart() {
intervalId = setInterval(() => {
const date = new Date();
const second = date.getSeconds();
const minute = date.getMinutes() + second / 60;
const hour = (date.getHours() % 12) + minute / 60;
setGaugeOptions({
animationDurationUpdate: 300,
series: [
{
name: 'hour',
animation: hour !== 0,
data: [{ value: hour }]
},
{
name: 'minute',
animation: minute !== 0,
data: [{ value: minute }]
},
{
animation: second !== 0,
name: 'second',
data: [{ value: second }]
}
]
});
}, 1000);
}
function clearGaugeChart() {
clearInterval(intervalId);
}
initGaugeChart();
onUnmounted(() => {
clearGaugeChart();
});
</script>
<template>
<NSpace vertical :size="16">
<NCard :bordered="false" class="card-wrapper">
<div ref="pieRef" class="h-400px" />
</NCard>
<NCard :bordered="false" class="card-wrapper">
<div ref="lineRef" class="h-400px" />
</NCard>
<NCard :bordered="false" class="card-wrapper">
<div ref="barRef" class="h-400px" />
</NCard>
<NCard :bordered="false" class="card-wrapper">
<div ref="radarRef" class="h-400px"></div>
</NCard>
<NCard :bordered="false" class="card-wrapper">
<div ref="scatterRef" class="h-600px"></div>
</NCard>
<NCard :bordered="false" class="card-wrapper">
<div ref="pictorialBarRef" class="h-600px" />
</NCard>
<NCard :bordered="false" class="card-wrapper">
<div ref="gaugeRef" class="h-640px" />
</NCard>
</NSpace>
</template>
<style scoped></style>

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useClipboard } from '@vueuse/core';
const { copy, isSupported } = useClipboard();
const source = ref('');
async function handleCopy() {
if (!isSupported) {
window.$message?.error('您的浏览器不支持Clipboard API');
return;
}
if (!source.value) {
window.$message?.error('请输入要复制的内容');
return;
}
await copy(source.value);
window.$message?.success(`复制成功:${source.value}`);
}
</script>
<template>
<div class="h-full">
<NCard title="文本复制" :bordered="false" class="h-full card-wrapper">
<NInputGroup>
<NInput v-model:value="source" placeholder="请输入要复制的内容吧" />
<NButton type="primary" @click="handleCopy">复制</NButton>
</NInputGroup>
</NCard>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { useThemeStore } from '@/store/modules/theme';
const theme = useThemeStore();
const vditor = ref<Vditor>();
const domRef = ref<HTMLElement>();
function renderVditor() {
if (!domRef.value) return;
vditor.value = new Vditor(domRef.value, {
minHeight: 400,
theme: theme.darkMode ? 'dark' : 'classic',
icon: 'material',
cache: { enable: false }
});
}
const stopHandle = watch(
() => theme.darkMode,
newValue => {
const themeMode = newValue ? 'dark' : 'classic';
vditor.value?.setTheme(themeMode);
}
);
onMounted(() => {
renderVditor();
});
onUnmounted(() => {
stopHandle();
});
</script>
<template>
<div class="h-full">
<NCard title="markdown插件" :bordered="false" class="card-wrapper">
<div ref="domRef"></div>
<template #footer>
<GithubLink link="https://github.com/Vanessa219/vditor" />
</template>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import WangEditor from 'wangeditor';
const editor = ref<WangEditor>();
const domRef = ref<HTMLElement>();
function renderWangEditor() {
editor.value = new WangEditor(domRef.value);
setEditorConfig();
editor.value.create();
}
function setEditorConfig() {
if (editor.value?.config?.zIndex) {
editor.value.config.zIndex = 10;
}
}
onMounted(() => {
renderWangEditor();
});
</script>
<template>
<div class="h-full">
<NCard title="富文本插件" :bordered="false" class="card-wrapper">
<div ref="domRef" class="bg-white dark:bg-dark"></div>
<template #footer>
<GithubLink link="https://github.com/wangeditor-team/wangEditor" />
</template>
</NCard>
</div>
</template>
<style scoped>
:deep(.w-e-toolbar) {
background: inherit !important;
border-color: #999 !important;
}
:deep(.w-e-text-container) {
background: inherit;
border-color: #999 !important;
}
</style>

View File

@@ -0,0 +1,199 @@
<script setup lang="tsx">
import { NButton, NTag } from 'naive-ui';
import { utils, writeFile } from 'xlsx';
import { useAppStore } from '@/store/modules/app';
import { useTable } from '@/hooks/common/table';
import { fetchGetUserList } from '@/service/api';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { $t } from '@/locales';
const appStore = useAppStore();
const { columns, data, loading } = useTable({
apiFn: fetchGetUserList,
showTotal: true,
apiParams: {
current: 1,
size: 999,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'userName',
title: $t('page.manage.user.userName'),
align: 'center',
minWidth: 100
},
{
key: 'userGender',
title: $t('page.manage.user.userGender'),
align: 'center',
width: 100,
render: row => {
if (row.userGender === null) {
return null;
}
const tagMap: Record<Api.SystemManage.UserGender, NaiveUI.ThemeColor> = {
1: 'primary',
2: 'error'
};
const label = $t(userGenderRecord[row.userGender]);
return <NTag type={tagMap[row.userGender]}>{label}</NTag>;
}
},
{
key: 'nickName',
title: $t('page.manage.user.nickName'),
align: 'center',
minWidth: 100
},
{
key: 'userPhone',
title: $t('page.manage.user.userPhone'),
align: 'center',
width: 120
},
{
key: 'userEmail',
title: $t('page.manage.user.userEmail'),
align: 'center',
minWidth: 200
},
{
key: 'status',
title: $t('page.manage.user.userStatus'),
align: 'center',
width: 100,
render: row => {
if (row.status === null) {
return null;
}
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
1: 'success',
2: 'warning'
};
const label = $t(enableStatusRecord[row.status]);
return <NTag type={tagMap[row.status]}>{label}</NTag>;
}
}
]
});
function exportExcel() {
const exportColumns = columns.value.slice(2);
const excelList = data.value.map(item => exportColumns.map(col => getTableValue(col, item)));
const titleList = exportColumns.map(col => (isTableColumnHasTitle(col) && col.title) || null);
excelList.unshift(titleList);
const workBook = utils.book_new();
const workSheet = utils.aoa_to_sheet(excelList);
workSheet['!cols'] = exportColumns.map(item => ({
width: Math.round(Number(item.width) / 10 || 20)
}));
utils.book_append_sheet(workBook, workSheet, '用户列表');
writeFile(workBook, '用户数据.xlsx');
}
function getTableValue(
col: NaiveUI.TableColumn<NaiveUI.TableDataWithIndex<Api.SystemManage.User>>,
item: NaiveUI.TableDataWithIndex<Api.SystemManage.User>
) {
if (!isTableColumnHasKey(col)) {
return null;
}
const { key } = col;
if (key === 'operate') {
return null;
}
if (key === 'userRoles') {
return item.userRoles.map(role => role).join(',');
}
if (key === 'status') {
return (item.status && $t(enableStatusRecord[item.status])) || null;
}
if (key === 'userGender') {
return (item.userGender && $t(userGenderRecord[item.userGender])) || null;
}
return item[key];
}
function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}
function isTableColumnHasTitle<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> & {
title: string;
} {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).title);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<NCard title="Excel导出" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<NSpace align="end" wrap justify="end" class="lt-sm:w-200px">
<NButton size="small" ghost type="primary" @click="exportExcel">
<template #icon>
<icon-file-icons:microsoft-excel class="text-icon" />
</template>
导出excel
</NButton>
</NSpace>
</template>
<NDataTable
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="false"
:virtual-scroll="true"
class="sm:h-full"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,173 @@
import type { Task } from 'dhtmlx-gantt';
export const ganttTasks: Task[] = [
{
id: 11,
text: 'Soybean 架构设计',
type: 'project',
progress: 0,
open: true,
start_date: new Date('2024-01-10 00:00'),
duration: 12,
parent: 0
},
{
id: 12,
text: '测试版本',
start_date: new Date('2024-03-20 00:00'),
type: 'project',
duration: 5,
render: 'split',
parent: '11',
progress: 0,
open: true
},
{
id: 99,
text: '测试版本1 发布',
start_date: new Date('2024-03-20 00:00'),
end_date: new Date('2024-03-25 00:00'),
parent: '12',
progress: 0,
open: true
},
{
id: 98,
text: '测试版本2 发布',
start_date: new Date('2024-03-26 00:00'),
duration: 4,
parent: '12',
progress: 0,
open: true
},
{
id: 97,
text: '测试版本3 发布',
start_date: new Date('2024-03-31 00:00'),
duration: 10,
parent: '12',
progress: 0,
open: true
},
{
id: 13,
text: '1.0 版本',
start_date: new Date('2024-03-31 00:00'),
type: 'project',
render: 'split',
parent: '11',
progress: 0.5,
open: false,
duration: 11
},
{
id: 17,
text: '1.0正式发布',
start_date: new Date('2024-03-31 00:00'),
end_date: new Date('2024-04-03 00:00'),
parent: '13',
progress: 0,
open: true
},
{
id: 18,
text: '1.0.1 版本',
start_date: new Date('2024-04-03 00:00'),
duration: 5,
parent: '13',
progress: 0,
open: true
},
{
id: 19,
text: '1.0.2 版本',
start_date: new Date('2024-04-08 00:00'),
duration: 6,
parent: '13',
progress: 0,
open: true
},
{
id: 20,
text: '1.0.3 版本',
start_date: new Date('2024-04-16 00:00'),
duration: 8,
parent: '13',
progress: 0,
open: true
},
{
id: 31,
text: '1.0.4 版本',
start_date: new Date('2024-04-17 00:00'),
duration: 8,
parent: '13',
progress: 0,
open: true
},
{
id: 32,
text: '1.0.5 版本',
start_date: new Date('2024-04-26 00:00'),
duration: 9,
parent: '13',
progress: 0,
open: true
},
{
id: 33,
text: '1.0.9 版本',
start_date: new Date('2024-05-05 00:00'),
duration: 2,
parent: '13',
progress: 0,
open: true
},
{
id: 14,
text: '1.1 版本',
start_date: new Date('2024-05-07 00:00'),
duration: 30,
parent: '11',
progress: 0,
open: true
},
{
id: 15,
text: '1.2 版本',
start_date: new Date('2024-06-06 00:00'),
duration: 46,
parent: '11',
progress: 0,
open: true
},
{
id: 16,
text: '1.3版本',
type: 'project',
render: 'split',
parent: '11',
progress: 0,
open: true,
start_date: new Date('2024-07-22 00:00'),
duration: 11
},
{
id: 21,
text: '1.3.1版本',
start_date: new Date('2024-07-22 00:00'),
duration: 7,
parent: '16',
progress: 0,
open: true
},
{
id: 22,
text: '1.3.2版本',
start_date: new Date('2024-07-29 00:00'),
duration: 7,
parent: '16',
progress: 0,
open: true
}
];

View File

@@ -0,0 +1,208 @@
<script setup lang="tsx">
import { onMounted, shallowRef } from 'vue';
import { gantt } from 'dhtmlx-gantt';
import type { GanttConfigOptions, ZoomLevels } from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import { ganttTasks } from './data';
const ganttRef = shallowRef<HTMLElement>();
type TimeType = 'day' | 'week' | 'month' | 'quarter' | 'year';
const timeType = shallowRef<TimeType>('quarter');
interface TimeData {
name: string;
code: TimeType;
}
const data: TimeData[] = [
{
name: '天',
code: 'day'
},
{
name: '周',
code: 'week'
},
{
name: '月',
code: 'month'
},
{
name: '季',
code: 'quarter'
},
{
name: '年',
code: 'year'
}
];
function initGantt() {
if (!ganttRef.value) return;
const config: Partial<GanttConfigOptions> = {
grid_width: 350,
add_column: false,
autofit: false,
row_height: 60,
bar_height: 34,
auto_types: true,
xml_date: '%Y-%m-%d',
columns: [
{
name: 'text',
label: '项目名称',
tree: true,
width: '*'
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 150
}
]
};
Object.assign(gantt.config, config);
gantt.i18n.setLocale('cn');
gantt.init(ganttRef.value);
gantt.parse({ data: ganttTasks });
const zoomLevels: ZoomLevels[] = [
{
name: 'day',
scale_height: 60,
scales: [{ unit: 'day', step: 1, format: '%d %M' }]
},
{
name: 'week',
scale_height: 60,
scales: [
{
unit: 'week',
step: 1,
format(date: Date) {
const dateToStr = gantt.date.date_to_str('%m-%d');
const endDate = gantt.date.add(date, -6, 'day'); // 第几周
return `${dateToStr(endDate)}${dateToStr(date)}`;
}
},
{
unit: 'day',
step: 1,
format: '%d',
css(date: Date) {
if (date.getDay() === 0 || date.getDay() === 6) {
return 'day-item weekend weekend-border-bottom';
}
return 'day-item';
}
}
]
},
{
name: 'month',
scale_height: 60,
min_column_width: 18,
scales: [
{ unit: 'month', format: '%Y-%m' },
{
unit: 'day',
step: 1,
format: '%d',
css(date: Date) {
if (date.getDay() === 0 || date.getDay() === 6) {
return 'day-item weekend weekend-border-bottom';
}
return 'day-item';
}
}
]
},
{
name: 'quarter',
height: 60,
min_column_width: 110,
scales: [
{
unit: 'quarter',
step: 1,
format(date: Date) {
const yearStr = `${new Date(date).getFullYear()}`;
const dateToStr = gantt.date.date_to_str('%M');
const endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day');
return `${yearStr + dateToStr(date)} - ${dateToStr(endDate)}`;
}
},
{
unit: 'week',
step: 1,
format(date: Date) {
const dateToStr = gantt.date.date_to_str('%m-%d');
const endDate = gantt.date.add(date, 6, 'day');
return `${dateToStr(date)}${dateToStr(endDate)}`;
}
}
]
},
{
name: 'year',
scale_height: 50,
min_column_width: 150,
scales: [
{ unit: 'year', step: 1, format: '%Y年' },
{ unit: 'month', format: '%Y-%m' }
]
}
];
gantt.ext.zoom.init({
levels: zoomLevels
});
gantt.ext.zoom.setLevel(timeType.value);
}
function changeTime(value: TimeType) {
timeType.value = value;
gantt.ext.zoom.setLevel(value);
}
onMounted(() => {
initGantt();
});
</script>
<template>
<div class="overflow-hidden lt-sm:overflow-auto">
<NCard
title="甘特图演示"
:bordered="false"
size="small"
content-class="overflow-y-hidden overflow-x-auto"
class="h-full card-wrapper"
>
<template #header-extra>
<NTabs
:value="timeType"
type="segment"
animated
size="small"
class="relative w-320px"
@update:value="changeTime"
>
<NTab v-for="item in data" :key="item.code" :name="item.code">
{{ item.name }}
</NTab>
</NTabs>
</template>
<div ref="ganttRef" class="size-full min-w-800px"></div>
</NCard>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,32 @@
export const icons = [
'mdi:emoticon',
'mdi:ab-testing',
'ph:alarm',
'ph:android-logo',
'ph:align-bottom',
'ph:archive-box-light',
'uil:basketball',
'uil:brightness-plus',
'uil:capture',
'mdi:apps-box',
'mdi:alert',
'mdi:airballoon',
'mdi:airplane-edit',
'mdi:alpha-f-box-outline',
'mdi:arm-flex-outline',
'ic:baseline-10mp',
'ic:baseline-access-time',
'ic:baseline-brightness-4',
'ic:baseline-brightness-5',
'ic:baseline-credit-card',
'ic:baseline-filter-1',
'ic:baseline-filter-2',
'ic:baseline-filter-3',
'ic:baseline-filter-4',
'ic:baseline-filter-5',
'ic:baseline-filter-6',
'ic:baseline-filter-7',
'ic:baseline-filter-8',
'ic:baseline-filter-9',
'ic:baseline-filter-9-plus'
];

View File

@@ -0,0 +1,51 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { icons } from './icons';
const selectValue = ref('');
const localIcons = ['custom-icon', 'activity', 'at-sign', 'cast', 'chrome', 'copy', 'wind'];
</script>
<template>
<div class="h-full">
<NCard title="Icon组件示例" :bordered="false" class="card-wrapper">
<div class="grid grid-cols-10">
<template v-for="item in icons" :key="item">
<div class="mt-5px flex-x-center">
<SvgIcon :icon="item" class="text-30px" />
</div>
</template>
</div>
<div class="mt-50px">
<h1 class="mb-20px text-18px font-500">Icon图标选择器</h1>
<CustomIconSelect v-model:value="selectValue" :icons="icons" />
</div>
<template #footer>
<WebSiteLink label="iconify地址" link="https://icones.js.org/" class="mt-10px" />
</template>
</NCard>
<NCard title="自定义图标示例" :bordered="false" class="mt-10px card-wrapper">
<div class="pb-12px text-16px">
在src/assets/svg-icon文件夹下的svg文件通过在template里面以 icon-local-{文件名} 直接渲染,
其中icon-local为.env文件里的 VITE_ICON_LOCAL_PREFIX
</div>
<div class="grid grid-cols-10">
<div class="mt-5px flex-x-center">
<icon-local-activity class="text-40px text-success" />
</div>
<div class="mt-5px flex-x-center">
<icon-local-cast class="text-20px text-error" />
</div>
</div>
<div class="py-12px text-16px">通过SvgIcon组件动态渲染, 菜单通过meta的localIcon属性渲染自定义图标</div>
<div class="grid grid-cols-10">
<div v-for="(fileName, index) in localIcons" :key="index" class="mt-5px flex-x-center">
<SvgIcon :local-icon="fileName" class="text-30px text-primary" />
</div>
</div>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { BAIDU_MAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'BaiduMap' });
window.HOST_TYPE = '2';
const { load } = useScriptTag(BAIDU_MAP_SDK_URL);
const domRef = ref<HTMLDivElement>();
async function renderMap() {
await load(true);
if (!domRef.value) return;
const map = new BMap.Map(domRef.value);
const point = new BMap.Point(114.05834626586915, 22.546789983033168);
map.centerAndZoom(point, 15);
map.enableScrollWheelZoom();
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { AMAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'GaodeMap' });
const { load } = useScriptTag(AMAP_SDK_URL);
const domRef = ref<HTMLDivElement>();
async function renderMap() {
await load(true);
if (!domRef.value) return;
const map = new AMap.Map(domRef.value, {
zoom: 11,
center: [114.05834626586915, 22.546789983033168],
viewMode: '3D'
});
map.getCenter();
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,5 @@
import BaiduMap from './baidu-map.vue';
import GaodeMap from './gaode-map.vue';
import TencentMap from './tencent-map.vue';
export { BaiduMap, GaodeMap, TencentMap };

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { TENCENT_MAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'TencentMap' });
const { load } = useScriptTag(TENCENT_MAP_SDK_URL);
const domRef = ref<HTMLDivElement | null>(null);
async function renderMap() {
await load(true);
if (!domRef.value) return;
// eslint-disable-next-line no-new
new TMap.Map(domRef.value, {
center: new TMap.LatLng(39.98412, 116.307484),
zoom: 11,
viewMode: '3D'
});
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { Component } from 'vue';
import { BaiduMap, GaodeMap, TencentMap } from './components';
interface Map {
id: string;
label: string;
component: Component;
}
const maps: Map[] = [
{ id: 'gaode', label: '高德地图', component: GaodeMap },
{ id: 'tencent', label: '腾讯地图', component: TencentMap },
{ id: 'baidu', label: '百度地图', component: BaiduMap }
];
</script>
<template>
<div class="h-full">
<NCard title="地图插件" :bordered="false" class="h-full card-wrapper" content-style="overflow:hidden">
<NTabs type="line" class="h-full flex-col-stretch" pane-class="flex-1-hidden">
<NTabPane v-for="item in maps" :key="item.id" :name="item.id" :tab="item.label">
<component :is="item.component" />
</NTabPane>
</NTabs>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import VuePdfEmbed from 'vue-pdf-embed';
import { useLoading } from '@sa/hooks';
const { loading, endLoading } = useLoading(true);
const pdfRef = shallowRef<InstanceType<typeof VuePdfEmbed> | null>(null);
const source = `https://xiaoxian521.github.io/hyperlink/pdf/Cookie%E5%92%8CSession%E5%8C%BA%E5%88%AB%E7%94%A8%E6%B3%95.pdf`;
const showAllPages = ref(false);
const currentPage = ref<undefined | number>(1);
const pageCount = ref(1);
function onPdfRendered() {
endLoading();
if (pdfRef.value?.doc) {
pageCount.value = pdfRef.value.doc.numPages;
}
}
function showAllPagesChange() {
currentPage.value = showAllPages.value ? undefined : 1;
}
const rotations = [0, 90, 180, 270];
const currentRotation = ref(0);
function handleRotate() {
currentRotation.value = (currentRotation.value + 1) % 4;
}
async function handlePrint() {
await pdfRef.value?.print(undefined, 'test.pdf', true);
}
async function handleDownload() {
await pdfRef.value?.download('test.pdf');
}
</script>
<template>
<div class="overflow-hidden">
<NCard title="PDF 预览" :bordered="false" class="h-full card-wrapper" content-class="overflow-hidden">
<div class="h-full flex-col-stretch">
<GithubLink link="https://github.com/hrynko/vue-pdf-embed" />
<WebSiteLink label="文档地址:" link="https://www.npmjs.com/package/vue-pdf-embed" />
<div class="flex-y-center justify-end gap-12px">
<NCheckbox v-model:checked="showAllPages" @update:checked="showAllPagesChange">显示所有页面</NCheckbox>
<ButtonIcon tooltip-content="旋转90度" @click="handleRotate">
<icon-material-symbols-light:rotate-90-degrees-ccw-outline-rounded />
</ButtonIcon>
<ButtonIcon tooltip-content="打印" @click="handlePrint">
<icon-mdi:printer />
</ButtonIcon>
<ButtonIcon tooltip-content="下载" @click="handleDownload">
<icon-charm:download />
</ButtonIcon>
</div>
<NScrollbar class="flex-1-hidden">
<NSkeleton v-if="loading" size="small" class="mt-12px" text :repeat="12" />
<VuePdfEmbed
ref="pdfRef"
class="overflow-auto container"
:class="{ 'h-0': loading }"
:rotation="rotations[currentRotation]"
:page="currentPage"
:source="source"
@rendered="onPdfRendered"
/>
</NScrollbar>
<div class="flex-y-center justify-between">
<div v-if="showAllPages" class="text-18px font-medium">{{ pageCount }}</div>
<NPagination v-else v-model:page="currentPage" :page-count="pageCount" :page-size="1" />
</div>
</div>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { html } from 'pinyin-pro';
import domPurify from 'dompurify';
const domRef = ref<HTMLElement | null>(null);
const domRef2 = ref<HTMLElement | null>(null);
const domRef3 = ref<HTMLElement | null>(null);
function renderHtml() {
if (!domRef.value || !domRef2.value || !domRef3.value) return;
const text = 'SoybeanAdmin是一个清新优雅、高颜值且功能强大的后台管理模板';
const code = domPurify.sanitize(html(text));
const code2 = domPurify.sanitize(html(text, { toneType: 'none' }));
domRef.value.innerHTML = code;
domRef2.value.innerHTML = code2;
domRef3.value.innerHTML = code;
}
onMounted(() => {
renderHtml();
});
</script>
<template>
<div>
<NCard title="pinyin 插件" :bordered="false" class="h-full card-wrapper">
<NSpace :vertical="true">
<GithubLink link="https://github.com/zh-lx/pinyin-pro" />
<WebSiteLink label="文档地址:" link="https://pinyin-pro.cn/" />
</NSpace>
<NDivider title-placement="left">常规使用</NDivider>
<p ref="domRef" class="text-18px"></p>
<NDivider title-placement="left">不带音调</NDivider>
<p ref="domRef2" class="text-18px"></p>
<NDivider title-placement="left">自定义样式</NDivider>
<p ref="domRef3" class="custom-style text-18px"></p>
</NCard>
</div>
</template>
<style lang="scss" scoped>
.custom-style {
:deep(.py-result-item) {
.py-chinese-item {
--uno: text-primary;
}
.py-pinyin-item {
--uno: text-error;
}
}
}
</style>

View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
import printJS from 'print-js';
function printTable() {
printJS({
printable: [
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' },
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' }
],
properties: ['name', 'wechat', 'remark'],
type: 'json'
});
}
function printImage() {
printJS({
printable: [
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg',
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg'
],
type: 'image',
header: 'Multiple Images',
imageStyle: 'width:100%;'
});
}
</script>
<template>
<div class="h-full">
<NCard title="打印" :bordered="false" class="card-wrapper">
<NButton type="primary" class="mr-10px" @click="printTable">打印表格</NButton>
<NButton type="primary" @click="printImage">打印图片</NButton>
<template #footer>
<GithubLink label="printJS" link="https://github.com/crabbly/Print.js" class="mt-10px" />
</template>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
import SwiperCore from 'swiper';
import { Navigation, Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/vue';
import type { SwiperOptions } from 'swiper/types';
type SwiperExampleOptions = Pick<
SwiperOptions,
'navigation' | 'pagination' | 'scrollbar' | 'slidesPerView' | 'slidesPerGroup' | 'spaceBetween' | 'direction' | 'loop'
>;
interface SwiperExample {
id: number;
label: string;
options: Partial<SwiperExampleOptions>;
}
SwiperCore.use([Navigation, Pagination]);
const swiperExample: SwiperExample[] = [
{ id: 0, label: 'Default', options: {} },
{
id: 1,
label: 'Navigation',
options: {
navigation: true
}
},
{
id: 2,
label: 'Pagination',
options: {
pagination: true
}
},
{
id: 3,
label: 'Pagination dynamic',
options: {
pagination: { dynamicBullets: true }
}
},
{
id: 4,
label: 'Pagination progress',
options: {
navigation: true,
pagination: {
type: 'progressbar'
}
}
},
{
id: 5,
label: 'Pagination fraction',
options: {
navigation: true,
pagination: {
type: 'fraction'
}
}
},
{
id: 6,
label: 'Slides per view',
options: {
pagination: {
clickable: true
},
slidesPerView: 3,
spaceBetween: 30
}
},
{
id: 7,
label: 'Infinite loop',
options: {
navigation: true,
pagination: {
clickable: true
},
loop: true
}
}
];
</script>
<template>
<div>
<NCard title="Swiper插件" :bordered="false" class="card-wrapper">
<NSpace :vertical="true">
<GithubLink link="https://github.com/nolimits4web/swiper" />
<WebSiteLink label="vue3版文档地址" link="https://swiperjs.com/vue" />
<WebSiteLink label="插件demo地址" link="https://swiperjs.com/demos" />
</NSpace>
<NSpace :vertical="true">
<div v-for="item in swiperExample" :key="item.id">
<h3 class="py-24px text-24px font-bold">{{ item.label }}</h3>
<Swiper v-bind="item.options">
<SwiperSlide v-for="i in 5" :key="i">
<div class="h-240px w-full flex-center border-1px border-#999 text-18px font-bold">Slide{{ i }}</div>
</SwiperSlide>
</Swiper>
</div>
</NSpace>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,42 @@
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue';
import TypeIt from 'typeit';
import type { Options } from 'typeit';
import type { El } from 'typeit/dist/types';
const textRef = shallowRef<El>();
function init() {
if (!textRef.value) return;
const options: Options = {
strings: 'SoybeanAdmin是一个清新优雅、高颜值且功能强大的后台管理模板',
lifeLike: true,
speed: 120,
loop: true
};
const initTypeIt = new TypeIt(textRef.value, options);
initTypeIt.go();
}
onMounted(() => {
init();
});
</script>
<template>
<div>
<NCard title="打字机 插件" :bordered="false" class="h-full card-wrapper">
<NSpace :vertical="true">
<GithubLink link="https://github.com/alexmacarthur/typeit" />
<WebSiteLink label="文档地址:" link="https://www.typeitjs.com/docs/vanilla/usage/" />
</NSpace>
<NDivider title-placement="left">基本示例</NDivider>
<span ref="textRef" class="text-18px"></span>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import Player from 'xgplayer';
import 'xgplayer/dist/index.min.css';
const domRef = ref<HTMLElement>();
const player = ref<Player>();
function renderXgPlayer() {
if (!domRef.value) return;
const url = 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4';
player.value = new Player({
el: domRef.value,
url,
playbackRate: [0.5, 0.75, 1, 1.5, 2]
});
}
function destroyXgPlayer() {
player.value?.destroy();
}
onMounted(() => {
renderXgPlayer();
});
onUnmounted(() => {
destroyXgPlayer();
});
</script>
<template>
<div>
<NCard title="视频播放器插件" :bordered="false" class="h-full card-wrapper">
<div class="flex-center">
<div ref="domRef" class="h-auto w-full shadow-md"></div>
</div>
</NCard>
</div>
</template>
<style scoped></style>