Compare commits

..

49 Commits

Author SHA1 Message Date
Soybean
5bfc7613f7 Merge branch 'main' into example 2024-09-07 11:38:35 +08:00
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
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
e7cd34ec37 chore(projects): Merge branch 'main' into example 2024-07-30 17:26:42 +08:00
Ohh
a9dce21878 feat(projects): add plugin pdf preview (#568) 2024-07-27 03:06:11 +08:00
Soybean
24320ab899 Merge branch 'main' into example 2024-07-22 19:05:11 +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
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
Ohh
c425822b3a feat(projects): add plugin excel export (#561) 2024-07-21 20:54:18 +08:00
Soybean
447bda2b48 chore(projects): merge branch main into example 2024-07-20 02:35:42 +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
110 changed files with 9316 additions and 1890 deletions

View File

@@ -6,6 +6,9 @@
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"formulahendry.auto-close-tag",
"formulahendry.auto-complete-tag",
"formulahendry.auto-rename-tag",
"lokalise.i18n-ally", "lokalise.i18n-ally",
"mhutchie.git-graph", "mhutchie.git-graph",
"mikestead.dotenv", "mikestead.dotenv",

View File

@@ -1,95 +1,6 @@
# Changelog # Changelog
## [v1.3.8](https://github.com/soybeanjs/soybean-admin/compare/v1.3.7...v1.3.8) (2024-10-25)
###    🚨 Breaking Changes
- **projects**: refactor route cache & support reset route cache strategy &nbsp;-&nbsp; by @soybeanjs [<samp>(b667e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b667eab)
### &nbsp;&nbsp;&nbsp;🚀 Features
- **packages**:
- add subpackage `@sa/alova` &nbsp;-&nbsp; by @JOU-amjs in https://github.com/soybeanjs/soybean-admin/issues/640 [<samp>(2072f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2072f58)
- optimistic subpackage `@sa/alova` &nbsp;-&nbsp; by @JOU-amjs in https://github.com/soybeanjs/soybean-admin/issues/646 [<samp>(4b3ac)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4b3ac11)
- **projects**:
- login supports accessible operation. &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/637 [<samp>(cfaab)</samp>](https://github.com/soybeanjs/soybean-admin/commit/cfaab85)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **utils**: fix `isPC`. fixed #644 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/644 [<samp>(47264)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4726498)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**:
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(8dcda)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8dcda38)
- **projects**:
- update vscode extensions &nbsp;-&nbsp; by @soybeanjs [<samp>(24bb6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/24bb6d9)
- update deps & fix sass usage &nbsp;-&nbsp; by @soybeanjs [<samp>(71e63)</samp>](https://github.com/soybeanjs/soybean-admin/commit/71e6307)
- **types**:
- remove type declaration for document.startViewTransition (TypeScript 5.6 includes it) &nbsp;-&nbsp; by @NHZEX in https://github.com/soybeanjs/soybean-admin/issues/633 [<samp>(83ba7)</samp>](https://github.com/soybeanjs/soybean-admin/commit/83ba798)
### &nbsp;&nbsp;&nbsp;🎨 Styles
- **projects**: reduce ambiguity in theme configuration instructions. &nbsp;-&nbsp; by @Azir-11 [<samp>(75cbf)</samp>](https://github.com/soybeanjs/soybean-admin/commit/75cbfbb)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![JOU-amjs](https://github.com/JOU-amjs.png?size=48)](https://github.com/JOU-amjs)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[![NHZEX](https://github.com/NHZEX.png?size=48)](https://github.com/NHZEX)&nbsp;&nbsp;
## [v1.3.7](https://github.com/soybeanjs/soybean-admin/compare/v1.3.6...v1.3.7) (2024-09-21)
### &nbsp;&nbsp;&nbsp;🚨 Breaking Changes
- **projects**: update scss config &nbsp;-&nbsp; by @soybeanjs [<samp>(24e9e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/24e9e57)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **projects**: fix global-tab click conflict with contextmenu &nbsp;-&nbsp; by @soybeanjs [<samp>(3e72c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3e72c3b)
### &nbsp;&nbsp;&nbsp;💅 Refactors
- **packages**: @sa/materials: remove tab close shortcut by mouse &nbsp;-&nbsp; by @soybeanjs [<samp>(4da58)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4da588c)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**: update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(baefd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/baefdfd)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;
## [v1.3.6](https://github.com/soybeanjs/soybean-admin/compare/v1.3.5...v1.3.6) (2024-09-20)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **components**:
- fix VerticalMixMenu name &nbsp;-&nbsp; by @soybeanjs [<samp>(20f8e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/20f8ed3)
- **projects**:
- fix click global-tab in iPad. fixed #624 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/624 [<samp>(04d05)</samp>](https://github.com/soybeanjs/soybean-admin/commit/04d0564)
- when the roles filter submenu is empty, the parent menu is not excluded. fixed #621. &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/626 and https://github.com/soybeanjs/soybean-admin/issues/621 [<samp>(0ac95)</samp>](https://github.com/soybeanjs/soybean-admin/commit/0ac95bd)
### &nbsp;&nbsp;&nbsp;🛠 Optimizations
- **projects**:
- optimize code &nbsp;-&nbsp; by @soybeanjs [<samp>(6561f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/6561f0b)
- optimize code &nbsp;-&nbsp; by @soybeanjs [<samp>(38eeb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/38eeb67)
- remove defineModel settingenabled by default &nbsp;-&nbsp; by @yanbowe in https://github.com/soybeanjs/soybean-admin/issues/620 [<samp>(60bbd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/60bbd2d)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: update CHANGELOG &nbsp;-&nbsp; by @soybeanjs [<samp>(5baf1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5baf19d)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**:
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(207d6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/207d6eb)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(f3562)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f35627e)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[![yanbowe](https://github.com/yanbowe.png?size=48)](https://github.com/yanbowe)&nbsp;&nbsp;
## [v1.3.5](https://github.com/soybeanjs/soybean-admin/compare/v1.3.4...v1.3.5) (2024-09-07) ## [v1.3.5](https://github.com/soybeanjs/soybean-admin/compare/v1.3.4...v1.3.5) (2024-09-07)
### &nbsp;&nbsp;&nbsp;🚀 Features ### &nbsp;&nbsp;&nbsp;🚀 Features

View File

@@ -1,92 +1,5 @@
# 更新日志 # 更新日志
## [v1.3.5](https://github.com/soybeanjs/soybean-admin/compare/v1.3.4...v1.3.5) (2024-09-07)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **packages**:
- @sa/axios: 成功时将响应添加到 flatRequest &nbsp;-&nbsp; by @soybeanjs [<samp>(c4e16)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c4e1610)
- **projects**:
- README.zh_CN.md 添加合作推广 &nbsp;-&nbsp; by @PZ-18664918826 in https://github.com/soybeanjs/soybean-admin/issues/601 [<samp>(2fa40)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2fa400b)
- 根据 Apache 规范添加更多提交类型 &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/610 [<samp>(878d9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/878d9c3)
- 配置是否支持自动更新。关闭#612 &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/617 和 https://github.com/soybeanjs/soybean-admin/issues/612 [<samp>(4c9f4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4c9f4e0)
- 添加应用错误处理程序。关闭 #587 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/587 [<samp>(be855)</samp>](https://github.com/soybeanjs/soybean-admin/commit/be8556c)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **deps**:
- 将 json5 从 devDependencies 移到 dependencies 以支持生产环境使用 &nbsp;-&nbsp; by @mufeng889 in https://github.com/soybeanjs/soybean-admin/issues/618 [<samp>(7cb43)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7cb43fc)
- **projects**:
- 避免检索缓存的 HTML &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/605 [<samp>(ef6cf)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ef6cf93)
- 修复登录重定向 &nbsp;-&nbsp; by @soybeanjs [<samp>(3830e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3830ec7)
- 修复侧边栏折叠时的 vertical-mix-menu。修复 #608 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/608 [<samp>(c3f1f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c3f1f69)
- 修复 activeMenu 为父菜单时的面包屑。修复 #589 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/589 [<samp>(79b2a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/79b2a28)
- 修复遇到多请求时的刷新令牌。修复 #581 &nbsp;-&nbsp; by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/581 [<samp>(27b52)</samp>](https://github.com/soybeanjs/soybean-admin/commit/27b5222)
- **types**:
- 修复 TableApiFn 的类型 &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/599 [<samp>(26c93)</samp>](https://github.com/soybeanjs/soybean-admin/commit/26c93df)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **projects**: 优化菜单 selectedKey &nbsp;-&nbsp; by @soybeanjs [<samp>(531bf)</samp>](https://github.com/soybeanjs/soybean-admin/commit/531bfaf)
### &nbsp;&nbsp;&nbsp;📖 文档
- **projects**:
- 更新 README &nbsp;-&nbsp; by @mufeng889 in https://github.com/soybeanjs/soybean-admin/issues/594 [<samp>(a8f92)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a8f923e)
- 更新 README &nbsp;-&nbsp; by @soybeanjs [<samp>(e9a2e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e9a2ee4)
- 更新 README &nbsp;-&nbsp; by @soybeanjs [<samp>(73e91)</samp>](https://github.com/soybeanjs/soybean-admin/commit/73e917a)
- 更新文档中重要信息的位置 &nbsp;-&nbsp; by **Azir** [<samp>(9c012)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9c012c7)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**:
- 更新依赖 &nbsp;-&nbsp; by @soybeanjs [<samp>(a1c14)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a1c14a1)
- 更新依赖 &nbsp;-&nbsp; by @soybeanjs [<samp>(7fa55)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7fa5590)
- 更新依赖 &nbsp;-&nbsp; by @soybeanjs [<samp>(a44ea)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a44ea62)
- **projects**:
- 使用 json5 解析环境变量 `VITE_OTHER_SERVICE_BASE_URL` 并修复代理启用 &nbsp;-&nbsp; by @soybeanjs [<samp>(b16a9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b16a963)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **projects**: 将脚本 czh 重命名为 commit:zh &nbsp;-&nbsp; by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/597 [<samp>(5094f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5094f0e)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![mufeng889](https://github.com/mufeng889.png?size=48)](https://github.com/mufeng889)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[![PZ-18664918826](https://github.com/PZ-18664918826.png?size=48)](https://github.com/PZ-18664918826)&nbsp;&nbsp;
[Azir](mailto:2075125282@qq.com),&nbsp;
## [v1.3.4](https://github.com/honghuangdc/soybean-admin/compare/v1.3.3...v1.3.4) (2024-08-01)
### &nbsp;&nbsp;&nbsp;🚨 重大变更
- **projects**: 当 vertical-mix 布局为 mixSiderFixed 时,不重置活动菜单 &nbsp;-&nbsp; by @honghuangdc [<samp>(939c5)</samp>](https://github.com/honghuangdc/soybean-admin/commit/939c512)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **projects**: 优化代码 &nbsp;-&nbsp; by @honghuangdc [<samp>(cb1d4)</samp>](https://github.com/honghuangdc/soybean-admin/commit/cb1d445)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **projects**: 更新依赖并修复 vue-router 类型 &nbsp;-&nbsp; by @honghuangdc [<samp>(96837)</samp>](https://github.com/honghuangdc/soybean-admin/commit/968370b)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![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 修复
- **projects**: 修复水印设置 &nbsp;-&nbsp; by @honghuangdc [<samp>(5646a)</samp>](https://github.com/honghuangdc/soybean-admin/commit/5646a50)
### &nbsp;&nbsp;&nbsp;📖 文档
- **projects**: 更新 CHANGELOG &nbsp;-&nbsp; by @honghuangdc [<samp>(ebc83)</samp>](https://github.com/honghuangdc/soybean-admin/commit/ebc838c)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![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) ## [v1.3.2](https://github.com/honghuangdc/soybean-admin/compare/v1.3.1...v1.3.2) (2024-07-30)
### &nbsp;&nbsp;&nbsp;🚀 新功能 ### &nbsp;&nbsp;&nbsp;🚀 新功能

View File

@@ -10,7 +10,11 @@ import { setupHtmlPlugin } from './html';
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) { export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
const plugins: PluginOption = [ const plugins: PluginOption = [
vue(), vue({
script: {
defineModel: true
}
}),
vueJsx(), vueJsx(),
VueDevtools(), VueDevtools(),
setupElegantRouter(), setupElegantRouter(),

View File

@@ -8,6 +8,20 @@ export function setupElegantRouter() {
base: 'src/layouts/base-layout/index.vue', base: 'src/layouts/base-layout/index.vue',
blank: 'src/layouts/blank-layout/index.vue' blank: 'src/layouts/blank-layout/index.vue'
}, },
customRoutes: {
names: [
'exception_403',
'exception_404',
'exception_500',
'document_project',
'document_project-link',
'document_vue',
'document_vite',
'document_unocss',
'document_naive',
'document_antd'
]
},
routePathTransformer(routeName, routePath) { routePathTransformer(routeName, routePath) {
const key = routeName as RouteKey; const key = routeName as RouteKey;

View File

@@ -1,7 +1,7 @@
{ {
"name": "soybean-admin", "name": "soybean-admin",
"type": "module", "type": "module",
"version": "1.3.8", "version": "1.3.5",
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。", "description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
"author": { "author": {
"name": "Soybean", "name": "Soybean",
@@ -47,6 +47,8 @@
"update-pkg": "sa update-pkg" "update-pkg": "sa update-pkg"
}, },
"dependencies": { "dependencies": {
"@antv/data-set": "0.11.8",
"@antv/g2": "5.2.5",
"@better-scroll/core": "2.5.1", "@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2", "@iconify/vue": "4.1.2",
"@sa/axios": "workspace:*", "@sa/axios": "workspace:*",
@@ -54,49 +56,64 @@
"@sa/hooks": "workspace:*", "@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*", "@sa/materials": "workspace:*",
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"@vueuse/core": "11.1.0", "@vueuse/core": "11.0.3",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"dhtmlx-gantt": "8.0.10",
"dompurify": "3.1.6",
"echarts": "5.5.1", "echarts": "5.5.1",
"jsbarcode": "3.11.6",
"json5": "2.2.3", "json5": "2.2.3",
"naive-ui": "2.40.1", "naive-ui": "2.39.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"pinia": "2.2.4", "pinia": "2.2.2",
"tailwind-merge": "2.5.4", "pinyin-pro": "3.24.2",
"vue": "3.5.12", "print-js": "1.6.0",
"vue-draggable-plus": "0.5.4", "swiper": "11.1.12",
"vue-i18n": "10.0.4", "tailwind-merge": "2.5.2",
"vue-router": "4.4.5" "typeit": "8.8.4",
"vditor": "3.10.5",
"vue": "3.5.3",
"vue-draggable-plus": "0.5.3",
"vue-i18n": "9.14.0",
"vue-pdf-embed": "2.1.0",
"vue-router": "4.4.3",
"wangeditor": "4.7.15",
"xgplayer": "3.0.20",
"xlsx": "0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@amap/amap-jsapi-types": "0.0.15",
"@elegant-router/vue": "0.3.8", "@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.263", "@iconify/json": "2.2.245",
"@sa/scripts": "workspace:*", "@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*", "@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.4.2", "@soybeanjs/eslint-config": "1.4.0",
"@types/node": "22.7.9", "@types/bmapgl": "0.0.7",
"@types/dompurify": "3.0.5",
"@types/node": "22.5.4",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@unocss/eslint-config": "0.63.6", "@unocss/eslint-config": "0.62.3",
"@unocss/preset-icons": "0.63.6", "@unocss/preset-icons": "0.62.3",
"@unocss/preset-uno": "0.63.6", "@unocss/preset-uno": "0.62.3",
"@unocss/transformer-directives": "0.63.6", "@unocss/transformer-directives": "0.62.3",
"@unocss/transformer-variant-group": "0.63.6", "@unocss/transformer-variant-group": "0.62.3",
"@unocss/vite": "0.63.6", "@unocss/vite": "0.62.3",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.3",
"@vitejs/plugin-vue-jsx": "4.0.1", "@vitejs/plugin-vue-jsx": "4.0.1",
"eslint": "9.13.0", "eslint": "9.10.0",
"eslint-plugin-vue": "9.29.1", "eslint-plugin-vue": "9.28.0",
"lint-staged": "15.2.10", "lint-staged": "15.2.10",
"sass": "1.80.4", "sass": "1.78.0",
"simple-git-hooks": "2.11.1", "simple-git-hooks": "2.11.1",
"tsx": "4.19.1", "tsx": "4.19.0",
"typescript": "5.6.3", "typescript": "5.5.4",
"unplugin-icons": "0.19.3", "unplugin-icons": "0.19.3",
"unplugin-vue-components": "0.27.4", "unplugin-vue-components": "0.27.4",
"vite": "5.4.10", "vite": "5.4.3",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.5.4", "vite-plugin-vue-devtools": "7.4.4",
"vue-eslint-parser": "9.4.3", "vue-eslint-parser": "9.4.3",
"vue-tsc": "2.1.6" "vue-tsc": "2.1.6"
}, },

View File

@@ -1,20 +0,0 @@
{
"name": "@sa/alova",
"version": "1.3.8",
"exports": {
".": "./src/index.ts",
"./fetch": "./src/fetch.ts",
"./client": "./src/client.ts",
"./mock": "./src/mock.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@alova/mock": "2.0.8",
"@sa/utils": "workspace:*",
"alova": "3.1.1"
}
}

View File

@@ -1 +0,0 @@
export * from 'alova/client';

View File

@@ -1,2 +0,0 @@
/** the backend error code key */
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';

View File

@@ -1,2 +0,0 @@
import adapterFetch from 'alova/fetch';
export default adapterFetch;

View File

@@ -1,77 +0,0 @@
import { createAlova } from 'alova';
import type { AlovaDefaultCacheAdapter, AlovaGenerics, AlovaGlobalCacheAdapter, AlovaRequestAdapter } from 'alova';
import VueHook from 'alova/vue';
import type { VueHookType } from 'alova/vue';
import adapterFetch from 'alova/fetch';
import { createServerTokenAuthentication } from 'alova/client';
import type { FetchRequestInit } from 'alova/fetch';
import { BACKEND_ERROR_CODE } from './constant';
import type { CustomAlovaConfig, RequestOptions } from './type';
export const createAlovaRequest = <
RequestConfig = FetchRequestInit,
ResponseType = Response,
ResponseHeader = Headers,
L1Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter,
L2Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter
>(
customConfig: CustomAlovaConfig<
AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>
>,
options: RequestOptions<AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>>
) => {
const { tokenRefresher } = options;
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
VueHookType,
AlovaRequestAdapter<RequestConfig, ResponseType, ResponseHeader>
>({
refreshTokenOnSuccess: {
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
handler: async (response, method) => tokenRefresher?.handler(response, method)
},
refreshTokenOnError: {
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
handler: async (response, method) => tokenRefresher?.handler(response, method)
}
});
const instance = createAlova({
...customConfig,
timeout: customConfig.timeout ?? 10 * 1000,
requestAdapter: (customConfig.requestAdapter as any) ?? adapterFetch(),
statesHook: VueHook,
beforeRequest: onAuthRequired(options.onRequest as any),
responded: onResponseRefreshToken({
onSuccess: async (response, method) => {
// check if http status is success
let error: any = null;
let transformedData: any = null;
try {
if (await options.isBackendSuccess(response)) {
transformedData = await options.transformBackendResponse(response);
} else {
error = new Error('the backend request error');
error.code = BACKEND_ERROR_CODE;
}
} catch (err) {
error = err;
}
if (error) {
await options.onError?.(error, response, method);
throw error;
}
return transformedData;
},
onComplete: options.onComplete,
onError: (error, method) => options.onError?.(error, null, method)
})
});
return instance;
};
export { BACKEND_ERROR_CODE };
export type * from './type';
export type * from 'alova';

View File

@@ -1 +0,0 @@
export * from '@alova/mock';

View File

@@ -1,52 +0,0 @@
import type { AlovaGenerics, AlovaOptions, AlovaRequestAdapter, Method, ResponseCompleteHandler } from 'alova';
export type CustomAlovaConfig<AG extends AlovaGenerics> = Omit<
AlovaOptions<AG>,
'statesHook' | 'beforeRequest' | 'responded' | 'requestAdapter'
> & {
/** request adapter. all request of alova will be sent by it. */
requestAdapter?: AlovaRequestAdapter<AG['RequestConfig'], AG['Response'], AG['ResponseHeader']>;
};
export interface RequestOptions<AG extends AlovaGenerics> {
/**
* The hook before request
*
* For example: You can add header token in this hook
*
* @param method alova Method Instance
*/
onRequest?: AlovaOptions<AG>['beforeRequest'];
/**
* The hook to check backend response is success or not
*
* @param response alova response
*/
isBackendSuccess: (response: AG['Response']) => Promise<boolean>;
/** The config to refresh token */
tokenRefresher?: {
/** detect the token is expired */
isExpired(response: AG['Response'], Method: Method<AG>): Promise<boolean> | boolean;
/** refresh token handler */
handler(response: AG['Response'], Method: Method<AG>): Promise<void>;
};
/** The hook after backend request complete */
onComplete?: ResponseCompleteHandler<AG>;
/**
* The hook to handle error
*
* For example: You can show error message in this hook
*
* @param error
*/
onError?: (error: any, response: AG['Response'] | null, methodInstance: Method<AG>) => any | Promise<any>;
/**
* transform backend response when the responseType is json
*
* @param response alova response
*/
transformBackendResponse: (response: AG['Response']) => any;
}

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/axios", "name": "@sa/axios",
"version": "1.3.8", "version": "1.3.5",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },
@@ -16,6 +16,6 @@
"qs": "6.13.0" "qs": "6.13.0"
}, },
"devDependencies": { "devDependencies": {
"@types/qs": "6.9.16" "@types/qs": "6.9.15"
} }
} }

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/hooks", "name": "@sa/hooks",
"version": "1.3.8", "version": "1.3.5",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

View File

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

View File

@@ -53,10 +53,23 @@ const bindProps = computed(() => {
function handleClose() { function handleClose() {
emit('close'); emit('close');
} }
function handleMouseup(e: MouseEvent) {
// close tab by mouse wheel button click
if (e.button === 1) {
handleClose();
}
}
</script> </script>
<template> <template>
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps"> <component
:is="activeTabComponent.component"
:class="activeTabComponent.class"
:style="cssVars"
v-bind="bindProps"
@mouseup="handleMouseup"
>
<template #prefix> <template #prefix>
<slot name="prefix"></slot> <slot name="prefix"></slot>
</template> </template>

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/fetch", "name": "@sa/fetch",
"version": "1.3.8", "version": "1.3.5",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },
@@ -10,6 +10,6 @@
} }
}, },
"dependencies": { "dependencies": {
"ofetch": "1.4.1" "ofetch": "1.3.4"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/scripts", "name": "@sa/scripts",
"version": "1.3.8", "version": "1.3.5",
"bin": { "bin": {
"sa": "./bin.ts" "sa": "./bin.ts"
}, },
@@ -14,14 +14,14 @@
}, },
"devDependencies": { "devDependencies": {
"@soybeanjs/changelog": "0.3.24", "@soybeanjs/changelog": "0.3.24",
"bumpp": "9.7.1", "bumpp": "9.5.2",
"c12": "2.0.1", "c12": "1.11.2",
"cac": "6.7.14", "cac": "6.7.14",
"consola": "3.2.3", "consola": "3.2.3",
"enquirer": "2.4.1", "enquirer": "2.4.1",
"execa": "9.4.1", "execa": "9.3.1",
"kolorist": "1.8.0", "kolorist": "1.8.0",
"npm-check-updates": "17.1.4", "npm-check-updates": "17.1.1",
"rimraf": "6.0.1" "rimraf": "6.0.1"
} }
} }

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/utils", "name": "@sa/utils",
"version": "1.3.8", "version": "1.3.5",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },

4180
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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

@@ -54,10 +54,3 @@ export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode,
}; };
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord); export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.resetCacheStrategy.close',
refresh: 'theme.resetCacheStrategy.refresh'
};
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);

29
src/constants/business.ts Normal file
View File

@@ -0,0 +1,29 @@
import { transformRecordToOption } from '@/utils/common';
export const enableStatusRecord: Record<Api.Common.EnableStatus, App.I18n.I18nKey> = {
'1': 'page.manage.common.status.enable',
'2': 'page.manage.common.status.disable'
};
export const enableStatusOptions = transformRecordToOption(enableStatusRecord);
export const userGenderRecord: Record<Api.SystemManage.UserGender, App.I18n.I18nKey> = {
'1': 'page.manage.user.gender.male',
'2': 'page.manage.user.gender.female'
};
export const userGenderOptions = transformRecordToOption(userGenderRecord);
export const menuTypeRecord: Record<Api.SystemManage.MenuType, App.I18n.I18nKey> = {
'1': 'page.manage.menu.type.directory',
'2': 'page.manage.menu.type.menu'
};
export const menuTypeOptions = transformRecordToOption(menuTypeRecord);
export const menuIconTypeRecord: Record<Api.SystemManage.IconType, App.I18n.I18nKey> = {
'1': 'page.manage.menu.iconType.iconify',
'2': 'page.manage.menu.iconType.local'
};
export const menuIconTypeOptions = transformRecordToOption(menuIconTypeRecord);

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

@@ -42,7 +42,7 @@ function resetScroll() {
@after-leave="resetScroll" @after-leave="resetScroll"
@after-enter="appStore.setContentXScrollable(false)" @after-enter="appStore.setContentXScrollable(false)"
> >
<KeepAlive :include="routeStore.cacheRoutes" :exclude="routeStore.excludeCacheRoutes"> <KeepAlive :include="routeStore.cacheRoutes">
<component <component
:is="Component" :is="Component"
v-if="appStore.reloadFlag" v-if="appStore.reloadFlag"

View File

@@ -18,7 +18,7 @@ function loginOrRegister() {
toLogin(); toLogin();
} }
type DropdownKey = 'logout'; type DropdownKey = 'user-center' | 'logout';
type DropdownOption = type DropdownOption =
| { | {
@@ -33,6 +33,15 @@ type DropdownOption =
const options = computed(() => { const options = computed(() => {
const opts: DropdownOption[] = [ const opts: DropdownOption[] = [
{
label: $t('common.userCenter'),
key: 'user-center',
icon: SvgIconVNode({ icon: 'ph:user-circle', fontSize: 18 })
},
{
type: 'divider',
key: 'divider'
},
{ {
label: $t('common.logout'), label: $t('common.logout'),
key: 'logout', key: 'logout',

View File

@@ -14,7 +14,7 @@ import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue'; import GlobalLogo from '../../global-logo/index.vue';
defineOptions({ defineOptions({
name: 'VerticalMixMenu' name: 'VerticalMenuMix'
}); });
const route = useRoute(); const route = useRoute();

View File

@@ -8,7 +8,6 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route'; import { useRouteStore } from '@/store/modules/route';
import { useTabStore } from '@/store/modules/tab'; import { useTabStore } from '@/store/modules/tab';
import { isPC } from '@/utils/agent';
import ContextMenu from './context-menu.vue'; import ContextMenu from './context-menu.vue';
defineOptions({ defineOptions({
@@ -25,7 +24,6 @@ const bsWrapper = ref<HTMLElement>();
const { width: bsWrapperWidth, left: bsWrapperLeft } = useElementBounding(bsWrapper); const { width: bsWrapperWidth, left: bsWrapperLeft } = useElementBounding(bsWrapper);
const bsScroll = ref<InstanceType<typeof BetterScroll>>(); const bsScroll = ref<InstanceType<typeof BetterScroll>>();
const tabRef = ref<HTMLElement>(); const tabRef = ref<HTMLElement>();
const isPCFlag = isPC();
const TAB_DATA_ID = 'data-tab-id'; const TAB_DATA_ID = 'data-tab-id';
@@ -84,10 +82,7 @@ function getContextMenuDisabledKeys(tabId: string) {
async function handleCloseTab(tab: App.Global.Tab) { async function handleCloseTab(tab: App.Global.Tab) {
await tabStore.removeTab(tab.id); await tabStore.removeTab(tab.id);
await routeStore.reCacheRoutesByKey(tab.routeKey);
if (themeStore.resetCacheStrategy === 'close') {
routeStore.resetRouteCache(tab.routeKey);
}
} }
async function refresh() { async function refresh() {
@@ -171,7 +166,11 @@ init();
<template> <template>
<DarkModeContainer class="size-full flex-y-center px-16px shadow-tab"> <DarkModeContainer class="size-full flex-y-center px-16px shadow-tab">
<div ref="bsWrapper" class="h-full flex-1-hidden"> <div ref="bsWrapper" class="h-full flex-1-hidden">
<BetterScroll ref="bsScroll" :options="{ scrollX: true, scrollY: false, click: !isPCFlag }" @click="removeFocus"> <BetterScroll
ref="bsScroll"
:options="{ scrollX: true, scrollY: false, click: appStore.isMobile }"
@click="removeFocus"
>
<div <div
ref="tabRef" ref="tabRef"
class="h-full flex pr-18px" class="h-full flex pr-18px"

View File

@@ -2,12 +2,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { import { themePageAnimationModeOptions, themeScrollModeOptions, themeTabModeOptions } from '@/constants/app';
resetCacheStrategyOptions,
themePageAnimationModeOptions,
themeScrollModeOptions,
themeTabModeOptions
} from '@/constants/app';
import { translateOptions } from '@/utils/common'; import { translateOptions } from '@/utils/common';
import SettingItem from '../components/setting-item.vue'; import SettingItem from '../components/setting-item.vue';
@@ -27,14 +22,6 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
<template> <template>
<NDivider>{{ $t('theme.pageFunTitle') }}</NDivider> <NDivider>{{ $t('theme.pageFunTitle') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px"> <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="0" :label="$t('theme.resetCacheStrategy.title')">
<NSelect
v-model:value="themeStore.resetCacheStrategy"
:options="translateOptions(resetCacheStrategyOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1" :label="$t('theme.scrollMode.title')"> <SettingItem key="1" :label="$t('theme.scrollMode.title')">
<NSelect <NSelect
v-model:value="themeStore.layout.scrollMode" v-model:value="themeStore.layout.scrollMode"

View File

@@ -113,7 +113,7 @@ const local: App.I18n.Schema = {
}, },
tab: { tab: {
visible: 'Tab Visible', visible: 'Tab Visible',
cache: 'Tag Bar Info Cache', cache: 'Tab Cache',
height: 'Tab Height', height: 'Tab Height',
mode: { mode: {
title: 'Tab Mode', title: 'Tab Mode',
@@ -141,11 +141,6 @@ const local: App.I18n.Schema = {
}, },
themeDrawerTitle: 'Theme Configuration', themeDrawerTitle: 'Theme Configuration',
pageFunTitle: 'Page Function', pageFunTitle: 'Page Function',
resetCacheStrategy: {
title: 'Reset Cache Strategy',
close: 'Close Page',
refresh: 'Refresh Page'
},
configOperation: { configOperation: {
copyConfig: 'Copy Config', copyConfig: 'Copy Config',
copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"', copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"',
@@ -159,7 +154,60 @@ const local: App.I18n.Schema = {
404: 'Page Not Found', 404: 'Page Not Found',
500: 'Server Error', 500: 'Server Error',
'iframe-page': 'Iframe', 'iframe-page': 'Iframe',
home: 'Home' home: 'Home',
document: 'Document',
document_project: 'Project Document',
'document_project-link': 'Project Document(External Link)',
document_vue: 'Vue Document',
document_vite: 'Vite Document',
document_unocss: 'UnoCSS Document',
document_naive: 'Naive UI Document',
document_antd: 'Ant Design Vue Document',
'user-center': 'User Center',
about: 'About',
function: 'System Function',
function_tab: 'Tab',
'function_multi-tab': 'Multi Tab',
'function_hide-child': 'Hide Child',
'function_hide-child_one': 'Hide Child',
'function_hide-child_two': 'Two',
'function_hide-child_three': 'Three',
function_request: 'Request',
'function_toggle-auth': 'Toggle Auth',
'function_super-page': 'Super Admin Visible',
manage: 'System Manage',
manage_user: 'User Manage',
'manage_user-detail': 'User Detail',
manage_role: 'Role Manage',
manage_menu: 'Menu Manage',
'multi-menu': 'Multi Menu',
'multi-menu_first': 'Menu One',
'multi-menu_first_child': 'Menu One Child',
'multi-menu_second': 'Menu Two',
'multi-menu_second_child': 'Menu Two Child',
'multi-menu_second_child_home': 'Menu Two Child Home',
exception: 'Exception',
exception_403: '403',
exception_404: '404',
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: { page: {
login: { login: {
@@ -208,6 +256,19 @@ const local: App.I18n.Schema = {
title: 'Bind WeChat' title: 'Bind WeChat'
} }
}, },
about: {
title: 'About',
introduction: `SoybeanAdmin is an elegant and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite5, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. SoybeanAdmin provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.`,
projectInfo: {
title: 'Project Info',
version: 'Version',
latestBuildTime: 'Latest Build Time',
githubLink: 'Github Link',
previewLink: 'Preview Link'
},
prdDep: 'Production Dependency',
devDep: 'Development Dependency'
},
home: { home: {
branchDesc: 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.', '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.',
@@ -236,6 +297,163 @@ const local: App.I18n.Schema = {
desc5: 'Soybean just wrote some of the workbench pages casually, and it was enough to see!' desc5: 'Soybean just wrote some of the workbench pages casually, and it was enough to see!'
}, },
creativity: 'Creativity' creativity: 'Creativity'
},
function: {
tab: {
tabOperate: {
title: 'Tab Operation',
addTab: 'Add Tab',
addTabDesc: 'To about page',
closeTab: 'Close Tab',
closeCurrentTab: 'Close Current Tab',
closeAboutTab: 'Close "About" Tab',
addMultiTab: 'Add Multi Tab',
addMultiTabDesc1: 'To MultiTab page',
addMultiTabDesc2: 'To MultiTab page(with query params)'
},
tabTitle: {
title: 'Tab Title',
changeTitle: 'Change Title',
change: 'Change',
resetTitle: 'Reset Title',
reset: 'Reset'
}
},
multiTab: {
routeParam: 'Route Param',
backTab: 'Back function_tab'
},
toggleAuth: {
toggleAccount: 'Toggle Account',
authHook: 'Auth Hook Function `hasAuth`',
superAdminVisible: 'Super Admin Visible',
adminVisible: 'Admin Visible',
adminOrUserVisible: 'Admin and User Visible'
},
request: {
repeatedErrorOccurOnce: 'Repeated Request Error Occurs Once',
repeatedError: 'Repeated Request Error',
repeatedErrorMsg1: 'Custom Request Error 1',
repeatedErrorMsg2: 'Custom Request Error 2'
}
},
manage: {
common: {
status: {
enable: 'Enable',
disable: 'Disable'
}
},
role: {
title: 'Role List',
roleName: 'Role Name',
roleCode: 'Role Code',
roleStatus: 'Role Status',
roleDesc: 'Role Description',
menuAuth: 'Menu Auth',
buttonAuth: 'Button Auth',
form: {
roleName: 'Please enter role name',
roleCode: 'Please enter role code',
roleStatus: 'Please select role status',
roleDesc: 'Please enter role description'
},
addRole: 'Add Role',
editRole: 'Edit Role'
},
user: {
title: 'User List',
userName: 'User Name',
userGender: 'Gender',
nickName: 'Nick Name',
userPhone: 'Phone Number',
userEmail: 'Email',
userStatus: 'User Status',
userRole: 'User Role',
form: {
userName: 'Please enter user name',
userGender: 'Please select gender',
nickName: 'Please enter nick name',
userPhone: 'Please enter phone number',
userEmail: 'Please enter email',
userStatus: 'Please select user status',
userRole: 'Please select user role'
},
addUser: 'Add User',
editUser: 'Edit User',
gender: {
male: 'Male',
female: 'Female'
}
},
menu: {
home: 'Home',
title: 'Menu List',
id: 'ID',
parentId: 'Parent ID',
menuType: 'Menu Type',
menuName: 'Menu Name',
routeName: 'Route Name',
routePath: 'Route Path',
pathParam: 'Path Param',
layout: 'Layout Component',
page: 'Page Component',
i18nKey: 'I18n Key',
icon: 'Icon',
localIcon: 'Local Icon',
iconTypeTitle: 'Icon Type',
order: 'Order',
constant: 'Constant',
keepAlive: 'Keep Alive',
href: 'Href',
hideInMenu: 'Hide In Menu',
activeMenu: 'Active Menu',
multiTab: 'Multi Tab',
fixedIndexInTab: 'Fixed Index In Tab',
query: 'Query Params',
button: 'Button',
buttonCode: 'Button Code',
buttonDesc: 'Button Desc',
menuStatus: 'Menu Status',
form: {
home: 'Please select home',
menuType: 'Please select menu type',
menuName: 'Please enter menu name',
routeName: 'Please enter route name',
routePath: 'Please enter route path',
pathParam: 'Please enter path param',
page: 'Please select page component',
layout: 'Please select layout component',
i18nKey: 'Please enter i18n key',
icon: 'Please enter iconify name',
localIcon: 'Please enter local icon name',
order: 'Please enter order',
keepAlive: 'Please select whether to cache route',
href: 'Please enter href',
hideInMenu: 'Please select whether to hide menu',
activeMenu: 'Please select route name of the highlighted menu',
multiTab: 'Please select whether to support multiple tabs',
fixedInTab: 'Please select whether to fix in the tab',
fixedIndexInTab: 'Please enter the index fixed in the tab',
queryKey: 'Please enter route parameter Key',
queryValue: 'Please enter route parameter Value',
button: 'Please select whether it is a button',
buttonCode: 'Please enter button code',
buttonDesc: 'Please enter button description',
menuStatus: 'Please select menu status'
},
addMenu: 'Add Menu',
editMenu: 'Edit Menu',
addChildMenu: 'Add Child Menu',
type: {
directory: 'Directory',
menu: 'Menu'
},
iconType: {
iconify: 'Iconify Icon',
local: 'Local Icon'
}
}
} }
}, },
form: { form: {

View File

@@ -113,7 +113,7 @@ const local: App.I18n.Schema = {
}, },
tab: { tab: {
visible: '显示标签栏', visible: '显示标签栏',
cache: '标签栏信息缓存', cache: '缓存标签页',
height: '标签栏高度', height: '标签栏高度',
mode: { mode: {
title: '标签栏风格', title: '标签栏风格',
@@ -141,11 +141,6 @@ const local: App.I18n.Schema = {
}, },
themeDrawerTitle: '主题配置', themeDrawerTitle: '主题配置',
pageFunTitle: '页面功能', pageFunTitle: '页面功能',
resetCacheStrategy: {
title: '重置缓存策略',
close: '关闭页面',
refresh: '刷新页面'
},
configOperation: { configOperation: {
copyConfig: '复制配置', copyConfig: '复制配置',
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings', copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
@@ -159,7 +154,60 @@ const local: App.I18n.Schema = {
404: '页面不存在', 404: '页面不存在',
500: '服务器错误', 500: '服务器错误',
'iframe-page': '外链页面', 'iframe-page': '外链页面',
home: '首页' home: '首页',
document: '文档',
document_project: '项目文档',
'document_project-link': '项目文档(外链)',
document_vue: 'Vue文档',
document_vite: 'Vite文档',
document_unocss: 'UnoCSS文档',
document_naive: 'Naive UI文档',
document_antd: 'Ant Design Vue文档',
'user-center': '个人中心',
about: '关于',
function: '系统功能',
function_tab: '标签页',
'function_multi-tab': '多标签页',
'function_hide-child': '隐藏子菜单',
'function_hide-child_one': '隐藏子菜单',
'function_hide-child_two': '菜单二',
'function_hide-child_three': '菜单三',
function_request: '请求',
'function_toggle-auth': '切换权限',
'function_super-page': '超级管理员可见',
manage: '系统管理',
manage_user: '用户管理',
'manage_user-detail': '用户详情',
manage_role: '角色管理',
manage_menu: '菜单管理',
'multi-menu': '多级菜单',
'multi-menu_first': '菜单一',
'multi-menu_first_child': '菜单一子菜单',
'multi-menu_second': '菜单二',
'multi-menu_second_child': '菜单二子菜单',
'multi-menu_second_child_home': '菜单二子菜单首页',
exception: '异常页',
exception_403: '403',
exception_404: '404',
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: { page: {
login: { login: {
@@ -208,6 +256,19 @@ const local: App.I18n.Schema = {
title: '绑定微信' title: '绑定微信'
} }
}, },
about: {
title: '关于',
introduction: `SoybeanAdmin 是一个优雅且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件代码规范严谨实现了自动化的文件路由系统。此外它还采用了基于 ApiFox 的在线Mock数据方案。SoybeanAdmin 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。`,
projectInfo: {
title: '项目信息',
version: '版本',
latestBuildTime: '最新构建时间',
githubLink: 'Github 地址',
previewLink: '预览地址'
},
prdDep: '生产依赖',
devDep: '开发依赖'
},
home: { home: {
branchDesc: branchDesc:
'为了方便大家开发和更新合并我们对main分支的代码进行了精简只保留了首页菜单其余内容已移至example分支进行维护。预览地址显示的内容即为example分支的内容。', '为了方便大家开发和更新合并我们对main分支的代码进行了精简只保留了首页菜单其余内容已移至example分支进行维护。预览地址显示的内容即为example分支的内容。',
@@ -236,6 +297,163 @@ const local: App.I18n.Schema = {
desc5: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!' desc5: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!'
}, },
creativity: '创意' creativity: '创意'
},
function: {
tab: {
tabOperate: {
title: '标签页操作',
addTab: '添加标签页',
addTabDesc: '跳转到关于页面',
closeTab: '关闭标签页',
closeCurrentTab: '关闭当前标签页',
closeAboutTab: '关闭"关于"标签页',
addMultiTab: '添加多标签页',
addMultiTabDesc1: '跳转到多标签页页面',
addMultiTabDesc2: '跳转到多标签页页面(带有查询参数)'
},
tabTitle: {
title: '标签页标题',
changeTitle: '修改标题',
change: '修改',
resetTitle: '重置标题',
reset: '重置'
}
},
multiTab: {
routeParam: '路由参数',
backTab: '返回 function_tab'
},
toggleAuth: {
toggleAccount: '切换账号',
authHook: '权限钩子函数 `hasAuth`',
superAdminVisible: '超级管理员可见',
adminVisible: '管理员可见',
adminOrUserVisible: '管理员和用户可见'
},
request: {
repeatedErrorOccurOnce: '重复请求错误只出现一次',
repeatedError: '重复请求错误',
repeatedErrorMsg1: '自定义请求错误 1',
repeatedErrorMsg2: '自定义请求错误 2'
}
},
manage: {
common: {
status: {
enable: '启用',
disable: '禁用'
}
},
role: {
title: '角色列表',
roleName: '角色名称',
roleCode: '角色编码',
roleStatus: '角色状态',
roleDesc: '角色描述',
menuAuth: '菜单权限',
buttonAuth: '按钮权限',
form: {
roleName: '请输入角色名称',
roleCode: '请输入角色编码',
roleStatus: '请选择角色状态',
roleDesc: '请输入角色描述'
},
addRole: '新增角色',
editRole: '编辑角色'
},
user: {
title: '用户列表',
userName: '用户名',
userGender: '性别',
nickName: '昵称',
userPhone: '手机号',
userEmail: '邮箱',
userStatus: '用户状态',
userRole: '用户角色',
form: {
userName: '请输入用户名',
userGender: '请选择性别',
nickName: '请输入昵称',
userPhone: '请输入手机号',
userEmail: '请输入邮箱',
userStatus: '请选择用户状态',
userRole: '请选择用户角色'
},
addUser: '新增用户',
editUser: '编辑用户',
gender: {
male: '男',
female: '女'
}
},
menu: {
home: '首页',
title: '菜单列表',
id: 'ID',
parentId: '父级菜单ID',
menuType: '菜单类型',
menuName: '菜单名称',
routeName: '路由名称',
routePath: '路由路径',
pathParam: '路径参数',
layout: '布局',
page: '页面组件',
i18nKey: '国际化key',
icon: '图标',
localIcon: '本地图标',
iconTypeTitle: '图标类型',
order: '排序',
constant: '常量路由',
keepAlive: '缓存路由',
href: '外链',
hideInMenu: '隐藏菜单',
activeMenu: '高亮的菜单',
multiTab: '支持多页签',
fixedIndexInTab: '固定在页签中的序号',
query: '路由参数',
button: '按钮',
buttonCode: '按钮编码',
buttonDesc: '按钮描述',
menuStatus: '菜单状态',
form: {
home: '请选择首页',
menuType: '请选择菜单类型',
menuName: '请输入菜单名称',
routeName: '请输入路由名称',
routePath: '请输入路由路径',
pathParam: '请输入路径参数',
page: '请选择页面组件',
layout: '请选择布局组件',
i18nKey: '请输入国际化key',
icon: '请输入图标',
localIcon: '请选择本地图标',
order: '请输入排序',
keepAlive: '请选择是否缓存路由',
href: '请输入外链',
hideInMenu: '请选择是否隐藏菜单',
activeMenu: '请选择高亮的菜单的路由名称',
multiTab: '请选择是否支持多标签',
fixedInTab: '请选择是否固定在页签中',
fixedIndexInTab: '请输入固定在页签中的序号',
queryKey: '请输入路由参数Key',
queryValue: '请输入路由参数Value',
button: '请选择是否按钮',
buttonCode: '请输入按钮编码',
buttonDesc: '请输入按钮描述',
menuStatus: '请选择菜单状态'
},
addMenu: '新增菜单',
editMenu: '编辑菜单',
addChildMenu: '新增子菜单',
type: {
directory: '目录',
menu: '菜单'
},
iconType: {
iconify: 'iconify图标',
local: '本地图标'
}
}
} }
}, },
form: { form: {

View File

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

View File

@@ -20,5 +20,36 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
500: () => import("@/views/_builtin/500/index.vue"), 500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"), "iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"), login: () => import("@/views/_builtin/login/index.vue"),
about: () => import("@/views/about/index.vue"),
"function_hide-child_one": () => import("@/views/function/hide-child/one/index.vue"),
"function_hide-child_three": () => import("@/views/function/hide-child/three/index.vue"),
"function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"),
"function_multi-tab": () => import("@/views/function/multi-tab/index.vue"),
function_request: () => import("@/views/function/request/index.vue"),
"function_super-page": () => import("@/views/function/super-page/index.vue"),
function_tab: () => import("@/views/function/tab/index.vue"),
"function_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
home: () => import("@/views/home/index.vue"), home: () => import("@/views/home/index.vue"),
manage_menu: () => import("@/views/manage/menu/index.vue"),
manage_role: () => import("@/views/manage/role/index.vue"),
"manage_user-detail": () => import("@/views/manage/user-detail/[id].vue"),
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

@@ -39,6 +39,135 @@ export const generatedRoutes: GeneratedRoute[] = [
hideInMenu: true hideInMenu: true
} }
}, },
{
name: 'about',
path: '/about',
component: 'layout.base$view.about',
meta: {
title: 'about',
i18nKey: 'route.about',
icon: 'fluent:book-information-24-regular',
order: 10
}
},
{
name: 'function',
path: '/function',
component: 'layout.base',
meta: {
title: 'function',
i18nKey: 'route.function',
icon: 'icon-park-outline:all-application',
order: 6
},
children: [
{
name: 'function_hide-child',
path: '/function/hide-child',
meta: {
title: 'function_hide-child',
i18nKey: 'route.function_hide-child',
icon: 'material-symbols:filter-list-off',
order: 2
},
redirect: '/function/hide-child/one',
children: [
{
name: 'function_hide-child_one',
path: '/function/hide-child/one',
component: 'view.function_hide-child_one',
meta: {
title: 'function_hide-child_one',
i18nKey: 'route.function_hide-child_one',
icon: 'material-symbols:filter-list-off',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
},
{
name: 'function_hide-child_three',
path: '/function/hide-child/three',
component: 'view.function_hide-child_three',
meta: {
title: 'function_hide-child_three',
i18nKey: 'route.function_hide-child_three',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
},
{
name: 'function_hide-child_two',
path: '/function/hide-child/two',
component: 'view.function_hide-child_two',
meta: {
title: 'function_hide-child_two',
i18nKey: 'route.function_hide-child_two',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
}
]
},
{
name: 'function_multi-tab',
path: '/function/multi-tab',
component: 'view.function_multi-tab',
meta: {
title: 'function_multi-tab',
i18nKey: 'route.function_multi-tab',
icon: 'ic:round-tab',
multiTab: true,
hideInMenu: true,
activeMenu: 'function_tab'
}
},
{
name: 'function_request',
path: '/function/request',
component: 'view.function_request',
meta: {
title: 'function_request',
i18nKey: 'route.function_request',
icon: 'carbon:network-overlay',
order: 3
}
},
{
name: 'function_super-page',
path: '/function/super-page',
component: 'view.function_super-page',
meta: {
title: 'function_super-page',
i18nKey: 'route.function_super-page',
icon: 'ic:round-supervisor-account',
order: 5,
roles: ['R_SUPER']
}
},
{
name: 'function_tab',
path: '/function/tab',
component: 'view.function_tab',
meta: {
title: 'function_tab',
i18nKey: 'route.function_tab',
icon: 'ic:round-tab',
order: 1
}
},
{
name: 'function_toggle-auth',
path: '/function/toggle-auth',
component: 'view.function_toggle-auth',
meta: {
title: 'function_toggle-auth',
i18nKey: 'route.function_toggle-auth',
icon: 'ic:round-construction',
order: 4
}
}
]
},
{ {
name: 'home', name: 'home',
path: '/home', path: '/home',
@@ -74,5 +203,327 @@ export const generatedRoutes: GeneratedRoute[] = [
constant: true, constant: true,
hideInMenu: true hideInMenu: true
} }
},
{
name: 'manage',
path: '/manage',
component: 'layout.base',
meta: {
title: 'manage',
i18nKey: 'route.manage',
icon: 'carbon:cloud-service-management',
order: 9,
roles: ['R_ADMIN']
},
children: [
{
name: 'manage_menu',
path: '/manage/menu',
component: 'view.manage_menu',
meta: {
title: 'manage_menu',
i18nKey: 'route.manage_menu',
icon: 'material-symbols:route',
order: 3,
roles: ['R_ADMIN'],
keepAlive: true
}
},
{
name: 'manage_role',
path: '/manage/role',
component: 'view.manage_role',
meta: {
title: 'manage_role',
i18nKey: 'route.manage_role',
icon: 'carbon:user-role',
order: 2,
roles: ['R_SUPER']
}
},
{
name: 'manage_user',
path: '/manage/user',
component: 'view.manage_user',
meta: {
title: 'manage_user',
i18nKey: 'route.manage_user',
icon: 'ic:round-manage-accounts',
order: 1,
roles: ['R_ADMIN']
}
},
{
name: 'manage_user-detail',
path: '/manage/user-detail/:id',
component: 'view.manage_user-detail',
props: true,
meta: {
title: 'manage_user-detail',
i18nKey: 'route.manage_user-detail',
hideInMenu: true,
roles: ['R_ADMIN'],
activeMenu: 'manage_user'
}
}
]
},
{
name: 'multi-menu',
path: '/multi-menu',
component: 'layout.base',
meta: {
title: 'multi-menu',
i18nKey: 'route.multi-menu',
order: 8
},
children: [
{
name: 'multi-menu_first',
path: '/multi-menu/first',
meta: {
title: 'multi-menu_first',
i18nKey: 'route.multi-menu_first',
order: 1
},
children: [
{
name: 'multi-menu_first_child',
path: '/multi-menu/first/child',
component: 'view.multi-menu_first_child',
meta: {
title: 'multi-menu_first_child',
i18nKey: 'route.multi-menu_first_child'
}
}
]
},
{
name: 'multi-menu_second',
path: '/multi-menu/second',
meta: {
title: 'multi-menu_second',
i18nKey: 'route.multi-menu_second',
order: 2
},
children: [
{
name: 'multi-menu_second_child',
path: '/multi-menu/second/child',
meta: {
title: 'multi-menu_second_child',
i18nKey: 'route.multi-menu_second_child'
},
children: [
{
name: 'multi-menu_second_child_home',
path: '/multi-menu/second/child/home',
component: 'view.multi-menu_second_child_home',
meta: {
title: 'multi-menu_second_child_home',
i18nKey: 'route.multi-menu_second_child_home'
}
}
]
}
]
}
]
},
{
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',
component: 'layout.base$view.user-center',
meta: {
title: 'user-center',
i18nKey: 'route.user-center',
hideInMenu: true
}
} }
]; ];

View File

@@ -163,12 +163,65 @@ function transformElegantRouteToVueRoute(
const routeMap: RouteMap = { const routeMap: RouteMap = {
"root": "/", "root": "/",
"not-found": "/:pathMatch(.*)*", "not-found": "/:pathMatch(.*)*",
"exception": "/exception",
"exception_403": "/exception/403",
"exception_404": "/exception/404",
"exception_500": "/exception/500",
"document": "/document",
"document_project": "/document/project",
"document_project-link": "/document/project-link",
"document_vue": "/document/vue",
"document_vite": "/document/vite",
"document_unocss": "/document/unocss",
"document_naive": "/document/naive",
"document_antd": "/document/antd",
"403": "/403", "403": "/403",
"404": "/404", "404": "/404",
"500": "/500", "500": "/500",
"about": "/about",
"function": "/function",
"function_hide-child": "/function/hide-child",
"function_hide-child_one": "/function/hide-child/one",
"function_hide-child_three": "/function/hide-child/three",
"function_hide-child_two": "/function/hide-child/two",
"function_multi-tab": "/function/multi-tab",
"function_request": "/function/request",
"function_super-page": "/function/super-page",
"function_tab": "/function/tab",
"function_toggle-auth": "/function/toggle-auth",
"home": "/home", "home": "/home",
"iframe-page": "/iframe-page/:url", "iframe-page": "/iframe-page/:url",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?" "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"manage": "/manage",
"manage_menu": "/manage/menu",
"manage_role": "/manage/role",
"manage_user": "/manage/user",
"manage_user-detail": "/manage/user-detail/:id",
"multi-menu": "/multi-menu",
"multi-menu_first": "/multi-menu/first",
"multi-menu_first_child": "/multi-menu/first/child",
"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

@@ -8,7 +8,160 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
* *
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route * @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/ */
const customRoutes: CustomRoute[] = []; const customRoutes: CustomRoute[] = [
{
name: 'exception',
path: '/exception',
component: 'layout.base',
meta: {
title: 'exception',
i18nKey: 'route.exception',
icon: 'ant-design:exception-outlined',
order: 7
},
children: [
{
name: 'exception_403',
path: '/exception/403',
component: 'view.403',
meta: {
title: 'exception_403',
i18nKey: 'route.exception_403',
icon: 'ic:baseline-block'
}
},
{
name: 'exception_404',
path: '/exception/404',
component: 'view.404',
meta: {
title: 'exception_404',
i18nKey: 'route.exception_404',
icon: 'ic:baseline-web-asset-off'
}
},
{
name: 'exception_500',
path: '/exception/500',
component: 'view.500',
meta: {
title: 'exception_500',
i18nKey: 'route.exception_500',
icon: 'ic:baseline-wifi-off'
}
}
]
},
{
name: 'document',
path: '/document',
component: 'layout.base',
meta: {
title: 'document',
i18nKey: 'route.document',
order: 2,
icon: 'mdi:file-document-multiple-outline'
},
children: [
{
name: 'document_antd',
path: '/document/antd',
component: 'view.iframe-page',
props: {
url: 'https://antdv.com/components/overview-cn'
},
meta: {
title: 'document_antd',
i18nKey: 'route.document_antd',
order: 7,
icon: 'logos:ant-design'
}
},
{
name: 'document_naive',
path: '/document/naive',
component: 'view.iframe-page',
props: {
url: 'https://www.naiveui.com/zh-CN/os-theme/docs/introduction'
},
meta: {
title: 'document_naive',
i18nKey: 'route.document_naive',
order: 6,
icon: 'logos:naiveui'
}
},
{
name: 'document_project',
path: '/document/project',
component: 'view.iframe-page',
props: {
url: 'https://docs.soybeanjs.cn/zh'
},
meta: {
title: 'document_project',
i18nKey: 'route.document_project',
order: 1,
localIcon: 'logo'
}
},
{
name: 'document_project-link',
path: '/document/project-link',
component: 'view.iframe-page',
meta: {
title: 'document_project-link',
i18nKey: 'route.document_project-link',
order: 2,
localIcon: 'logo',
href: 'https://docs.soybeanjs.cn/zh'
}
},
{
name: 'document_unocss',
path: '/document/unocss',
component: 'view.iframe-page',
props: {
url: 'https://unocss.dev/'
},
meta: {
title: 'document_unocss',
i18nKey: 'route.document_unocss',
order: 5,
icon: 'logos:unocss'
}
},
{
name: 'document_vite',
path: '/document/vite',
component: 'view.iframe-page',
props: {
url: 'https://cn.vitejs.dev/'
},
meta: {
title: 'document_vite',
i18nKey: 'route.document_vite',
order: 4,
icon: 'logos:vitejs'
}
},
{
name: 'document_vue',
path: '/document/vue',
component: 'view.iframe-page',
props: {
url: 'https://cn.vuejs.org/'
},
meta: {
title: 'document_vue',
i18nKey: 'route.document_vue',
order: 3,
icon: 'logos:vue'
}
}
]
}
];
/** create routes when the auth route mode is static */ /** create routes when the auth route mode is static */
export function createStaticRoutes() { export function createStaticRoutes() {

View File

@@ -1,2 +1,3 @@
export * from './auth'; export * from './auth';
export * from './route'; export * from './route';
export * from './system-manage';

View File

@@ -0,0 +1,55 @@
import { request } from '../request';
/** get role list */
export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
return request<Api.SystemManage.RoleList>({
url: '/systemManage/getRoleList',
method: 'get',
params
});
}
/**
* get all roles
*
* these roles are all enabled
*/
export function fetchGetAllRoles() {
return request<Api.SystemManage.AllRole[]>({
url: '/systemManage/getAllRoles',
method: 'get'
});
}
/** get user list */
export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) {
return request<Api.SystemManage.UserList>({
url: '/systemManage/getUserList',
method: 'get',
params
});
}
/** get menu list */
export function fetchGetMenuList() {
return request<Api.SystemManage.MenuList>({
url: '/systemManage/getMenuList/v2',
method: 'get'
});
}
/** get all pages */
export function fetchGetAllPages() {
return request<string[]>({
url: '/systemManage/getAllPages',
method: 'get'
});
}
/** get menu tree */
export function fetchGetMenuTree() {
return request<Api.SystemManage.MenuTree[]>({
url: '/systemManage/getMenuTree',
method: 'get'
});
}

View File

@@ -46,10 +46,6 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
}); });
setReloadFlag(true); setReloadFlag(true);
if (themeStore.resetCacheStrategy === 'refresh') {
routeStore.resetRouteCache();
}
} }
const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'zh-CN'); const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'zh-CN');

View File

@@ -1,4 +1,4 @@
import { computed, nextTick, ref, shallowRef } from 'vue'; import { computed, ref, shallowRef } from 'vue';
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useBoolean } from '@sa/hooks'; import { useBoolean } from '@sa/hooks';
@@ -9,6 +9,7 @@ import { createStaticRoutes, getAuthVueRoutes } from '@/router/routes';
import { ROOT_ROUTE } from '@/router/routes/builtin'; import { ROOT_ROUTE } from '@/router/routes/builtin';
import { getRouteName, getRoutePath } from '@/router/elegant/transform'; import { getRouteName, getRoutePath } from '@/router/elegant/transform';
import { fetchGetConstantRoutes, fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api'; import { fetchGetConstantRoutes, fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api';
import { useAppStore } from '../app';
import { useAuthStore } from '../auth'; import { useAuthStore } from '../auth';
import { useTabStore } from '../tab'; import { useTabStore } from '../tab';
import { import {
@@ -24,6 +25,7 @@ import {
} from './shared'; } from './shared';
export const useRouteStore = defineStore(SetupStoreId.Route, () => { export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const appStore = useAppStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const tabStore = useTabStore(); const tabStore = useTabStore();
const { bool: isInitConstantRoute, setBool: setIsInitConstantRoute } = useBoolean(); const { bool: isInitConstantRoute, setBool: setIsInitConstantRoute } = useBoolean();
@@ -95,12 +97,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
/** Cache routes */ /** Cache routes */
const cacheRoutes = ref<RouteKey[]>([]); const cacheRoutes = ref<RouteKey[]>([]);
/** /** All cache routes */
* Exclude cache routes const allCacheRoutes = shallowRef<RouteKey[]>([]);
*
* for reset route cache
*/
const excludeCacheRoutes = ref<RouteKey[]>([]);
/** /**
* Get cache routes * Get cache routes
@@ -108,23 +106,69 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
* @param routes Vue routes * @param routes Vue routes
*/ */
function getCacheRoutes(routes: RouteRecordRaw[]) { function getCacheRoutes(routes: RouteRecordRaw[]) {
cacheRoutes.value = getCacheRouteNames(routes); const alls = getCacheRouteNames(routes);
cacheRoutes.value = alls;
allCacheRoutes.value = [...alls];
} }
/** /**
* Reset route cache * Add cache routes
* *
* @default router.currentRoute.value.name current route name
* @param routeKey * @param routeKey
*/ */
async function resetRouteCache(routeKey?: RouteKey) { function addCacheRoutes(routeKey: RouteKey) {
const routeName = routeKey || (router.currentRoute.value.name as RouteKey); if (cacheRoutes.value.includes(routeKey)) return;
excludeCacheRoutes.value.push(routeName); cacheRoutes.value.push(routeKey);
}
await nextTick(); /**
* Remove cache routes
*
* @param routeKey
*/
function removeCacheRoutes(routeKey: RouteKey) {
const index = cacheRoutes.value.findIndex(item => item === routeKey);
excludeCacheRoutes.value = []; if (index === -1) return;
cacheRoutes.value.splice(index, 1);
}
/**
* Is cached route
*
* @param routeKey
*/
function isCachedRoute(routeKey: RouteKey) {
return allCacheRoutes.value.includes(routeKey);
}
/**
* Re cache routes by route key
*
* @param routeKey
*/
async function reCacheRoutesByKey(routeKey: RouteKey) {
if (!isCachedRoute(routeKey)) return;
removeCacheRoutes(routeKey);
await appStore.reloadPage();
addCacheRoutes(routeKey);
}
/**
* Re cache routes by route keys
*
* @param routeKeys
*/
async function reCacheRoutesByKeys(routeKeys: RouteKey[]) {
for await (const key of routeKeys) {
await reCacheRoutesByKey(key);
}
} }
/** Global breadcrumbs */ /** Global breadcrumbs */
@@ -317,8 +361,8 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
searchMenus, searchMenus,
updateGlobalMenusByLocale, updateGlobalMenusByLocale,
cacheRoutes, cacheRoutes,
excludeCacheRoutes, reCacheRoutesByKey,
resetRouteCache, reCacheRoutesByKeys,
breadcrumbs, breadcrumbs,
initConstantRoute, initConstantRoute,
isInitConstantRoute, isInitConstantRoute,

View File

@@ -19,7 +19,7 @@ export function filterAuthRoutesByRoles(routes: ElegantConstRoute[], roles: stri
* @param route Auth route * @param route Auth route
* @param roles Roles * @param roles Roles
*/ */
function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]): ElegantConstRoute[] { function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]) {
const routeRoles = (route.meta && route.meta.roles) || []; const routeRoles = (route.meta && route.meta.roles) || [];
// if the route's "roles" is empty, then it is allowed to access // if the route's "roles" is empty, then it is allowed to access
@@ -34,11 +34,6 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]): Eleg
filterRoute.children = filterRoute.children.flatMap(item => filterAuthRouteByRoles(item, roles)); filterRoute.children = filterRoute.children.flatMap(item => filterAuthRouteByRoles(item, roles));
} }
// Exclude the route if it has no children after filtering
if (filterRoute.children?.length === 0) {
return [];
}
return hasPermission || isEmptyRoles ? [filterRoute] : []; return hasPermission || isEmptyRoles ? [filterRoute] : [];
} }
@@ -288,7 +283,8 @@ export function getBreadcrumbsByRoute(
for (const menu of menus) { for (const menu of menus) {
if (menu.key === key) { if (menu.key === key) {
return [transformMenuToBreadcrumb(menu)]; const breadcrumbMenu = menu;
return [transformMenuToBreadcrumb(breadcrumbMenu)];
} }
if (menu.key === activeKey) { if (menu.key === activeKey) {

View File

@@ -1 +1 @@
@forward 'scrollbar'; @import './scrollbar.scss';

View File

@@ -12,7 +12,6 @@ export const themeSettings: App.Theme.ThemeSetting = {
error: '#f5222d' error: '#f5222d'
}, },
isInfoFollowPrimary: true, isInfoFollowPrimary: true,
resetCacheStrategy: 'close',
layout: { layout: {
mode: 'vertical', mode: 'vertical',
scrollMode: 'content', scrollMode: 'content',
@@ -84,7 +83,6 @@ export const themeSettings: App.Theme.ThemeSetting = {
* If publish new version, use `overrideThemeSettings` to override certain theme settings * 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> = {
resetCacheStrategy: 'close',
watermark: { watermark: {
visible: false, visible: false,
text: 'SoybeanAdmin' text: 'SoybeanAdmin'

138
src/typings/api.d.ts vendored
View File

@@ -84,4 +84,142 @@ declare namespace Api {
home: import('@elegant-router/types').LastLevelRouteKey; home: import('@elegant-router/types').LastLevelRouteKey;
} }
} }
/**
* namespace SystemManage
*
* backend api module: "systemManage"
*/
namespace SystemManage {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
/** role */
type Role = Common.CommonRecord<{
/** role name */
roleName: string;
/** role code */
roleCode: string;
/** role description */
roleDesc: string;
}>;
/** role search params */
type RoleSearchParams = CommonType.RecordNullable<
Pick<Api.SystemManage.Role, 'roleName' | 'roleCode' | 'status'> & CommonSearchParams
>;
/** role list */
type RoleList = Common.PaginatingQueryRecord<Role>;
/** all role */
type AllRole = Pick<Role, 'id' | 'roleName' | 'roleCode'>;
/**
* user gender
*
* - "1": "male"
* - "2": "female"
*/
type UserGender = '1' | '2';
/** user */
type User = Common.CommonRecord<{
/** user name */
userName: string;
/** user gender */
userGender: UserGender | null;
/** user nick name */
nickName: string;
/** user phone */
userPhone: string;
/** user email */
userEmail: string;
/** user role code collection */
userRoles: string[];
}>;
/** user search params */
type UserSearchParams = CommonType.RecordNullable<
Pick<Api.SystemManage.User, 'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'status'> &
CommonSearchParams
>;
/** user list */
type UserList = Common.PaginatingQueryRecord<User>;
/**
* menu type
*
* - "1": directory
* - "2": menu
*/
type MenuType = '1' | '2';
type MenuButton = {
/**
* button code
*
* it can be used to control the button permission
*/
code: string;
/** button description */
desc: string;
};
/**
* icon type
*
* - "1": iconify icon
* - "2": local icon
*/
type IconType = '1' | '2';
type MenuPropsOfRoute = Pick<
import('vue-router').RouteMeta,
| 'i18nKey'
| 'keepAlive'
| 'constant'
| 'order'
| 'href'
| 'hideInMenu'
| 'activeMenu'
| 'multiTab'
| 'fixedIndexInTab'
| 'query'
>;
type Menu = Common.CommonRecord<{
/** parent menu id */
parentId: number;
/** menu type */
menuType: MenuType;
/** menu name */
menuName: string;
/** route name */
routeName: string;
/** route path */
routePath: string;
/** component */
component?: string;
/** iconify icon name or local icon name */
icon: string;
/** icon type */
iconType: IconType;
/** buttons */
buttons?: MenuButton[] | null;
/** children menu */
children?: Menu[] | null;
}> &
MenuPropsOfRoute;
/** menu list */
type MenuList = Common.PaginatingQueryRecord<Menu>;
type MenuTree = {
id: number;
label: string;
pId: number;
children?: MenuTree[];
};
}
} }

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

@@ -20,8 +20,6 @@ declare namespace App {
otherColor: OtherColor; otherColor: OtherColor;
/** Whether info color is followed by the primary color */ /** Whether info color is followed by the primary color */
isInfoFollowPrimary: boolean; isInfoFollowPrimary: boolean;
/** Reset cache strategy */
resetCacheStrategy?: UnionKey.ResetCacheStrategy;
/** Layout */ /** Layout */
layout: { layout: {
/** Layout mode */ /** Layout mode */
@@ -390,7 +388,6 @@ declare namespace App {
}; };
themeDrawerTitle: string; themeDrawerTitle: string;
pageFunTitle: string; pageFunTitle: string;
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
configOperation: { configOperation: {
copyConfig: string; copyConfig: string;
copySuccessMsg: string; copySuccessMsg: string;
@@ -446,6 +443,19 @@ declare namespace App {
title: string; title: string;
}; };
}; };
about: {
title: string;
introduction: string;
projectInfo: {
title: string;
version: string;
latestBuildTime: string;
githubLink: string;
previewLink: string;
};
prdDep: string;
devDep: string;
};
home: { home: {
branchDesc: string; branchDesc: string;
greeting: string; greeting: string;
@@ -474,6 +484,163 @@ declare namespace App {
}; };
creativity: string; creativity: string;
}; };
function: {
tab: {
tabOperate: {
title: string;
addTab: string;
addTabDesc: string;
closeTab: string;
closeCurrentTab: string;
closeAboutTab: string;
addMultiTab: string;
addMultiTabDesc1: string;
addMultiTabDesc2: string;
};
tabTitle: {
title: string;
changeTitle: string;
change: string;
resetTitle: string;
reset: string;
};
};
multiTab: {
routeParam: string;
backTab: string;
};
toggleAuth: {
toggleAccount: string;
authHook: string;
superAdminVisible: string;
adminVisible: string;
adminOrUserVisible: string;
};
request: {
repeatedErrorOccurOnce: string;
repeatedError: string;
repeatedErrorMsg1: string;
repeatedErrorMsg2: string;
};
};
manage: {
common: {
status: {
enable: string;
disable: string;
};
};
role: {
title: string;
roleName: string;
roleCode: string;
roleStatus: string;
roleDesc: string;
form: {
roleName: string;
roleCode: string;
roleStatus: string;
roleDesc: string;
};
addRole: string;
editRole: string;
menuAuth: string;
buttonAuth: string;
};
user: {
title: string;
userName: string;
userGender: string;
nickName: string;
userPhone: string;
userEmail: string;
userStatus: string;
userRole: string;
form: {
userName: string;
userGender: string;
nickName: string;
userPhone: string;
userEmail: string;
userStatus: string;
userRole: string;
};
addUser: string;
editUser: string;
gender: {
male: string;
female: string;
};
};
menu: {
home: string;
title: string;
id: string;
parentId: string;
menuType: string;
menuName: string;
routeName: string;
routePath: string;
pathParam: string;
layout: string;
page: string;
i18nKey: string;
icon: string;
localIcon: string;
iconTypeTitle: string;
order: string;
constant: string;
keepAlive: string;
href: string;
hideInMenu: string;
activeMenu: string;
multiTab: string;
fixedIndexInTab: string;
query: string;
button: string;
buttonCode: string;
buttonDesc: string;
menuStatus: string;
form: {
home: string;
menuType: string;
menuName: string;
routeName: string;
routePath: string;
pathParam: string;
layout: string;
page: string;
i18nKey: string;
icon: string;
localIcon: string;
order: string;
keepAlive: string;
href: string;
hideInMenu: string;
activeMenu: string;
multiTab: string;
fixedInTab: string;
fixedIndexInTab: string;
queryKey: string;
queryValue: string;
button: string;
buttonCode: string;
buttonDesc: string;
menuStatus: string;
};
addMenu: string;
editMenu: string;
addChildMenu: string;
type: {
directory: string;
menu: string;
};
iconType: {
iconify: string;
local: string;
};
};
};
}; };
form: { form: {
required: string; required: string;

View File

@@ -11,12 +11,16 @@ declare module 'vue' {
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default'] BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
CountTo: typeof import('./../components/custom/count-to.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'] DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default'] ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
FullScreen: typeof import('./../components/common/full-screen.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'] IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default'] IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-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'] IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['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'] IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default'] IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default'] IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalActivity: typeof import('~icons/local/activity')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default'] IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalCast: typeof import('~icons/local/cast')['default']
IconLocalLogo: typeof import('~icons/local/logo')['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'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default'] IconMdiDrag: typeof import('~icons/mdi/drag')['default']
@@ -37,12 +45,13 @@ declare module 'vue' {
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default'] LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default'] LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default'] MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb'] NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem'] NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard'] NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox'] NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker'] NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable'] NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions'] NDescriptions: typeof import('naive-ui')['NDescriptions']
@@ -69,16 +78,19 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal'] NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPagination: typeof import('naive-ui')['NPagination']
NPopconfirm: typeof import('naive-ui')['NPopconfirm'] NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover'] NPopover: typeof import('naive-ui')['NPopover']
NRadio: typeof import('naive-ui')['NRadio'] NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect'] NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
NStatistic: typeof import('naive-ui')['NStatistic'] NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab'] NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs'] NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag'] NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing'] NThing: typeof import('naive-ui')['NThing']
@@ -96,5 +108,6 @@ declare module 'vue' {
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
WebSiteLink: typeof import('./../components/custom/web-site-link.vue')['default']
} }
} }

View File

@@ -17,12 +17,65 @@ declare module "@elegant-router/types" {
export type RouteMap = { export type RouteMap = {
"root": "/"; "root": "/";
"not-found": "/:pathMatch(.*)*"; "not-found": "/:pathMatch(.*)*";
"exception": "/exception";
"exception_403": "/exception/403";
"exception_404": "/exception/404";
"exception_500": "/exception/500";
"document": "/document";
"document_project": "/document/project";
"document_project-link": "/document/project-link";
"document_vue": "/document/vue";
"document_vite": "/document/vite";
"document_unocss": "/document/unocss";
"document_naive": "/document/naive";
"document_antd": "/document/antd";
"403": "/403"; "403": "/403";
"404": "/404"; "404": "/404";
"500": "/500"; "500": "/500";
"about": "/about";
"function": "/function";
"function_hide-child": "/function/hide-child";
"function_hide-child_one": "/function/hide-child/one";
"function_hide-child_three": "/function/hide-child/three";
"function_hide-child_two": "/function/hide-child/two";
"function_multi-tab": "/function/multi-tab";
"function_request": "/function/request";
"function_super-page": "/function/super-page";
"function_tab": "/function/tab";
"function_toggle-auth": "/function/toggle-auth";
"home": "/home"; "home": "/home";
"iframe-page": "/iframe-page/:url"; "iframe-page": "/iframe-page/:url";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"manage": "/manage";
"manage_menu": "/manage/menu";
"manage_role": "/manage/role";
"manage_user": "/manage/user";
"manage_user-detail": "/manage/user-detail/:id";
"multi-menu": "/multi-menu";
"multi-menu_first": "/multi-menu/first";
"multi-menu_first_child": "/multi-menu/first/child";
"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";
}; };
/** /**
@@ -42,6 +95,18 @@ declare module "@elegant-router/types" {
RouteKey, RouteKey,
| "root" | "root"
| "not-found" | "not-found"
| "exception"
| "exception_403"
| "exception_404"
| "exception_500"
| "document"
| "document_project"
| "document_project-link"
| "document_vue"
| "document_vite"
| "document_unocss"
| "document_naive"
| "document_antd"
>; >;
/** /**
@@ -57,9 +122,15 @@ declare module "@elegant-router/types" {
| "403" | "403"
| "404" | "404"
| "500" | "500"
| "about"
| "function"
| "home" | "home"
| "iframe-page" | "iframe-page"
| "login" | "login"
| "manage"
| "multi-menu"
| "plugin"
| "user-center"
>; >;
/** /**
@@ -69,6 +140,8 @@ declare module "@elegant-router/types" {
CustomRouteKey, CustomRouteKey,
| "root" | "root"
| "not-found" | "not-found"
| "exception"
| "document"
>; >;
/** /**
@@ -81,7 +154,38 @@ declare module "@elegant-router/types" {
| "500" | "500"
| "iframe-page" | "iframe-page"
| "login" | "login"
| "about"
| "function_hide-child_one"
| "function_hide-child_three"
| "function_hide-child_two"
| "function_multi-tab"
| "function_request"
| "function_super-page"
| "function_tab"
| "function_toggle-auth"
| "home" | "home"
| "manage_menu"
| "manage_role"
| "manage_user-detail"
| "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"
>; >;
/** /**
@@ -91,6 +195,16 @@ declare module "@elegant-router/types" {
CustomRouteKey, CustomRouteKey,
| "root" | "root"
| "not-found" | "not-found"
| "exception_403"
| "exception_404"
| "exception_500"
| "document_project"
| "document_project-link"
| "document_vue"
| "document_vite"
| "document_unocss"
| "document_naive"
| "document_antd"
>; >;
/** /**

View File

@@ -14,6 +14,14 @@ declare global {
$notification?: import('naive-ui').NotificationProviderInst; $notification?: import('naive-ui').NotificationProviderInst;
} }
interface ViewTransition {
ready: Promise<void>;
}
export interface Document {
startViewTransition?: (callback: () => Promise<void> | void) => ViewTransition;
}
/** Build time of the project */ /** Build time of the project */
export const BUILD_TIME: string; export const BUILD_TIME: string;
} }

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

@@ -14,14 +14,6 @@ declare namespace UnionKey {
/** Theme scheme */ /** Theme scheme */
type ThemeScheme = 'light' | 'dark' | 'auto'; type ThemeScheme = 'light' | 'dark' | 'auto';
/**
* Reset cache strategy
*
* - close: re-cache when close page
* - refresh: re-cache when refresh page
*/
type ResetCacheStrategy = 'close' | 'refresh';
/** /**
* The layout mode * The layout mode
* *

View File

@@ -1,7 +0,0 @@
export function isPC() {
const agents = ['Android', 'iPhone', 'webOS', 'BlackBerry', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod'];
const isMobile = agents.some(agent => window.navigator.userAgent.includes(agent));
return !isMobile;
}

View File

@@ -11,7 +11,7 @@ export function createServiceConfig(env: Env.ImportMeta) {
let other = {} as Record<App.Service.OtherBaseURLKey, string>; let other = {} as Record<App.Service.OtherBaseURLKey, string>;
try { try {
other = json5.parse(VITE_OTHER_SERVICE_BASE_URL); other = json5.parse(VITE_OTHER_SERVICE_BASE_URL);
} catch { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('VITE_OTHER_SERVICE_BASE_URL is not a valid json5 string'); console.error('VITE_OTHER_SERVICE_BASE_URL is not a valid json5 string');
} }

View File

@@ -40,7 +40,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone"> <NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" /> <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem> </NFormItem>

View File

@@ -75,7 +75,7 @@ async function handleAccountLogin(account: Account) {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="userName"> <NFormItem path="userName">
<NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" /> <NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem> </NFormItem>

View File

@@ -46,7 +46,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone"> <NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" /> <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem> </NFormItem>

View File

@@ -45,7 +45,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone"> <NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" /> <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem> </NFormItem>

85
src/views/about/index.vue Normal file
View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import pkg from '~/package.json';
const appStore = useAppStore();
const column = computed(() => (appStore.isMobile ? 1 : 2));
interface PkgJson {
name: string;
version: string;
dependencies: PkgVersionInfo[];
devDependencies: PkgVersionInfo[];
}
interface PkgVersionInfo {
name: string;
version: string;
}
const { name, version, dependencies, devDependencies } = pkg;
function transformVersionData(tuple: [string, string]): PkgVersionInfo {
const [$name, $version] = tuple;
return {
name: $name,
version: $version
};
}
const pkgJson: PkgJson = {
name,
version,
dependencies: Object.entries(dependencies).map(item => transformVersionData(item)),
devDependencies: Object.entries(devDependencies).map(item => transformVersionData(item))
};
const latestBuildTime = BUILD_TIME;
</script>
<template>
<NSpace vertical :size="16">
<NCard :title="$t('page.about.title')" :bordered="false" size="small" segmented class="card-wrapper">
<p>{{ $t('page.about.introduction') }}</p>
</NCard>
<NCard :title="$t('page.about.projectInfo.title')" :bordered="false" size="small" segmented class="card-wrapper">
<NDescriptions label-placement="left" bordered size="small" :column="column">
<NDescriptionsItem :label="$t('page.about.projectInfo.version')">
<NTag type="primary">{{ pkgJson.version }}</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.about.projectInfo.latestBuildTime')">
<NTag type="primary">{{ latestBuildTime }}</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.about.projectInfo.githubLink')">
<a class="text-primary" :href="pkg.homepage" target="_blank" rel="noopener noreferrer">
{{ $t('page.about.projectInfo.githubLink') }}
</a>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.about.projectInfo.previewLink')">
<a class="text-primary" :href="pkg.website" target="_blank" rel="noopener noreferrer">
{{ $t('page.about.projectInfo.previewLink') }}
</a>
</NDescriptionsItem>
</NDescriptions>
</NCard>
<NCard :title="$t('page.about.prdDep')" :bordered="false" size="small" segmented class="card-wrapper">
<NDescriptions label-placement="left" bordered size="small" :column="column">
<NDescriptionsItem v-for="item in pkgJson.dependencies" :key="item.name" :label="item.name">
{{ item.version }}
</NDescriptionsItem>
</NDescriptions>
</NCard>
<NCard :title="$t('page.about.devDep')" :bordered="false" size="small" segmented class="card-wrapper">
<NDescriptions label-placement="left" bordered size="small" :column="column">
<NDescriptionsItem v-for="item in pkgJson.devDependencies" :key="item.name" :label="item.name">
{{ item.version }}
</NDescriptionsItem>
</NDescriptions>
</NCard>
</NSpace>
</template>
<style scoped></style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
const route = useRoute();
const { routerPushByKey } = useRouterPush();
const routeQuery = computed(() => JSON.stringify(route.query));
</script>
<template>
<div>
<LookForward>
<div>
<NButton @click="routerPushByKey('function_tab')">{{ $t('page.function.multiTab.backTab') }}</NButton>
<div class="py-24px">{{ $t('page.function.multiTab.routeParam') }}: {{ routeQuery }}</div>
</div>
</LookForward>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { $t } from '@/locales';
import { fetchCustomBackendError } from '@/service/api';
async function logout() {
await fetchCustomBackendError('8888', $t('request.logoutMsg'));
}
async function logoutWithModal() {
await fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'));
}
async function refreshToken() {
await fetchCustomBackendError('9999', $t('request.tokenExpired'));
}
async function handleRepeatedMessageError() {
await Promise.all([
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2'))
]);
}
async function handleRepeatedModalError() {
await Promise.all([
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'))
]);
}
</script>
<template>
<NSpace vertical :size="16">
<NCard :title="$t('request.logout')" :bordered="false" size="small" segmented class="card-wrapper">
<NButton @click="logout">{{ $t('common.trigger') }}</NButton>
</NCard>
<NCard :title="$t('request.logoutWithModal')" :bordered="false" size="small" segmented class="card-wrapper">
<NButton @click="logoutWithModal">{{ $t('common.trigger') }}</NButton>
</NCard>
<NCard :title="$t('request.refreshToken')" :bordered="false" size="small" segmented class="card-wrapper">
<NButton @click="refreshToken">{{ $t('common.trigger') }}</NButton>
</NCard>
<NCard
:title="$t('page.function.request.repeatedErrorOccurOnce')"
:bordered="false"
size="small"
segmented
class="card-wrapper"
>
<NButton @click="handleRepeatedMessageError">{{ $t('page.function.request.repeatedError') }}(Message)</NButton>
<NButton class="ml-12px" @click="handleRepeatedModalError">
{{ $t('page.function.request.repeatedError') }}(Modal)
</NButton>
</NCard>
</NSpace>
</template>
<style scoped></style>

View File

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

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import { useTabStore } from '@/store/modules/tab';
const tabStore = useTabStore();
const { routerPushByKey } = useRouterPush();
const tabLabel = ref('');
function changeTabLabel() {
tabStore.setTabLabel(tabLabel.value);
}
function resetTabLabel() {
tabStore.resetTabLabel();
}
</script>
<template>
<NSpace vertical :size="16">
<NCard
:title="$t('page.function.tab.tabOperate.title')"
:bordered="false"
size="small"
segmented
class="card-wrapper"
>
<NDivider title-placement="left">{{ $t('page.function.tab.tabOperate.addTab') }}</NDivider>
<NButton @click="routerPushByKey('about')">{{ $t('page.function.tab.tabOperate.addTabDesc') }}</NButton>
<NDivider title-placement="left">{{ $t('page.function.tab.tabOperate.closeTab') }}</NDivider>
<NSpace>
<NButton @click="tabStore.removeActiveTab">
{{ $t('page.function.tab.tabOperate.closeCurrentTab') }}
</NButton>
<NButton @click="tabStore.removeTabByRouteName('about')">
{{ $t('page.function.tab.tabOperate.closeAboutTab') }}
</NButton>
</NSpace>
<NDivider title-placement="left">{{ $t('page.function.tab.tabOperate.addMultiTab') }}</NDivider>
<NSpace>
<NButton @click="routerPushByKey('function_multi-tab')">
{{ $t('page.function.tab.tabOperate.addMultiTabDesc1') }}
</NButton>
<NButton @click="routerPushByKey('function_multi-tab', { query: { a: '1' } })">
{{ $t('page.function.tab.tabOperate.addMultiTabDesc2') }}
</NButton>
</NSpace>
</NCard>
<NCard
:title="$t('page.function.tab.tabTitle.title')"
:bordered="false"
size="small"
segmented
class="card-wrapper"
>
<NDivider title-placement="left">{{ $t('page.function.tab.tabTitle.changeTitle') }}</NDivider>
<NInputGroup class="max-w-240px">
<NInput v-model:value="tabLabel" />
<NButton type="primary" @click="changeTabLabel">{{ $t('page.function.tab.tabTitle.change') }}</NButton>
</NInputGroup>
<NDivider title-placement="left">{{ $t('page.function.tab.tabTitle.resetTitle') }}</NDivider>
<NButton type="error" ghost class="w-80px" @click="resetTabLabel">
{{ $t('page.function.tab.tabTitle.reset') }}
</NButton>
</NCard>
</NSpace>
</template>
<style scoped></style>

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useLoading } from '@sa/hooks';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useAuthStore } from '@/store/modules/auth';
import { useTabStore } from '@/store/modules/tab';
import { useAuth } from '@/hooks/business/auth';
const route = useRoute();
const appStore = useAppStore();
const authStore = useAuthStore();
const tabStore = useTabStore();
const { hasAuth } = useAuth();
const { loading, startLoading, endLoading } = useLoading();
type AccountKey = 'super' | 'admin' | 'user';
interface Account {
key: AccountKey;
label: string;
userName: string;
password: string;
}
const accounts = computed<Account[]>(() => [
{
key: 'super',
label: $t('page.login.pwdLogin.superAdmin'),
userName: 'Super',
password: '123456'
},
{
key: 'admin',
label: $t('page.login.pwdLogin.admin'),
userName: 'Admin',
password: '123456'
},
{
key: 'user',
label: $t('page.login.pwdLogin.user'),
userName: 'User',
password: '123456'
}
]);
const loginAccount = ref<AccountKey>('super');
async function handleToggleAccount(account: Account) {
loginAccount.value = account.key;
startLoading();
await authStore.login(account.userName, account.password, false);
tabStore.initTabStore(route);
endLoading();
appStore.reloadPage();
}
</script>
<template>
<NSpace vertical :size="16">
<NCard :title="$t('route.function_toggle-auth')" :bordered="false" size="small" segmented class="card-wrapper">
<NDescriptions bordered :column="1">
<NDescriptionsItem :label="$t('page.manage.user.userRole')">
<NSpace>
<NTag v-for="role in authStore.userInfo.roles" :key="role">{{ role }}</NTag>
</NSpace>
</NDescriptionsItem>
<NDescriptionsItem ions-item :label="$t('page.function.toggleAuth.toggleAccount')">
<NSpace>
<NButton
v-for="account in accounts"
:key="account.key"
:loading="loading && loginAccount === account.key"
:disabled="loading && loginAccount !== account.key"
@click="handleToggleAccount(account)"
>
{{ account.label }}
</NButton>
</NSpace>
</NDescriptionsItem>
</NDescriptions>
</NCard>
<NCard
:title="$t('page.function.toggleAuth.authHook')"
:bordered="false"
size="small"
segmented
class="card-wrapper"
>
<NSpace>
<NButton v-if="hasAuth('B_CODE1')">{{ $t('page.function.toggleAuth.superAdminVisible') }}</NButton>
<NButton v-if="hasAuth('B_CODE2')">{{ $t('page.function.toggleAuth.adminVisible') }}</NButton>
<NButton v-if="hasAuth('B_CODE3')">
{{ $t('page.function.toggleAuth.adminOrUserVisible') }}
</NButton>
</NSpace>
</NCard>
</NSpace>
</template>
<style scoped></style>

View File

@@ -15,9 +15,6 @@ const gap = computed(() => (appStore.isMobile ? 0 : 16));
<template> <template>
<NSpace vertical :size="16"> <NSpace vertical :size="16">
<NAlert :title="$t('common.warning')" type="warning">
{{ $t('page.home.branchDesc') }}
</NAlert>
<HeaderBanner /> <HeaderBanner />
<CardData /> <CardData />
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive> <NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>

View File

@@ -0,0 +1,266 @@
<script setup lang="tsx">
import { ref } from 'vue';
import type { Ref } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { useBoolean } from '@sa/hooks';
import { fetchGetAllPages, fetchGetMenuList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import { yesOrNoRecord } from '@/constants/common';
import { enableStatusRecord, menuTypeRecord } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue';
import MenuOperateModal, { type OperateType } from './modules/menu-operate-modal.vue';
const appStore = useAppStore();
const { bool: visible, setTrue: openModal } = useBoolean();
const wrapperRef = ref<HTMLElement | null>(null);
const { columns, columnChecks, data, loading, pagination, getData, getDataByPage } = useTable({
apiFn: fetchGetMenuList,
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'id',
title: $t('page.manage.menu.id'),
align: 'center'
},
{
key: 'menuType',
title: $t('page.manage.menu.menuType'),
align: 'center',
width: 80,
render: row => {
const tagMap: Record<Api.SystemManage.MenuType, NaiveUI.ThemeColor> = {
1: 'default',
2: 'primary'
};
const label = $t(menuTypeRecord[row.menuType]);
return <NTag type={tagMap[row.menuType]}>{label}</NTag>;
}
},
{
key: 'menuName',
title: $t('page.manage.menu.menuName'),
align: 'center',
minWidth: 120,
render: row => {
const { i18nKey, menuName } = row;
const label = i18nKey ? $t(i18nKey) : menuName;
return <span>{label}</span>;
}
},
{
key: 'icon',
title: $t('page.manage.menu.icon'),
align: 'center',
width: 60,
render: row => {
const icon = row.iconType === '1' ? row.icon : undefined;
const localIcon = row.iconType === '2' ? row.icon : undefined;
return (
<div class="flex-center">
<SvgIcon icon={icon} localIcon={localIcon} class="text-icon" />
</div>
);
}
},
{
key: 'routeName',
title: $t('page.manage.menu.routeName'),
align: 'center',
minWidth: 120
},
{
key: 'routePath',
title: $t('page.manage.menu.routePath'),
align: 'center',
minWidth: 120
},
{
key: 'status',
title: $t('page.manage.menu.menuStatus'),
align: 'center',
width: 80,
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>;
}
},
{
key: 'hideInMenu',
title: $t('page.manage.menu.hideInMenu'),
align: 'center',
width: 80,
render: row => {
const hide: CommonType.YesOrNo = row.hideInMenu ? 'Y' : 'N';
const tagMap: Record<CommonType.YesOrNo, NaiveUI.ThemeColor> = {
Y: 'error',
N: 'default'
};
const label = $t(yesOrNoRecord[hide]);
return <NTag type={tagMap[hide]}>{label}</NTag>;
}
},
{
key: 'parentId',
title: $t('page.manage.menu.parentId'),
width: 90,
align: 'center'
},
{
key: 'order',
title: $t('page.manage.menu.order'),
align: 'center',
width: 60
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 230,
render: row => (
<div class="flex-center justify-end gap-8px">
{row.menuType === '1' && (
<NButton type="primary" ghost size="small" onClick={() => handleAddChildMenu(row)}>
{$t('page.manage.menu.addChildMenu')}
</NButton>
)}
<NButton type="primary" ghost size="small" onClick={() => handleEdit(row)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, getData);
const operateType = ref<OperateType>('add');
function handleAdd() {
operateType.value = 'add';
openModal();
}
async function handleBatchDelete() {
// request
console.log(checkedRowKeys.value);
onBatchDeleted();
}
function handleDelete(id: number) {
// request
console.log(id);
onDeleted();
}
/** the edit menu data or the parent menu data when adding a child menu */
const editingData: Ref<Api.SystemManage.Menu | null> = ref(null);
function handleEdit(item: Api.SystemManage.Menu) {
operateType.value = 'edit';
editingData.value = { ...item };
openModal();
}
function handleAddChildMenu(item: Api.SystemManage.Menu) {
operateType.value = 'addChild';
editingData.value = { ...item };
openModal();
}
const allPages = ref<string[]>([]);
async function getAllPages() {
const { data: pages } = await fetchGetAllPages();
allPages.value = pages || [];
}
function init() {
getAllPages();
}
// init
init();
</script>
<template>
<div ref="wrapperRef" class="flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<NCard :title="$t('page.manage.menu.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="1088"
:loading="loading"
:row-key="row => row.id"
remote
:pagination="pagination"
class="sm:h-full"
/>
<MenuOperateModal
v-model:visible="visible"
:operate-type="operateType"
:row-data="editingData"
:all-pages="allPages"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,467 @@
<script setup lang="tsx">
import { computed, reactive, ref, watch } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { getLocalIcons } from '@/utils/icon';
import { fetchGetAllRoles } from '@/service/api';
import {
getLayoutAndPage,
getPathParamFromRoutePath,
getRoutePathByRouteName,
getRoutePathWithParam,
transformLayoutAndPageToComponent
} from './shared';
defineOptions({
name: 'MenuOperateModal'
});
export type OperateType = NaiveUI.TableOperateType | 'addChild';
interface Props {
/** the type of operation */
operateType: OperateType;
/** the edit menu data or the parent menu data when adding a child menu */
rowData?: Api.SystemManage.Menu | null;
/** all pages */
allPages: string[];
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<OperateType, string> = {
add: $t('page.manage.menu.addMenu'),
addChild: $t('page.manage.menu.addChildMenu'),
edit: $t('page.manage.menu.editMenu')
};
return titles[props.operateType];
});
type Model = Pick<
Api.SystemManage.Menu,
| 'menuType'
| 'menuName'
| 'routeName'
| 'routePath'
| 'component'
| 'order'
| 'i18nKey'
| 'icon'
| 'iconType'
| 'status'
| 'parentId'
| 'keepAlive'
| 'constant'
| 'href'
| 'hideInMenu'
| 'activeMenu'
| 'multiTab'
| 'fixedIndexInTab'
> & {
query: NonNullable<Api.SystemManage.Menu['query']>;
buttons: NonNullable<Api.SystemManage.Menu['buttons']>;
layout: string;
page: string;
pathParam: string;
};
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
menuType: '1',
menuName: '',
routeName: '',
routePath: '',
pathParam: '',
component: '',
layout: '',
page: '',
i18nKey: null,
icon: '',
iconType: '1',
parentId: 0,
status: '1',
keepAlive: false,
constant: false,
order: 0,
href: null,
hideInMenu: false,
activeMenu: null,
multiTab: false,
fixedIndexInTab: null,
query: [],
buttons: []
};
}
type RuleKey = Extract<keyof Model, 'menuName' | 'status' | 'routeName' | 'routePath'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
menuName: defaultRequiredRule,
status: defaultRequiredRule,
routeName: defaultRequiredRule,
routePath: defaultRequiredRule
};
const disabledMenuType = computed(() => props.operateType === 'edit');
const localIcons = getLocalIcons();
const localIconOptions = localIcons.map<SelectOption>(item => ({
label: () => (
<div class="flex-y-center gap-16px">
<SvgIcon localIcon={item} class="text-icon" />
<span>{item}</span>
</div>
),
value: item
}));
const showLayout = computed(() => model.parentId === 0);
const showPage = computed(() => model.menuType === '2');
const pageOptions = computed(() => {
const allPages = [...props.allPages];
if (model.routeName && !allPages.includes(model.routeName)) {
allPages.unshift(model.routeName);
}
const opts: CommonType.Option[] = allPages.map(page => ({
label: page,
value: page
}));
return opts;
});
const layoutOptions: CommonType.Option[] = [
{
label: 'base',
value: 'base'
},
{
label: 'blank',
value: 'blank'
}
];
/** the enabled role options */
const roleOptions = ref<CommonType.Option<string>[]>([]);
async function getRoleOptions() {
const { error, data } = await fetchGetAllRoles();
if (!error) {
const options = data.map(item => ({
label: item.roleName,
value: item.roleCode
}));
roleOptions.value = [...options];
}
}
function handleInitModel() {
Object.assign(model, createDefaultModel());
if (!props.rowData) return;
if (props.operateType === 'addChild') {
const { id } = props.rowData;
Object.assign(model, { parentId: id });
}
if (props.operateType === 'edit') {
const { component, ...rest } = props.rowData;
const { layout, page } = getLayoutAndPage(component);
const { path, param } = getPathParamFromRoutePath(rest.routePath);
Object.assign(model, rest, { layout, page, routePath: path, pathParam: param });
}
if (!model.query) {
model.query = [];
}
if (!model.buttons) {
model.buttons = [];
}
}
function closeDrawer() {
visible.value = false;
}
function handleUpdateRoutePathByRouteName() {
if (model.routeName) {
model.routePath = getRoutePathByRouteName(model.routeName);
} else {
model.routePath = '';
}
}
function handleUpdateI18nKeyByRouteName() {
if (model.routeName) {
model.i18nKey = `route.${model.routeName}` as App.I18n.I18nKey;
} else {
model.i18nKey = null;
}
}
function handleCreateButton() {
const buttonItem: Api.SystemManage.MenuButton = {
code: '',
desc: ''
};
return buttonItem;
}
function getSubmitParams() {
const { layout, page, pathParam, ...params } = model;
const component = transformLayoutAndPageToComponent(layout, page);
const routePath = getRoutePathWithParam(model.routePath, pathParam);
params.component = component;
params.routePath = routePath;
return params;
}
async function handleSubmit() {
await validate();
const params = getSubmitParams();
console.log('params: ', params);
// request
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleInitModel();
restoreValidation();
getRoleOptions();
}
});
watch(
() => model.routeName,
() => {
handleUpdateRoutePathByRouteName();
handleUpdateI18nKeyByRouteName();
}
);
</script>
<template>
<NModal v-model:show="visible" :title="title" preset="card" class="w-800px">
<NScrollbar class="h-480px pr-20px">
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.menuType')" path="menuType">
<NRadioGroup v-model:value="model.menuType" :disabled="disabledMenuType">
<NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.menuName')" path="menuName">
<NInput v-model:value="model.menuName" :placeholder="$t('page.manage.menu.form.menuName')" />
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.routeName')" path="routeName">
<NInput v-model:value="model.routeName" :placeholder="$t('page.manage.menu.form.routeName')" />
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.routePath')" path="routePath">
<NInput v-model:value="model.routePath" disabled :placeholder="$t('page.manage.menu.form.routePath')" />
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.pathParam')" path="pathParam">
<NInput v-model:value="model.pathParam" :placeholder="$t('page.manage.menu.form.pathParam')" />
</NFormItemGi>
<NFormItemGi v-if="showLayout" span="24 m:12" :label="$t('page.manage.menu.layout')" path="layout">
<NSelect
v-model:value="model.layout"
:options="layoutOptions"
:placeholder="$t('page.manage.menu.form.layout')"
/>
</NFormItemGi>
<NFormItemGi v-if="showPage" span="24 m:12" :label="$t('page.manage.menu.page')" path="page">
<NSelect
v-model:value="model.page"
:options="pageOptions"
:placeholder="$t('page.manage.menu.form.page')"
/>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.i18nKey')" path="i18nKey">
<NInput v-model:value="model.i18nKey" :placeholder="$t('page.manage.menu.form.i18nKey')" />
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.order')" path="order">
<NInputNumber v-model:value="model.order" class="w-full" :placeholder="$t('page.manage.menu.form.order')" />
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.iconTypeTitle')" path="iconType">
<NRadioGroup v-model:value="model.iconType">
<NRadio
v-for="item in menuIconTypeOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.icon')" path="icon">
<template v-if="model.iconType === '1'">
<NInput v-model:value="model.icon" :placeholder="$t('page.manage.menu.form.icon')" class="flex-1">
<template #suffix>
<SvgIcon v-if="model.icon" :icon="model.icon" class="text-icon" />
</template>
</NInput>
</template>
<template v-if="model.iconType === '2'">
<NSelect
v-model:value="model.icon"
:placeholder="$t('page.manage.menu.form.localIcon')"
:options="localIconOptions"
/>
</template>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.menuStatus')" path="status">
<NRadioGroup v-model:value="model.status">
<NRadio
v-for="item in enableStatusOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.keepAlive')" path="keepAlive">
<NRadioGroup v-model:value="model.keepAlive">
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.constant')" path="constant">
<NRadioGroup v-model:value="model.constant">
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.href')" path="href">
<NInput v-model:value="model.href" :placeholder="$t('page.manage.menu.form.href')" />
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.hideInMenu')" path="hideInMenu">
<NRadioGroup v-model:value="model.hideInMenu">
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi
v-if="model.hideInMenu"
span="24 m:12"
:label="$t('page.manage.menu.activeMenu')"
path="activeMenu"
>
<NSelect
v-model:value="model.activeMenu"
:options="pageOptions"
clearable
:placeholder="$t('page.manage.menu.form.activeMenu')"
/>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.multiTab')" path="multiTab">
<NRadioGroup v-model:value="model.multiTab">
<NRadio :value="true" :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="24 m:12" :label="$t('page.manage.menu.fixedIndexInTab')" path="fixedIndexInTab">
<NInputNumber
v-model:value="model.fixedIndexInTab"
class="w-full"
clearable
:placeholder="$t('page.manage.menu.form.fixedIndexInTab')"
/>
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.query')">
<NDynamicInput
v-model:value="model.query"
preset="pair"
:key-placeholder="$t('page.manage.menu.form.queryKey')"
:value-placeholder="$t('page.manage.menu.form.queryValue')"
>
<template #action="{ index, create, remove }">
<NSpace class="ml-12px">
<NButton size="medium" @click="() => create(index)">
<icon-ic:round-plus class="text-icon" />
</NButton>
<NButton size="medium" @click="() => remove(index)">
<icon-ic-round-remove class="text-icon" />
</NButton>
</NSpace>
</template>
</NDynamicInput>
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.button')">
<NDynamicInput v-model:value="model.buttons" :on-create="handleCreateButton">
<template #default="{ value }">
<div class="ml-8px flex-y-center flex-1 gap-12px">
<NInput
v-model:value="value.code"
:placeholder="$t('page.manage.menu.form.buttonCode')"
class="flex-1"
/>
<NInput
v-model:value="value.desc"
:placeholder="$t('page.manage.menu.form.buttonDesc')"
class="flex-1"
/>
</div>
</template>
<template #action="{ index, create, remove }">
<NSpace class="ml-12px">
<NButton size="medium" @click="() => create(index)">
<icon-ic:round-plus class="text-icon" />
</NButton>
<NButton size="medium" @click="() => remove(index)">
<icon-ic-round-remove class="text-icon" />
</NButton>
</NSpace>
</template>
</NDynamicInput>
</NFormItemGi>
</NGrid>
</NForm>
</NScrollbar>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NModal>
</template>
<style scoped></style>

View File

@@ -0,0 +1,79 @@
const LAYOUT_PREFIX = 'layout.';
const VIEW_PREFIX = 'view.';
const FIRST_LEVEL_ROUTE_COMPONENT_SPLIT = '$';
export function getLayoutAndPage(component?: string | null) {
let layout = '';
let page = '';
const [layoutOrPage = '', pageItem = ''] = component?.split(FIRST_LEVEL_ROUTE_COMPONENT_SPLIT) || [];
layout = getLayout(layoutOrPage);
page = getPage(pageItem || layoutOrPage);
return { layout, page };
}
function getLayout(layout: string) {
return layout.startsWith(LAYOUT_PREFIX) ? layout.replace(LAYOUT_PREFIX, '') : '';
}
function getPage(page: string) {
return page.startsWith(VIEW_PREFIX) ? page.replace(VIEW_PREFIX, '') : '';
}
export function transformLayoutAndPageToComponent(layout: string, page: string) {
const hasLayout = Boolean(layout);
const hasPage = Boolean(page);
if (hasLayout && hasPage) {
return `${LAYOUT_PREFIX}${layout}${FIRST_LEVEL_ROUTE_COMPONENT_SPLIT}${VIEW_PREFIX}${page}`;
}
if (hasLayout) {
return `${LAYOUT_PREFIX}${layout}`;
}
if (hasPage) {
return `${VIEW_PREFIX}${page}`;
}
return '';
}
/**
* Get route name by route path
*
* @param routeName
*/
export function getRoutePathByRouteName(routeName: string) {
return `/${routeName.replace(/_/g, '/')}`;
}
/**
* Get path param from route path
*
* @param routePath route path
*/
export function getPathParamFromRoutePath(routePath: string) {
const [path, param = ''] = routePath.split('/:');
return {
path,
param
};
}
/**
* Get route path with param
*
* @param routePath route path
* @param param path param
*/
export function getRoutePathWithParam(routePath: string, param: string) {
if (param.trim()) {
return `${routePath}/:${param}`;
}
return routePath;
}

View File

@@ -0,0 +1,177 @@
<script setup lang="tsx">
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { fetchGetRoleList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import { enableStatusRecord } from '@/constants/business';
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
import RoleSearch from './modules/role-search.vue';
const appStore = useAppStore();
const {
columns,
columnChecks,
data,
loading,
getData,
getDataByPage,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetRoleList,
apiParams: {
current: 1,
size: 10,
// 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,
roleName: null,
roleCode: null
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
width: 64,
align: 'center'
},
{
key: 'roleName',
title: $t('page.manage.role.roleName'),
align: 'center',
minWidth: 120
},
{
key: 'roleCode',
title: $t('page.manage.role.roleCode'),
align: 'center',
minWidth: 120
},
{
key: 'roleDesc',
title: $t('page.manage.role.roleDesc'),
minWidth: 120
},
{
key: 'status',
title: $t('page.manage.role.roleStatus'),
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>;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
async function handleBatchDelete() {
// request
console.log(checkedRowKeys.value);
onBatchDeleted();
}
function handleDelete(id: number) {
// request
console.log(id);
onDeleted();
}
function edit(id: number) {
handleEdit(id);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<RoleSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard :title="$t('page.manage.role.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="702"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<RoleOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, shallowRef } from 'vue';
import { $t } from '@/locales';
defineOptions({
name: 'ButtonAuthModal'
});
interface Props {
/** the roleId */
roleId: number;
}
const props = defineProps<Props>();
const visible = defineModel<boolean>('visible', {
default: false
});
function closeModal() {
visible.value = false;
}
const title = computed(() => $t('common.edit') + $t('page.manage.role.buttonAuth'));
type ButtonConfig = {
id: number;
label: string;
code: string;
};
const tree = shallowRef<ButtonConfig[]>([]);
async function getAllButtons() {
// request
tree.value = [
{ id: 1, label: 'button1', code: 'code1' },
{ id: 2, label: 'button2', code: 'code2' },
{ id: 3, label: 'button3', code: 'code3' },
{ id: 4, label: 'button4', code: 'code4' },
{ id: 5, label: 'button5', code: 'code5' },
{ id: 6, label: 'button6', code: 'code6' },
{ id: 7, label: 'button7', code: 'code7' },
{ id: 8, label: 'button8', code: 'code8' },
{ id: 9, label: 'button9', code: 'code9' },
{ id: 10, label: 'button10', code: 'code10' }
];
}
const checks = shallowRef<number[]>([]);
async function getChecks() {
console.log(props.roleId);
// request
checks.value = [1, 2, 3, 4, 5];
}
function handleSubmit() {
console.log(checks.value, props.roleId);
// request
window.$message?.success?.($t('common.modifySuccess'));
closeModal();
}
function init() {
getAllButtons();
getChecks();
}
// init
init();
</script>
<template>
<NModal v-model:show="visible" :title="title" preset="card" class="w-480px">
<NTree
v-model:checked-keys="checks"
:data="tree"
key-field="id"
block-line
checkable
expand-on-click
virtual-scroll
class="h-280px"
/>
<template #footer>
<NSpace justify="end">
<NButton size="small" class="mt-16px" @click="closeModal">
{{ $t('common.cancel') }}
</NButton>
<NButton type="primary" size="small" class="mt-16px" @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
</NSpace>
</template>
</NModal>
</template>
<style scoped></style>

View File

@@ -0,0 +1,130 @@
<script setup lang="ts">
import { computed, shallowRef, watch } from 'vue';
import { $t } from '@/locales';
import { fetchGetAllPages, fetchGetMenuTree } from '@/service/api';
defineOptions({
name: 'MenuAuthModal'
});
interface Props {
/** the roleId */
roleId: number;
}
const props = defineProps<Props>();
const visible = defineModel<boolean>('visible', {
default: false
});
function closeModal() {
visible.value = false;
}
const title = computed(() => $t('common.edit') + $t('page.manage.role.menuAuth'));
const home = shallowRef('');
async function getHome() {
console.log(props.roleId);
home.value = 'home';
}
async function updateHome(val: string) {
// request
home.value = val;
}
const pages = shallowRef<string[]>([]);
async function getPages() {
const { error, data } = await fetchGetAllPages();
if (!error) {
pages.value = data;
}
}
const pageSelectOptions = computed(() => {
const opts: CommonType.Option[] = pages.value.map(page => ({
label: page,
value: page
}));
return opts;
});
const tree = shallowRef<Api.SystemManage.MenuTree[]>([]);
async function getTree() {
const { error, data } = await fetchGetMenuTree();
if (!error) {
tree.value = data;
}
}
const checks = shallowRef<number[]>([]);
async function getChecks() {
console.log(props.roleId);
// request
checks.value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
}
function handleSubmit() {
console.log(checks.value, props.roleId);
// request
window.$message?.success?.($t('common.modifySuccess'));
closeModal();
}
function init() {
getHome();
getPages();
getTree();
getChecks();
}
watch(visible, val => {
if (val) {
init();
}
});
</script>
<template>
<NModal v-model:show="visible" :title="title" preset="card" class="w-480px">
<div class="flex-y-center gap-16px pb-12px">
<div>{{ $t('page.manage.menu.home') }}</div>
<NSelect :value="home" :options="pageSelectOptions" size="small" class="w-160px" @update:value="updateHome" />
</div>
<NTree
v-model:checked-keys="checks"
:data="tree"
key-field="id"
checkable
expand-on-click
virtual-scroll
block-line
class="h-280px"
/>
<template #footer>
<NSpace justify="end">
<NButton size="small" class="mt-16px" @click="closeModal">
{{ $t('common.cancel') }}
</NButton>
<NButton type="primary" size="small" class="mt-16px" @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
</NSpace>
</template>
</NModal>
</template>
<style scoped></style>

View File

@@ -0,0 +1,134 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { useBoolean } from '@sa/hooks';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { enableStatusOptions } from '@/constants/business';
import MenuAuthModal from './menu-auth-modal.vue';
import ButtonAuthModal from './button-auth-modal.vue';
defineOptions({
name: 'RoleOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.SystemManage.Role | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const { bool: menuAuthVisible, setTrue: openMenuAuthModal } = useBoolean();
const { bool: buttonAuthVisible, setTrue: openButtonAuthModal } = useBoolean();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.manage.role.addRole'),
edit: $t('page.manage.role.editRole')
};
return titles[props.operateType];
});
type Model = Pick<Api.SystemManage.Role, 'roleName' | 'roleCode' | 'roleDesc' | 'status'>;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
roleName: '',
roleCode: '',
roleDesc: '',
status: null
};
}
type RuleKey = Exclude<keyof Model, 'roleDesc'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
roleName: defaultRequiredRule,
roleCode: defaultRequiredRule,
status: defaultRequiredRule
};
const roleId = computed(() => props.rowData?.id || -1);
const isEdit = computed(() => props.operateType === 'edit');
function handleInitModel() {
Object.assign(model, createDefaultModel());
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleInitModel();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" display-directive="show" :width="360">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.manage.role.roleName')" path="roleName">
<NInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
</NFormItem>
<NFormItem :label="$t('page.manage.role.roleCode')" path="roleCode">
<NInput v-model:value="model.roleCode" :placeholder="$t('page.manage.role.form.roleCode')" />
</NFormItem>
<NFormItem :label="$t('page.manage.role.roleStatus')" path="status">
<NRadioGroup v-model:value="model.status">
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.role.roleDesc')" path="roleDesc">
<NInput v-model:value="model.roleDesc" :placeholder="$t('page.manage.role.form.roleDesc')" />
</NFormItem>
</NForm>
<NSpace v-if="isEdit">
<NButton @click="openMenuAuthModal">{{ $t('page.manage.role.menuAuth') }}</NButton>
<MenuAuthModal v-model:visible="menuAuthVisible" :role-id="roleId" />
<NButton @click="openButtonAuthModal">{{ $t('page.manage.role.buttonAuth') }}</NButton>
<ButtonAuthModal v-model:visible="buttonAuthVisible" :role-id="roleId" />
</NSpace>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { $t } from '@/locales';
import { enableStatusOptions } from '@/constants/business';
import { translateOptions } from '@/utils/common';
defineOptions({
name: 'RoleSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const model = defineModel<Api.SystemManage.RoleSearchParams>('model', { required: true });
function reset() {
emit('reset');
}
function search() {
emit('search');
}
</script>
<template>
<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>
<style scoped></style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
interface Props {
id: string;
}
defineProps<Props>();
</script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@@ -0,0 +1,208 @@
<script setup lang="tsx">
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { fetchGetUserList } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { useTable, useTableOperate } from '@/hooks/common/table';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserSearch from './modules/user-search.vue';
const appStore = useAppStore();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetUserList,
showTotal: true,
apiParams: {
current: 1,
size: 10,
// 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>;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
async function handleBatchDelete() {
// request
console.log(checkedRowKeys.value);
onBatchDeleted();
}
function handleDelete(id: number) {
// request
console.log(id);
onDeleted();
}
function edit(id: number) {
handleEdit(id);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<UserSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<UserOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,166 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { fetchGetAllRoles } from '@/service/api';
import { $t } from '@/locales';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
defineOptions({
name: 'UserOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.SystemManage.User | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.manage.user.addUser'),
edit: $t('page.manage.user.editUser')
};
return titles[props.operateType];
});
type Model = Pick<
Api.SystemManage.User,
'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'userRoles' | 'status'
>;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
userName: '',
userGender: null,
nickName: '',
userPhone: '',
userEmail: '',
userRoles: [],
status: null
};
}
type RuleKey = Extract<keyof Model, 'userName' | 'status'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
userName: defaultRequiredRule,
status: defaultRequiredRule
};
/** the enabled role options */
const roleOptions = ref<CommonType.Option<string>[]>([]);
async function getRoleOptions() {
const { error, data } = await fetchGetAllRoles();
if (!error) {
const options = data.map(item => ({
label: item.roleName,
value: item.roleCode
}));
// the mock data does not have the roleCode, so fill it
// if the real request, remove the following code
const userRoleOptions = model.userRoles.map(item => ({
label: item,
value: item
}));
// end
roleOptions.value = [...userRoleOptions, ...options];
}
}
function handleInitModel() {
Object.assign(model, createDefaultModel());
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleInitModel();
restoreValidation();
getRoleOptions();
}
});
</script>
<template>
<NDrawer v-model:show="visible" display-directive="show" :width="360">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.manage.user.userName')" path="userName">
<NInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userGender')" path="userGender">
<NRadioGroup v-model:value="model.userGender">
<NRadio v-for="item in userGenderOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.user.nickName')" path="nickName">
<NInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userPhone')" path="userPhone">
<NInput v-model:value="model.userPhone" :placeholder="$t('page.manage.user.form.userPhone')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userEmail')" path="email">
<NInput v-model:value="model.userEmail" :placeholder="$t('page.manage.user.form.userEmail')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userStatus')" path="status">
<NRadioGroup v-model:value="model.status">
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.user.userRole')" path="roles">
<NSelect
v-model:value="model.userRoles"
multiple
:options="roleOptions"
:placeholder="$t('page.manage.user.form.userRole')"
/>
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@@ -0,0 +1,112 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@/locales';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
import { translateOptions } from '@/utils/common';
defineOptions({
name: 'UserSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'userEmail' | 'userPhone'>;
const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
const { patternRules } = useFormRules(); // inside computed to make locale reactive
return {
userEmail: patternRules.email,
userPhone: patternRules.phone
};
});
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<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>
<style scoped></style>

View File

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

View File

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

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, ZoomLevel } 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: ZoomLevel[] = [
{
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 };

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