mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-11-17 06:03:42 +08:00
Compare commits
72 Commits
v1.3.4
...
5bfc7613f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bfc7613f7 | ||
|
|
75997c1655 | ||
|
|
c4e16102cb | ||
|
|
be8556cd2b | ||
|
|
27b5222cfb | ||
|
|
79b2a28b5a | ||
|
|
531bfaf1b9 | ||
|
|
c3f1f69833 | ||
|
|
a44ea624f1 | ||
|
|
7cb43fc3c2 | ||
|
|
4c9f4e09b1 | ||
|
|
9c012c7d13 | ||
|
|
878d9c395c | ||
|
|
3830ec7a69 | ||
|
|
ef6cf93d96 | ||
|
|
7fa55905fc | ||
|
|
2fa400b113 | ||
|
|
73e917ad59 | ||
|
|
e9a2ee4a23 | ||
|
|
a1c14a1570 | ||
|
|
b16a96323a | ||
|
|
26c93dff9a | ||
|
|
5094f0eea8 | ||
|
|
7710bcdb0a | ||
|
|
b36d914e48 | ||
|
|
a8f923eb38 | ||
|
|
bbacff0581 | ||
|
|
f86bca063a | ||
|
|
48e005bcaf | ||
|
|
d825b6e260 | ||
|
|
05b0147b47 | ||
|
|
e7cd34ec37 | ||
|
|
a9dce21878 | ||
|
|
24320ab899 | ||
|
|
2f1f2c2b2a | ||
|
|
3a970a090d | ||
|
|
4a7e9c2669 | ||
|
|
d6f3effce0 | ||
|
|
fa4367a7b9 | ||
|
|
c425822b3a | ||
|
|
447bda2b48 | ||
|
|
c0303a8637 | ||
|
|
c7ab61bbd4 | ||
|
|
ec6a497452 | ||
|
|
074077dfaa | ||
|
|
fcf866783c | ||
|
|
1c9e36c47b | ||
|
|
d3210edc6a | ||
|
|
799ebcc714 | ||
|
|
4a8b9032cd | ||
|
|
8fea803fca | ||
|
|
599d0f10e8 | ||
|
|
781f88d227 | ||
|
|
5739346484 | ||
|
|
d194f0b1b6 | ||
|
|
19bea283c9 | ||
|
|
53a8e5678a | ||
|
|
10b231918a | ||
|
|
81d51a9630 | ||
|
|
fbebef3acb | ||
|
|
12d7bf94ee | ||
|
|
0478154db8 | ||
|
|
994ef2b9b1 | ||
|
|
ef32eecfdb | ||
|
|
83ae0bad0f | ||
|
|
9a9e2d144a | ||
|
|
a2110a98cf | ||
|
|
daf6600c21 | ||
|
|
a9b3a961af | ||
|
|
3a35d62e6f | ||
|
|
aaf28a42c2 | ||
|
|
019f80faef |
5
.env
5
.env
@@ -36,7 +36,7 @@ VITE_SERVICE_LOGOUT_CODES=8888,8889
|
||||
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
|
||||
|
||||
# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
|
||||
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998
|
||||
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998,3333
|
||||
|
||||
# when the route mode is static, the defined super role
|
||||
VITE_STATIC_SUPER_ROLE=R_SUPER
|
||||
@@ -46,3 +46,6 @@ VITE_SOURCE_MAP=N
|
||||
|
||||
# Used to differentiate storage across different domains
|
||||
VITE_STORAGE_PREFIX=SOY_
|
||||
|
||||
# used to control whether the program automatically detects updates
|
||||
VITE_AUTOMATICALLY_DETECT_UPDATE=Y
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,6 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## [v1.3.5](https://github.com/soybeanjs/soybean-admin/compare/v1.3.4...v1.3.5) (2024-09-07)
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- **packages**:
|
||||
- @sa/axios: add response to flatRequest when success - by @soybeanjs [<samp>(c4e16)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c4e1610)
|
||||
- **projects**:
|
||||
- README.zh_CN.md 添加合作推广 - by @PZ-18664918826 in https://github.com/soybeanjs/soybean-admin/issues/601 [<samp>(2fa40)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2fa400b)
|
||||
- Add more commit types according to Apache specifications - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/610 [<samp>(878d9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/878d9c3)
|
||||
- does the configuration support automatic updates. close#612 - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/617 and https://github.com/soybeanjs/soybean-admin/issues/612 [<samp>(4c9f4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4c9f4e0)
|
||||
- add app error handler. close #587 - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/587 [<samp>(be855)</samp>](https://github.com/soybeanjs/soybean-admin/commit/be8556c)
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
- **deps**:
|
||||
- move json5 from devDependencies to dependencies to support production usage - by @mufeng889 in https://github.com/soybeanjs/soybean-admin/issues/618 [<samp>(7cb43)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7cb43fc)
|
||||
- **projects**:
|
||||
- avoid retrieving cached HTML - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/605 [<samp>(ef6cf)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ef6cf93)
|
||||
- fix login redirect - by @soybeanjs [<samp>(3830e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3830ec7)
|
||||
- fix vertical-mix-menu when sider collapse. fixed #608 - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/608 [<samp>(c3f1f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c3f1f69)
|
||||
- fix breadcrumb when activeMenu is parent menu. fixed #589 - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/589 [<samp>(79b2a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/79b2a28)
|
||||
- fix refresh token when meet multi requests. fixed #581 - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/581 [<samp>(27b52)</samp>](https://github.com/soybeanjs/soybean-admin/commit/27b5222)
|
||||
- **types**:
|
||||
- fix the type of TableApiFn - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/599 [<samp>(26c93)</samp>](https://github.com/soybeanjs/soybean-admin/commit/26c93df)
|
||||
|
||||
### 🛠 Optimizations
|
||||
|
||||
- **projects**: optimize menu selectedKey - by @soybeanjs [<samp>(531bf)</samp>](https://github.com/soybeanjs/soybean-admin/commit/531bfaf)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **projects**:
|
||||
- update README - by @mufeng889 in https://github.com/soybeanjs/soybean-admin/issues/594 [<samp>(a8f92)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a8f923e)
|
||||
- update README - by @soybeanjs [<samp>(e9a2e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e9a2ee4)
|
||||
- update README - by @soybeanjs [<samp>(73e91)</samp>](https://github.com/soybeanjs/soybean-admin/commit/73e917a)
|
||||
- update the location of important information in the document - by **Azir** [<samp>(9c012)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9c012c7)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps**:
|
||||
- update deps - by @soybeanjs [<samp>(a1c14)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a1c14a1)
|
||||
- update deps - by @soybeanjs [<samp>(7fa55)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7fa5590)
|
||||
- update deps - by @soybeanjs [<samp>(a44ea)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a44ea62)
|
||||
- **projects**:
|
||||
- use json5 resolve env `VITE_OTHER_SERVICE_BASE_URL` & fix proxy enable - by @soybeanjs [<samp>(b16a9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b16a963)
|
||||
|
||||
### 🎨 Styles
|
||||
|
||||
- **projects**: rename script czh to commit:zh - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/597 [<samp>(5094f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5094f0e)
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
[](https://github.com/soybeanjs) [](https://github.com/mufeng889) [](https://github.com/Azir-11) [](https://github.com/PZ-18664918826)
|
||||
[Azir](mailto:2075125282@qq.com),
|
||||
|
||||
## [v1.3.4](https://github.com/honghuangdc/soybean-admin/compare/v1.3.3...v1.3.4) (2024-08-01)
|
||||
|
||||
### 🚨 Breaking Changes
|
||||
|
||||
@@ -111,6 +111,7 @@ pnpm build
|
||||
|
||||
## Ecosystem
|
||||
|
||||
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): SoybeanAdmin based version of React.
|
||||
- [electron-mock-admin](https://github.com/lixin59/electron-mock-api): A Mock Api management system that helps front-end developers quickly implement interface mocks.
|
||||
- [T-Shell](https://github.com/TheBlindM/T-Shell): A terminal emulator and SSH client with configurable command prompts.
|
||||
- [pea](https://github.com/haitang1894/pea) : Adopting SpringBoot3.2 + JDK21, MyBatis-Plus, SpringSecurity security framework, etc., suitable for the simple permission system developed by [soybean-admin](https://gitee.com/honghuangdc/soybean-admin).
|
||||
@@ -155,7 +156,7 @@ Thanks the following people for their contributions. If you want to contribute t
|
||||
|
||||
<div>
|
||||
<p>QQ Group</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-2.jpg" style="width:200px" />
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-3.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>WeChat Group</p>
|
||||
@@ -165,10 +166,6 @@ Thanks the following people for their contributions. If you want to contribute t
|
||||
<p>Add the following WeChat to invite to the WeChat group</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybeanjs.jpg" style="width:200px" />
|
||||
</div>
|
||||
<div>
|
||||
<p>Add Soybean's WeChat for business consultation, cooperation, project architecture, one-on-one guidance, etc.</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybean.jpg" style="width:200px" />
|
||||
</div>
|
||||
|
||||
## Star Trend
|
||||
|
||||
|
||||
@@ -57,6 +57,36 @@
|
||||
- [地址](https://docs.soybeanjs.cn)
|
||||
- [旧版文档](https://legacy-docs.soybeanjs.cn)
|
||||
|
||||
|
||||
## 合作事项
|
||||
|
||||
我们非常感谢大家对 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 的支持!为了进一步回馈社区,并助力企业和开发者实现个性化需求,我们现提供多种合作服务,期待与您携手共赢。
|
||||
|
||||
##### 1、定制化管理后台开发
|
||||
|
||||
针对企业和开发者的特定业务需求,我们提供基于 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 的定制化管理后台开发服务。我们的团队具备丰富的行业经验,能够迅速理解并实现您的需求,打造高效、灵活且安全的定制化解决方案。
|
||||
|
||||
- **定制开发**:我们将根据您的具体需求,提供从需求分析、UI设计到功能实现的全方位服务,确保项目高效交付。
|
||||
- **功能扩展**:在 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 基础上,扩展您所需的特定功能模块,提升管理后台的功能和用户体验。
|
||||
|
||||
##### 2、企业外包服务
|
||||
|
||||
我们承接各类企业级外包项目,特别是在管理后台系统的开发、集成与运维方面。我们以精益求精的态度,确保项目的质量和进度,为您的业务提供强有力的技术支持。
|
||||
|
||||
- **项目开发**:无论是全新的项目,还是现有系统的优化与集成,我们都将为您量身打造高效可靠的解决方案。
|
||||
- **系统集成与维护**:我们也提供基于 [`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 的系统集成与长期维护服务,确保您的系统稳定、安全地运行。
|
||||
|
||||
##### 3、联系方式
|
||||
|
||||
如有合作意向或项目咨询,请通过以下方式与我们联系:
|
||||
|
||||
- **Email**: [soybeanjs@outlook.com](mailto:soybeanjs@outlook.com)
|
||||
- **GitHub Issues**: 欢迎通过 [GitHub Issues](https://github.com/soybeanjs/soybean-admin/issues/new) 联系我们,进行初步的合作洽谈。
|
||||
- **商务合作微信**: honghuangdc
|
||||
|
||||
期待与您开展深入合作,共同推动 SoybeanAdmin 项目及其在更多领域的成功应用!
|
||||
|
||||
|
||||
## 示例图片
|
||||
|
||||

|
||||
@@ -109,6 +139,7 @@ pnpm build
|
||||
|
||||
## 周边生态
|
||||
|
||||
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): 基于SoybeanAdmin的React版本.
|
||||
- [electron-mock-admin](https://github.com/lixin59/electron-mock-api): 一个 Mock Api 管理系统,帮助前端开发伙伴快速实现接口的 mock。
|
||||
- [T-Shell](https://github.com/TheBlindM/T-Shell): 是一个可配置命令提示的终端模拟器和 SSH 客户端。
|
||||
- [pea](https://github.com/haitang1894/pea) : 采用SpringBoot3.2 + JDK21、MyBatis-Plus、SpringSecurity安全框架等,适配 [soybean-admin](https://gitee.com/honghuangdc/soybean-admin) 开发的简单权限系统。
|
||||
@@ -155,7 +186,7 @@ pnpm build
|
||||
|
||||
<div>
|
||||
<p>QQ交流群</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-2.jpg" style="width:200px" />
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-3.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>微信群</p>
|
||||
@@ -165,10 +196,6 @@ pnpm build
|
||||
<p>添加下面微信邀请进微信群</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybeanjs.jpg" style="width:200px" />
|
||||
</div>
|
||||
<div>
|
||||
<p>添加 Soybean 的微信,业务咨询、合作、项目架构、一对一指导等</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/wechat-soybean.jpg" style="width:200px" />
|
||||
</div>
|
||||
|
||||
## Star 趋势
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import { createServiceConfig } from '../../src/utils/service';
|
||||
* Set http proxy
|
||||
*
|
||||
* @param env - The current env
|
||||
* @param isDev - Is development environment
|
||||
* @param enable - If enable http proxy
|
||||
*/
|
||||
export function createViteProxy(env: Env.ImportMeta, isDev: boolean) {
|
||||
const isEnableHttpProxy = isDev && env.VITE_HTTP_PROXY === 'Y';
|
||||
export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
|
||||
const isEnableHttpProxy = enable && env.VITE_HTTP_PROXY === 'Y';
|
||||
|
||||
if (!isEnableHttpProxy) return undefined;
|
||||
|
||||
|
||||
@@ -8,6 +8,20 @@ export function setupElegantRouter() {
|
||||
base: 'src/layouts/base-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) {
|
||||
const key = routeName as RouteKey;
|
||||
|
||||
|
||||
78
package.json
78
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "soybean-admin",
|
||||
"type": "module",
|
||||
"version": "1.3.4",
|
||||
"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的清新优雅的中后台模版。",
|
||||
"author": {
|
||||
"name": "Soybean",
|
||||
@@ -35,7 +35,7 @@
|
||||
"build:test": "vite build --mode test",
|
||||
"cleanup": "sa cleanup",
|
||||
"commit": "sa git-commit",
|
||||
"czh": "sa git-commit -l=zh-cn",
|
||||
"commit:zh": "sa git-commit -l=zh-cn",
|
||||
"dev": "vite --mode test",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"gen-route": "sa gen-route",
|
||||
@@ -47,6 +47,8 @@
|
||||
"update-pkg": "sa update-pkg"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/data-set": "0.11.8",
|
||||
"@antv/g2": "5.2.5",
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@iconify/vue": "4.1.2",
|
||||
"@sa/axios": "workspace:*",
|
||||
@@ -54,50 +56,66 @@
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@vueuse/core": "10.11.0",
|
||||
"@vueuse/core": "11.0.3",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.12",
|
||||
"dayjs": "1.11.13",
|
||||
"dhtmlx-gantt": "8.0.10",
|
||||
"dompurify": "3.1.6",
|
||||
"echarts": "5.5.1",
|
||||
"jsbarcode": "3.11.6",
|
||||
"json5": "2.2.3",
|
||||
"naive-ui": "2.39.0",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "2.2.0",
|
||||
"tailwind-merge": "2.4.0",
|
||||
"vue": "3.4.35",
|
||||
"vue-draggable-plus": "0.5.2",
|
||||
"vue-i18n": "9.13.1",
|
||||
"vue-router": "4.4.1"
|
||||
"pinia": "2.2.2",
|
||||
"pinyin-pro": "3.24.2",
|
||||
"print-js": "1.6.0",
|
||||
"swiper": "11.1.12",
|
||||
"tailwind-merge": "2.5.2",
|
||||
"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": {
|
||||
"@amap/amap-jsapi-types": "0.0.15",
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.232",
|
||||
"@iconify/json": "2.2.245",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.4.0",
|
||||
"@types/node": "22.0.1",
|
||||
"@types/bmapgl": "0.0.7",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "0.61.9",
|
||||
"@unocss/preset-icons": "0.61.9",
|
||||
"@unocss/preset-uno": "0.61.9",
|
||||
"@unocss/transformer-directives": "0.61.9",
|
||||
"@unocss/transformer-variant-group": "0.61.9",
|
||||
"@unocss/vite": "0.61.9",
|
||||
"@vitejs/plugin-vue": "5.1.1",
|
||||
"@vitejs/plugin-vue-jsx": "4.0.0",
|
||||
"eslint": "9.8.0",
|
||||
"eslint-plugin-vue": "9.27.0",
|
||||
"lint-staged": "15.2.7",
|
||||
"sass": "1.77.8",
|
||||
"@unocss/eslint-config": "0.62.3",
|
||||
"@unocss/preset-icons": "0.62.3",
|
||||
"@unocss/preset-uno": "0.62.3",
|
||||
"@unocss/transformer-directives": "0.62.3",
|
||||
"@unocss/transformer-variant-group": "0.62.3",
|
||||
"@unocss/vite": "0.62.3",
|
||||
"@vitejs/plugin-vue": "5.1.3",
|
||||
"@vitejs/plugin-vue-jsx": "4.0.1",
|
||||
"eslint": "9.10.0",
|
||||
"eslint-plugin-vue": "9.28.0",
|
||||
"lint-staged": "15.2.10",
|
||||
"sass": "1.78.0",
|
||||
"simple-git-hooks": "2.11.1",
|
||||
"tsx": "4.16.3",
|
||||
"tsx": "4.19.0",
|
||||
"typescript": "5.5.4",
|
||||
"unplugin-icons": "0.19.1",
|
||||
"unplugin-vue-components": "0.27.3",
|
||||
"vite": "5.3.5",
|
||||
"unplugin-icons": "0.19.3",
|
||||
"unplugin-vue-components": "0.27.4",
|
||||
"vite": "5.4.3",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "7.3.7",
|
||||
"vite-plugin-vue-devtools": "7.4.4",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
"vue-tsc": "2.0.29"
|
||||
"vue-tsc": "2.1.6"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/axios",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
@@ -11,9 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"axios": "1.7.2",
|
||||
"axios-retry": "4.4.2",
|
||||
"qs": "6.12.3"
|
||||
"axios": "1.7.7",
|
||||
"axios-retry": "4.5.0",
|
||||
"qs": "6.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "6.9.15"
|
||||
|
||||
@@ -162,12 +162,12 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
|
||||
if (responseType === 'json') {
|
||||
const data = opts.transformBackendResponse(response);
|
||||
|
||||
return { data, error: null };
|
||||
return { data, error: null, response };
|
||||
}
|
||||
|
||||
return { data: response.data as MappedType<R, T>, error: null };
|
||||
} catch (error) {
|
||||
return { data: null, error };
|
||||
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||
}
|
||||
} as FlatRequestInstance<State, ResponseData>;
|
||||
|
||||
|
||||
@@ -92,18 +92,20 @@ export interface RequestInstance<S = Record<string, unknown>> extends RequestIns
|
||||
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
|
||||
}
|
||||
|
||||
export type FlatResponseSuccessData<T = any> = {
|
||||
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
|
||||
data: T;
|
||||
error: null;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseFailData<ResponseData = any> = {
|
||||
data: null;
|
||||
error: AxiosError<ResponseData>;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseData<T = any, ResponseData = any> =
|
||||
| FlatResponseSuccessData<T>
|
||||
| FlatResponseSuccessData<T, ResponseData>
|
||||
| FlatResponseFailData<ResponseData>;
|
||||
|
||||
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/color",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/hooks",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/materials",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/fetch",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/scripts",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"bin": {
|
||||
"sa": "./bin.ts"
|
||||
},
|
||||
@@ -14,14 +14,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.3.24",
|
||||
"bumpp": "9.4.1",
|
||||
"c12": "1.11.1",
|
||||
"bumpp": "9.5.2",
|
||||
"c12": "1.11.2",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.2.3",
|
||||
"enquirer": "2.4.1",
|
||||
"execa": "9.3.0",
|
||||
"execa": "9.3.1",
|
||||
"kolorist": "1.8.0",
|
||||
"npm-check-updates": "17.0.0",
|
||||
"npm-check-updates": "17.1.1",
|
||||
"rimraf": "6.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,15 @@ export const locales = {
|
||||
},
|
||||
gitCommitTypes: [
|
||||
['feat', '新功能'],
|
||||
['feat-wip', '开发中的功能,比如某功能的部分代码'],
|
||||
['fix', '修复Bug'],
|
||||
['docs', '只更新文档'],
|
||||
['docs', '只涉及文档更新'],
|
||||
['typo', '代码或文档勘误,比如错误拼写'],
|
||||
['style', '修改代码风格,不影响代码含义的变更'],
|
||||
['refactor', '代码重构,既不修复 bug 也不添加功能的代码变更'],
|
||||
['perf', '可提高性能的代码更改'],
|
||||
['optimize', '优化代码质量的代码更改'],
|
||||
['test', '添加缺失的测试或更正现有测'],
|
||||
['test', '添加缺失的测试或更正现有测试'],
|
||||
['build', '影响构建系统或外部依赖项的更改'],
|
||||
['ci', '对 CI 配置文件和脚本的更改'],
|
||||
['chore', '没有修改src或测试文件的其他变更'],
|
||||
@@ -47,8 +49,10 @@ export const locales = {
|
||||
},
|
||||
gitCommitTypes: [
|
||||
['feat', 'A new feature'],
|
||||
['feat-wip', 'Features in development, such as partial code for a certain feature'],
|
||||
['fix', 'A bug fix'],
|
||||
['docs', 'Documentation only changes'],
|
||||
['typo', 'Code or document corrections, such as spelling errors'],
|
||||
['style', 'Changes that do not affect the meaning of the code'],
|
||||
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
|
||||
['perf', 'A code change that improves performance'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/uno-preset",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/utils",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
||||
3640
pnpm-lock.yaml
generated
3640
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
78
src/components/custom/custom-icon-select.vue
Normal file
78
src/components/custom/custom-icon-select.vue
Normal 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>
|
||||
18
src/components/custom/github-link.vue
Normal file
18
src/components/custom/github-link.vue
Normal 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>
|
||||
23
src/components/custom/web-site-link.vue
Normal file
23
src/components/custom/web-site-link.vue
Normal 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>
|
||||
29
src/constants/business.ts
Normal file
29
src/constants/business.ts
Normal 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
8
src/constants/map-sdk.ts
Normal 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';
|
||||
@@ -93,11 +93,15 @@ export function useRouterPush(inSetup = true) {
|
||||
return routerPushByKey('login', { query, params: { module } });
|
||||
}
|
||||
|
||||
/** Redirect from login */
|
||||
async function redirectFromLogin() {
|
||||
/**
|
||||
* Redirect from login
|
||||
*
|
||||
* @param [needRedirect=true] Whether to redirect after login. Default is `true`
|
||||
*/
|
||||
async function redirectFromLogin(needRedirect = true) {
|
||||
const redirect = route.value.query?.redirect as string;
|
||||
|
||||
if (redirect) {
|
||||
if (needRedirect && redirect) {
|
||||
routerPush(redirect);
|
||||
} else {
|
||||
toHome();
|
||||
|
||||
@@ -8,6 +8,7 @@ export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } =
|
||||
function useMixMenu() {
|
||||
const route = useRoute();
|
||||
const routeStore = useRouteStore();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const activeFirstLevelMenuKey = ref('');
|
||||
|
||||
@@ -16,12 +17,7 @@ function useMixMenu() {
|
||||
}
|
||||
|
||||
function getActiveFirstLevelMenuKey() {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
const [firstLevelRouteName] = routeName.split('_');
|
||||
const [firstLevelRouteName] = selectedKey.value.split('_');
|
||||
|
||||
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
||||
}
|
||||
@@ -68,3 +64,20 @@ function useMixMenu() {
|
||||
getActiveFirstLevelMenuKey
|
||||
};
|
||||
}
|
||||
|
||||
export function useMenu() {
|
||||
const route = useRoute();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
|
||||
return {
|
||||
selectedKey
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function loginOrRegister() {
|
||||
toLogin();
|
||||
}
|
||||
|
||||
type DropdownKey = 'logout';
|
||||
type DropdownKey = 'user-center' | 'logout';
|
||||
|
||||
type DropdownOption =
|
||||
| {
|
||||
@@ -33,6 +33,15 @@ type DropdownOption =
|
||||
|
||||
const options = computed(() => {
|
||||
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'),
|
||||
key: 'logout',
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMenu } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'HorizontalMenu'
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
const { selectedKey } = useMenu();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import { useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'HorizontalMixMenu'
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
||||
setActiveFirstLevelMenuKey(menu.key);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
@@ -8,7 +8,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'ReversedHorizontalMixMenu'
|
||||
@@ -18,6 +18,7 @@ const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
const {
|
||||
firstLevelMenus,
|
||||
childLevelMenus,
|
||||
@@ -25,16 +26,7 @@ const {
|
||||
setActiveFirstLevelMenuKey,
|
||||
isActiveFirstLevelMenuHasChildren
|
||||
} = useMixMenuContext();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
function handleSelectMixMenu(key: RouteKey) {
|
||||
setActiveFirstLevelMenuKey(key);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useMenu } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'VerticalMenu'
|
||||
@@ -17,18 +18,10 @@ const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function updateExpandedKeys() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import GlobalLogo from '../../global-logo/index.vue';
|
||||
|
||||
@@ -31,6 +31,7 @@ const {
|
||||
getActiveFirstLevelMenuKey
|
||||
//
|
||||
} = useMixMenuContext();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||
|
||||
@@ -56,15 +57,6 @@ function handleResetActiveMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function updateExpandedKeys() {
|
||||
@@ -122,9 +114,6 @@ watch(
|
||||
mode="vertical"
|
||||
:value="selectedKey"
|
||||
:options="childLevelMenus"
|
||||
:collapsed="appStore.siderCollapse"
|
||||
:collapsed-width="themeStore.sider.collapsedWidth"
|
||||
:collapsed-icon-size="22"
|
||||
:inverted="inverted"
|
||||
:indent="18"
|
||||
@update:value="routerPushByKeyWithMetaQuery"
|
||||
|
||||
@@ -154,7 +154,60 @@ const local: App.I18n.Schema = {
|
||||
404: 'Page Not Found',
|
||||
500: 'Server Error',
|
||||
'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: {
|
||||
login: {
|
||||
@@ -203,6 +256,19 @@ const local: App.I18n.Schema = {
|
||||
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: {
|
||||
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.',
|
||||
@@ -231,6 +297,163 @@ const local: App.I18n.Schema = {
|
||||
desc5: 'Soybean just wrote some of the workbench pages casually, and it was enough to see!'
|
||||
},
|
||||
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: {
|
||||
|
||||
@@ -154,7 +154,60 @@ const local: App.I18n.Schema = {
|
||||
404: '页面不存在',
|
||||
500: '服务器错误',
|
||||
'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: {
|
||||
login: {
|
||||
@@ -203,6 +256,19 @@ const local: App.I18n.Schema = {
|
||||
title: '绑定微信'
|
||||
}
|
||||
},
|
||||
about: {
|
||||
title: '关于',
|
||||
introduction: `SoybeanAdmin 是一个优雅且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。SoybeanAdmin 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。`,
|
||||
projectInfo: {
|
||||
title: '项目信息',
|
||||
version: '版本',
|
||||
latestBuildTime: '最新构建时间',
|
||||
githubLink: 'Github 地址',
|
||||
previewLink: '预览地址'
|
||||
},
|
||||
prdDep: '生产依赖',
|
||||
devDep: '开发依赖'
|
||||
},
|
||||
home: {
|
||||
branchDesc:
|
||||
'为了方便大家开发和更新合并,我们对main分支的代码进行了精简,只保留了首页菜单,其余内容已移至example分支进行维护。预览地址显示的内容即为example分支的内容。',
|
||||
@@ -231,6 +297,163 @@ const local: App.I18n.Schema = {
|
||||
desc5: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!'
|
||||
},
|
||||
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: {
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { h } from 'vue';
|
||||
import type { App } from 'vue';
|
||||
import { NButton } from 'naive-ui';
|
||||
import { $t } from '../locales';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function setupAppErrorHandle(app: App) {
|
||||
app.config.errorHandler = (err, vm, info) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err, vm, info);
|
||||
};
|
||||
}
|
||||
|
||||
export function setupAppVersionNotification() {
|
||||
const canAutoUpdateApp = import.meta.env.VITE_AUTOMATICALLY_DETECT_UPDATE === 'Y';
|
||||
|
||||
if (!canAutoUpdateApp) return;
|
||||
|
||||
let isShow = false;
|
||||
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
@@ -52,9 +64,7 @@ export function setupAppVersionNotification() {
|
||||
}
|
||||
|
||||
async function getHtmlBuildTime() {
|
||||
const baseURL = import.meta.env.VITE_BASE_URL;
|
||||
|
||||
const res = await fetch(`${baseURL}index.html`);
|
||||
const res = await fetch(`/index.html?time=${Date.now()}`);
|
||||
|
||||
const html = await res.text();
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'virtual:svg-icons-register';
|
||||
import 'uno.css';
|
||||
import '../styles/css/global.css';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
import 'swiper/css/pagination';
|
||||
|
||||
@@ -20,5 +20,36 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
500: () => import("@/views/_builtin/500/index.vue"),
|
||||
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].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"),
|
||||
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"),
|
||||
};
|
||||
|
||||
@@ -39,6 +39,135 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
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',
|
||||
path: '/home',
|
||||
@@ -74,5 +203,327 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
constant: 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
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -163,12 +163,65 @@ function transformElegantRouteToVueRoute(
|
||||
const routeMap: RouteMap = {
|
||||
"root": "/",
|
||||
"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",
|
||||
"404": "/404",
|
||||
"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",
|
||||
"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"
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,160 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
|
||||
*
|
||||
* @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 */
|
||||
export function createStaticRoutes() {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './route';
|
||||
export * from './system-manage';
|
||||
|
||||
55
src/service/api/system-manage.ts
Normal file
55
src/service/api/system-manage.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { handleRefreshToken, showErrorMsg } from './shared';
|
||||
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
@@ -19,12 +19,8 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
},
|
||||
{
|
||||
async onRequest(config) {
|
||||
const { headers } = config;
|
||||
|
||||
// set token
|
||||
const token = localStg.get('token');
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
Object.assign(headers, { Authorization });
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(config.headers, { Authorization });
|
||||
|
||||
return config;
|
||||
},
|
||||
@@ -83,15 +79,13 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
// when the backend response code is in `expiredTokenCodes`, it means the token is expired, and refresh token
|
||||
// the api `refreshToken` can not return error code in `expiredTokenCodes`, otherwise it will be a dead loop, should return `logoutCodes` or `modalLogoutCodes`
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||
if (expiredTokenCodes.includes(responseCode) && !request.state.isRefreshingToken) {
|
||||
request.state.isRefreshingToken = true;
|
||||
if (expiredTokenCodes.includes(responseCode)) {
|
||||
const success = await handleExpiredRequest(request.state);
|
||||
if (success) {
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(response.config.headers, { Authorization });
|
||||
|
||||
const refreshConfig = await handleRefreshToken(response.config);
|
||||
|
||||
request.state.isRefreshingToken = false;
|
||||
|
||||
if (refreshConfig) {
|
||||
return instance.request(refreshConfig) as Promise<AxiosResponse>;
|
||||
return instance.request(response.config) as Promise<AxiosResponse>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { fetchRefreshToken } from '../api';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
/**
|
||||
* refresh token
|
||||
*
|
||||
* @param axiosConfig - request config when the token is expired
|
||||
*/
|
||||
export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
|
||||
export function getAuthorization() {
|
||||
const token = localStg.get('token');
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
|
||||
return Authorization;
|
||||
}
|
||||
|
||||
/** refresh token */
|
||||
async function handleRefreshToken() {
|
||||
const { resetStore } = useAuthStore();
|
||||
|
||||
const refreshToken = localStg.get('refreshToken') || '';
|
||||
const { error, data } = await fetchRefreshToken(refreshToken);
|
||||
const rToken = localStg.get('refreshToken') || '';
|
||||
const { error, data } = await fetchRefreshToken(rToken);
|
||||
if (!error) {
|
||||
localStg.set('token', data.token);
|
||||
localStg.set('refreshToken', data.refreshToken);
|
||||
|
||||
const config = { ...axiosConfig };
|
||||
if (config.headers) {
|
||||
config.headers.Authorization = data.token;
|
||||
}
|
||||
|
||||
return config;
|
||||
return true;
|
||||
}
|
||||
|
||||
resetStore();
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||
if (!state.refreshTokenFn) {
|
||||
state.refreshTokenFn = handleRefreshToken();
|
||||
}
|
||||
|
||||
const success = await state.refreshTokenFn;
|
||||
|
||||
setTimeout(() => {
|
||||
state.refreshTokenFn = null;
|
||||
}, 1000);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
export function showErrorMsg(state: RequestInstanceState, message: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface RequestInstanceState {
|
||||
/** whether the request is refreshing token */
|
||||
isRefreshingToken: boolean;
|
||||
refreshTokenFn: Promise<boolean> | null;
|
||||
/** the request error message stack */
|
||||
errMsgStack: string[];
|
||||
}
|
||||
|
||||
@@ -71,9 +71,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
if (pass) {
|
||||
await routeStore.initAuthRoute();
|
||||
|
||||
if (redirect) {
|
||||
await redirectFromLogin();
|
||||
}
|
||||
await redirectFromLogin(redirect);
|
||||
|
||||
if (routeStore.isInitAuthRoute) {
|
||||
window.$notification?.success({
|
||||
|
||||
@@ -281,15 +281,25 @@ export function getBreadcrumbsByRoute(
|
||||
const key = route.name as string;
|
||||
const activeKey = route.meta?.activeMenu;
|
||||
|
||||
const menuKey = activeKey || key;
|
||||
|
||||
for (const menu of menus) {
|
||||
if (menu.key === menuKey) {
|
||||
const breadcrumbMenu = menuKey !== activeKey ? menu : getGlobalMenuByBaseRoute(route);
|
||||
|
||||
if (menu.key === key) {
|
||||
const breadcrumbMenu = menu;
|
||||
return [transformMenuToBreadcrumb(breadcrumbMenu)];
|
||||
}
|
||||
|
||||
if (menu.key === activeKey) {
|
||||
const ROUTE_DEGREE_SPLITTER = '_';
|
||||
|
||||
const parentKey = key.split(ROUTE_DEGREE_SPLITTER).slice(0, -1).join(ROUTE_DEGREE_SPLITTER);
|
||||
|
||||
const breadcrumbMenu = getGlobalMenuByBaseRoute(route);
|
||||
if (parentKey !== activeKey) {
|
||||
return [transformMenuToBreadcrumb(breadcrumbMenu)];
|
||||
}
|
||||
|
||||
return [transformMenuToBreadcrumb(menu), transformMenuToBreadcrumb(breadcrumbMenu)];
|
||||
}
|
||||
|
||||
if (menu.children?.length) {
|
||||
const result = getBreadcrumbsByRoute(route, menu.children);
|
||||
if (result.length > 0) {
|
||||
|
||||
@@ -82,4 +82,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
*
|
||||
* If publish new version, use `overrideThemeSettings` to override certain theme settings
|
||||
*/
|
||||
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {};
|
||||
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {
|
||||
watermark: {
|
||||
visible: false,
|
||||
text: 'SoybeanAdmin'
|
||||
}
|
||||
};
|
||||
|
||||
141
src/typings/api.d.ts
vendored
141
src/typings/api.d.ts
vendored
@@ -20,6 +20,9 @@ declare namespace Api {
|
||||
records: T[];
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
@@ -81,4 +84,142 @@ declare namespace Api {
|
||||
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[];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
170
src/typings/app.d.ts
vendored
170
src/typings/app.d.ts
vendored
@@ -443,6 +443,19 @@ declare namespace App {
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
about: {
|
||||
title: string;
|
||||
introduction: string;
|
||||
projectInfo: {
|
||||
title: string;
|
||||
version: string;
|
||||
latestBuildTime: string;
|
||||
githubLink: string;
|
||||
previewLink: string;
|
||||
};
|
||||
prdDep: string;
|
||||
devDep: string;
|
||||
};
|
||||
home: {
|
||||
branchDesc: string;
|
||||
greeting: string;
|
||||
@@ -471,6 +484,163 @@ declare namespace App {
|
||||
};
|
||||
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: {
|
||||
required: string;
|
||||
|
||||
15
src/typings/components.d.ts
vendored
15
src/typings/components.d.ts
vendored
@@ -11,12 +11,16 @@ declare module 'vue' {
|
||||
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
||||
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||
CustomIconSelect: typeof import('./../components/custom/custom-icon-select.vue')['default']
|
||||
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
|
||||
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
|
||||
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
GithubLink: typeof import('./../components/custom/github-link.vue')['default']
|
||||
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
|
||||
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
|
||||
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
|
||||
'IconCharm:download': typeof import('~icons/charm/download')['default']
|
||||
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
|
||||
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['default']
|
||||
@@ -25,8 +29,12 @@ declare module 'vue' {
|
||||
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
|
||||
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||
IconLocalActivity: typeof import('~icons/local/activity')['default']
|
||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
IconLocalCast: typeof import('~icons/local/cast')['default']
|
||||
IconLocalLogo: typeof import('~icons/local/logo')['default']
|
||||
'IconMaterialSymbolsLight:rotate90DegreesCcwOutlineRounded': typeof import('~icons/material-symbols-light/rotate90-degrees-ccw-outline-rounded')['default']
|
||||
'IconMdi:printer': typeof import('~icons/mdi/printer')['default']
|
||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
@@ -37,12 +45,13 @@ declare module 'vue' {
|
||||
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
|
||||
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
||||
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||
@@ -69,16 +78,19 @@ declare module 'vue' {
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
@@ -96,5 +108,6 @@ declare module 'vue' {
|
||||
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
|
||||
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
|
||||
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
||||
WebSiteLink: typeof import('./../components/custom/web-site-link.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
114
src/typings/elegant-router.d.ts
vendored
114
src/typings/elegant-router.d.ts
vendored
@@ -17,12 +17,65 @@ declare module "@elegant-router/types" {
|
||||
export type RouteMap = {
|
||||
"root": "/";
|
||||
"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";
|
||||
"404": "/404";
|
||||
"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";
|
||||
"iframe-page": "/iframe-page/:url";
|
||||
"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,
|
||||
| "root"
|
||||
| "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"
|
||||
| "404"
|
||||
| "500"
|
||||
| "about"
|
||||
| "function"
|
||||
| "home"
|
||||
| "iframe-page"
|
||||
| "login"
|
||||
| "manage"
|
||||
| "multi-menu"
|
||||
| "plugin"
|
||||
| "user-center"
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -69,6 +140,8 @@ declare module "@elegant-router/types" {
|
||||
CustomRouteKey,
|
||||
| "root"
|
||||
| "not-found"
|
||||
| "exception"
|
||||
| "document"
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -81,7 +154,38 @@ declare module "@elegant-router/types" {
|
||||
| "500"
|
||||
| "iframe-page"
|
||||
| "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"
|
||||
| "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,
|
||||
| "root"
|
||||
| "not-found"
|
||||
| "exception_403"
|
||||
| "exception_404"
|
||||
| "exception_500"
|
||||
| "document_project"
|
||||
| "document_project-link"
|
||||
| "document_vue"
|
||||
| "document_vite"
|
||||
| "document_unocss"
|
||||
| "document_naive"
|
||||
| "document_antd"
|
||||
>;
|
||||
|
||||
/**
|
||||
|
||||
2
src/typings/env.d.ts
vendored
2
src/typings/env.d.ts
vendored
@@ -103,6 +103,8 @@ declare namespace Env {
|
||||
readonly VITE_ICONIFY_URL?: string;
|
||||
/** Used to differentiate storage across different domains */
|
||||
readonly VITE_STORAGE_PREFIX?: string;
|
||||
/** Whether to automatically detect updates after configuring application packaging */
|
||||
readonly VITE_AUTOMATICALLY_DETECT_UPDATE?: CommonType.YesOrNo;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
src/typings/naive-ui.d.ts
vendored
2
src/typings/naive-ui.d.ts
vendored
@@ -26,7 +26,7 @@ declare namespace NaiveUI {
|
||||
|
||||
type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>;
|
||||
|
||||
type TableApiFn<T = any, R = Api.SystemManage.CommonSearchParams> = (
|
||||
type TableApiFn<T = any, R = Api.Common.CommonSearchParams> = (
|
||||
params: R
|
||||
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
|
||||
|
||||
|
||||
20
src/typings/package.d.ts
vendored
Normal file
20
src/typings/package.d.ts
vendored
Normal 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';
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import json5 from 'json5';
|
||||
|
||||
/**
|
||||
* Create service config by current env
|
||||
*
|
||||
@@ -8,10 +10,10 @@ export function createServiceConfig(env: Env.ImportMeta) {
|
||||
|
||||
let other = {} as Record<App.Service.OtherBaseURLKey, string>;
|
||||
try {
|
||||
other = JSON.parse(VITE_OTHER_SERVICE_BASE_URL);
|
||||
other = json5.parse(VITE_OTHER_SERVICE_BASE_URL);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('VITE_OTHER_SERVICE_BASE_URL is not a valid JSON string');
|
||||
console.error('VITE_OTHER_SERVICE_BASE_URL is not a valid json5 string');
|
||||
}
|
||||
|
||||
const httpConfig: App.Service.SimpleServiceConfig = {
|
||||
|
||||
85
src/views/about/index.vue
Normal file
85
src/views/about/index.vue
Normal 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>
|
||||
7
src/views/function/hide-child/one/index.vue
Normal file
7
src/views/function/hide-child/one/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/function/hide-child/three/index.vue
Normal file
7
src/views/function/hide-child/three/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/function/hide-child/two/index.vue
Normal file
7
src/views/function/hide-child/two/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
24
src/views/function/multi-tab/index.vue
Normal file
24
src/views/function/multi-tab/index.vue
Normal 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>
|
||||
63
src/views/function/request/index.vue
Normal file
63
src/views/function/request/index.vue
Normal 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>
|
||||
7
src/views/function/super-page/index.vue
Normal file
7
src/views/function/super-page/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
71
src/views/function/tab/index.vue
Normal file
71
src/views/function/tab/index.vue
Normal 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>
|
||||
103
src/views/function/toggle-auth/index.vue
Normal file
103
src/views/function/toggle-auth/index.vue
Normal 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>
|
||||
@@ -15,9 +15,6 @@ const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NAlert :title="$t('common.warning')" type="warning">
|
||||
{{ $t('page.home.branchDesc') }}
|
||||
</NAlert>
|
||||
<HeaderBanner />
|
||||
<CardData />
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||
|
||||
266
src/views/manage/menu/index.vue
Normal file
266
src/views/manage/menu/index.vue
Normal 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>
|
||||
467
src/views/manage/menu/modules/menu-operate-modal.vue
Normal file
467
src/views/manage/menu/modules/menu-operate-modal.vue
Normal 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>
|
||||
79
src/views/manage/menu/modules/shared.ts
Normal file
79
src/views/manage/menu/modules/shared.ts
Normal 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;
|
||||
}
|
||||
177
src/views/manage/role/index.vue
Normal file
177
src/views/manage/role/index.vue
Normal 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>
|
||||
101
src/views/manage/role/modules/button-auth-modal.vue
Normal file
101
src/views/manage/role/modules/button-auth-modal.vue
Normal 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>
|
||||
130
src/views/manage/role/modules/menu-auth-modal.vue
Normal file
130
src/views/manage/role/modules/menu-auth-modal.vue
Normal 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>
|
||||
134
src/views/manage/role/modules/role-operate-drawer.vue
Normal file
134
src/views/manage/role/modules/role-operate-drawer.vue
Normal 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>
|
||||
71
src/views/manage/role/modules/role-search.vue
Normal file
71
src/views/manage/role/modules/role-search.vue
Normal 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>
|
||||
13
src/views/manage/user-detail/[id].vue
Normal file
13
src/views/manage/user-detail/[id].vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
208
src/views/manage/user/index.vue
Normal file
208
src/views/manage/user/index.vue
Normal 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>
|
||||
166
src/views/manage/user/modules/user-operate-drawer.vue
Normal file
166
src/views/manage/user/modules/user-operate-drawer.vue
Normal 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>
|
||||
112
src/views/manage/user/modules/user-search.vue
Normal file
112
src/views/manage/user/modules/user-search.vue
Normal 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>
|
||||
7
src/views/multi-menu/first_child/index.vue
Normal file
7
src/views/multi-menu/first_child/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/multi-menu/second_child_home/index.vue
Normal file
7
src/views/multi-menu/second_child_home/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
114
src/views/plugin/barcode/index.vue
Normal file
114
src/views/plugin/barcode/index.vue
Normal 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>
|
||||
706
src/views/plugin/charts/echarts/data.ts
Normal file
706
src/views/plugin/charts/echarts/data.ts
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
91
src/views/plugin/charts/echarts/index.vue
Normal file
91
src/views/plugin/charts/echarts/index.vue
Normal 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>
|
||||
34
src/views/plugin/copy/index.vue
Normal file
34
src/views/plugin/copy/index.vue
Normal 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>
|
||||
50
src/views/plugin/editor/markdown/index.vue
Normal file
50
src/views/plugin/editor/markdown/index.vue
Normal 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>
|
||||
45
src/views/plugin/editor/quill/index.vue
Normal file
45
src/views/plugin/editor/quill/index.vue
Normal 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>
|
||||
199
src/views/plugin/excel/index.vue
Normal file
199
src/views/plugin/excel/index.vue
Normal 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>
|
||||
173
src/views/plugin/gantt/data.ts
Normal file
173
src/views/plugin/gantt/data.ts
Normal 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
|
||||
}
|
||||
];
|
||||
208
src/views/plugin/gantt/index.vue
Normal file
208
src/views/plugin/gantt/index.vue
Normal 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>
|
||||
32
src/views/plugin/icon/icons.ts
Normal file
32
src/views/plugin/icon/icons.ts
Normal 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'
|
||||
];
|
||||
51
src/views/plugin/icon/index.vue
Normal file
51
src/views/plugin/icon/index.vue
Normal 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>
|
||||
32
src/views/plugin/map/components/baidu-map.vue
Normal file
32
src/views/plugin/map/components/baidu-map.vue
Normal 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>
|
||||
32
src/views/plugin/map/components/gaode-map.vue
Normal file
32
src/views/plugin/map/components/gaode-map.vue
Normal 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>
|
||||
5
src/views/plugin/map/components/index.ts
Normal file
5
src/views/plugin/map/components/index.ts
Normal 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 };
|
||||
32
src/views/plugin/map/components/tencent-map.vue
Normal file
32
src/views/plugin/map/components/tencent-map.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useScriptTag } from '@vueuse/core';
|
||||
import { TENCENT_MAP_SDK_URL } from '@/constants/map-sdk';
|
||||
|
||||
defineOptions({ name: 'TencentMap' });
|
||||
|
||||
const { load } = useScriptTag(TENCENT_MAP_SDK_URL);
|
||||
|
||||
const domRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
async function renderMap() {
|
||||
await load(true);
|
||||
if (!domRef.value) return;
|
||||
// eslint-disable-next-line no-new
|
||||
new TMap.Map(domRef.value, {
|
||||
center: new TMap.LatLng(39.98412, 116.307484),
|
||||
zoom: 11,
|
||||
viewMode: '3D'
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="domRef" class="h-full w-full"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
30
src/views/plugin/map/index.vue
Normal file
30
src/views/plugin/map/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
import { BaiduMap, GaodeMap, TencentMap } from './components';
|
||||
|
||||
interface Map {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
const maps: Map[] = [
|
||||
{ id: 'gaode', label: '高德地图', component: GaodeMap },
|
||||
{ id: 'tencent', label: '腾讯地图', component: TencentMap },
|
||||
{ id: 'baidu', label: '百度地图', component: BaiduMap }
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<NCard title="地图插件" :bordered="false" class="h-full card-wrapper" content-style="overflow:hidden">
|
||||
<NTabs type="line" class="h-full flex-col-stretch" pane-class="flex-1-hidden">
|
||||
<NTabPane v-for="item in maps" :key="item.id" :name="item.id" :tab="item.label">
|
||||
<component :is="item.component" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
82
src/views/plugin/pdf/index.vue
Normal file
82
src/views/plugin/pdf/index.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import VuePdfEmbed from 'vue-pdf-embed';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
|
||||
const { loading, endLoading } = useLoading(true);
|
||||
|
||||
const pdfRef = shallowRef<InstanceType<typeof VuePdfEmbed> | null>(null);
|
||||
const source = `https://xiaoxian521.github.io/hyperlink/pdf/Cookie%E5%92%8CSession%E5%8C%BA%E5%88%AB%E7%94%A8%E6%B3%95.pdf`;
|
||||
|
||||
const showAllPages = ref(false);
|
||||
const currentPage = ref<undefined | number>(1);
|
||||
const pageCount = ref(1);
|
||||
|
||||
function onPdfRendered() {
|
||||
endLoading();
|
||||
|
||||
if (pdfRef.value?.doc) {
|
||||
pageCount.value = pdfRef.value.doc.numPages;
|
||||
}
|
||||
}
|
||||
|
||||
function showAllPagesChange() {
|
||||
currentPage.value = showAllPages.value ? undefined : 1;
|
||||
}
|
||||
|
||||
const rotations = [0, 90, 180, 270];
|
||||
const currentRotation = ref(0);
|
||||
|
||||
function handleRotate() {
|
||||
currentRotation.value = (currentRotation.value + 1) % 4;
|
||||
}
|
||||
|
||||
async function handlePrint() {
|
||||
await pdfRef.value?.print(undefined, 'test.pdf', true);
|
||||
}
|
||||
|
||||
async function handleDownload() {
|
||||
await pdfRef.value?.download('test.pdf');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<NCard title="PDF 预览" :bordered="false" class="h-full card-wrapper" content-class="overflow-hidden">
|
||||
<div class="h-full flex-col-stretch">
|
||||
<GithubLink link="https://github.com/hrynko/vue-pdf-embed" />
|
||||
<WebSiteLink label="文档地址:" link="https://www.npmjs.com/package/vue-pdf-embed" />
|
||||
<div class="flex-y-center justify-end gap-12px">
|
||||
<NCheckbox v-model:checked="showAllPages" @update:checked="showAllPagesChange">显示所有页面</NCheckbox>
|
||||
<ButtonIcon tooltip-content="旋转90度" @click="handleRotate">
|
||||
<icon-material-symbols-light:rotate-90-degrees-ccw-outline-rounded />
|
||||
</ButtonIcon>
|
||||
<ButtonIcon tooltip-content="打印" @click="handlePrint">
|
||||
<icon-mdi:printer />
|
||||
</ButtonIcon>
|
||||
<ButtonIcon tooltip-content="下载" @click="handleDownload">
|
||||
<icon-charm:download />
|
||||
</ButtonIcon>
|
||||
</div>
|
||||
<NScrollbar class="flex-1-hidden">
|
||||
<NSkeleton v-if="loading" size="small" class="mt-12px" text :repeat="12" />
|
||||
<VuePdfEmbed
|
||||
ref="pdfRef"
|
||||
class="overflow-auto container"
|
||||
:class="{ 'h-0': loading }"
|
||||
:rotation="rotations[currentRotation]"
|
||||
:page="currentPage"
|
||||
:source="source"
|
||||
@rendered="onPdfRendered"
|
||||
/>
|
||||
</NScrollbar>
|
||||
<div class="flex-y-center justify-between">
|
||||
<div v-if="showAllPages" class="text-18px font-medium">共{{ pageCount }}页</div>
|
||||
<NPagination v-else v-model:page="currentPage" :page-count="pageCount" :page-size="1" />
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
57
src/views/plugin/pinyin/index.vue
Normal file
57
src/views/plugin/pinyin/index.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { html } from 'pinyin-pro';
|
||||
import domPurify from 'dompurify';
|
||||
|
||||
const domRef = ref<HTMLElement | null>(null);
|
||||
const domRef2 = ref<HTMLElement | null>(null);
|
||||
const domRef3 = ref<HTMLElement | null>(null);
|
||||
|
||||
function renderHtml() {
|
||||
if (!domRef.value || !domRef2.value || !domRef3.value) return;
|
||||
|
||||
const text = 'SoybeanAdmin是一个清新优雅、高颜值且功能强大的后台管理模板';
|
||||
|
||||
const code = domPurify.sanitize(html(text));
|
||||
const code2 = domPurify.sanitize(html(text, { toneType: 'none' }));
|
||||
|
||||
domRef.value.innerHTML = code;
|
||||
domRef2.value.innerHTML = code2;
|
||||
domRef3.value.innerHTML = code;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderHtml();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NCard title="pinyin 插件" :bordered="false" class="h-full card-wrapper">
|
||||
<NSpace :vertical="true">
|
||||
<GithubLink link="https://github.com/zh-lx/pinyin-pro" />
|
||||
<WebSiteLink label="文档地址:" link="https://pinyin-pro.cn/" />
|
||||
</NSpace>
|
||||
<NDivider title-placement="left">常规使用</NDivider>
|
||||
<p ref="domRef" class="text-18px"></p>
|
||||
<NDivider title-placement="left">不带音调</NDivider>
|
||||
<p ref="domRef2" class="text-18px"></p>
|
||||
<NDivider title-placement="left">自定义样式</NDivider>
|
||||
<p ref="domRef3" class="custom-style text-18px"></p>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-style {
|
||||
:deep(.py-result-item) {
|
||||
.py-chinese-item {
|
||||
--uno: text-primary;
|
||||
}
|
||||
|
||||
.py-pinyin-item {
|
||||
--uno: text-error;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
src/views/plugin/print/index.vue
Normal file
39
src/views/plugin/print/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import printJS from 'print-js';
|
||||
|
||||
function printTable() {
|
||||
printJS({
|
||||
printable: [
|
||||
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' },
|
||||
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' }
|
||||
],
|
||||
properties: ['name', 'wechat', 'remark'],
|
||||
type: 'json'
|
||||
});
|
||||
}
|
||||
function printImage() {
|
||||
printJS({
|
||||
printable: [
|
||||
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg',
|
||||
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg'
|
||||
],
|
||||
type: 'image',
|
||||
header: 'Multiple Images',
|
||||
imageStyle: 'width:100%;'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<NCard title="打印" :bordered="false" class="card-wrapper">
|
||||
<NButton type="primary" class="mr-10px" @click="printTable">打印表格</NButton>
|
||||
<NButton type="primary" @click="printImage">打印图片</NButton>
|
||||
<template #footer>
|
||||
<GithubLink label="printJS:" link="https://github.com/crabbly/Print.js" class="mt-10px" />
|
||||
</template>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
110
src/views/plugin/swiper/index.vue
Normal file
110
src/views/plugin/swiper/index.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import SwiperCore from 'swiper';
|
||||
import { Navigation, Pagination } from 'swiper/modules';
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||
import type { SwiperOptions } from 'swiper/types';
|
||||
|
||||
type SwiperExampleOptions = Pick<
|
||||
SwiperOptions,
|
||||
'navigation' | 'pagination' | 'scrollbar' | 'slidesPerView' | 'slidesPerGroup' | 'spaceBetween' | 'direction' | 'loop'
|
||||
>;
|
||||
|
||||
interface SwiperExample {
|
||||
id: number;
|
||||
label: string;
|
||||
options: Partial<SwiperExampleOptions>;
|
||||
}
|
||||
|
||||
SwiperCore.use([Navigation, Pagination]);
|
||||
|
||||
const swiperExample: SwiperExample[] = [
|
||||
{ id: 0, label: 'Default', options: {} },
|
||||
{
|
||||
id: 1,
|
||||
label: 'Navigation',
|
||||
options: {
|
||||
navigation: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Pagination',
|
||||
options: {
|
||||
pagination: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'Pagination dynamic',
|
||||
options: {
|
||||
pagination: { dynamicBullets: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'Pagination progress',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
type: 'progressbar'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'Pagination fraction',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
type: 'fraction'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: 'Slides per view',
|
||||
options: {
|
||||
pagination: {
|
||||
clickable: true
|
||||
},
|
||||
slidesPerView: 3,
|
||||
spaceBetween: 30
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: 'Infinite loop',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
clickable: true
|
||||
},
|
||||
loop: true
|
||||
}
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NCard title="Swiper插件" :bordered="false" class="card-wrapper">
|
||||
<NSpace :vertical="true">
|
||||
<GithubLink link="https://github.com/nolimits4web/swiper" />
|
||||
<WebSiteLink label="vue3版文档地址:" link="https://swiperjs.com/vue" />
|
||||
<WebSiteLink label="插件demo地址:" link="https://swiperjs.com/demos" />
|
||||
</NSpace>
|
||||
<NSpace :vertical="true">
|
||||
<div v-for="item in swiperExample" :key="item.id">
|
||||
<h3 class="py-24px text-24px font-bold">{{ item.label }}</h3>
|
||||
<Swiper v-bind="item.options">
|
||||
<SwiperSlide v-for="i in 5" :key="i">
|
||||
<div class="h-240px w-full flex-center border-1px border-#999 text-18px font-bold">Slide{{ i }}</div>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
</div>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user