mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-04-06 03:57:54 +08:00
Compare commits
105 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a5c3697a6b | ||
|
d97306d5b5 | ||
|
99840ed604 | ||
|
75dd7b0c83 | ||
|
2f2d8726d4 | ||
|
30f0ac0904 | ||
|
5cb0ca39dd | ||
|
e5222bbbc6 | ||
|
52614819e8 | ||
|
4f31476cf8 | ||
|
85b0826eea | ||
|
5975d05437 | ||
|
aa9aece0ca | ||
|
a487141cfb | ||
|
e7148081a9 | ||
|
308e0b4bf9 | ||
|
367406258c | ||
|
aee3e52f15 | ||
|
89d78b7ec7 | ||
|
242c94723b | ||
|
dc3563969b | ||
|
7da454563b | ||
|
e450a029ac | ||
|
c14ebc2343 | ||
|
5e88b8d01f | ||
|
5fb8881763 | ||
|
efc1ccbc9a | ||
|
f20d2a5fb6 | ||
|
fad054e8df | ||
|
df0cf9f72b | ||
|
d69bd796a4 | ||
|
a9c7708119 | ||
|
de4cd17548 | ||
|
70c43a276c | ||
|
5cc410c7b4 | ||
|
5c24fa1502 | ||
|
4d82a24d22 | ||
|
20d9fbef2e | ||
|
4b1d3f2912 | ||
|
6c03ef53a3 | ||
|
23afe39c65 | ||
|
309d723e43 | ||
|
0e9bf396f3 | ||
|
2c0b2fb26c | ||
|
daf3cf9ca7 | ||
|
48054e04f9 | ||
|
6b40f45ae3 | ||
|
0741c564dd | ||
|
2305fff569 | ||
|
cf76ef71f7 | ||
|
e7c6f7c177 | ||
|
eb82842fad | ||
|
e1d440b45a | ||
|
1a1ffcb9aa | ||
|
8981f42571 | ||
|
21544139df | ||
|
9ce6bd3b86 | ||
|
8f5f11f4d3 | ||
|
9aed794344 | ||
|
808c4d0cdf | ||
|
6449845ab6 | ||
|
46766a54fb | ||
|
57739f960b | ||
|
530231a5cb | ||
|
86ef62f841 | ||
|
89e8b0e3e1 | ||
|
ec85ffaea2 | ||
|
02ce6568b7 | ||
|
8a5d8a67ea | ||
|
f426bffbc7 | ||
|
8ead40457b | ||
|
39d185b132 | ||
|
bf5445b6e4 | ||
|
bf1cfcdd27 | ||
|
a149e301dd | ||
|
e201ff071f | ||
|
4aa3a66ce9 | ||
|
448f3ba494 | ||
|
648a0ba098 | ||
|
5945e63324 | ||
|
712bd53bf9 | ||
|
2fc1dd4467 | ||
|
5f7c77d9c6 | ||
|
4dde8b78dd | ||
|
8419f29d84 | ||
|
04a93e667b | ||
|
4959d0f1b9 | ||
|
bdc8764a2b | ||
|
344baa7cd1 | ||
|
8bafc3aa36 | ||
|
922e82d12f | ||
|
3b3b964067 | ||
|
35df1832f4 | ||
|
df6c8e5aef | ||
|
8ae6acd62d | ||
|
1387db71bc | ||
|
1ccc3f371a | ||
|
61bbdedec1 | ||
|
187c26832c | ||
|
6ea0c7645f | ||
|
a9626d3ace | ||
|
c7483141ce | ||
|
b2c4585927 | ||
|
21a51b935c | ||
|
5046531816 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
/node_modules
|
||||
/.git
|
||||
/.gitignore
|
||||
/.vscode
|
||||
/.DS_Store
|
||||
/*.md
|
||||
/dist
|
||||
|
24
.env
24
.env
@ -1,14 +1,26 @@
|
||||
# 项目根目录
|
||||
VITE_BASE_URL=/
|
||||
VITE_BASE_URL = /
|
||||
|
||||
# 项目名称
|
||||
VITE_APP_NAME=Nova - Admin
|
||||
# 路由模式
|
||||
VITE_APP_NAME = Nova - Admin
|
||||
|
||||
# 路由模式 web | hash
|
||||
VITE_ROUTE_MODE = web
|
||||
# 权限路由模式: static | dynamic
|
||||
VITE_AUTH_ROUTE_MODE=static
|
||||
|
||||
# 路由加载模式 static | dynamic
|
||||
VITE_ROUTE_LOAD_MODE = static
|
||||
|
||||
# 设置登陆后跳转地址
|
||||
VITE_HOME_PATH = /dashboard/workbench
|
||||
|
||||
# 本地存储前缀
|
||||
VITE_STORAGE_PREFIX=
|
||||
VITE_STORAGE_PREFIX =
|
||||
|
||||
# 版权信息
|
||||
VITE_COPYRIGHT_INFO = Copyright © 2024 chansee97
|
||||
|
||||
# 自动刷新token
|
||||
VITE_AUTO_REFRESH_TOKEN = Y
|
||||
|
||||
# 默认多语言 enUS | zhCN
|
||||
VITE_DEFAULT_LANG = enUS
|
||||
|
42
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: 🐞 Bug report
|
||||
description: Create a report to help us improve
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please explain clearly how the bug reappears. If possible, it is best to add the cause of the problem.
|
||||
placeholder: bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-comments
|
||||
attributes:
|
||||
label: Additional comments
|
||||
description: e.g. some background/context of how you ran into this bug.
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Ensure this issue not a bug proposal.
|
||||
required: true
|
||||
- label: Read the [docs](https://nova-admin-docs.pages.dev/).
|
||||
required: true
|
||||
- label: Check that there isn't [already an issue](https://github.com/chansee97/nova-admin/issues) that descript the same thing to avoid creating a duplicate.
|
||||
required: true
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
45
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: ✨ New feature
|
||||
|
||||
description: Propose a new feature to be added to Nova-admin
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Clear and concise description of the problem. Please make the reason and usecases as detailed as possible. If you intend to submit a PR for this issue, tell us in the description. Thanks!
|
||||
placeholder: As a developer using Nova-admin I want [goal / wish] so that [benefit]...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: Suggestion
|
||||
description: In module [xy] we could provide following implementation...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any other context or screenshots about the feature request here.
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Ensure this issue not a feature proposal.
|
||||
required: true
|
||||
- label: Read the [docs](https://nova-admin-docs.pages.dev/).
|
||||
required: true
|
||||
- label: Check that there isn't [already an issue](https://github.com/chansee97/nova-admin/issues) that descript the same thing to avoid creating a duplicate.
|
||||
required: true
|
31
.github/ISSUE_TEMPLATE/others.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/others.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: 👓 Others
|
||||
|
||||
description: Create an issue for Nova-admin
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to create this issue!
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Clear and concise description of the issue. Thanks!
|
||||
placeholder: There are some thing I want to ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Ensure this issue neither a bug report nor a feature proposal.
|
||||
required: true
|
||||
- label: Read the [docs](https://nova-admin-docs.pages.dev/).
|
||||
required: true
|
||||
- label: Check that there isn't [already an issue](https://github.com/chansee97/nova-admin/issues) that descript the same thing to avoid creating a duplicate.
|
||||
required: true
|
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -15,11 +15,12 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
node-version: 20.x
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
- run: npx changelogithub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -25,8 +25,8 @@ stats.html
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/src/typings/components.d.ts
|
||||
/src/typings/auto-imports.d.ts
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
components.d.ts
|
||||
auto-imports.d.ts
|
||||
|
||||
# Lock files
|
||||
*-lock.yaml
|
||||
|
76
.vscode/settings.json
vendored
76
.vscode/settings.json
vendored
@ -1,6 +1,4 @@
|
||||
{
|
||||
// Enable the ESlint flat config support
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
@ -11,46 +9,16 @@
|
||||
},
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{
|
||||
"rule": "style/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "format/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-indent",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spacing",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spaces",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-order",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-dangle",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-newline",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*quotes",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*semi",
|
||||
"severity": "off"
|
||||
}
|
||||
{ "rule": "style/*", "severity": "off" },
|
||||
{ "rule": "format/*", "severity": "off" },
|
||||
{ "rule": "*-indent", "severity": "off" },
|
||||
{ "rule": "*-spacing", "severity": "off" },
|
||||
{ "rule": "*-spaces", "severity": "off" },
|
||||
{ "rule": "*-order", "severity": "off" },
|
||||
{ "rule": "*-dangle", "severity": "off" },
|
||||
{ "rule": "*-newline", "severity": "off" },
|
||||
{ "rule": "*quotes", "severity": "off" },
|
||||
{ "rule": "*semi", "severity": "off" }
|
||||
],
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
@ -64,7 +32,16 @@
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml"
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
],
|
||||
"i18n-ally.sourceLanguage": "zh_CN",
|
||||
"i18n-ally.displayLanguage": "zh_CN",
|
||||
@ -74,5 +51,16 @@
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
],
|
||||
"commentTranslate.source": "Google"
|
||||
// File collapse
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
|
||||
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
|
||||
"*.env": "$(capture).env.*",
|
||||
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
||||
"eslint.config.js": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
|
||||
"docker-compose.product.yml": ".dockerignore,nginx.conf"
|
||||
}
|
||||
}
|
||||
|
34
README.md
34
README.md
@ -5,7 +5,8 @@
|
||||
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/github/license/chansee97/nova-admin"/>
|
||||
<img src="https://img.shields.io/github/stars/chansee97/nova-admin"/>
|
||||
<img src="https://badgen.net/github/stars/chansee97/nova-admin?icon=github"/>
|
||||
<img src="https://gitee.com/chansee97/nova-admin/badge/star.svg"/>
|
||||
<img src="https://img.shields.io/github/forks/chansee97/nova-admin"/>
|
||||
</div>
|
||||
|
||||
@ -18,8 +19,8 @@
|
||||
|
||||
[Nova-admin](https://github.com/chansee97/nova-admin) is a clean and concise back-end management template based on Vue3, Vite5, Typescript, and Naive UI. It implements complete functionality in a simple way, while also considering code standards, readability, and avoiding excessive encapsulation to facilitate secondary development.
|
||||
|
||||
- [Nova-Admin preview](https://nova-admin-site.netlify.app/)
|
||||
- [Nova-Admin docs](https://nova-admin-docs.netlify.app/)
|
||||
- [Nova-Admin preview](https://nova-admin.pages.dev/)
|
||||
- [Nova-Admin docs](https://nova-admin-docs.pages.dev/)
|
||||
|
||||
## Features
|
||||
|
||||
@ -47,14 +48,17 @@
|
||||
- [Gitee](https://gitee.com/chansee97/nova-admin)
|
||||
- [Github](https://github.com/chansee97/nova-admin)
|
||||
|
||||
## Related projects
|
||||
## Interface document
|
||||
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (under development) Nova-Admin supporting background project based on TS, NestJs, typeorm
|
||||
This project uses ApiFox for interface mock, check the online documentation for more interface details
|
||||
[online aipfox docs](https://nova-admin.apifox.cn)
|
||||
|
||||
## Install and use
|
||||
|
||||
The local development environment is recommended to use pnpm 8.x, Node.js must be version 20.x.
|
||||
|
||||
It is recommended to directly download the compressed package from [Releases](https://github.com/chansee97/nova-admin/releases)
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
pnpm i
|
||||
@ -67,20 +71,26 @@ pnpm build
|
||||
|
||||
```
|
||||
|
||||
## Interface document
|
||||
You can deploy **nova-admin** in a production environment using docker-compose.
|
||||
```bash
|
||||
# Build product
|
||||
docker compose -f docker-compose.product.yml up --build -d
|
||||
```
|
||||
> The nginx.conf provided is for reference only. You can adjust it according to your own needs.
|
||||
|
||||
This project uses ApiFox for interface mock, check the online documentation for more interface details
|
||||
[online aipfox docs](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
|
||||
## Related projects
|
||||
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (under development) Nova-Admin supporting background project based on TS, NestJs, typeorm
|
||||
|
||||
## Learn to communicate
|
||||
|
||||
Nova-Admin is a completely open-source and free project. It is still being optimized and iterated. It is designed to help developers more conveniently develop medium and large management systems. If you have any questions, please ask questions in the QQ exchange group.
|
||||
|
||||
| Q-Group | wechat-Group |wechat |
|
||||
| :--: |:--: |:--: |
|
||||
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> | <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/wx-group.png" width=170>|<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
|
||||
| Q-Group | wechat-Group |
|
||||
| :--: |:--: |
|
||||
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> |<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
|
||||
|
||||
> The WeChat group QR code is invalid, please add me as a friend.
|
||||
> Please indicate the purpose of adding WeChat.
|
||||
|
||||
## Contribution
|
||||
|
||||
|
@ -5,7 +5,8 @@
|
||||
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/github/license/chansee97/nova-admin"/>
|
||||
<img src="https://img.shields.io/github/stars/chansee97/nova-admin"/>
|
||||
<img src="https://badgen.net/github/stars/chansee97/nova-admin?icon=github"/>
|
||||
<img src="https://gitee.com/chansee97/nova-admin/badge/star.svg"/>
|
||||
<img src="https://img.shields.io/github/forks/chansee97/nova-admin"/>
|
||||
</div>
|
||||
|
||||
@ -18,8 +19,8 @@
|
||||
|
||||
[Nova-admin](https://github.com/chansee97/nova-admin)是一个基于Vue3、Vite5、Typescript、Naive UI, 简洁干净后台管理模板,用简单的方式实现完整功能,并尽可能的考虑代码规范,易读易理解无过度封装,方便二次开发。
|
||||
|
||||
- [Nova-Admin 预览](https://nova-admin-site.netlify.app/)
|
||||
- [Nova-Admin 文档](https://nova-admin-docs.netlify.app/)
|
||||
- [Nova-Admin 预览](https://nova-admin.pages.dev/)
|
||||
- [Nova-Admin 文档](https://nova-admin-docs.pages.dev/)
|
||||
|
||||
## 特性
|
||||
|
||||
@ -47,14 +48,17 @@
|
||||
- [Gitee](https://gitee.com/chansee97/nova-admin)
|
||||
- [Github](https://github.com/chansee97/nova-admin)
|
||||
|
||||
## 相关项目
|
||||
## 接口文档
|
||||
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (开发中)基于TS, NestJs, typeorm的Nova-Admin配套后台项目
|
||||
本项目使用ApiFox进行接口mock,查看在线文档以了解更多接口详情
|
||||
[在线apifox文档](https://nova-admin.apifox.cn)
|
||||
|
||||
## 安装使用
|
||||
|
||||
本地开发环境建议使用 pnpm 8.x 、Node.js 必须 20.x
|
||||
|
||||
推荐直接下载[Releases](https://github.com/chansee97/nova-admin/releases)压缩包
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
pnpm i
|
||||
@ -67,20 +71,26 @@ pnpm build
|
||||
|
||||
```
|
||||
|
||||
## 接口文档
|
||||
在生产环境也可以使用 docker-compose 部署 **nova-admin**
|
||||
```bash
|
||||
# Build product
|
||||
docker compose -f docker-compose.product.yml up --build -d
|
||||
```
|
||||
> 关于 nginx.conf 只供参考,你可以根据自己的需求进行调整。
|
||||
|
||||
本项目使用ApiFox进行接口mock,查看在线文档以了解更多接口详情
|
||||
[在线apifox文档](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
|
||||
## 相关项目
|
||||
|
||||
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (开发中)基于TS, NestJs, typeorm的Nova-Admin配套后台项目
|
||||
|
||||
## 学习交流
|
||||
|
||||
Nova-Admin 是完全开源免费的项目,目前仍然在优化迭代中,旨在帮助开发者更方便地进行中大型管理系统开发,有使用问题欢迎在交流群内提问。
|
||||
|
||||
| Q群 | 微信群 | 个人微信 |
|
||||
| :--: |:--: |:--: |
|
||||
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> | <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/wx-group.png" width=170>|<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
|
||||
| Q群 | 微信群 |
|
||||
| :--: |:--: |
|
||||
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> |<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
|
||||
|
||||
> 微信群二维码失效请加我为好友
|
||||
> 添加微信请注明来意
|
||||
|
||||
## 贡献
|
||||
|
||||
@ -101,6 +111,7 @@ Nova-Admin 是完全开源免费的项目,目前仍然在优化迭代中,旨
|
||||
<a href="https://github.com/chansee97/nova-admin/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=chansee97/nova-admin" alt="contributors" />
|
||||
</a>
|
||||
|
||||
## Star 历史
|
||||
|
||||
[](https://star-history.com/#chansee97/nova-admin&Date)
|
||||
|
@ -1,15 +1,16 @@
|
||||
import UnoCSS from '@unocss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||
|
||||
// https://github.com/antfu/unplugin-icons
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
/**
|
||||
* @description: 设置vite插件配置
|
||||
@ -21,13 +22,29 @@ export function createVitePlugins(env: ImportMetaEnv) {
|
||||
// support vue
|
||||
vue(),
|
||||
vueJsx(),
|
||||
VueDevTools(),
|
||||
|
||||
// support unocss
|
||||
UnoCSS(),
|
||||
|
||||
// auto import api of lib
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'pinia',
|
||||
'@vueuse/core',
|
||||
'vue-i18n',
|
||||
{
|
||||
'naive-ui': [
|
||||
'useDialog',
|
||||
'useMessage',
|
||||
'useNotification',
|
||||
'useLoadingBar',
|
||||
'useModal',
|
||||
],
|
||||
},
|
||||
],
|
||||
include: [
|
||||
/\.[tj]sx?$/,
|
||||
/\.vue$/,
|
||||
|
8
docker-compose.product.yml
Normal file
8
docker-compose.product.yml
Normal file
@ -0,0 +1,8 @@
|
||||
services:
|
||||
nove-admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/dockerfile.product
|
||||
container_name: nove-admin
|
||||
ports:
|
||||
- 80:80
|
23
docker/dockerfile.product
Normal file
23
docker/dockerfile.product
Normal file
@ -0,0 +1,23 @@
|
||||
FROM node:20-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm run build
|
||||
|
||||
FROM nginx:1.23.1-alpine
|
||||
|
||||
WORKDIR /www
|
||||
|
||||
COPY --from=builder /app/dist/ .
|
||||
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
@ -2,4 +2,21 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
// https://github.com/antfu/eslint-config
|
||||
export default antfu()
|
||||
export default antfu(
|
||||
{
|
||||
typescript: {
|
||||
overrides: {
|
||||
'perfectionist/sort-exports': 'off',
|
||||
'perfectionist/sort-imports': 'off',
|
||||
'ts/no-unused-expressions': ['error', { allowShortCircuit: true }],
|
||||
},
|
||||
},
|
||||
vue: {
|
||||
overrides: {
|
||||
'vue/no-unused-refs': 'off', // 暂时关闭,等待vue-lint的分支合并
|
||||
'vue/no-reserved-component-names': 'off',
|
||||
'vue/component-definition-name-casing': 'off',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -3,14 +3,18 @@
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"close": "Closure",
|
||||
"reload": "Refresh"
|
||||
"reload": "Refresh",
|
||||
"choose": "Choose",
|
||||
"navigate": "Navigate",
|
||||
"inputPlaceholder": "please enter",
|
||||
"selectPlaceholder": "please choose"
|
||||
},
|
||||
"app": {
|
||||
"loginOut": "Login out",
|
||||
"loginOutContent": "Confirm to log out of current account?",
|
||||
"loginOutTitle": "Sign out",
|
||||
"userCenter": "Personal center",
|
||||
"lignt": "Light",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System",
|
||||
"backTop": "Back to top",
|
||||
@ -38,6 +42,7 @@
|
||||
"themeSetting": "Theme settings",
|
||||
"todos": "Todos",
|
||||
"toggleFullScreen": "Toggle full screen",
|
||||
"togglContentFullScreen": "Toggle content full screen",
|
||||
"topProgress": "Top progress",
|
||||
"transitionFadeBottom": "Bottom fade",
|
||||
"transitionFadeScale": "Scale fade",
|
||||
@ -55,7 +60,8 @@
|
||||
"getRouteError": "Failed to obtain route, please try again later.",
|
||||
"layoutSetting": "Layout settings",
|
||||
"leftMenu": "Left menu",
|
||||
"topMenu": "Top menu"
|
||||
"topMenu": "Top menu",
|
||||
"mixMenu": "Mix menu"
|
||||
},
|
||||
"login": {
|
||||
"signInTitle": "Login",
|
||||
@ -83,17 +89,18 @@
|
||||
"route": {
|
||||
"appRoot": "Home",
|
||||
"cardList": "Card list",
|
||||
"draggableList": "Draggable list",
|
||||
"commonList": "Common list",
|
||||
"dashboard": "Dashboard",
|
||||
"demo": "Function example",
|
||||
"fetch": "Request example",
|
||||
"list": "List",
|
||||
"monitor": "Monitoring",
|
||||
"test": "Multi-level menu",
|
||||
"test2": "Multi-level menu subpage",
|
||||
"test2Detail": "Details page of multi-level menu",
|
||||
"test3": "multi-level menu",
|
||||
"test4": "Multi-level menu 3-1",
|
||||
"multi": "Multi-level menu",
|
||||
"multi2": "Multi-level menu subpage",
|
||||
"multi2Detail": "Details page of multi-level menu",
|
||||
"multi3": "multi-level menu",
|
||||
"multi4": "Multi-level menu 3-1",
|
||||
"workbench": "Workbench",
|
||||
"QRCode": "QR code",
|
||||
"about": "About",
|
||||
@ -102,10 +109,11 @@
|
||||
"demo404": "404",
|
||||
"demo500": "500",
|
||||
"dictionarySetting": "Dictionary settings",
|
||||
"docments": "Document",
|
||||
"docmentsVite": "Vite",
|
||||
"docmentsVue": "Vue",
|
||||
"docmentsVueuse": "VueUse (external link)",
|
||||
"documents": "Document",
|
||||
"documentsVite": "Vite",
|
||||
"documentsVue": "Vue",
|
||||
"documentsVueuse": "VueUse (external link)",
|
||||
"documentsNova": "Nova docs",
|
||||
"echarts": "Echarts",
|
||||
"editor": "Editor",
|
||||
"editorMd": "MarkDown editor",
|
||||
@ -119,7 +127,9 @@
|
||||
"permissionDemo": "Permissions example",
|
||||
"setting": "System settings",
|
||||
"userCenter": "Personal Center",
|
||||
"accountSetting": "User settings"
|
||||
"accountSetting": "User settings",
|
||||
"cascader": "Administrative region selection",
|
||||
"dict": "Dictionary example"
|
||||
},
|
||||
"http": {
|
||||
"400": "Syntax error in the request",
|
||||
@ -139,7 +149,15 @@
|
||||
"components": {
|
||||
"iconSelector": {
|
||||
"inputPlaceholder": "Select target icon",
|
||||
"searchPlaceholder": "Search icon"
|
||||
"searchPlaceholder": "Search icon",
|
||||
"clearIcon": "Clear icon",
|
||||
"selectorTitle": "Icon selection"
|
||||
},
|
||||
"copyText": {
|
||||
"message": "Copied successfully",
|
||||
"tooltip": "Copy",
|
||||
"unsupportedError": "Your browser does not support Clipboard API",
|
||||
"unpermittedError": "Crrently not permitted to use Clipboard API"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,24 @@
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"reload": "刷新",
|
||||
"close": "关闭"
|
||||
"close": "关闭",
|
||||
"choose": "选择",
|
||||
"navigate": "切换",
|
||||
"inputPlaceholder": "请输入",
|
||||
"selectPlaceholder": "请选择"
|
||||
},
|
||||
"app": {
|
||||
"loginOut": "退出登录",
|
||||
"loginOutTitle": "退出登录",
|
||||
"loginOutContent": "确认退出当前账号?",
|
||||
"userCenter": "个人中心",
|
||||
"lignt": "浅色",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "跟随系统",
|
||||
"backTop": "返回顶部",
|
||||
"toggleSider": "切换侧边栏",
|
||||
"toggleFullScreen": "切换全屏",
|
||||
"togglContentFullScreen": "切换内容全屏",
|
||||
"notificationsTips": "消息通知",
|
||||
"notifications": "通知",
|
||||
"messages": "消息",
|
||||
@ -55,7 +60,8 @@
|
||||
"getRouteError": "获取路由失败,请稍后再试",
|
||||
"layoutSetting": "布局设置",
|
||||
"leftMenu": "左侧菜单",
|
||||
"topMenu": "顶部菜单"
|
||||
"topMenu": "顶部菜单",
|
||||
"mixMenu": "混合菜单"
|
||||
},
|
||||
"http": {
|
||||
"400": "请求出现语法错误",
|
||||
@ -74,8 +80,16 @@
|
||||
},
|
||||
"components": {
|
||||
"iconSelector": {
|
||||
"selectorTitle": "图标选择",
|
||||
"inputPlaceholder": "选择目标图标",
|
||||
"searchPlaceholder": "搜索图标"
|
||||
"searchPlaceholder": "搜索图标",
|
||||
"clearIcon": "清除图标"
|
||||
},
|
||||
"copyText": {
|
||||
"tooltip": "复制",
|
||||
"message": "复制成功",
|
||||
"unsupportedError": "您的浏览器不支持剪贴板API",
|
||||
"unpermittedError": "目前不允许使用剪贴板API"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@ -106,14 +120,15 @@
|
||||
"dashboard": "仪表盘",
|
||||
"workbench": "工作台",
|
||||
"monitor": "监控页",
|
||||
"test": "多级菜单演示",
|
||||
"test2": "多级菜单子页",
|
||||
"test2Detail": "多级菜单的详情页",
|
||||
"test3": "多级菜单",
|
||||
"test4": "多级菜单3-1",
|
||||
"multi": "多级菜单演示",
|
||||
"multi2": "多级菜单子页",
|
||||
"multi2Detail": "多级菜单的详情页",
|
||||
"multi3": "多级菜单",
|
||||
"multi4": "多级菜单3-1",
|
||||
"list": "列表页",
|
||||
"commonList": "常用列表",
|
||||
"cardList": "卡片列表",
|
||||
"draggableList": "拖拽列表",
|
||||
"demo": "功能示例",
|
||||
"fetch": "请求示例",
|
||||
"echarts": "Echarts示例",
|
||||
@ -124,10 +139,11 @@
|
||||
"clipboard": "剪贴板",
|
||||
"icons": "图标",
|
||||
"QRCode": "二维码",
|
||||
"docments": "文档",
|
||||
"docmentsVue": "Vue",
|
||||
"docmentsVite": "Vite",
|
||||
"docmentsVueuse": "VueUse(外链)",
|
||||
"documents": "文档",
|
||||
"documentsVue": "Vue",
|
||||
"documentsVite": "Vite",
|
||||
"documentsVueuse": "VueUse(外链)",
|
||||
"documentsNova": "Nova 文档",
|
||||
"permission": "权限",
|
||||
"permissionDemo": "权限示例",
|
||||
"justSuper": "super可见",
|
||||
@ -140,6 +156,8 @@
|
||||
"dictionarySetting": "字典设置",
|
||||
"menuSetting": "菜单设置",
|
||||
"userCenter": "个人中心",
|
||||
"about": "关于"
|
||||
"about": "关于",
|
||||
"cascader": "省市区联动",
|
||||
"dict": "字典示例"
|
||||
}
|
||||
}
|
||||
|
66
nginx.conf
Normal file
66
nginx.conf
Normal file
@ -0,0 +1,66 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
# 启用 gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 10240;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
# 设定 MIME types
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
# 基本安全设定
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
# 增加伺服器效能的配置
|
||||
client_max_body_size 100M;
|
||||
client_body_buffer_size 128k;
|
||||
proxy_connect_timeout 90;
|
||||
proxy_send_timeout 90;
|
||||
proxy_read_timeout 90;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 32k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
|
||||
location / {
|
||||
root /www;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# 设定快取控制
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# 动态内容不快取
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
expires -1;
|
||||
}
|
||||
|
||||
# 错误处理
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||
proxy_intercept_errors on;
|
||||
|
||||
# 基本的代理设定
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 禁止访问隐藏文件
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
76
package.json
76
package.json
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "nova-admin",
|
||||
"type": "module",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.12",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"description": "a clean and concise back-end management template based on Vue3, Vite5, Typescript, and Naive UI.",
|
||||
"author": {
|
||||
"name": "chansee97",
|
||||
"email": "chen.dev@foxmail.com",
|
||||
@ -40,54 +40,52 @@
|
||||
"dev": "vite --mode dev --port 9980",
|
||||
"dev:test": "vite --mode test",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"build": "vue-tsc --noEmit && vite build --mode prod",
|
||||
"build:dev": "vue-tsc --noEmit && vite build --mode dev",
|
||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||
"build": "vite build --mode prod",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:test": "vite build --mode test",
|
||||
"preview": "vite preview --port 9981",
|
||||
"lint": "eslint .",
|
||||
"lint": "eslint . && vue-tsc --noEmit",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:check": "npx @eslint/config-inspector",
|
||||
"sizecheck": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/scene-vue": "^1.4.6",
|
||||
"@tinymce/tinymce-vue": "^5.1.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"alova": "^2.19.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"alova": "^3.2.10",
|
||||
"colord": "^2.9.3",
|
||||
"echarts": "^5.5.0",
|
||||
"md-editor-v3": "^4.11.3",
|
||||
"performant-array-to-tree": "^1.11.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"qs": "^6.12.0",
|
||||
"echarts": "^5.6.0",
|
||||
"md-editor-v3": "^5.4.5",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"quill": "^2.0.3",
|
||||
"radash": "^12.1.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.11.1",
|
||||
"vue-router": "^4.3.0"
|
||||
"vue": "^3.5.13",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.13.3",
|
||||
"@iconify-json/icon-park-outline": "^1.1.15",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/qs": "^6.9.14",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"eslint": "^9.0.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"naive-ui": "^2.38.1",
|
||||
"sass": "^1.75.0",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"typescript": "^5.4.5",
|
||||
"unocss": "^0.59.1",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.2.8",
|
||||
"vite-bundle-visualizer": "^1.1.0",
|
||||
"@antfu/eslint-config": "^4.11.0",
|
||||
"@iconify-json/icon-park-outline": "^1.2.2",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"eslint": "^9.24.0",
|
||||
"lint-staged": "^15.5.0",
|
||||
"naive-ui": "^2.41.0",
|
||||
"sass": "^1.86.3",
|
||||
"simple-git-hooks": "^2.12.1",
|
||||
"typescript": "^5.8.3",
|
||||
"unocss": "^0.65.4",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.5",
|
||||
"vite-bundle-visualizer": "^1.2.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue-tsc": "^2.0.12"
|
||||
"vite-plugin-vue-devtools": "7.7.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
|
@ -1,12 +1,12 @@
|
||||
/** 不同请求服务的环境配置 */
|
||||
export const serviceConfig: Record<ServiceEnvType, Record<string, string>> = {
|
||||
dev: {
|
||||
url: 'https://mock.apifox.com/m1/4071143-0-default',
|
||||
url: 'https://mock.apifox.cn/m1/4071143-0-default',
|
||||
},
|
||||
test: {
|
||||
url: 'https://mock.apifox.com/m1/4071143-0-default',
|
||||
url: 'https://mock.apifox.cn/m1/4071143-0-default',
|
||||
},
|
||||
prod: {
|
||||
url: 'https://mock.apifox.com/m1/4071143-0-default',
|
||||
url: 'https://mock.apifox.cn/m1/4071143-0-default',
|
||||
},
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { naiveI18nOptions } from '@/utils'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import { useAppStore } from './store'
|
||||
import { naiveI18nOptions } from '@/utils'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
|
@ -2,34 +2,36 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="loading-container">
|
||||
<div class="boxes">
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<naive-provider>
|
||||
<div id="loading-container">
|
||||
<div class="boxes">
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="box">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</naive-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { NFlex } from 'naive-ui'
|
||||
import { useAppStore } from '@/store'
|
||||
import IconSun from '~icons/icon-park-outline/sun-one'
|
||||
import IconMoon from '~icons/icon-park-outline/moon'
|
||||
import IconAuto from '~icons/icon-park-outline/laptop-computer'
|
||||
import IconMoon from '~icons/icon-park-outline/moon'
|
||||
import IconSun from '~icons/icon-park-outline/sun-one'
|
||||
import { NFlex } from 'naive-ui'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@ -12,7 +12,7 @@ const appStore = useAppStore()
|
||||
const options = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('app.lignt'),
|
||||
label: t('app.light'),
|
||||
value: 'light',
|
||||
icon: IconSun,
|
||||
},
|
||||
|
@ -3,14 +3,14 @@ interface Props {
|
||||
message: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { message } = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip :show-arrow="false" trigger="hover">
|
||||
<template #trigger>
|
||||
<icon-park-outline-help />
|
||||
<icon-park-outline-help class="op-50 cursor-help" />
|
||||
</template>
|
||||
{{ props.message }}
|
||||
{{ message }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
202
src/components/common/IconSelect.vue
Normal file
202
src/components/common/IconSelect.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import { mapEntries } from 'radash'
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
disabled = false,
|
||||
} = defineProps<Props>()
|
||||
|
||||
interface IconList {
|
||||
prefix: string
|
||||
icons: string[]
|
||||
title: string
|
||||
total: number
|
||||
categories: Record<string, string[]>
|
||||
}
|
||||
const value = defineModel('value', { type: String })
|
||||
|
||||
// 包含的图标库系列名
|
||||
const nameList = ['icon-park-outline', 'carbon']
|
||||
|
||||
// 获取单个图标库数据
|
||||
async function fetchIconList(name: string): Promise<IconList> {
|
||||
return await fetch(`https://api.iconify.design/collection?prefix=${name}`).then(res => res.json())
|
||||
}
|
||||
|
||||
// 获取所有图标库数据
|
||||
async function fetchIconAllList(nameList: string[]) {
|
||||
const namePromises = nameList.map(name => fetchIconList(name))
|
||||
const targets = await Promise.all(namePromises)
|
||||
|
||||
return targets.map((i) => {
|
||||
i.icons = Object.entries(i.categories).reduce((prev, next) => {
|
||||
const [_key, value] = next
|
||||
return prev.concat(value)
|
||||
}, [] as string[])
|
||||
return i
|
||||
})
|
||||
}
|
||||
// 获取svg文件名
|
||||
function getSvgName(path: string) {
|
||||
const regex = /\/([^/]+)\.svg$/
|
||||
const match = path.match(regex)
|
||||
if (match) {
|
||||
const fileName = match[1]
|
||||
return fileName
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// 获取所有本地图标
|
||||
function generateLocalIconList() {
|
||||
const localSvgList = import.meta.glob('@/assets/svg-icons/*.svg', {
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
eager: true,
|
||||
})
|
||||
|
||||
return mapEntries(localSvgList, (key, value) => {
|
||||
return [getSvgName(key), value]
|
||||
})
|
||||
}
|
||||
|
||||
const iconList = shallowRef<IconList[]>([])
|
||||
const LocalIconList = shallowRef({})
|
||||
|
||||
onMounted(async () => {
|
||||
iconList.value = await fetchIconAllList(nameList)
|
||||
LocalIconList.value = generateLocalIconList()
|
||||
})
|
||||
|
||||
// 当前tab
|
||||
const currentTab = shallowRef(0)
|
||||
// 当前tag
|
||||
const currentTag = shallowRef('')
|
||||
|
||||
// 切换tab
|
||||
function handleChangeTab(index: number) {
|
||||
currentTab.value = index
|
||||
currentTag.value = ''
|
||||
}
|
||||
// 搜索图标输入框值
|
||||
const searchValue = ref('')
|
||||
|
||||
// 当前页数
|
||||
const currentPage = shallowRef(1)
|
||||
|
||||
// 选择分类tag
|
||||
function handleSelectIconTag(icon: string) {
|
||||
currentTag.value = currentTag.value === icon ? '' : icon
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 包含当前分类或所有图标列表
|
||||
const icons = computed(() => {
|
||||
const hasTag = !!currentTag.value
|
||||
if (hasTag)
|
||||
return iconList.value[currentTab.value]?.categories[currentTag.value]
|
||||
else
|
||||
return iconList.value[currentTab.value].icons
|
||||
})
|
||||
|
||||
// 符合搜索条件的图标列表
|
||||
const visibleIcons = computed(() => {
|
||||
return icons.value?.filter(i => i
|
||||
.includes(searchValue.value))?.slice((currentPage.value - 1) * 200, (currentPage.value) * 200)
|
||||
})
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
// 选择图标
|
||||
function handleSelectIcon(icon: string) {
|
||||
value.value = icon
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
// 清除图标
|
||||
function clearIcon() {
|
||||
value.value = ''
|
||||
showModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-input-group disabled>
|
||||
<n-button v-if="value" :disabled="disabled" type="primary">
|
||||
<template #icon>
|
||||
<nova-icon :icon="value" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-input :value="value" readonly :placeholder="$t('components.iconSelector.inputPlaceholder')" />
|
||||
<n-button type="primary" ghost :disabled="disabled" @click="showModal = true">
|
||||
{{ $t('common.choose') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-modal
|
||||
v-model:show="showModal" preset="card" :title="$t('components.iconSelector.selectorTitle')" size="small" class="w-800px" :bordered="false"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button type="warning" size="small" ghost @click="clearIcon">
|
||||
{{ $t('components.iconSelector.clearIcon') }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<n-tabs :value="currentTab" type="line" animated placement="left" @update:value="handleChangeTab">
|
||||
<n-tab-pane name="local" tab="local">
|
||||
<n-flex :size="2">
|
||||
<n-el
|
||||
v-for="(_icon, key) in LocalIconList" :key="key"
|
||||
class="hover:(text-[var(--primary-color)] ring-1) ring-[var(--primary-color)] p-1 rounded flex-center"
|
||||
:title="`local:${key}`"
|
||||
@click="handleSelectIcon(`local:${key}`)"
|
||||
>
|
||||
<nova-icon :icon="`local:${key}`" :size="24" />
|
||||
</n-el>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-for="(list, index) in iconList" :key="list.prefix" :name="index" :tab="list.title">
|
||||
<n-flex vertical>
|
||||
<n-flex size="small">
|
||||
<n-tag
|
||||
v-for="(_v, k) in list.categories" :key="k"
|
||||
:checked="currentTag === k" round checkable size="small"
|
||||
@update:checked="handleSelectIconTag(k)"
|
||||
>
|
||||
{{ k }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
|
||||
<n-input
|
||||
v-model:value="searchValue" type="text" clearable
|
||||
:placeholder="$t('components.iconSelector.searchPlaceholder')"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<n-flex :size="2">
|
||||
<n-el
|
||||
v-for="(icon) in visibleIcons" :key="icon"
|
||||
class="hover:(text-[var(--primary-color)] ring-1) ring-[var(--primary-color)] p-1 rounded flex-center"
|
||||
:title="`${list.prefix}:${icon}`"
|
||||
@click="handleSelectIcon(`${list.prefix}:${icon}`)"
|
||||
>
|
||||
<nova-icon :icon="`${list.prefix}:${icon}`" :size="24" />
|
||||
</n-el>
|
||||
<n-empty v-if="visibleIcons.length === 0" class="w-full" />
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<n-flex justify="center">
|
||||
<n-pagination
|
||||
v-model:page="currentPage"
|
||||
:item-count="icons?.length"
|
||||
:page-size="200"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-modal>
|
||||
</template>
|
@ -1,35 +0,0 @@
|
||||
export const icons: string[] = [
|
||||
'icon-park-outline:ad-product',
|
||||
'icon-park-outline:all-application',
|
||||
'icon-park-outline:hamburger-button',
|
||||
'icon-park-outline:setting',
|
||||
'icon-park-outline:add-one',
|
||||
'icon-park-outline:reduce-one',
|
||||
'icon-park-outline:close-one',
|
||||
'icon-park-outline:help',
|
||||
'icon-park-outline:info',
|
||||
'icon-park-outline:grid-four',
|
||||
'icon-park-outline:key-two',
|
||||
'icon-park-outline:write',
|
||||
'icon-park-outline:fire',
|
||||
'icon-park-outline:memory-card-one',
|
||||
'icon-park-outline:coupon',
|
||||
'icon-park-outline:ticket-one',
|
||||
'icon-park-outline:pay-code-two',
|
||||
'icon-park-outline:wallet-one',
|
||||
'icon-park-outline:gift',
|
||||
'icon-park-outline:mail',
|
||||
'icon-park-outline:log',
|
||||
'icon-park-outline:people',
|
||||
'icon-park-outline:alarm-clock',
|
||||
'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',
|
||||
]
|
@ -1,51 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { icons } from './icons'
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const value = defineModel('value', { type: String })
|
||||
const searchValue = ref('')
|
||||
const showPopover = ref(false)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const iconList = computed(() => icons.filter(item => item.includes(searchValue.value)))
|
||||
|
||||
function handleSelectIcon(icon: string) {
|
||||
value.value = icon
|
||||
showPopover.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-popover v-model:show="showPopover" placement="bottom" trigger="click" :disabled="props.disabled">
|
||||
<template #trigger>
|
||||
<n-input :value="value" readonly :placeholder="t('components.iconSelector.inputPlaceholder')">
|
||||
<template #suffix>
|
||||
<nova-icon :icon="value" />
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-input v-model:value="searchValue" type="text" :placeholder="t('components.iconSelector.searchPlaceholder')" />
|
||||
</template>
|
||||
<div class="w-400px">
|
||||
<div v-if="iconList.length > 0" class="grid grid-cols-9 h-auto overflow-auto gap-1">
|
||||
<div
|
||||
v-for="(item, index) in iconList" :key="index" class="border border-gray-200 m-2px p-5px flex-center"
|
||||
@click="handleSelectIcon(item)"
|
||||
>
|
||||
<nova-icon :icon="item" :size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<n-empty v-else class="w-full" />
|
||||
</div>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -11,20 +11,36 @@ interface iconPorps {
|
||||
/* 图标深度 */
|
||||
depth?: 1 | 2 | 3 | 4 | 5
|
||||
}
|
||||
const props = withDefaults(defineProps<iconPorps>(), {
|
||||
size: 18,
|
||||
const { size = 18, icon } = defineProps<iconPorps>()
|
||||
|
||||
const isLocal = computed(() => {
|
||||
return icon && icon.startsWith('local:')
|
||||
})
|
||||
|
||||
function getLocalIcon(icon: string) {
|
||||
const svgName = icon.replace('local:', '')
|
||||
const svg = import.meta.glob<string>('@/assets/svg-icons/*.svg', {
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
eager: true,
|
||||
})
|
||||
|
||||
return svg[`/src/assets/svg-icons/${svgName}.svg`]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-icon
|
||||
v-if="props.icon"
|
||||
:size="props.size"
|
||||
:depth="props.depth"
|
||||
:color="props.color"
|
||||
v-if="icon"
|
||||
:size="size"
|
||||
:depth="depth"
|
||||
:color="color"
|
||||
>
|
||||
<Icon :icon="props.icon" />
|
||||
<template v-if="isLocal">
|
||||
<i v-html="getLocalIcon(icon)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Icon :icon="icon" />
|
||||
</template>
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['change'])
|
||||
interface Props {
|
||||
count?: number
|
||||
}
|
||||
const {
|
||||
count = 0,
|
||||
} = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [page: number, pageSize: number] // 具名元组语法
|
||||
}>()
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const displayOrder: Array<'pages' | 'size-picker' | 'quick-jumper'> = ['size-picker', 'pages']
|
||||
@ -17,10 +21,11 @@ function changePage() {
|
||||
|
||||
<template>
|
||||
<n-pagination
|
||||
v-if="props.count > 0"
|
||||
v-if="count > 0"
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="props.count"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
:item-count="count"
|
||||
:display-order="displayOrder"
|
||||
show-size-picker
|
||||
@update-page="changePage"
|
||||
|
@ -3,16 +3,15 @@ interface Props {
|
||||
showWatermark: boolean
|
||||
text?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showWatermark: false,
|
||||
text: 'Watermark',
|
||||
})
|
||||
const {
|
||||
text = 'Watermark',
|
||||
} = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-watermark
|
||||
v-if="props.showWatermark"
|
||||
:content="props.text"
|
||||
v-if="showWatermark"
|
||||
:content="text"
|
||||
cross
|
||||
fullscreen
|
||||
:font-size="16"
|
||||
|
@ -1,17 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
maxLength?: string
|
||||
}>()
|
||||
const modelValue = defineModel<string>()
|
||||
}
|
||||
const { maxLength } = defineProps<Props>()
|
||||
const modelValue = defineModel<string>('value')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="modelValue" class="inline-flex items-center gap-0.5em">
|
||||
<n-ellipsis :style="{ 'max-width': props.maxLength || '12em' }">
|
||||
<n-ellipsis :style="{ 'max-width': maxLength || '12em' }">
|
||||
{{ modelValue }}
|
||||
</n-ellipsis>
|
||||
<span v-copy="modelValue" class="cursor-pointer">
|
||||
<icon-park-outline-copy />
|
||||
</span>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<span v-copy="modelValue" class="cursor-pointer">
|
||||
<icon-park-outline-copy />
|
||||
</span>
|
||||
</template>
|
||||
{{ $t('components.copyText.tooltip') }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,26 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { ToolbarNames } from 'md-editor-v3'
|
||||
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
|
||||
// https://imzbf.github.io/md-editor-v3/zh-CN/docs
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
// https://imzbf.github.io/md-editor-v3/zh-CN/docs
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const model = defineModel<string>()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const data = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const theme = computed(() => {
|
||||
return appStore.colorMode ? 'dark' : 'light'
|
||||
})
|
||||
|
||||
const toolbarsExclude: ToolbarNames[] = [
|
||||
'mermaid',
|
||||
'katex',
|
||||
@ -32,7 +22,7 @@ const toolbarsExclude: ToolbarNames[] = [
|
||||
|
||||
<template>
|
||||
<MdEditor
|
||||
v-model="data" :theme="theme" :toolbars-exclude="toolbarsExclude"
|
||||
v-model="model" :theme="appStore.colorMode" :toolbars-exclude="toolbarsExclude"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -1,77 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import Editor from '@tinymce/tinymce-vue'
|
||||
import Quill from 'quill'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
defineOptions({
|
||||
name: 'RichTextEditor',
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { disabled } = defineProps<Props>()
|
||||
interface Props {
|
||||
disabled?: boolean
|
||||
}
|
||||
const model = defineModel<string>()
|
||||
|
||||
const data = useVModel(props, 'modelValue', emit)
|
||||
let editorInst = null
|
||||
|
||||
function imagesUploadHandler(blobInfo: any, _progress: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', blobInfo.blob())
|
||||
fetch('www.example.com/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok)
|
||||
throw new Error('上传失败')
|
||||
const editorModel = ref<string>()
|
||||
|
||||
// 处理上传成功后的响应数据
|
||||
resolve('上传成功')
|
||||
})
|
||||
.catch((error) => {
|
||||
// 处理上传失败的情况
|
||||
reject(error)
|
||||
})
|
||||
onMounted(() => {
|
||||
initEditor()
|
||||
})
|
||||
|
||||
const editorRef = useTemplateRef<HTMLElement>('editorRef')
|
||||
function initEditor() {
|
||||
const options = {
|
||||
modules: {
|
||||
toolbar: [
|
||||
{ header: [1, 2, 3, 4, 5, 6, false] }, // 标题
|
||||
'bold', // 加粗
|
||||
'italic', // 斜体
|
||||
'strike', // 删除线
|
||||
{ size: ['small', false, 'large', 'huge'] }, // 字体大小
|
||||
{ font: [] }, // 字体种类
|
||||
{ color: [] }, // 字体颜色、
|
||||
{ background: [] }, // 字体背景颜色
|
||||
'link', // 插入链接
|
||||
'image', // 插入图片
|
||||
'blockquote', // 引用
|
||||
'link', // 超链接
|
||||
'image', // 插入图片
|
||||
'video', // 插入视频
|
||||
{ list: 'bullet' }, // 无序列表
|
||||
{ list: 'ordered' }, // 有序列表
|
||||
{ script: 'sub' }, // 下标
|
||||
{ script: 'super' }, // 上标
|
||||
{ align: [] }, // 对齐方式
|
||||
'formula', // 公式
|
||||
'clean', // remove formatting button
|
||||
],
|
||||
},
|
||||
|
||||
placeholder: 'Insert text here ...',
|
||||
theme: 'snow',
|
||||
}
|
||||
const quill = new Quill(editorRef.value!, options)
|
||||
|
||||
quill.on('text-change', (_delta, _oldDelta, _source) => {
|
||||
editorModel.value = quill.getSemanticHTML()
|
||||
})
|
||||
}
|
||||
const initConfig = {
|
||||
language: 'zh_CN', // 语言类型
|
||||
min_height: 700,
|
||||
content_css: 'dark',
|
||||
placeholder: '请输入内容', // textarea中的提示信息
|
||||
branding: false,
|
||||
font_formats:
|
||||
'微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;', // 字体样式
|
||||
plugins:
|
||||
'print preview searchreplace autolink directionality visualblocks visualchars fullscreen code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount textpattern autosave emoticons', // 插件配置 axupimgs indent2em
|
||||
toolbar: [
|
||||
'fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough anchor | alignleft aligncenter alignright alignjustify outdent indent | bullist numlist | blockquote subscript superscript removeformat ',
|
||||
'styleselect formatselect fontselect fontsizeselect | table emoticons charmap hr pagebreak insertdatetime selectall visualblocks | code preview | indent2em lineheight formatpainter',
|
||||
],
|
||||
paste_data_images: true, // 图片是否可粘贴
|
||||
// 此处为图片上传处理函数
|
||||
images_upload_handler: imagesUploadHandler,
|
||||
|
||||
if (disabled)
|
||||
quill.enable(false)
|
||||
|
||||
editorInst = quill
|
||||
|
||||
if (model.value)
|
||||
setContents(model.value)
|
||||
}
|
||||
|
||||
function setContents(html: string) {
|
||||
editorInst!.setContents(editorInst!.clipboard.convert({ html }))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => model.value,
|
||||
(newValue, _oldValue) => {
|
||||
if (newValue && newValue !== editorModel.value) {
|
||||
setContents(newValue)
|
||||
}
|
||||
else if (!newValue) {
|
||||
setContents('')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(editorModel, (newValue, oldValue) => {
|
||||
if (newValue && newValue !== oldValue)
|
||||
model.value = newValue
|
||||
|
||||
else if (!newValue)
|
||||
editorInst!.setContents([])
|
||||
})
|
||||
|
||||
watch(
|
||||
() => disabled,
|
||||
(newValue, _oldValue) => {
|
||||
editorInst!.enable(!newValue)
|
||||
},
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => editorInst = null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tinymce-boxz">
|
||||
<Editor
|
||||
v-model="data"
|
||||
api-key="no-api"
|
||||
:init="initConfig"
|
||||
/>
|
||||
</div>
|
||||
<div ref="editorRef" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tinymce-boxz > textarea {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 隐藏apikey没有绑定这个域名的提示 */
|
||||
.tox-notifications-container .tox-notification--warning {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.tox.tox-tinymce {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
27
src/components/custom/PcaCascader.vue
Normal file
27
src/components/custom/PcaCascader.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { CascaderOption } from 'naive-ui'
|
||||
|
||||
defineOptions({
|
||||
name: 'PcaCascader',
|
||||
})
|
||||
|
||||
// 获取原始数据,数据来源仓库 https://github.com/modood/Administrative-divisions-of-China
|
||||
const pcaCode = shallowRef<CascaderOption[]>()
|
||||
async function fetchPcaCode() {
|
||||
return await fetch('https://cdn.jsdelivr.net/gh/modood/Administrative-divisions-of-China/dist/pca-code.json').then(res => res.json())
|
||||
}
|
||||
onMounted(async () => {
|
||||
pcaCode.value = await fetchPcaCode()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-cascader
|
||||
:options="pcaCode"
|
||||
value-field="code"
|
||||
label-field="name"
|
||||
check-strategy="all"
|
||||
filterable
|
||||
clearable
|
||||
/>
|
||||
</template>
|
@ -1,7 +1,3 @@
|
||||
// export const genderLabels: Record<NonNullable<CommonList.GenderType>, string> = {
|
||||
// 0: '女',
|
||||
// 1: '男',
|
||||
// }
|
||||
/** Gender */
|
||||
export enum Gender {
|
||||
male,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { App, Directive } from 'vue'
|
||||
import { $t } from '@/utils'
|
||||
|
||||
interface CopyHTMLElement extends HTMLElement {
|
||||
_copyText: string
|
||||
@ -10,12 +11,12 @@ export function install(app: App) {
|
||||
|
||||
function clipboardEnable() {
|
||||
if (!isSupported.value) {
|
||||
window.$message.error('Your browser does not support Clipboard API')
|
||||
window.$message.error($t('components.copyText.unsupportedError'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (permissionWrite.value !== 'granted') {
|
||||
window.$message.error('Currently not permitted to use Clipboard API')
|
||||
window.$message.error($t('components.copyText.unpermittedError'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -25,7 +26,7 @@ export function install(app: App) {
|
||||
if (!clipboardEnable())
|
||||
return
|
||||
copy(this._copyText)
|
||||
window.$message.success('复制成功')
|
||||
window.$message.success($t('components.copyText.message'))
|
||||
}
|
||||
|
||||
function updataClipboard(el: CopyHTMLElement, text: string) {
|
||||
|
@ -4,7 +4,7 @@ import { usePermission } from '@/hooks'
|
||||
export function install(app: App) {
|
||||
const { hasPermission } = usePermission()
|
||||
|
||||
function updatapermission(el: HTMLElement, permission: Auth.RoleType | Auth.RoleType[]) {
|
||||
function updatapermission(el: HTMLElement, permission: Entity.RoleType | Entity.RoleType[]) {
|
||||
if (!permission)
|
||||
throw new Error('v-permissson Directive with no explicit role attached')
|
||||
|
||||
@ -12,7 +12,7 @@ export function install(app: App) {
|
||||
el.parentElement?.removeChild(el)
|
||||
}
|
||||
|
||||
const permissionDirective: Directive<HTMLElement, Auth.RoleType | Auth.RoleType[]> = {
|
||||
const permissionDirective: Directive<HTMLElement, Entity.RoleType | Entity.RoleType[]> = {
|
||||
mounted(el, binding) {
|
||||
updatapermission(el, binding.value)
|
||||
},
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './useBoolean'
|
||||
export * from './useLoading'
|
||||
export * from './useEcharts'
|
||||
export * from './usePermission'
|
||||
|
@ -1,6 +1,3 @@
|
||||
import * as echarts from 'echarts/core'
|
||||
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
|
||||
|
||||
// 系列类型的定义后缀都为 SeriesOption
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
@ -8,7 +5,6 @@ import type {
|
||||
PieSeriesOption,
|
||||
RadarSeriesOption,
|
||||
} from 'echarts/charts'
|
||||
|
||||
// 组件类型的定义后缀都为 ComponentOption
|
||||
import type {
|
||||
DatasetComponentOption,
|
||||
@ -18,6 +14,9 @@ import type {
|
||||
ToolboxComponentOption,
|
||||
TooltipComponentOption,
|
||||
} from 'echarts/components'
|
||||
import { useAppStore } from '@/store'
|
||||
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
|
||||
|
||||
import {
|
||||
DatasetComponent, // 数据集组件
|
||||
GridComponent,
|
||||
@ -27,10 +26,11 @@ import {
|
||||
TooltipComponent,
|
||||
TransformComponent, // 内置数据转换器组件 (filter, sort)
|
||||
} from 'echarts/components'
|
||||
import * as echarts from 'echarts/core'
|
||||
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
|
||||
export type ECOption = echarts.ComposeOption<
|
||||
@ -66,73 +66,61 @@ echarts.use([
|
||||
|
||||
/**
|
||||
* Echarts hooks函数
|
||||
* @param options - 图表配置
|
||||
* @description 按需引入图表组件,没注册的组件需要先引入
|
||||
*/
|
||||
export function useEcharts(options: Ref<ECOption>) {
|
||||
const appStore = useAppStore()
|
||||
export function useEcharts(ref: string, chartOptions: Ref<ECOption>) {
|
||||
const el = useTemplateRef<HTMLLIElement>(ref)
|
||||
|
||||
const domRef = ref<HTMLElement>()
|
||||
const appStore = useAppStore()
|
||||
|
||||
let chart: echarts.ECharts | null = null
|
||||
|
||||
const initialSize = { width: 0, height: 0 }
|
||||
const { width, height } = useElementSize(domRef, initialSize)
|
||||
const { width, height } = useElementSize(el)
|
||||
|
||||
function canRender() {
|
||||
return initialSize.width > 0 && initialSize.height > 0
|
||||
}
|
||||
const isRendered = () => Boolean(el && chart)
|
||||
|
||||
function isRendered() {
|
||||
return Boolean(domRef.value && chart)
|
||||
}
|
||||
async function render() {
|
||||
// 宽或高不存在时不渲染
|
||||
if (!width || !height)
|
||||
return
|
||||
|
||||
const chartTheme = appStore.colorMode ? 'dark' : 'light'
|
||||
await nextTick()
|
||||
if (domRef.value) {
|
||||
chart = echarts.init(domRef.value, chartTheme)
|
||||
update(options.value)
|
||||
if (el) {
|
||||
chart = echarts.init(el.value, chartTheme)
|
||||
update(chartOptions.value)
|
||||
}
|
||||
}
|
||||
|
||||
function update(updateOptions: ECOption) {
|
||||
if (isRendered())
|
||||
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' })
|
||||
}
|
||||
|
||||
function resize() {
|
||||
chart?.resize()
|
||||
async function update(updateOptions: ECOption) {
|
||||
if (isRendered()) {
|
||||
chart!.setOption({ backgroundColor: 'transparent', ...updateOptions })
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
chart?.dispose()
|
||||
chart = null
|
||||
}
|
||||
const sizeWatch = watch([width, height], async ([newWidth, newHeight]) => {
|
||||
initialSize.width = newWidth
|
||||
initialSize.height = newHeight
|
||||
if (newWidth === 0 && newHeight === 0) {
|
||||
// 节点被删除 将chart置为空
|
||||
chart = null
|
||||
}
|
||||
if (!canRender())
|
||||
return
|
||||
if (isRendered())
|
||||
resize()
|
||||
else await render()
|
||||
|
||||
watch([width, height], async ([newWidth, newHeight]) => {
|
||||
if (isRendered() && newWidth && newHeight)
|
||||
chart?.resize()
|
||||
})
|
||||
|
||||
const OptionWatch = watch(options, (newValue) => {
|
||||
watch(chartOptions, (newValue) => {
|
||||
update(newValue)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
render()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
sizeWatch()
|
||||
OptionWatch()
|
||||
destroy()
|
||||
})
|
||||
|
||||
return {
|
||||
domRef,
|
||||
destroy,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { useBoolean } from './useBoolean'
|
||||
|
||||
export function useLoading(initValue = false) {
|
||||
const {
|
||||
bool: loading,
|
||||
setTrue: startLoading,
|
||||
setFalse: endLoading,
|
||||
} = useBoolean(initValue)
|
||||
|
||||
return {
|
||||
loading,
|
||||
startLoading,
|
||||
endLoading,
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { isArray, isString } from 'radash'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
/** 权限判断 */
|
||||
@ -6,7 +5,7 @@ export function usePermission() {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
function hasPermission(
|
||||
permission: Auth.RoleType | Auth.RoleType[] | undefined,
|
||||
permission?: Entity.RoleType[],
|
||||
) {
|
||||
if (!permission)
|
||||
return true
|
||||
@ -15,13 +14,9 @@ export function usePermission() {
|
||||
return false
|
||||
const { role } = authStore.userInfo
|
||||
|
||||
let has = role === 'super'
|
||||
let has = role.includes('super')
|
||||
if (!has) {
|
||||
if (isArray(permission))
|
||||
has = permission.includes(role)
|
||||
|
||||
if (isString(permission))
|
||||
has = permission === role
|
||||
has = permission.every(i => role.includes(i))
|
||||
}
|
||||
return has
|
||||
}
|
||||
|
35
src/hooks/useTableDrag.ts
Normal file
35
src/hooks/useTableDrag.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { NDataTable } from 'naive-ui'
|
||||
import { useDraggable } from 'vue-draggable-plus'
|
||||
|
||||
export function useTableDrag<T = unknown>(params: {
|
||||
tableRef: Ref<InstanceType<typeof NDataTable> | undefined>
|
||||
data: Ref<T[]>
|
||||
onRowDrag: (rows: T[]) => void
|
||||
}) {
|
||||
const tableEl = computed(() => params.tableRef?.value?.$el as HTMLElement)
|
||||
const tableBodyRef = ref<HTMLElement | undefined>(undefined)
|
||||
|
||||
const { start } = useDraggable(tableBodyRef, params.data, {
|
||||
immediate: false,
|
||||
animation: 150,
|
||||
handle: '.drag-handle',
|
||||
onEnd: (event) => {
|
||||
const { oldIndex, newIndex } = event
|
||||
const start = Math.min(oldIndex!, newIndex!)
|
||||
const end = Math.max(oldIndex!, newIndex!) - start + 1
|
||||
const changedRows = [...params.data.value].splice(start, end)
|
||||
params.onRowDrag(unref([...changedRows]))
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
while (!tableBodyRef.value) {
|
||||
tableBodyRef.value = tableEl.value?.querySelector('tbody') || undefined
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
})
|
||||
|
||||
watchOnce(() => tableBodyRef.value, (el) => {
|
||||
el && start()
|
||||
})
|
||||
}
|
@ -12,11 +12,10 @@ const value = defineModel<LayoutMode>('value', { required: true })
|
||||
:class="{
|
||||
'outline outline-2': value === 'leftMenu',
|
||||
}"
|
||||
class="grid grid-cols-[20%_1fr] grid-rows-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2)"
|
||||
class="grid grid-cols-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
|
||||
@click="value = 'leftMenu'"
|
||||
>
|
||||
<div class="bg-[var(--primary-color)] row-span-2" />
|
||||
<div class="bg-[var(--primary-color-suppl)]" />
|
||||
<div class="bg-[var(--primary-color)]" />
|
||||
<div class="bg-[var(--divider-color)]" />
|
||||
</n-el>
|
||||
</template>
|
||||
@ -29,7 +28,7 @@ const value = defineModel<LayoutMode>('value', { required: true })
|
||||
:class="{
|
||||
'outline outline-2': value === 'topMenu',
|
||||
}"
|
||||
class="grid grid-rows-[30%_1fr] outline-[var(--primary-color)] hover:(outline outline-2)"
|
||||
class="grid grid-rows-[30%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
|
||||
@click="value = 'topMenu'"
|
||||
>
|
||||
<div class="bg-[var(--primary-color)]" />
|
||||
@ -38,10 +37,27 @@ const value = defineModel<LayoutMode>('value', { required: true })
|
||||
</template>
|
||||
<span> {{ $t('app.topMenu') }} </span>
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-el
|
||||
:class="{
|
||||
'outline outline-2': value === 'mixMenu',
|
||||
}"
|
||||
class="grid grid-cols-[20%_1fr] grid-rows-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
|
||||
@click="value = 'mixMenu'"
|
||||
>
|
||||
<div class="bg-[var(--primary-color)] row-span-2" />
|
||||
<div class="bg-[var(--primary-color)]" />
|
||||
<div class="bg-[var(--divider-color)]" />
|
||||
</n-el>
|
||||
</template>
|
||||
<span> {{ $t('app.mixMenu') }} </span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.grid{
|
||||
height: 60px;
|
||||
width: 86px;
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
list?: Message.List[]
|
||||
list?: Entity.Message[]
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const { list } = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
interface Emits {
|
||||
@ -13,7 +13,7 @@ interface Emits {
|
||||
<template>
|
||||
<n-scrollbar style="height: 400px">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="(item) in props.list" :key="item.id" @click="emit('read', item.id)">
|
||||
<n-list-item v-for="(item) in list" :key="item.id" @click="emit('read', item.id)">
|
||||
<n-thing content-indented :class="{ 'opacity-30': item.isRead }">
|
||||
<template #header>
|
||||
<n-ellipsis :line-clamp="1">
|
||||
|
18
src/layouts/components/common/Setting.vue
Normal file
18
src/layouts/components/common/Setting.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="appStore.showSetting = !appStore.showSetting">
|
||||
<div>
|
||||
<icon-park-outline-setting-two />
|
||||
</div>
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>{{ $t('app.setting') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
143
src/layouts/components/common/SettingDrawer.vue
Normal file
143
src/layouts/components/common/SettingDrawer.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store'
|
||||
import LayoutSelector from './LayoutSelector.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const transitionSelectorOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('app.transitionNull'),
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeSlide'),
|
||||
value: 'fade-slide',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeBottom'),
|
||||
value: 'fade-bottom',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeScale'),
|
||||
value: 'fade-scale',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionZoomFade'),
|
||||
value: 'zoom-fade',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionZoomOut'),
|
||||
value: 'zoom-out',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionSoft'),
|
||||
value: 'fade',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const palette = [
|
||||
'#ffb8b8',
|
||||
'#d03050',
|
||||
'#F0A020',
|
||||
'#fff200',
|
||||
'#ffda79',
|
||||
'#18A058',
|
||||
'#006266',
|
||||
'#22a6b3',
|
||||
'#18dcff',
|
||||
'#2080F0',
|
||||
'#c56cf0',
|
||||
'#be2edd',
|
||||
'#706fd3',
|
||||
'#4834d4',
|
||||
'#130f40',
|
||||
'#4b4b4b',
|
||||
]
|
||||
|
||||
function resetSetting() {
|
||||
window.$dialog.warning({
|
||||
title: t('app.resetSettingTitle'),
|
||||
content: t('app.resetSettingContent'),
|
||||
positiveText: t('common.confirm'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: () => {
|
||||
appStore.resetAlltheme()
|
||||
window.$message.success(t('app.resetSettingMeaasge'))
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-drawer v-model:show="appStore.showSetting" :width="360">
|
||||
<n-drawer-content :title="t('app.systemSetting')" closable>
|
||||
<n-space vertical>
|
||||
<n-divider>{{ $t('app.layoutSetting') }}</n-divider>
|
||||
<LayoutSelector v-model:value="appStore.layoutMode" />
|
||||
<n-divider>{{ $t('app.themeSetting') }}</n-divider>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.colorWeak') }}
|
||||
<n-switch :value="appStore.colorWeak" @update:value="appStore.toggleColorWeak" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.blackAndWhite') }}
|
||||
<n-switch :value="appStore.grayMode" @update:value="appStore.toggleGrayMode" />
|
||||
</n-space>
|
||||
<n-space align="center" justify="space-between">
|
||||
{{ $t('app.themeColor') }}
|
||||
<n-color-picker
|
||||
v-model:value="appStore.primaryColor" class="w-10em" :swatches="palette"
|
||||
@update:value="appStore.setPrimaryColor"
|
||||
/>
|
||||
</n-space>
|
||||
<n-space align="center" justify="space-between">
|
||||
{{ $t('app.pageTransition') }}
|
||||
<n-select
|
||||
v-model:value="appStore.transitionAnimation" class="w-10em"
|
||||
:options="transitionSelectorOptions" @update:value="appStore.reloadPage"
|
||||
/>
|
||||
</n-space>
|
||||
|
||||
<n-divider>{{ $t('app.interfaceDisplay') }}</n-divider>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.logoDisplay') }}
|
||||
<n-switch v-model:value="appStore.showLogo" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.topProgress') }}
|
||||
<n-switch v-model:value="appStore.showProgress" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.multitab') }}
|
||||
<n-switch v-model:value="appStore.showTabs" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.bottomCopyright') }}
|
||||
<n-switch v-model:value="appStore.showFooter" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.breadcrumb') }}
|
||||
<n-switch v-model:value="appStore.showBreadcrumb" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.BreadcrumbIcon') }}
|
||||
<n-switch v-model:value="appStore.showBreadcrumbIcon" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.watermake') }}
|
||||
<n-switch v-model:value="appStore.showWatermark" />
|
||||
</n-space>
|
||||
</n-space>
|
||||
|
||||
<template #footer>
|
||||
<n-button type="error" @click="resetSetting">
|
||||
{{ $t('app.reset') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
@ -2,18 +2,26 @@
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
if (e.key === 'F11' && e.type === 'keydown') {
|
||||
e.preventDefault()
|
||||
appStore.toggleFullScreen()
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="appStore.toggleFullScreen()">
|
||||
<icon-park-outline-off-screen-two v-if="appStore.fullScreen" />
|
||||
<icon-park-outline-full-screen-two v-else />
|
||||
<CommonWrapper @click="appStore.toggleFullScreen">
|
||||
<icon-park-outline-off-screen v-if="appStore.fullScreen" />
|
||||
<icon-park-outline-full-screen v-else />
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>{{ $t('app.toggleFullScreen') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { group } from 'radash'
|
||||
import NoticeList from '../common/NoticeList.vue'
|
||||
|
||||
const MassageData = ref<Message.List[]>([
|
||||
const MassageData = ref<Entity.Message[]>([
|
||||
{
|
||||
id: 0,
|
||||
type: 0,
|
||||
|
@ -1,59 +1,217 @@
|
||||
<script setup lang="ts">
|
||||
import { NFlex, NTag, NText } from 'naive-ui'
|
||||
import { useBoolean } from '@/hooks'
|
||||
import { useRouteStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const routeStore = useRouteStore()
|
||||
|
||||
// 搜索值
|
||||
const searchValue = ref('')
|
||||
|
||||
// 选中索引
|
||||
const selectedIndex = ref<number>(0)
|
||||
|
||||
const { bool: showModal, setTrue: openModal, setFalse: closeModal, toggle: toggleModal } = useBoolean(false)
|
||||
|
||||
// 鼠标和键盘操作切换锁,防止鼠标和键盘操作冲突
|
||||
const { bool: keyboardFlag, setTrue: setKeyboardTrue, setFalse: setKeyboardFalse } = useBoolean(false)
|
||||
|
||||
const { ctrl_k, arrowup, arrowdown, enter/* keys you want to monitor */ } = useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown')
|
||||
e.preventDefault()
|
||||
},
|
||||
})
|
||||
|
||||
// 监听全局热键
|
||||
watchEffect(() => {
|
||||
if (ctrl_k.value)
|
||||
toggleModal()
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 计算符合条件的菜单选项
|
||||
const options = computed(() => {
|
||||
if (!searchValue.value)
|
||||
return []
|
||||
|
||||
return routeStore.rowRoutes.filter((item) => {
|
||||
const conditions = [
|
||||
t(`route.${String(item.name)}`, item['meta.title'] || item.name)?.includes(searchValue.value),
|
||||
t(`route.${String(item.name)}`, item.title || item.name)?.includes(searchValue.value),
|
||||
item.path?.includes(searchValue.value),
|
||||
]
|
||||
return conditions.some(condition => condition)
|
||||
return conditions.some(condition => !item.hide && condition)
|
||||
}).map((item) => {
|
||||
return {
|
||||
label: t(`route.${String(item.name)}`, item['meta.title'] || item.name),
|
||||
label: t(`route.${String(item.name)}`, item.title || item.name),
|
||||
value: item.path,
|
||||
icon: item['meta.icon'],
|
||||
icon: item.icon,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function renderLabel(option: any) {
|
||||
return h(NFlex, {}, {
|
||||
default: () => [
|
||||
h(NTag, { size: 'small', type: 'primary', bordered: false }, { icon: renderIcon(option.icon), default: () => option.label }),
|
||||
h(NText, { depth: 3 }, { default: () => option.value }),
|
||||
],
|
||||
})
|
||||
const router = useRouter()
|
||||
|
||||
// 关闭回调
|
||||
function handleClose() {
|
||||
searchValue.value = ''
|
||||
selectedIndex.value = 0
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
// 输入框改变,索引重置
|
||||
function handleInputChange() {
|
||||
selectedIndex.value = 0
|
||||
}
|
||||
|
||||
// 选择菜单选项
|
||||
function handleSelect(value: string) {
|
||||
handleClose()
|
||||
router.push(value)
|
||||
nextTick(() => {
|
||||
searchValue.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
// 没有打开弹窗或没有搜索结果时,不操作
|
||||
if (!showModal.value || !options.value.length)
|
||||
return
|
||||
|
||||
// 设置键盘操作锁,设置后不会被动触发mouseover
|
||||
setKeyboardTrue()
|
||||
if (arrowup.value)
|
||||
handleArrowup()
|
||||
|
||||
if (arrowdown.value)
|
||||
handleArrowdown()
|
||||
|
||||
if (enter.value)
|
||||
handleEnter()
|
||||
})
|
||||
|
||||
const scrollbarRef = ref()
|
||||
|
||||
// 上箭头操作
|
||||
function handleArrowup() {
|
||||
if (selectedIndex.value === 0)
|
||||
selectedIndex.value = options.value.length - 1
|
||||
|
||||
else
|
||||
selectedIndex.value--
|
||||
|
||||
handleScroll(selectedIndex.value)
|
||||
}
|
||||
|
||||
// 下箭头操作
|
||||
function handleArrowdown() {
|
||||
if (selectedIndex.value === options.value.length - 1)
|
||||
selectedIndex.value = 0
|
||||
|
||||
else
|
||||
selectedIndex.value++
|
||||
|
||||
handleScroll(selectedIndex.value)
|
||||
}
|
||||
|
||||
function handleScroll(currentIndex: number) {
|
||||
// 保持6个选项在可视区域内,6个后开始滚动
|
||||
const keepIndex = 5
|
||||
// 单个元素的高度,包括了元素的gap和容器的padding
|
||||
const elHeight = 70
|
||||
const distance = currentIndex * elHeight > keepIndex * elHeight ? currentIndex * elHeight - keepIndex * elHeight : 0
|
||||
scrollbarRef.value?.scrollTo({
|
||||
top: distance,
|
||||
})
|
||||
}
|
||||
// 回车键操作
|
||||
function handleEnter() {
|
||||
const target = options.value[selectedIndex.value]
|
||||
if (target)
|
||||
handleSelect(target.value)
|
||||
}
|
||||
|
||||
// 鼠标移入操作
|
||||
function handleMouseEnter(index: number) {
|
||||
if (keyboardFlag.value)
|
||||
return
|
||||
|
||||
selectedIndex.value = index
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-auto-complete
|
||||
v-model:value="searchValue" class="w-20em m-r-1em" :input-props="{
|
||||
autocomplete: 'disabled',
|
||||
}" :options="options" :render-label="renderLabel" :placeholder="$t('app.searchPlaceholder')" clearable @select="handleSelect"
|
||||
<CommonWrapper @click="openModal">
|
||||
<icon-park-outline-search /><n-tag round size="small" class="font-mono cursor-pointer">
|
||||
CtrlK
|
||||
</n-tag>
|
||||
</CommonWrapper>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
class="w-560px fixed top-60px inset-x-0"
|
||||
size="small"
|
||||
preset="card"
|
||||
:segmented="{
|
||||
content: true,
|
||||
footer: true,
|
||||
}"
|
||||
:closable="false"
|
||||
@after-leave="handleClose"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<icon-park-outline-search />
|
||||
</n-icon>
|
||||
<template #header>
|
||||
<n-input v-model:value="searchValue" :placeholder="$t('app.searchPlaceholder')" clearable size="large" @input="handleInputChange">
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<icon-park-outline-search />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
</n-auto-complete>
|
||||
</template>
|
||||
<n-scrollbar ref="scrollbarRef" class="h-450px">
|
||||
<ul
|
||||
v-if="options.length"
|
||||
class="flex flex-col gap-8px p-8px p-r-3"
|
||||
>
|
||||
<n-el
|
||||
v-for="(option, index) in options"
|
||||
:key="option.value" tag="li" role="option"
|
||||
class="cursor-pointer shadow h-62px"
|
||||
:class="{ 'text-[var(--base-color)] bg-[var(--primary-color-hover)]': index === selectedIndex }"
|
||||
@click="handleSelect(option.value)"
|
||||
@mouseenter="handleMouseEnter(index)"
|
||||
@mousemove="setKeyboardFalse"
|
||||
>
|
||||
<div class="grid grid-rows-2 grid-cols-[40px_1fr_30px] h-full p-2">
|
||||
<div class="row-span-2 place-self-center">
|
||||
<nova-icon :icon="option.icon" />
|
||||
</div>
|
||||
<span>{{ option.label }}</span>
|
||||
<icon-park-outline-right class="row-span-2 place-self-center" />
|
||||
<span class="op-70">{{ option.value }}</span>
|
||||
</div>
|
||||
</n-el>
|
||||
</ul>
|
||||
|
||||
<style scoped></style>
|
||||
<n-empty v-else size="large" class="h-450px flex-center" />
|
||||
</n-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<n-flex>
|
||||
<div class="flex-y-center gap-1">
|
||||
<svg width="15" height="15" aria-label="Enter key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3" /></g></svg>
|
||||
<span>{{ $t('common.choose') }}</span>
|
||||
</div>
|
||||
<div class="flex-y-center gap-1">
|
||||
<svg width="15" height="15" aria-label="Arrow down" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 3.5v8M10.5 8.5l-3 3-3-3" /></g></svg>
|
||||
<svg width="15" height="15" aria-label="Arrow up" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3" /></g></svg>
|
||||
<span>{{ $t('common.navigate') }}</span>
|
||||
</div>
|
||||
<div class="flex-y-center gap-1">
|
||||
<svg width="15" height="15" aria-label="Escape key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956" /></g></svg>
|
||||
<span>{{ $t('common.close') }}</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
@ -1,158 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutSelector from '../common/LayoutSelector.vue'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const drawerActive = ref(false)
|
||||
function openSetting() {
|
||||
drawerActive.value = !drawerActive.value
|
||||
}
|
||||
|
||||
const transitionSelectorOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('app.transitionNull'),
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeSlide'),
|
||||
value: 'fade-slide',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeBottom'),
|
||||
value: 'fade-bottom',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionFadeScale'),
|
||||
value: 'fade-scale',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionZoomFade'),
|
||||
value: 'zoom-fade',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionZoomOut'),
|
||||
value: 'zoom-out',
|
||||
},
|
||||
{
|
||||
label: t('app.transitionSoft'),
|
||||
value: 'fade',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const palette = [
|
||||
'#ffb8b8',
|
||||
'#d03050',
|
||||
'#F0A020',
|
||||
'#fff200',
|
||||
'#ffda79',
|
||||
'#18A058',
|
||||
'#006266',
|
||||
'#22a6b3',
|
||||
'#18dcff',
|
||||
'#2080F0',
|
||||
'#c56cf0',
|
||||
'#be2edd',
|
||||
'#706fd3',
|
||||
'#4834d4',
|
||||
'#130f40',
|
||||
'#4b4b4b',
|
||||
]
|
||||
|
||||
function resetSetting() {
|
||||
window.$dialog.warning({
|
||||
title: t('app.resetSettingTitle'),
|
||||
content: t('app.resetSettingContent'),
|
||||
positiveText: t('common.confirm'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: () => {
|
||||
appStore.resetAlltheme()
|
||||
window.$message.success(t('app.resetSettingMeaasge'))
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="openSetting">
|
||||
<div>
|
||||
<icon-park-outline-setting-two />
|
||||
<n-drawer v-model:show="drawerActive" :width="360">
|
||||
<n-drawer-content :title="t('app.systemSetting')" closable>
|
||||
<n-space vertical>
|
||||
<n-divider>{{ $t('app.layoutSetting') }}</n-divider>
|
||||
<LayoutSelector v-model:value="appStore.layoutMode" />
|
||||
<n-divider>{{ $t('app.themeSetting') }}</n-divider>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.colorWeak') }}
|
||||
<n-switch :value="appStore.colorWeak" @update:value="appStore.toggleColorWeak" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.blackAndWhite') }}
|
||||
<n-switch :value="appStore.grayMode" @update:value="appStore.toggleGrayMode" />
|
||||
</n-space>
|
||||
<n-space align="center" justify="space-between">
|
||||
{{ $t('app.themeColor') }}
|
||||
<n-color-picker
|
||||
v-model:value="appStore.primaryColor" class="w-10em" :swatches="palette"
|
||||
@update:value="appStore.setPrimaryColor"
|
||||
/>
|
||||
</n-space>
|
||||
<n-space align="center" justify="space-between">
|
||||
{{ $t('app.pageTransition') }}
|
||||
<n-select
|
||||
v-model:value="appStore.transitionAnimation" class="w-10em"
|
||||
:options="transitionSelectorOptions" @update:value="appStore.reloadPage"
|
||||
/>
|
||||
</n-space>
|
||||
|
||||
<n-divider>{{ $t('app.interfaceDisplay') }}</n-divider>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.logoDisplay') }}
|
||||
<n-switch v-model:value="appStore.showLogo" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.topProgress') }}
|
||||
<n-switch v-model:value="appStore.showProgress" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.multitab') }}
|
||||
<n-switch v-model:value="appStore.showTabs" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.bottomCopyright') }}
|
||||
<n-switch v-model:value="appStore.showFooter" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.breadcrumb') }}
|
||||
<n-switch v-model:value="appStore.showBreadcrumb" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.BreadcrumbIcon') }}
|
||||
<n-switch v-model:value="appStore.showBreadcrumbIcon" />
|
||||
</n-space>
|
||||
<n-space justify="space-between">
|
||||
{{ $t('app.watermake') }}
|
||||
<n-switch v-model:value="appStore.showWatermark" />
|
||||
</n-space>
|
||||
</n-space>
|
||||
|
||||
<template #footer>
|
||||
<n-button type="error" @click="resetSetting">
|
||||
{{ $t('app.reset') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</div>
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>{{ $t('app.setting') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { useAuthStore } from '@/store'
|
||||
import IconGithub from '~icons/icon-park-outline/github'
|
||||
import IconUser from '~icons/icon-park-outline/user'
|
||||
import IconLogout from '~icons/icon-park-outline/logout'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import IconBookOpen from '~icons/icon-park-outline/book-open'
|
||||
import IconGithub from '~icons/icon-park-outline/github'
|
||||
import IconLogout from '~icons/icon-park-outline/logout'
|
||||
import IconUser from '~icons/icon-park-outline/user'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { userInfo, resetAuthStore } = useAuthStore()
|
||||
const { userInfo, logout } = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const options = computed(() => {
|
||||
@ -56,7 +56,7 @@ function handleSelect(key: string | number) {
|
||||
positiveText: t('common.confirm'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: () => {
|
||||
resetAuthStore()
|
||||
logout()
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -70,7 +70,7 @@ function handleSelect(key: string | number) {
|
||||
window.open('https://gitee.com/chansee97/nova-admin')
|
||||
|
||||
if (key === 'docs')
|
||||
window.open('https://nova-admin-docs.netlify.app/')
|
||||
window.open('https://nova-admin-docs.pages.dev/')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -82,7 +82,7 @@ function handleSelect(key: string | number) {
|
||||
>
|
||||
<n-avatar
|
||||
round
|
||||
|
||||
class="cursor-pointer"
|
||||
:src="userInfo?.avatar"
|
||||
>
|
||||
<template #fallback>
|
||||
|
@ -1,33 +1,30 @@
|
||||
/* 侧边栏组件 */
|
||||
import Logo from './sider/Logo.vue'
|
||||
import Menu from './sider/Menu.vue'
|
||||
import BackTop from './common/BackTop.vue'
|
||||
import Setting from './common/Setting.vue'
|
||||
import SettingDrawer from './common/SettingDrawer.vue'
|
||||
|
||||
/* 头部栏组件 */
|
||||
import Breadcrumb from './header/Breadcrumb.vue'
|
||||
import CollapaseButton from './header/CollapaseButton.vue'
|
||||
import FullScreen from './header/FullScreen.vue'
|
||||
import Setting from './header/Setting.vue'
|
||||
import Notices from './header/Notices.vue'
|
||||
import UserCenter from './header/UserCenter.vue'
|
||||
import Search from './header/Search.vue'
|
||||
import UserCenter from './header/UserCenter.vue'
|
||||
|
||||
import Logo from './sider/Logo.vue'
|
||||
import Menu from './sider/Menu.vue'
|
||||
|
||||
/* 标签栏组件 */
|
||||
import TabBar from './tab/TabBar.vue'
|
||||
|
||||
/* 其他组件 */
|
||||
// 返回顶部
|
||||
import BackTop from './common/BackTop.vue'
|
||||
|
||||
export {
|
||||
BackTop,
|
||||
Breadcrumb,
|
||||
CollapaseButton,
|
||||
Menu,
|
||||
Logo,
|
||||
FullScreen,
|
||||
Setting,
|
||||
Logo,
|
||||
Menu,
|
||||
Notices,
|
||||
UserCenter,
|
||||
Search,
|
||||
Setting,
|
||||
SettingDrawer,
|
||||
TabBar,
|
||||
BackTop,
|
||||
UserCenter,
|
||||
}
|
||||
|
@ -1,19 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store'
|
||||
import { useRouteStore } from '@/store/route'
|
||||
import type { MenuInst } from 'naive-ui'
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const routesStore = useRouteStore()
|
||||
const routeStore = useRouteStore()
|
||||
|
||||
const menuInstRef = ref<MenuInst | null>(null)
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
menuInstRef.value?.showOption(routeStore.activeMenu as string)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-menu
|
||||
ref="menuInstRef"
|
||||
:collapsed="appStore.collapsed"
|
||||
:indent="20"
|
||||
:collapsed-width="64"
|
||||
:options="routesStore.menus"
|
||||
:value="routesStore.activeMenu"
|
||||
:options="routeStore.menus"
|
||||
:value="routeStore.activeMenu"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
17
src/layouts/components/tab/ContentFullScreen.vue
Normal file
17
src/layouts/components/tab/ContentFullScreen.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<CommonWrapper @click="appStore.contentFullScreen = !appStore.contentFullScreen">
|
||||
<icon-park-outline-off-screen-one v-if="appStore.contentFullScreen" />
|
||||
<icon-park-outline-full-screen-one v-else />
|
||||
</CommonWrapper>
|
||||
</template>
|
||||
<span>{{ $t('app.togglContentFullScreen') }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
@ -1,24 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Reload from './Reload.vue'
|
||||
import DropTabs from './DropTabs.vue'
|
||||
import { useAppStore, useTabStore } from '@/store'
|
||||
import IconRedo from '~icons/icon-park-outline/redo'
|
||||
import { useDraggable } from 'vue-draggable-plus'
|
||||
import IconClose from '~icons/icon-park-outline/close'
|
||||
import IconDelete from '~icons/icon-park-outline/delete-four'
|
||||
import IconFullwith from '~icons/icon-park-outline/fullwidth'
|
||||
import IconRedo from '~icons/icon-park-outline/redo'
|
||||
import IconLeft from '~icons/icon-park-outline/to-left'
|
||||
import IconRight from '~icons/icon-park-outline/to-right'
|
||||
import IconFullwith from '~icons/icon-park-outline/fullwidth'
|
||||
import ContentFullScreen from './ContentFullScreen.vue'
|
||||
import DropTabs from './DropTabs.vue'
|
||||
import Reload from './Reload.vue'
|
||||
import TabBarItem from './TabBarItem.vue'
|
||||
|
||||
const tabStore = useTabStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const router = useRouter()
|
||||
function handleTab(route: RouteLocationNormalized) {
|
||||
router.push(route.path)
|
||||
}
|
||||
function handleClose(path: string) {
|
||||
tabStore.closeTab(path)
|
||||
router.push(route.fullPath)
|
||||
}
|
||||
const { t } = useI18n()
|
||||
const options = computed(() => {
|
||||
@ -70,16 +70,16 @@ function handleSelect(key: string) {
|
||||
appStore.reloadPage()
|
||||
},
|
||||
closeCurrent() {
|
||||
tabStore.closeTab(currentRoute.value.path)
|
||||
tabStore.closeTab(currentRoute.value.fullPath)
|
||||
},
|
||||
closeOther() {
|
||||
tabStore.closeOtherTabs(currentRoute.value.path)
|
||||
tabStore.closeOtherTabs(currentRoute.value.fullPath)
|
||||
},
|
||||
closeLeft() {
|
||||
tabStore.closeLeftTabs(currentRoute.value.path)
|
||||
tabStore.closeLeftTabs(currentRoute.value.fullPath)
|
||||
},
|
||||
closeRight() {
|
||||
tabStore.closeRightTabs(currentRoute.value.path)
|
||||
tabStore.closeRightTabs(currentRoute.value.fullPath)
|
||||
},
|
||||
closeAll() {
|
||||
tabStore.closeAllTabs()
|
||||
@ -100,55 +100,49 @@ function handleContextMenu(e: MouseEvent, route: RouteLocationNormalized) {
|
||||
function onClickoutside() {
|
||||
showDropdown.value = false
|
||||
}
|
||||
|
||||
// const [DefineTabItem, ReuseTabItem] = createReusableTemplate<{ route: RouteLocationNormalized }>()
|
||||
|
||||
const el = ref()
|
||||
|
||||
useDraggable(el, tabStore.tabs, {
|
||||
animation: 150,
|
||||
ghostClass: 'ghost',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wh-full flex items-end">
|
||||
<n-tabs
|
||||
type="card"
|
||||
size="small"
|
||||
:tabs-padding="15"
|
||||
:value="tabStore.currentTabPath"
|
||||
@close="handleClose"
|
||||
>
|
||||
<n-tab
|
||||
v-for="item in tabStore.pinTabs"
|
||||
:key="item.path"
|
||||
:name="item.path"
|
||||
@click="router.push(item.path)"
|
||||
>
|
||||
<div class="flex-x-center gap-2">
|
||||
<nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }}
|
||||
</div>
|
||||
</n-tab>
|
||||
<n-tab
|
||||
v-for="item in tabStore.tabs"
|
||||
:key="item.path"
|
||||
closable
|
||||
:name="item.path as string"
|
||||
<div class="p-l-2 flex w-full relative">
|
||||
<div class="flex items-end">
|
||||
<TabBarItem
|
||||
v-for="item in tabStore.pinTabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item"
|
||||
@click="handleTab(item)"
|
||||
/>
|
||||
</div>
|
||||
<div ref="el" class="flex items-end flex-1">
|
||||
<TabBarItem
|
||||
v-for="item in tabStore.tabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item" closable
|
||||
@close="tabStore.closeTab"
|
||||
@click="handleTab(item)"
|
||||
@contextmenu="handleContextMenu($event, item)"
|
||||
>
|
||||
<div class="flex-x-center gap-2">
|
||||
<nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }}
|
||||
</div>
|
||||
</n-tab>
|
||||
<template #suffix>
|
||||
<Reload />
|
||||
<DropTabs />
|
||||
</template>
|
||||
</n-tabs>
|
||||
<n-dropdown
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:x="x"
|
||||
:y="y"
|
||||
:options="options"
|
||||
:show="showDropdown"
|
||||
:on-clickoutside="onClickoutside"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
/>
|
||||
<n-dropdown
|
||||
placement="bottom-start" trigger="manual" :x="x" :y="y" :options="options" :show="showDropdown"
|
||||
:on-clickoutside="onClickoutside" @select="handleSelect"
|
||||
/>
|
||||
</div>
|
||||
<!-- <span class="m-l-auto" /> -->
|
||||
<n-el class="absolute right-0 flex items-center gap-1 bg-[var(--base-color)] h-full">
|
||||
<Reload />
|
||||
<ContentFullScreen />
|
||||
<DropTabs />
|
||||
</n-el>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>./DropTabs.vue
|
||||
<style scoped>
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #c4f6d5;
|
||||
}
|
||||
</style>
|
||||
|
41
src/layouts/components/tab/TabBarItem.vue
Normal file
41
src/layouts/components/tab/TabBarItem.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
const { route, value, closable = false } = defineProps<{
|
||||
route: RouteLocationNormalized
|
||||
value: string
|
||||
closable?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-el
|
||||
class="cursor-pointer p-x-4 p-y-2 m-x-2px b b-[--divider-color] b-b-[#0000] rounded-[--border-radius]"
|
||||
:class="[
|
||||
value === route.fullPath ? 'c-[--primary-color]' : 'c-[--text-color-2]',
|
||||
value === route.fullPath ? 'bg-[#0000]' : 'bg-[--tab-color]',
|
||||
closable && 'p-r-2',
|
||||
]"
|
||||
style="transition: box-shadow .3s var(--n-bezier), color .3s var(--n-bezier), background-color .3s var(--n-bezier), border-color .3s var(--n-bezier);"
|
||||
>
|
||||
<div class="flex-center gap-2 text-nowrap">
|
||||
<nova-icon :icon="route.meta.icon" />
|
||||
<span>{{ $t(`route.${String(route.name)}`, route.meta.title) }}</span>
|
||||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
class="bg-transparent h-18px w-18px flex-center text-[var(--close-icon-color)] hover:bg-[var(--close-color-hover)] rounded-3px"
|
||||
style="transition: background-color .3s var(--n-bezier), color .3s var(--n-bezier);"
|
||||
@click.stop="emit('close', route.fullPath)"
|
||||
>
|
||||
<n-icon size="14">
|
||||
<svg viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g fill="currentColor" fill-rule="nonzero"><path d="M2.08859116,2.2156945 L2.14644661,2.14644661 C2.32001296,1.97288026 2.58943736,1.95359511 2.7843055,2.08859116 L2.85355339,2.14644661 L6,5.293 L9.14644661,2.14644661 C9.34170876,1.95118446 9.65829124,1.95118446 9.85355339,2.14644661 C10.0488155,2.34170876 10.0488155,2.65829124 9.85355339,2.85355339 L6.707,6 L9.85355339,9.14644661 C10.0271197,9.32001296 10.0464049,9.58943736 9.91140884,9.7843055 L9.85355339,9.85355339 C9.67998704,10.0271197 9.41056264,10.0464049 9.2156945,9.91140884 L9.14644661,9.85355339 L6,6.707 L2.85355339,9.85355339 C2.65829124,10.0488155 2.34170876,10.0488155 2.14644661,9.85355339 C1.95118446,9.65829124 1.95118446,9.34170876 2.14644661,9.14644661 L5.293,6 L2.14644661,2.85355339 C1.97288026,2.67998704 1.95359511,2.41056264 2.08859116,2.2156945 L2.14644661,2.14644661 L2.08859116,2.2156945 Z" /></g></g></svg>
|
||||
</n-icon>
|
||||
</button>
|
||||
</div>
|
||||
</n-el>
|
||||
</template>
|
@ -1,15 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import leftMenu from './leftMenu.layout.vue'
|
||||
import topMenu from './topMenu.layout.vue'
|
||||
import { useAppStore } from '@/store/app'
|
||||
import { SettingDrawer } from './components'
|
||||
import leftMenu from './leftMenu.layout.vue'
|
||||
import mixMenu from './mixMenu.layout.vue'
|
||||
import topMenu from './topMenu.layout.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const layoutMap = {
|
||||
leftMenu,
|
||||
topMenu,
|
||||
mixMenu,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingDrawer />
|
||||
<component :is="layoutMap[appStore.layoutMode]" />
|
||||
</template>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
import {
|
||||
BackTop,
|
||||
Breadcrumb,
|
||||
@ -12,7 +13,6 @@ import {
|
||||
TabBar,
|
||||
UserCenter,
|
||||
} from './components'
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
|
||||
const routeStore = useRouteStore()
|
||||
const appStore = useAppStore()
|
||||
@ -25,6 +25,7 @@ const appStore = useAppStore()
|
||||
embedded
|
||||
>
|
||||
<n-layout-sider
|
||||
v-if="!appStore.contentFullScreen"
|
||||
bordered
|
||||
:collapsed="appStore.collapsed"
|
||||
collapse-mode="width"
|
||||
@ -43,8 +44,8 @@ const appStore = useAppStore()
|
||||
embedded
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<n-layout-header bordered position="absolute" class="z-1">
|
||||
<div class="h-60px flex-y-center justify-between">
|
||||
<n-layout-header bordered position="absolute" class="z-999">
|
||||
<div v-if="!appStore.contentFullScreen" class="h-60px flex-y-center justify-between">
|
||||
<div class="flex-y-center h-full">
|
||||
<CollapaseButton />
|
||||
<Breadcrumb />
|
||||
@ -61,9 +62,17 @@ const appStore = useAppStore()
|
||||
</div>
|
||||
<TabBar v-if="appStore.showTabs" class="h-45px" />
|
||||
</n-layout-header>
|
||||
<div class="flex-1 p-16px flex flex-col">
|
||||
<div class="h-60px" />
|
||||
<div v-if="appStore.showTabs" class="h-45px" />
|
||||
<!-- 121 = 16 + 45 + 60 45是面包屑高度 60是标签栏高度 -->
|
||||
<!-- 56 = 16 + 40 40是页脚高度 -->
|
||||
<div
|
||||
class="flex-1 p-16px flex flex-col"
|
||||
:class="{
|
||||
'p-t-121px': appStore.showTabs,
|
||||
'p-b-56px': appStore.showFooter && !appStore.contentFullScreen,
|
||||
'p-t-76px': !appStore.showTabs,
|
||||
'p-t-61px': appStore.contentFullScreen,
|
||||
}"
|
||||
>
|
||||
<router-view v-slot="{ Component, route }" class="flex-1">
|
||||
<transition
|
||||
:name="appStore.transitionAnimation"
|
||||
@ -78,10 +87,9 @@ const appStore = useAppStore()
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
<div v-if="appStore.showFooter" class="h-40px" />
|
||||
</div>
|
||||
<n-layout-footer
|
||||
v-if="appStore.showFooter"
|
||||
v-if="appStore.showFooter && !appStore.contentFullScreen"
|
||||
bordered
|
||||
position="absolute"
|
||||
class="h-40px flex-center"
|
||||
|
160
src/layouts/mixMenu.layout.vue
Normal file
160
src/layouts/mixMenu.layout.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MenuInst, MenuOption } from 'naive-ui'
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
import {
|
||||
BackTop,
|
||||
CollapaseButton,
|
||||
FullScreen,
|
||||
Logo,
|
||||
Notices,
|
||||
Search,
|
||||
Setting,
|
||||
TabBar,
|
||||
UserCenter,
|
||||
} from './components'
|
||||
|
||||
const routeStore = useRouteStore()
|
||||
const appStore = useAppStore()
|
||||
const pageRoute = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const menuInstRef = ref<MenuInst | null>(null)
|
||||
|
||||
watch(
|
||||
() => pageRoute.path,
|
||||
() => {
|
||||
menuInstRef.value?.showOption(routeStore.activeMenu as string)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const topMenu = ref<MenuOption[]>([])
|
||||
const activeTopMenu = ref<string>('')
|
||||
function handleTopMenu(rowMenu: MenuOption[]) {
|
||||
topMenu.value = rowMenu.map((i) => {
|
||||
const { icon, label, key } = i
|
||||
return {
|
||||
icon,
|
||||
label,
|
||||
key,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleTopMenu(routeStore.menus)
|
||||
|
||||
// 根据当前页面获取选中菜单和对应侧边菜单
|
||||
const currentMenuKey = pageRoute.matched[1].path
|
||||
handleSideMenu(currentMenuKey)
|
||||
activeTopMenu.value = currentMenuKey
|
||||
})
|
||||
|
||||
const sideMenu = ref<MenuOption[]>([])
|
||||
function handleSideMenu(key: string) {
|
||||
const routeMenu = routeStore.menus as MenuOption[]
|
||||
const targetMenu = routeMenu.find(i => i.key === key)
|
||||
if (targetMenu) {
|
||||
sideMenu.value = targetMenu.children ? targetMenu.children : [targetMenu]
|
||||
}
|
||||
}
|
||||
|
||||
function updateTopMenu(key: string) {
|
||||
handleSideMenu(key)
|
||||
activeTopMenu.value = key
|
||||
router.push(key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout
|
||||
has-sider
|
||||
class="wh-full"
|
||||
embedded
|
||||
>
|
||||
<n-layout-sider
|
||||
v-if="!appStore.contentFullScreen"
|
||||
bordered
|
||||
:collapsed="appStore.collapsed"
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="240"
|
||||
content-style="display: flex;flex-direction: column;min-height:100%;"
|
||||
>
|
||||
<Logo v-if="appStore.showLogo" />
|
||||
<n-scrollbar class="flex-1">
|
||||
<n-menu
|
||||
ref="menuInstRef"
|
||||
:collapsed="appStore.collapsed"
|
||||
:indent="20"
|
||||
:collapsed-width="64"
|
||||
:options="sideMenu"
|
||||
:value="routeStore.activeMenu"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
</n-layout-sider>
|
||||
<n-layout
|
||||
class="h-full flex flex-col"
|
||||
content-style="display: flex;flex-direction: column;min-height:100%;"
|
||||
embedded
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<n-layout-header bordered position="absolute" class="z-999">
|
||||
<div v-if="!appStore.contentFullScreen" class="h-60px flex-y-center justify-between">
|
||||
<CollapaseButton />
|
||||
<n-menu
|
||||
ref="menuInstRef"
|
||||
mode="horizontal"
|
||||
responsive
|
||||
:options="topMenu"
|
||||
:value="activeTopMenu"
|
||||
@update:value="updateTopMenu"
|
||||
/>
|
||||
<div class="flex-y-center gap-1 h-full p-x-xl">
|
||||
<Search />
|
||||
<Notices />
|
||||
<FullScreen />
|
||||
<DarkModeSwitch />
|
||||
<LangsSwitch />
|
||||
<Setting />
|
||||
<UserCenter />
|
||||
</div>
|
||||
</div>
|
||||
<TabBar v-if="appStore.showTabs" class="h-45px" />
|
||||
</n-layout-header>
|
||||
<div
|
||||
class="flex-1 p-16px flex flex-col"
|
||||
:class="{
|
||||
'p-t-121px': appStore.showTabs,
|
||||
'p-b-56px': appStore.showFooter && !appStore.contentFullScreen,
|
||||
'p-t-76px': !appStore.showTabs,
|
||||
'p-t-61px': appStore.contentFullScreen,
|
||||
}"
|
||||
>
|
||||
<router-view v-slot="{ Component, route }" class="flex-1">
|
||||
<transition
|
||||
:name="appStore.transitionAnimation"
|
||||
mode="out-in"
|
||||
>
|
||||
<keep-alive :include="routeStore.cacheRoutes">
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="appStore.loadFlag"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
<n-layout-footer
|
||||
v-if="appStore.showFooter && !appStore.contentFullScreen"
|
||||
bordered
|
||||
position="absolute"
|
||||
class="h-40px flex-center"
|
||||
>
|
||||
{{ appStore.footerText }}
|
||||
</n-layout-footer>
|
||||
<BackTop />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
import {
|
||||
BackTop,
|
||||
FullScreen,
|
||||
@ -10,7 +11,6 @@ import {
|
||||
TabBar,
|
||||
UserCenter,
|
||||
} from './components'
|
||||
import { useAppStore, useRouteStore } from '@/store'
|
||||
|
||||
const routeStore = useRouteStore()
|
||||
const appStore = useAppStore()
|
||||
@ -22,8 +22,8 @@ const appStore = useAppStore()
|
||||
class="h-full flex flex-col" content-style="display: flex;flex-direction: column;min-height:100%;"
|
||||
embedded :native-scrollbar="false"
|
||||
>
|
||||
<n-layout-header bordered position="absolute" class="z-1">
|
||||
<div class="h-60px flex-y-center justify-between shrink-0">
|
||||
<n-layout-header bordered position="absolute" class="z-999">
|
||||
<div v-if="!appStore.contentFullScreen" class="h-60px flex-y-center justify-between shrink-0">
|
||||
<Logo v-if="appStore.showLogo" />
|
||||
<Menu mode="horizontal" responsive />
|
||||
<div class="flex-y-center gap-1 h-full p-x-xl">
|
||||
@ -38,9 +38,15 @@ const appStore = useAppStore()
|
||||
</div>
|
||||
<TabBar v-if="appStore.showTabs" class="h-45px" />
|
||||
</n-layout-header>
|
||||
<div class="flex-1 p-16px flex flex-col">
|
||||
<div class="h-60px" />
|
||||
<div v-if="appStore.showTabs" class="h-45px" />
|
||||
<div
|
||||
class="flex-1 p-16px flex flex-col"
|
||||
:class="{
|
||||
'p-t-121px': appStore.showTabs,
|
||||
'p-b-56px': appStore.showFooter && !appStore.contentFullScreen,
|
||||
'p-t-76px': !appStore.showTabs,
|
||||
'p-t-61px': appStore.contentFullScreen,
|
||||
}"
|
||||
>
|
||||
<router-view v-slot="{ Component, route }" class="flex-1">
|
||||
<transition :name="appStore.transitionAnimation" mode="out-in">
|
||||
<keep-alive :include="routeStore.cacheRoutes">
|
||||
@ -48,9 +54,11 @@ const appStore = useAppStore()
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
<div v-if="appStore.showFooter" class="h-40px" />
|
||||
</div>
|
||||
<n-layout-footer v-if="appStore.showFooter" bordered position="absolute" class="h-40px flex-center">
|
||||
<n-layout-footer
|
||||
v-if="appStore.showFooter && !appStore.contentFullScreen"
|
||||
bordered position="absolute" class="h-40px flex-center"
|
||||
>
|
||||
{{ appStore.footerText }}
|
||||
</n-layout-footer>
|
||||
<BackTop />
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { App } from 'vue'
|
||||
import AppVue from './App.vue'
|
||||
import AppLoading from './components/common/AppLoading.vue'
|
||||
import { installRouter } from '@/router'
|
||||
import { installPinia } from '@/store'
|
||||
import AppVue from './App.vue'
|
||||
import AppLoading from './components/common/AppLoading.vue'
|
||||
|
||||
async function setupApp() {
|
||||
// 载入全局loading加载状态
|
||||
|
@ -1,17 +1,24 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { App } from 'vue'
|
||||
import { local } from '@/utils'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import enUS from '../../locales/en_US.json'
|
||||
import zhCN from '../../locales/zh_CN.json'
|
||||
import { local } from '@/utils'
|
||||
|
||||
const { VITE_DEFAULT_LANG } = import.meta.env
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: local.get('lang') || 'enUS', // 默认显示语言
|
||||
fallbackLocale: 'enUS',
|
||||
locale: local.get('lang') || VITE_DEFAULT_LANG, // 默认显示语言
|
||||
fallbackLocale: VITE_DEFAULT_LANG,
|
||||
messages: {
|
||||
zhCN,
|
||||
enUS,
|
||||
},
|
||||
// 缺失国际化键警告
|
||||
// missingWarn: false,
|
||||
|
||||
// 缺失回退内容警告
|
||||
fallbackWarn: false,
|
||||
})
|
||||
|
||||
export function install(app: App) {
|
||||
|
@ -11,8 +11,8 @@ export function setupRouterGuard(router: Router) {
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 判断是否是外链,如果是直接打开网页并拦截跳转
|
||||
if (to.meta.herf) {
|
||||
window.open(to.meta.herf)
|
||||
if (to.meta.href) {
|
||||
window.open(to.meta.href)
|
||||
return false
|
||||
}
|
||||
// 开始 loadingBar
|
||||
@ -61,7 +61,7 @@ export function setupRouterGuard(router: Router) {
|
||||
// 添加tabs
|
||||
tabStore.addTab(to)
|
||||
// 设置高亮标签;
|
||||
tabStore.setCurrentTab(to.path as string)
|
||||
tabStore.setCurrentTab(to.fullPath as string)
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { App } from 'vue'
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||
import { routes } from './routes.inner'
|
||||
import { setupRouterGuard } from './guard'
|
||||
import { routes } from './routes.inner'
|
||||
|
||||
const { VITE_ROUTE_MODE = 'hash', VITE_BASE_URL } = import.meta.env
|
||||
export const router = createRouter({
|
||||
|
@ -6,7 +6,6 @@ export const routes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
name: 'root',
|
||||
redirect: '/appRoot',
|
||||
// component: () => import('@/layouts/index'),
|
||||
children: [
|
||||
],
|
||||
},
|
||||
|
@ -1,407 +1,448 @@
|
||||
export const staticRoutes: AppRoute.RowRoute[] = [
|
||||
{
|
||||
'name': 'dashboard',
|
||||
'path': '/dashboard',
|
||||
'meta.title': '仪表盘',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:analysis',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 1,
|
||||
'pid': null,
|
||||
name: 'dashboard',
|
||||
path: '/dashboard',
|
||||
title: '仪表盘',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:analysis',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 1,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'workbench',
|
||||
'path': '/dashboard/workbench',
|
||||
'meta.title': '工作台',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:alarm',
|
||||
'meta.pinTab': true,
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/dashboard/workbench/index.vue',
|
||||
'id': 2,
|
||||
'pid': 1,
|
||||
name: 'workbench',
|
||||
path: '/dashboard/workbench',
|
||||
title: '工作台',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:alarm',
|
||||
pinTab: true,
|
||||
menuType: 'page',
|
||||
componentPath: '/dashboard/workbench/index.vue',
|
||||
id: 2,
|
||||
pid: 1,
|
||||
},
|
||||
{
|
||||
'name': 'monitor',
|
||||
'path': '/dashboard/monitor',
|
||||
'meta.title': '监控页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:anchor',
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/dashboard/monitor/index.vue',
|
||||
'id': 3,
|
||||
'pid': 1,
|
||||
name: 'monitor',
|
||||
path: '/dashboard/monitor',
|
||||
title: '监控页',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:anchor',
|
||||
menuType: 'page',
|
||||
componentPath: '/dashboard/monitor/index.vue',
|
||||
id: 3,
|
||||
pid: 1,
|
||||
},
|
||||
{
|
||||
'name': 'test',
|
||||
'path': '/test',
|
||||
'meta.title': '多级菜单演示',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 4,
|
||||
'pid': null,
|
||||
name: 'multi',
|
||||
path: '/multi',
|
||||
title: '多级菜单演示',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 4,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'test2',
|
||||
'path': '/test/test2',
|
||||
'meta.title': '多级菜单子页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/test/test2/index.vue',
|
||||
'id': 6,
|
||||
'pid': 4,
|
||||
name: 'multi2',
|
||||
path: '/multi/multi2',
|
||||
title: '多级菜单子页',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
menuType: 'page',
|
||||
componentPath: '/demo/multi/multi2/index.vue',
|
||||
id: 6,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
'name': 'test2Detail',
|
||||
'path': '/test/test2/detail',
|
||||
'meta.title': '多级菜单的详情页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.hide': true,
|
||||
'meta.activeMenu': '/test/test2',
|
||||
'meta.menuType': 'page',
|
||||
'componentPath': '/test/test2/detail/index.vue',
|
||||
'id': 7,
|
||||
'pid': 4,
|
||||
name: 'multi2Detail',
|
||||
path: '/multi/multi2/detail',
|
||||
title: '菜单详情页',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
hide: true,
|
||||
activeMenu: '/multi/multi2',
|
||||
menuType: 'page',
|
||||
componentPath: '/demo/multi/multi2/detail/index.vue',
|
||||
id: 7,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
'name': 'test3',
|
||||
'path': '/test/test3',
|
||||
'meta.title': '多级菜单',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 8,
|
||||
'pid': 4,
|
||||
name: 'multi3',
|
||||
path: '/multi/multi3',
|
||||
title: '多级菜单',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 8,
|
||||
pid: 4,
|
||||
},
|
||||
{
|
||||
'name': 'test4',
|
||||
'path': '/test/test3/test4',
|
||||
'meta.title': '多级菜单3-1',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list',
|
||||
'componentPath': '/test/test3/test4/index.vue',
|
||||
'id': 9,
|
||||
'pid': 8,
|
||||
name: 'multi4',
|
||||
path: '/multi/multi3/multi4',
|
||||
title: '多级菜单3-1',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list',
|
||||
componentPath: '/demo/multi/multi3/multi4/index.vue',
|
||||
id: 9,
|
||||
pid: 8,
|
||||
},
|
||||
{
|
||||
'name': 'list',
|
||||
'path': '/list',
|
||||
'meta.title': '列表页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list-two',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 10,
|
||||
'pid': null,
|
||||
name: 'list',
|
||||
path: '/list',
|
||||
title: '列表页',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list-two',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 10,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'commonList',
|
||||
'path': '/list/commonList',
|
||||
'meta.title': '常用列表',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:list-view',
|
||||
'componentPath': '/list/commonList/index.vue',
|
||||
'id': 11,
|
||||
'pid': 10,
|
||||
name: 'commonList',
|
||||
path: '/list/commonList',
|
||||
title: '常用列表',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:list-view',
|
||||
componentPath: '/demo/list/commonList/index.vue',
|
||||
id: 11,
|
||||
pid: 10,
|
||||
},
|
||||
{
|
||||
'name': 'cardList',
|
||||
'path': '/list/cardList',
|
||||
'meta.title': '卡片列表',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:view-grid-list',
|
||||
'componentPath': '/list/cardList/index.vue',
|
||||
'id': 12,
|
||||
'pid': 10,
|
||||
name: 'cardList',
|
||||
path: '/list/cardList',
|
||||
title: '卡片列表',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:view-grid-list',
|
||||
componentPath: '/demo/list/cardList/index.vue',
|
||||
id: 12,
|
||||
pid: 10,
|
||||
},
|
||||
{
|
||||
'name': 'demo',
|
||||
'path': '/demo',
|
||||
'meta.title': '功能示例',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:application-one',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 13,
|
||||
'pid': null,
|
||||
name: 'demo',
|
||||
path: '/demo',
|
||||
title: '功能示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:application-one',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 13,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'fetch',
|
||||
'path': '/demo/fetch',
|
||||
'meta.title': '请求示例',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:international',
|
||||
'componentPath': '/demo/fetch/index.vue',
|
||||
'id': 5,
|
||||
'pid': 13,
|
||||
name: 'fetch',
|
||||
path: '/demo/fetch',
|
||||
title: '请求示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:international',
|
||||
componentPath: '/demo/fetch/index.vue',
|
||||
id: 5,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
'name': 'echarts',
|
||||
'path': '/demo/echarts',
|
||||
'meta.title': 'ECharts',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:chart-proportion',
|
||||
'componentPath': '/demo/echarts/index.vue',
|
||||
'id': 15,
|
||||
'pid': 13,
|
||||
name: 'echarts',
|
||||
path: '/demo/echarts',
|
||||
title: 'ECharts',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:chart-proportion',
|
||||
componentPath: '/demo/echarts/index.vue',
|
||||
id: 15,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
'name': 'map',
|
||||
'path': '/demo/map',
|
||||
'meta.title': '地图',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'carbon:map',
|
||||
'meta.keepAlive': true,
|
||||
'componentPath': '/demo/map/index.vue',
|
||||
'id': 17,
|
||||
'pid': 13,
|
||||
name: 'map',
|
||||
path: '/demo/map',
|
||||
title: '地图',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:map',
|
||||
keepAlive: true,
|
||||
componentPath: '/demo/map/index.vue',
|
||||
id: 17,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
'name': 'editor',
|
||||
'path': '/demo/editor',
|
||||
'meta.title': '编辑器',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:editor',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 18,
|
||||
'pid': 13,
|
||||
name: 'editor',
|
||||
path: '/demo/editor',
|
||||
title: '编辑器',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:editor',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 18,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
'name': 'editorMd',
|
||||
'path': '/demo/editor/md',
|
||||
'meta.title': 'MarkDown',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'ri:markdown-line',
|
||||
'componentPath': '/demo/editor/md/index.vue',
|
||||
'id': 19,
|
||||
'pid': 18,
|
||||
name: 'editorMd',
|
||||
path: '/demo/editor/md',
|
||||
title: 'MarkDown',
|
||||
requiresAuth: true,
|
||||
icon: 'ri:markdown-line',
|
||||
componentPath: '/demo/editor/md/index.vue',
|
||||
id: 19,
|
||||
pid: 18,
|
||||
},
|
||||
{
|
||||
'name': 'editorRich',
|
||||
'path': '/demo/editor/rich',
|
||||
'meta.title': '富文本',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:edit-one',
|
||||
'componentPath': '/demo/editor/rich/index.vue',
|
||||
'id': 20,
|
||||
'pid': 18,
|
||||
name: 'editorRich',
|
||||
path: '/demo/editor/rich',
|
||||
title: '富文本',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:edit-one',
|
||||
componentPath: '/demo/editor/rich/index.vue',
|
||||
id: 20,
|
||||
pid: 18,
|
||||
},
|
||||
{
|
||||
'name': 'clipboard',
|
||||
'path': '/demo/clipboard',
|
||||
'meta.title': '剪贴板',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:clipboard',
|
||||
'componentPath': '/demo/clipboard/index.vue',
|
||||
'id': 21,
|
||||
'pid': 13,
|
||||
name: 'clipboard',
|
||||
path: '/demo/clipboard',
|
||||
title: '剪贴板',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:clipboard',
|
||||
componentPath: '/demo/clipboard/index.vue',
|
||||
id: 21,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
'name': 'icons',
|
||||
'path': '/demo/icons',
|
||||
'meta.title': '图标',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:winking-face-with-open-eyes',
|
||||
'componentPath': '/demo/icons/index.vue',
|
||||
'id': 22,
|
||||
'pid': 13,
|
||||
name: 'icons',
|
||||
path: '/demo/icons',
|
||||
title: '图标',
|
||||
requiresAuth: true,
|
||||
icon: 'local:cool',
|
||||
componentPath: '/demo/icons/index.vue',
|
||||
id: 22,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
'name': 'QRCode',
|
||||
'path': '/demo/QRCode',
|
||||
'meta.title': '二维码',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:two-dimensional-code',
|
||||
'componentPath': '/demo/QRCode/index.vue',
|
||||
'id': 23,
|
||||
'pid': 13,
|
||||
name: 'QRCode',
|
||||
path: '/demo/QRCode',
|
||||
title: '二维码',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:two-dimensional-code',
|
||||
componentPath: '/demo/QRCode/index.vue',
|
||||
id: 23,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
'name': 'docments',
|
||||
'path': '/docments',
|
||||
'meta.title': '外链文档',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:file-doc',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 24,
|
||||
'pid': null,
|
||||
name: 'documents',
|
||||
path: '/documents',
|
||||
title: '外链文档',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:file-doc',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 24,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'docmentsVue',
|
||||
'path': '/docments/vue',
|
||||
'meta.title': 'Vue',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'logos:vue',
|
||||
'componentPath': '/docments/vue/index.vue',
|
||||
'id': 25,
|
||||
'pid': 24,
|
||||
name: 'documentsVue',
|
||||
path: '/documents/vue',
|
||||
title: 'Vue',
|
||||
requiresAuth: true,
|
||||
icon: 'logos:vue',
|
||||
componentPath: '/demo/documents/vue/index.vue',
|
||||
id: 25,
|
||||
pid: 24,
|
||||
},
|
||||
{
|
||||
'name': 'docmentsVite',
|
||||
'path': '/docments/vite',
|
||||
'meta.title': 'Vite',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'logos:vitejs',
|
||||
'componentPath': '/docments/vite/index.vue',
|
||||
'id': 26,
|
||||
'pid': 24,
|
||||
name: 'documentsVite',
|
||||
path: '/documents/vite',
|
||||
title: 'Vite',
|
||||
requiresAuth: true,
|
||||
icon: 'logos:vitejs',
|
||||
componentPath: '/demo/documents/vite/index.vue',
|
||||
id: 26,
|
||||
pid: 24,
|
||||
},
|
||||
{
|
||||
'name': 'docmentsVueuse',
|
||||
'path': '/docments/vueuse',
|
||||
'meta.title': 'VueUse(外链)',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'logos:vueuse',
|
||||
'meta.herf': 'https://vueuse.org/guide/',
|
||||
'componentPath': 'null',
|
||||
'id': 27,
|
||||
'pid': 24,
|
||||
name: 'documentsVueuse',
|
||||
path: '/documents/vueuse',
|
||||
title: 'VueUse(外链)',
|
||||
requiresAuth: true,
|
||||
icon: 'logos:vueuse',
|
||||
href: 'https://vueuse.org/guide/',
|
||||
componentPath: 'null',
|
||||
id: 27,
|
||||
pid: 24,
|
||||
},
|
||||
{
|
||||
'name': 'permission',
|
||||
'path': '/permission',
|
||||
'meta.title': '权限',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:people-safe',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 28,
|
||||
'pid': null,
|
||||
name: 'permission',
|
||||
path: '/permission',
|
||||
title: '权限',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:people-safe',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 28,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'permissionDemo',
|
||||
'path': '/permission/permission',
|
||||
'meta.title': '权限示例',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:right-user',
|
||||
'componentPath': '/permission/permission/index.vue',
|
||||
'id': 29,
|
||||
'pid': 28,
|
||||
name: 'permissionDemo',
|
||||
path: '/permission/permission',
|
||||
title: '权限示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:right-user',
|
||||
componentPath: '/demo/permission/permission/index.vue',
|
||||
id: 29,
|
||||
pid: 28,
|
||||
},
|
||||
{
|
||||
'name': 'justSuper',
|
||||
'path': '/permission/justSuper',
|
||||
'meta.title': 'super可见',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.roles': [
|
||||
name: 'justSuper',
|
||||
path: '/permission/justSuper',
|
||||
title: 'super可见',
|
||||
requiresAuth: true,
|
||||
roles: [
|
||||
'super',
|
||||
],
|
||||
'meta.icon': 'icon-park-outline:wrong-user',
|
||||
'componentPath': '/permission/justSuper/index.vue',
|
||||
'id': 30,
|
||||
'pid': 28,
|
||||
icon: 'icon-park-outline:wrong-user',
|
||||
componentPath: '/demo/permission/justSuper/index.vue',
|
||||
id: 30,
|
||||
pid: 28,
|
||||
},
|
||||
{
|
||||
'name': 'error',
|
||||
'path': '/error',
|
||||
'meta.title': '异常页',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:error-computer',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 31,
|
||||
'pid': null,
|
||||
name: 'error',
|
||||
path: '/error',
|
||||
title: '异常页',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:error-computer',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 31,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'demo403',
|
||||
'path': '/error/403',
|
||||
'meta.title': '403',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'carbon:error',
|
||||
'meta.order': 3,
|
||||
'componentPath': '/error/403/index.vue',
|
||||
'id': 32,
|
||||
'pid': 31,
|
||||
name: 'demo403',
|
||||
path: '/error/403',
|
||||
title: '403',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:error',
|
||||
order: 3,
|
||||
componentPath: '/error/403/index.vue',
|
||||
id: 32,
|
||||
pid: 31,
|
||||
},
|
||||
{
|
||||
'name': 'demo404',
|
||||
'path': '/error/404',
|
||||
'meta.title': '404',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:error',
|
||||
'meta.order': 2,
|
||||
'componentPath': '/error/404/index.vue',
|
||||
'id': 33,
|
||||
'pid': 31,
|
||||
name: 'demo404',
|
||||
path: '/error/404',
|
||||
title: '404',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:error',
|
||||
order: 2,
|
||||
componentPath: '/error/404/index.vue',
|
||||
id: 33,
|
||||
pid: 31,
|
||||
},
|
||||
{
|
||||
'name': 'demo500',
|
||||
'path': '/error/500',
|
||||
'meta.title': '500',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'carbon:data-error',
|
||||
'meta.order': 1,
|
||||
'componentPath': '/error/500/index.vue',
|
||||
'id': 34,
|
||||
'pid': 31,
|
||||
name: 'demo500',
|
||||
path: '/error/500',
|
||||
title: '500',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:data-error',
|
||||
order: 1,
|
||||
componentPath: '/error/500/index.vue',
|
||||
id: 34,
|
||||
pid: 31,
|
||||
},
|
||||
{
|
||||
'name': 'setting',
|
||||
'path': '/setting',
|
||||
'meta.title': '系统设置',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:setting',
|
||||
'meta.menuType': 'dir',
|
||||
'componentPath': null,
|
||||
'id': 35,
|
||||
'pid': null,
|
||||
name: 'setting',
|
||||
path: '/setting',
|
||||
title: '系统设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:setting',
|
||||
menuType: 'dir',
|
||||
componentPath: null,
|
||||
id: 35,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'accountSetting',
|
||||
'path': '/setting/account',
|
||||
'meta.title': '用户设置',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:every-user',
|
||||
'componentPath': '/setting/account/index.vue',
|
||||
'id': 36,
|
||||
'pid': 35,
|
||||
name: 'accountSetting',
|
||||
path: '/setting/account',
|
||||
title: '用户设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:every-user',
|
||||
componentPath: '/setting/account/index.vue',
|
||||
id: 36,
|
||||
pid: 35,
|
||||
},
|
||||
{
|
||||
'name': 'dictionarySetting',
|
||||
'path': '/setting/dictionary',
|
||||
'meta.title': '字典设置',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:book-one',
|
||||
'componentPath': '/setting/dictionary/index.vue',
|
||||
'id': 37,
|
||||
'pid': 35,
|
||||
name: 'dictionarySetting',
|
||||
path: '/setting/dictionary',
|
||||
title: '字典设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/setting/dictionary/index.vue',
|
||||
id: 37,
|
||||
pid: 35,
|
||||
},
|
||||
{
|
||||
'name': 'menuSetting',
|
||||
'path': '/setting/menu',
|
||||
'meta.title': '菜单设置',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:application-menu',
|
||||
'componentPath': '/setting/menu/index.vue',
|
||||
'id': 38,
|
||||
'pid': 35,
|
||||
name: 'menuSetting',
|
||||
path: '/setting/menu',
|
||||
title: '菜单设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:application-menu',
|
||||
componentPath: '/setting/menu/index.vue',
|
||||
id: 38,
|
||||
pid: 35,
|
||||
},
|
||||
{
|
||||
'name': 'userCenter',
|
||||
'path': '/userCenter',
|
||||
'meta.title': '个人中心',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'carbon:user-avatar-filled-alt',
|
||||
'componentPath': '/userCenter/index.vue',
|
||||
'id': 39,
|
||||
'pid': null,
|
||||
name: 'userCenter',
|
||||
path: '/userCenter',
|
||||
title: '个人中心',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:user-avatar-filled-alt',
|
||||
componentPath: '/demo/userCenter/index.vue',
|
||||
id: 39,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
'name': 'about',
|
||||
'path': '/about',
|
||||
'meta.title': '关于',
|
||||
'meta.requiresAuth': true,
|
||||
'meta.icon': 'icon-park-outline:info',
|
||||
'componentPath': '/about/index.vue',
|
||||
'id': 40,
|
||||
'pid': null,
|
||||
name: 'about',
|
||||
path: '/about',
|
||||
title: '关于',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:info',
|
||||
componentPath: '/demo/about/index.vue',
|
||||
id: 40,
|
||||
pid: null,
|
||||
},
|
||||
{
|
||||
name: 'cascader',
|
||||
path: '/demo/cascader',
|
||||
title: '省市区联动',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:add-subset',
|
||||
componentPath: '/demo/cascader/index.vue',
|
||||
id: 41,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
name: 'documentsNova',
|
||||
path: '/documents/nova',
|
||||
title: 'Nova docs',
|
||||
requiresAuth: true,
|
||||
icon: 'local:logo',
|
||||
href: 'https://nova-admin-docs.netlify.app/',
|
||||
componentPath: '2333333',
|
||||
id: 42,
|
||||
pid: 24,
|
||||
},
|
||||
{
|
||||
name: 'dict',
|
||||
path: '/demo/dict',
|
||||
title: '字典示例',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:book-one',
|
||||
componentPath: '/demo/dict/index.vue',
|
||||
id: 43,
|
||||
pid: 13,
|
||||
},
|
||||
{
|
||||
name: 'draggableList',
|
||||
path: '/list/draggableList',
|
||||
title: '拖拽列表',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-park-outline:menu-fold',
|
||||
componentPath: '/demo/list/draggableList/index.vue',
|
||||
id: 44,
|
||||
pid: 10,
|
||||
},
|
||||
]
|
||||
|
@ -5,15 +5,15 @@ interface Ilogin {
|
||||
password: string
|
||||
}
|
||||
|
||||
export function fetchLogin(params: Ilogin) {
|
||||
const methodInstance = request.Post<Service.ResponseResult<ApiAuth.loginInfo>>('/login', params)
|
||||
export function fetchLogin(data: Ilogin) {
|
||||
const methodInstance = request.Post<Service.ResponseResult<Api.Login.Info>>('/login', data)
|
||||
methodInstance.meta = {
|
||||
authRole: null,
|
||||
}
|
||||
return methodInstance
|
||||
}
|
||||
export function fetchUpdateToken(data: any) {
|
||||
const method = request.Post<Service.ResponseResult<ApiAuth.loginInfo>>('/updateToken', data)
|
||||
const method = request.Post<Service.ResponseResult<Api.Login.Info>>('/updateToken', data)
|
||||
method.meta = {
|
||||
authRole: 'refreshToken',
|
||||
}
|
||||
|
@ -1,5 +1,26 @@
|
||||
import { request } from '../http'
|
||||
|
||||
// 获取所有路由信息
|
||||
export function fetchAllRoutes() {
|
||||
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]> >('/getUserRoutes')
|
||||
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]>>('/getUserRoutes')
|
||||
}
|
||||
|
||||
// 获取所有用户信息
|
||||
export function fetchUserPage() {
|
||||
return request.Get<Service.ResponseResult<Entity.User[]>>('/userPage')
|
||||
}
|
||||
// 获取所有角色列表
|
||||
export function fetchRoleList() {
|
||||
return request.Get<Service.ResponseResult<Entity.Role[]>>('/role/list')
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求获取字典列表
|
||||
*
|
||||
* @param code - 字典编码,用于筛选特定的字典列表
|
||||
* @returns 返回的字典列表数据
|
||||
*/
|
||||
export function fetchDictList(code?: string) {
|
||||
const params = { code }
|
||||
return request.Get<Service.ResponseResult<Entity.Dict[]>>('/dict/list', { params })
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { blankInstance, request } from '../http'
|
||||
|
||||
/* get方法测试 */
|
||||
export function fetachGet(params?: any) {
|
||||
export function fetchGet(params?: any) {
|
||||
return request.Get('/getAPI', { params })
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ export function withoutToken() {
|
||||
/* 接口数据转换 */
|
||||
export function dictData() {
|
||||
return request.Get('/getDictData', {
|
||||
transformData(rawData, _headers) {
|
||||
transform(rawData, _headers) {
|
||||
const response = rawData as any
|
||||
return {
|
||||
...response,
|
||||
@ -53,7 +53,7 @@ export function dictData() {
|
||||
export function getBlob(url: string) {
|
||||
const methodInstance = blankInstance.Get<Blob>(url)
|
||||
methodInstance.meta = {
|
||||
// 标识为bolb数据
|
||||
// 标识为blob数据
|
||||
isBlob: true,
|
||||
}
|
||||
return methodInstance
|
||||
@ -61,12 +61,9 @@ export function getBlob(url: string) {
|
||||
|
||||
/* 带进度的下载文件 */
|
||||
export function downloadFile(url: string) {
|
||||
const methodInstance = blankInstance.Get<Blob>(url, {
|
||||
// 开启下载进度
|
||||
enableDownload: true,
|
||||
})
|
||||
const methodInstance = blankInstance.Get<Blob>(url)
|
||||
methodInstance.meta = {
|
||||
// 标识为bolb数据
|
||||
// 标识为blob数据
|
||||
isBlob: true,
|
||||
}
|
||||
return methodInstance
|
||||
|
@ -1,30 +1,36 @@
|
||||
import { local } from '@/utils'
|
||||
import { createAlova } from 'alova'
|
||||
import VueHook from 'alova/vue'
|
||||
import GlobalFetch from 'alova/GlobalFetch'
|
||||
import { createServerTokenAuthentication } from '@alova/scene-vue'
|
||||
import qs from 'qs'
|
||||
import { createServerTokenAuthentication } from 'alova/client'
|
||||
import adapterFetch from 'alova/fetch'
|
||||
import VueHook, { type VueHookType } from 'alova/vue'
|
||||
import {
|
||||
DEFAULT_ALOVA_OPTIONS,
|
||||
DEFAULT_BACKEND_OPTIONS,
|
||||
} from './config'
|
||||
import {
|
||||
handleBusinessError,
|
||||
handleRefreshToken,
|
||||
handleResponseError,
|
||||
handleServiceResult,
|
||||
} from './handle'
|
||||
import {
|
||||
DEFAULT_ALOVA_OPTIONS,
|
||||
DEFAULT_BACKEND_OPTIONS,
|
||||
} from './config'
|
||||
import { local } from '@/utils'
|
||||
|
||||
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
|
||||
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<VueHookType>({
|
||||
// 服务端判定token过期
|
||||
refreshTokenOnSuccess: {
|
||||
// 当服务端返回401时,表示token过期
|
||||
isExpired: (response, _method) => {
|
||||
return response.status === 401
|
||||
isExpired: (response, method) => {
|
||||
const isExpired = method.meta && method.meta.isExpired
|
||||
return response.status === 401 && !isExpired
|
||||
},
|
||||
|
||||
// 当token过期时触发,在此函数中触发刷新token
|
||||
handler: async (_response, _method) => {
|
||||
handler: async (_response, method) => {
|
||||
// 此处采取限制,防止过期请求无限循环重发
|
||||
if (!method.meta)
|
||||
method.meta = { isExpired: true }
|
||||
else
|
||||
method.meta.isExpired = true
|
||||
|
||||
await handleRefreshToken()
|
||||
},
|
||||
},
|
||||
@ -44,15 +50,15 @@ export function createAlovaInstance(
|
||||
|
||||
return createAlova({
|
||||
statesHook: VueHook,
|
||||
requestAdapter: GlobalFetch(),
|
||||
localCache: null,
|
||||
requestAdapter: adapterFetch(),
|
||||
cacheFor: null,
|
||||
baseURL: _alovaConfig.baseURL,
|
||||
timeout: _alovaConfig.timeout,
|
||||
|
||||
beforeRequest: onAuthRequired((method) => {
|
||||
if (method.meta?.isFormPost) {
|
||||
method.config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
method.data = qs.stringify(method.data)
|
||||
method.data = new URLSearchParams(method.data as URLSearchParams).toString()
|
||||
}
|
||||
alovaConfig.beforeRequest?.(method)
|
||||
}),
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { fetchUpdateToken } from '@/service'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { local } from '@/utils'
|
||||
import {
|
||||
ERROR_NO_TIP_STATUS,
|
||||
ERROR_STATUS,
|
||||
} from './config'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { fetchUpdateToken } from '@/service'
|
||||
import { local } from '@/utils'
|
||||
|
||||
type ErrorStatus = keyof typeof ERROR_STATUS
|
||||
|
||||
@ -70,6 +70,13 @@ export function handleServiceResult(data: any, isSuccess: boolean = true) {
|
||||
*/
|
||||
export async function handleRefreshToken() {
|
||||
const authStore = useAuthStore()
|
||||
const isAutoRefresh = import.meta.env.VITE_AUTO_REFRESH_TOKEN === 'Y'
|
||||
if (!isAutoRefresh) {
|
||||
await authStore.logout()
|
||||
return
|
||||
}
|
||||
|
||||
// 刷新token
|
||||
const { data } = await fetchUpdateToken({ refreshToken: local.get('refreshToken') })
|
||||
if (data) {
|
||||
local.set('accessToken', data.accessToken)
|
||||
@ -77,7 +84,7 @@ export async function handleRefreshToken() {
|
||||
}
|
||||
else {
|
||||
// 刷新失败,退出
|
||||
await authStore.resetAuthStore()
|
||||
await authStore.logout()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createAlovaInstance } from './alova'
|
||||
import { serviceConfig } from '@/../service.config'
|
||||
import { generateProxyPattern } from '@/../build/proxy'
|
||||
import { serviceConfig } from '@/../service.config'
|
||||
import { createAlovaInstance } from './alova'
|
||||
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from './api/system'
|
||||
export * from './api/login'
|
||||
export * from './api/list'
|
||||
export * from './api/login'
|
||||
export * from './api/system'
|
||||
export * from './api/test'
|
||||
|
@ -1,11 +1,13 @@
|
||||
import type { GlobalThemeOverrides } from 'naive-ui'
|
||||
import { local, setLocale } from '@/utils'
|
||||
import { colord } from 'colord'
|
||||
import { set } from 'radash'
|
||||
import themeConfig from './theme.json'
|
||||
import { local, setLocale } from '@/utils'
|
||||
|
||||
type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out'
|
||||
export type LayoutMode = 'leftMenu' | 'topMenu'
|
||||
export type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out'
|
||||
export type LayoutMode = 'leftMenu' | 'topMenu' | 'mixMenu'
|
||||
|
||||
const { VITE_DEFAULT_LANG, VITE_COPYRIGHT_INFO } = import.meta.env
|
||||
|
||||
const docEle = ref(document.documentElement)
|
||||
|
||||
@ -18,8 +20,8 @@ const { system, store } = useColorMode({
|
||||
export const useAppStore = defineStore('app-store', {
|
||||
state: () => {
|
||||
return {
|
||||
footerText: 'Copyright © 2024 chansee97',
|
||||
lang: 'enUS' as App.lang,
|
||||
footerText: VITE_COPYRIGHT_INFO,
|
||||
lang: VITE_DEFAULT_LANG,
|
||||
theme: themeConfig as GlobalThemeOverrides,
|
||||
primaryColor: themeConfig.common.primaryColor,
|
||||
collapsed: false,
|
||||
@ -33,8 +35,10 @@ export const useAppStore = defineStore('app-store', {
|
||||
showBreadcrumb: true,
|
||||
showBreadcrumbIcon: true,
|
||||
showWatermark: false,
|
||||
showSetting: false,
|
||||
transitionAnimation: 'fade-slide' as TransitionAnimation,
|
||||
layoutMode: 'leftMenu' as LayoutMode,
|
||||
contentFullScreen: false,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@ -59,13 +63,13 @@ export const useAppStore = defineStore('app-store', {
|
||||
this.loadFlag = true
|
||||
this.showLogo = true
|
||||
this.showTabs = true
|
||||
this.showLogo = true
|
||||
this.showFooter = true
|
||||
this.showBreadcrumb = true
|
||||
this.showBreadcrumbIcon = true
|
||||
this.showWatermark = false
|
||||
this.transitionAnimation = 'fade-slide'
|
||||
this.layoutMode = 'leftMenu'
|
||||
this.contentFullScreen = false
|
||||
|
||||
// 重置所有配色
|
||||
this.setPrimaryColor(this.primaryColor)
|
||||
@ -77,7 +81,7 @@ export const useAppStore = defineStore('app-store', {
|
||||
},
|
||||
/* 设置主题色 */
|
||||
setPrimaryColor(color: string) {
|
||||
const brightenColor = colord(color).lighten(0.1).toHex()
|
||||
const brightenColor = colord(color).lighten(0.05).toHex()
|
||||
const darkenColor = colord(color).darken(0.05).toHex()
|
||||
set(this.theme, 'common.primaryColor', color)
|
||||
set(this.theme, 'common.primaryColorHover', brightenColor)
|
||||
@ -124,11 +128,6 @@ export const useAppStore = defineStore('app-store', {
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
storage: localStorage,
|
||||
},
|
||||
],
|
||||
storage: localStorage,
|
||||
},
|
||||
})
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useRouteStore } from './route'
|
||||
import { useTabStore } from './tab'
|
||||
import { fetchLogin } from '@/service'
|
||||
import { router } from '@/router'
|
||||
import { fetchLogin } from '@/service'
|
||||
import { local } from '@/utils'
|
||||
import { useRouteStore } from './router'
|
||||
import { useTabStore } from './tab'
|
||||
|
||||
interface AuthStatus {
|
||||
userInfo: ApiAuth.loginInfo | null
|
||||
userInfo: Api.Login.Info | null
|
||||
token: string
|
||||
}
|
||||
export const useAuthStore = defineStore('auth-store', {
|
||||
@ -23,7 +23,7 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
},
|
||||
actions: {
|
||||
/* 登录退出,重置用户信息等 */
|
||||
async resetAuthStore() {
|
||||
async logout() {
|
||||
const route = unref(router.currentRoute)
|
||||
// 清除本地缓存
|
||||
this.clearAuthStorage()
|
||||
@ -33,7 +33,7 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
// 清空标签栏数据
|
||||
const tabStore = useTabStore()
|
||||
tabStore.clearAllTabs()
|
||||
// 重制当前存储库
|
||||
// 重置当前存储库
|
||||
this.$reset()
|
||||
// 重定向到登录页
|
||||
if (route.meta.requiresAuth) {
|
||||
@ -53,16 +53,21 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
|
||||
/* 用户登录 */
|
||||
async login(userName: string, password: string) {
|
||||
const { isSuccess, data } = await fetchLogin({ userName, password })
|
||||
if (!isSuccess)
|
||||
return
|
||||
try {
|
||||
const { isSuccess, data } = await fetchLogin({ userName, password })
|
||||
if (!isSuccess)
|
||||
return
|
||||
|
||||
// 处理登录信息
|
||||
await this.handleAfterLogin(data)
|
||||
// 处理登录信息
|
||||
await this.handleLoginInfo(data)
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('[Login Error]:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/* 登录后的处理函数 */
|
||||
async handleAfterLogin(data: ApiAuth.loginInfo) {
|
||||
/* 处理登录返回的数据 */
|
||||
async handleLoginInfo(data: Api.Login.Info) {
|
||||
// 将token和userInfo保存下来
|
||||
local.set('userInfo', data)
|
||||
local.set('accessToken', data.accessToken)
|
||||
|
58
src/store/dict.ts
Normal file
58
src/store/dict.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { fetchDictList } from '@/service'
|
||||
import { session } from '@/utils'
|
||||
|
||||
export const useDictStore = defineStore('dict-store', {
|
||||
state: () => {
|
||||
return {
|
||||
dictMap: {} as DictMap,
|
||||
isInitDict: false,
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async dict(code: string) {
|
||||
// 调用前初始化
|
||||
if (!this.dictMap) {
|
||||
this.initDict()
|
||||
}
|
||||
|
||||
const targetDict = await this.getDict(code)
|
||||
|
||||
return {
|
||||
data: () => targetDict,
|
||||
enum: () => Object.fromEntries(targetDict.map(({ value, label }) => [value, label])),
|
||||
valueMap: () => Object.fromEntries(targetDict.map(({ value, ...data }) => [value, data])),
|
||||
labelMap: () => Object.fromEntries(targetDict.map(({ label, ...data }) => [label, data])),
|
||||
}
|
||||
},
|
||||
async getDict(code: string) {
|
||||
const isExist = Reflect.has(this.dictMap, code)
|
||||
|
||||
if (isExist) {
|
||||
return this.dictMap[code]
|
||||
}
|
||||
else {
|
||||
return await this.getDictByNet(code)
|
||||
}
|
||||
},
|
||||
|
||||
async getDictByNet(code: string) {
|
||||
const { data, isSuccess } = await fetchDictList(code)
|
||||
if (isSuccess) {
|
||||
Reflect.set(this.dictMap, code, data)
|
||||
// 同步至session
|
||||
session.set('dict', this.dictMap)
|
||||
return data
|
||||
}
|
||||
else {
|
||||
throw new Error(`Failed to get ${code} dictionary from network, check ${code} field or network`)
|
||||
}
|
||||
},
|
||||
initDict() {
|
||||
const dict = session.get('dict')
|
||||
if (dict) {
|
||||
Object.assign(this.dictMap, dict)
|
||||
}
|
||||
this.isInitDict = true
|
||||
},
|
||||
},
|
||||
})
|
@ -1,14 +1,15 @@
|
||||
import type { App } from 'vue'
|
||||
import piniaPluginPersist from 'pinia-plugin-persist'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
export * from './app/index'
|
||||
export * from './auth'
|
||||
export * from './route'
|
||||
export * from './dict'
|
||||
export * from './router'
|
||||
export * from './tab'
|
||||
|
||||
// 安装pinia全局状态库
|
||||
export function installPinia(app: App) {
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersist)
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
app.use(pinia)
|
||||
}
|
||||
|
@ -1,207 +0,0 @@
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { h } from 'vue'
|
||||
import { clone, construct, min } from 'radash'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { $t, arrayToTree, local, renderIcon } from '@/utils'
|
||||
import { router } from '@/router'
|
||||
import { fetchUserRoutes } from '@/service'
|
||||
import { staticRoutes } from '@/router/routes.static'
|
||||
import { usePermission } from '@/hooks'
|
||||
import Layout from '@/layouts/index.vue'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
|
||||
interface RoutesStatus {
|
||||
isInitAuthRoute: boolean
|
||||
menus: AppRoute.Route[]
|
||||
rowRoutes: AppRoute.RowRoute[]
|
||||
activeMenu: string | null
|
||||
cacheRoutes: string[]
|
||||
}
|
||||
export const useRouteStore = defineStore('route-store', {
|
||||
state: (): RoutesStatus => {
|
||||
return {
|
||||
isInitAuthRoute: false,
|
||||
menus: [],
|
||||
rowRoutes: [],
|
||||
activeMenu: null,
|
||||
cacheRoutes: [],
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
resetRouteStore() {
|
||||
this.resetRoutes()
|
||||
this.$reset()
|
||||
},
|
||||
resetRoutes() {
|
||||
router.removeRoute('appRoot')
|
||||
},
|
||||
// set the currently highlighted menu key
|
||||
setActiveMenu(key: string) {
|
||||
this.activeMenu = key
|
||||
},
|
||||
/* 生成侧边菜单的数据 */
|
||||
createMenus(userRoutes: AppRoute.RowRoute[]) {
|
||||
const resultMenus = clone(userRoutes).map(i => construct(i)) as AppRoute.Route[]
|
||||
|
||||
// filter menus that do not need to be displayed
|
||||
const visibleMenus = resultMenus.filter(route => !route.meta.hide)
|
||||
|
||||
// generate side menu
|
||||
this.menus = arrayToTree(this.transformAuthRoutesToMenus(visibleMenus))
|
||||
},
|
||||
|
||||
// render the returned routing table as a sidebar
|
||||
transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]): MenuOption[] {
|
||||
const { hasPermission } = usePermission()
|
||||
// Filter out side menus without permission
|
||||
return userRoutes.filter(i => hasPermission(i.meta.roles))
|
||||
// Sort the menu according to the order size
|
||||
.sort((a, b) => {
|
||||
if (a.meta && a.meta.order && b.meta && b.meta.order)
|
||||
return a.meta.order - b.meta.order
|
||||
else if (a.meta && a.meta.order)
|
||||
return -1
|
||||
else if (b.meta && b.meta.order)
|
||||
return 1
|
||||
else return 0
|
||||
})
|
||||
|
||||
// Convert to side menu data structure
|
||||
.map((item) => {
|
||||
const target: MenuOption = {
|
||||
id: item.id,
|
||||
pid: item.pid,
|
||||
label:
|
||||
(!item.meta.menuType || item.meta.menuType === 'page')
|
||||
? () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: item.path,
|
||||
},
|
||||
},
|
||||
{ default: () => $t(`route.${String(item.name)}`, item.meta.title) },
|
||||
)
|
||||
: () => $t(`route.${String(item.name)}`, item.meta.title),
|
||||
key: item.path,
|
||||
icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined,
|
||||
}
|
||||
return target
|
||||
})
|
||||
},
|
||||
createRoutes(routes: AppRoute.RowRoute[]) {
|
||||
const { hasPermission } = usePermission()
|
||||
|
||||
// Structure the meta field
|
||||
let resultRouter = clone(routes).map(i => construct(i)) as AppRoute.Route[]
|
||||
|
||||
// Route permission filtering
|
||||
resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles))
|
||||
|
||||
// Generate an array of route names that need to be kept alive
|
||||
this.cacheRoutes = resultRouter.filter((i) => {
|
||||
return i.meta.keepAlive
|
||||
})
|
||||
.map(i => i.name)
|
||||
|
||||
// Generate routes, no need to import files for those with redirect
|
||||
const modules = import.meta.glob('@/views/**/*.vue')
|
||||
resultRouter = resultRouter.map((item: AppRoute.Route) => {
|
||||
if (item.componentPath && !item.redirect)
|
||||
item.component = modules[`/src/views${item.componentPath}`]
|
||||
return item
|
||||
})
|
||||
|
||||
// Generate route tree
|
||||
resultRouter = arrayToTree(resultRouter) as AppRoute.Route[]
|
||||
|
||||
const appRootRoute: RouteRecordRaw = {
|
||||
path: '/appRoot',
|
||||
name: 'appRoot',
|
||||
redirect: import.meta.env.VITE_HOME_PATH,
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '',
|
||||
icon: 'icon-park-outline:home',
|
||||
},
|
||||
children: [],
|
||||
}
|
||||
|
||||
// Set the correct redirect path for the route
|
||||
this.setRedirect(resultRouter)
|
||||
|
||||
// Insert the processed route into the root route
|
||||
appRootRoute.children = resultRouter as unknown as RouteRecordRaw[]
|
||||
router.addRoute(appRootRoute)
|
||||
},
|
||||
setRedirect(routes: AppRoute.Route[]) {
|
||||
routes.forEach((route) => {
|
||||
if (route.children) {
|
||||
if (!route.redirect) {
|
||||
// Filter out a collection of child elements that are not hidden
|
||||
const visibleChilds = route.children.filter(child => !child.meta.hide)
|
||||
|
||||
// Redirect page to the path of the first child element by default
|
||||
let target = visibleChilds[0]
|
||||
|
||||
// Filter out pages with the order attribute
|
||||
const orderChilds = visibleChilds.filter(child => child.meta.order)
|
||||
|
||||
if (orderChilds.length > 0)
|
||||
target = min(orderChilds, i => i.meta.order!) as AppRoute.Route
|
||||
|
||||
route.redirect = target.path
|
||||
}
|
||||
|
||||
this.setRedirect(route.children)
|
||||
}
|
||||
})
|
||||
},
|
||||
async initRouteInfo() {
|
||||
if (import.meta.env.VITE_AUTH_ROUTE_MODE === 'dynamic') {
|
||||
const userInfo = local.get('userInfo')
|
||||
|
||||
if (!userInfo || !userInfo.id) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.resetAuthStore()
|
||||
return
|
||||
}
|
||||
|
||||
// Get user's route
|
||||
const { data } = await fetchUserRoutes({
|
||||
id: userInfo.id,
|
||||
})
|
||||
|
||||
if (!data)
|
||||
return
|
||||
|
||||
return data
|
||||
}
|
||||
else {
|
||||
this.rowRoutes = staticRoutes
|
||||
return staticRoutes
|
||||
}
|
||||
},
|
||||
async initAuthRoute() {
|
||||
this.isInitAuthRoute = false
|
||||
|
||||
// Initialize route information
|
||||
const rowRoutes = await this.initRouteInfo()
|
||||
if (!rowRoutes) {
|
||||
window.$message.error($t(`app.getRouteError`))
|
||||
return
|
||||
}
|
||||
this.rowRoutes = rowRoutes
|
||||
|
||||
// Generate actual route and insert
|
||||
this.createRoutes(rowRoutes)
|
||||
|
||||
// Generate side menu
|
||||
this.createMenus(rowRoutes)
|
||||
|
||||
this.isInitAuthRoute = true
|
||||
},
|
||||
},
|
||||
})
|
143
src/store/router/helper.ts
Normal file
143
src/store/router/helper.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { usePermission } from '@/hooks'
|
||||
import Layout from '@/layouts/index.vue'
|
||||
import { $t, arrayToTree, renderIcon } from '@/utils'
|
||||
import { clone, min, omit, pick } from 'radash'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const metaFields: AppRoute.MetaKeys[]
|
||||
= ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType']
|
||||
|
||||
function standardizedRoutes(route: AppRoute.RowRoute[]) {
|
||||
return clone(route).map((i) => {
|
||||
const route = omit(i, metaFields)
|
||||
|
||||
Reflect.set(route, 'meta', pick(i, metaFields))
|
||||
return route
|
||||
}) as AppRoute.Route[]
|
||||
}
|
||||
|
||||
export function createRoutes(routes: AppRoute.RowRoute[]) {
|
||||
const { hasPermission } = usePermission()
|
||||
|
||||
// Structure the meta field
|
||||
let resultRouter = standardizedRoutes(routes)
|
||||
|
||||
// Route permission filtering
|
||||
resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles))
|
||||
|
||||
// Generate routes, no need to import files for those with redirect
|
||||
const modules = import.meta.glob('@/views/**/*.vue')
|
||||
resultRouter = resultRouter.map((item: AppRoute.Route) => {
|
||||
if (item.componentPath && !item.redirect)
|
||||
item.component = modules[`/src/views${item.componentPath}`]
|
||||
return item
|
||||
})
|
||||
|
||||
// Generate route tree
|
||||
resultRouter = arrayToTree(resultRouter) as AppRoute.Route[]
|
||||
|
||||
const appRootRoute: RouteRecordRaw = {
|
||||
path: '/appRoot',
|
||||
name: 'appRoot',
|
||||
redirect: import.meta.env.VITE_HOME_PATH,
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '',
|
||||
icon: 'icon-park-outline:home',
|
||||
},
|
||||
children: [],
|
||||
}
|
||||
|
||||
// Set the correct redirect path for the route
|
||||
setRedirect(resultRouter)
|
||||
|
||||
// Insert the processed route into the root route
|
||||
appRootRoute.children = resultRouter as unknown as RouteRecordRaw[]
|
||||
return appRootRoute
|
||||
}
|
||||
|
||||
// Generate an array of route names that need to be kept alive
|
||||
export function generateCacheRoutes(routes: AppRoute.RowRoute[]) {
|
||||
return routes
|
||||
.filter(i => i.keepAlive)
|
||||
.map(i => i.name)
|
||||
}
|
||||
|
||||
function setRedirect(routes: AppRoute.Route[]) {
|
||||
routes.forEach((route) => {
|
||||
if (route.children) {
|
||||
if (!route.redirect) {
|
||||
// Filter out a collection of child elements that are not hidden
|
||||
const visibleChilds = route.children.filter(child => !child.meta.hide)
|
||||
|
||||
// Redirect page to the path of the first child element by default
|
||||
let target = visibleChilds[0]
|
||||
|
||||
// Filter out pages with the order attribute
|
||||
const orderChilds = visibleChilds.filter(child => child.meta.order)
|
||||
|
||||
if (orderChilds.length > 0)
|
||||
target = min(orderChilds, i => i.meta.order!) as AppRoute.Route
|
||||
|
||||
if (target)
|
||||
route.redirect = target.path
|
||||
}
|
||||
|
||||
setRedirect(route.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 生成侧边菜单的数据 */
|
||||
export function createMenus(userRoutes: AppRoute.RowRoute[]) {
|
||||
const resultMenus = standardizedRoutes(userRoutes)
|
||||
|
||||
// filter menus that do not need to be displayed
|
||||
const visibleMenus = resultMenus.filter(route => !route.meta.hide)
|
||||
|
||||
// generate side menu
|
||||
return arrayToTree(transformAuthRoutesToMenus(visibleMenus))
|
||||
}
|
||||
|
||||
// render the returned routing table as a sidebar
|
||||
function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
|
||||
const { hasPermission } = usePermission()
|
||||
return userRoutes
|
||||
// Filter out side menus without permission
|
||||
.filter(i => hasPermission(i.meta.roles))
|
||||
// Sort the menu according to the order size
|
||||
.sort((a, b) => {
|
||||
if (a.meta && a.meta.order && b.meta && b.meta.order)
|
||||
return a.meta.order - b.meta.order
|
||||
else if (a.meta && a.meta.order)
|
||||
return -1
|
||||
else if (b.meta && b.meta.order)
|
||||
return 1
|
||||
else return 0
|
||||
})
|
||||
// Convert to side menu data structure
|
||||
.map((item) => {
|
||||
const target: MenuOption = {
|
||||
id: item.id,
|
||||
pid: item.pid,
|
||||
label:
|
||||
(!item.meta.menuType || item.meta.menuType === 'page')
|
||||
? () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: item.path,
|
||||
},
|
||||
},
|
||||
{ default: () => $t(`route.${String(item.name)}`, item.meta.title) },
|
||||
)
|
||||
: () => $t(`route.${String(item.name)}`, item.meta.title),
|
||||
key: item.path,
|
||||
icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined,
|
||||
}
|
||||
return target
|
||||
})
|
||||
}
|
89
src/store/router/index.ts
Normal file
89
src/store/router/index.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import { router } from '@/router'
|
||||
import { staticRoutes } from '@/router/routes.static'
|
||||
import { fetchUserRoutes } from '@/service'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { $t, local } from '@/utils'
|
||||
import { createMenus, createRoutes, generateCacheRoutes } from './helper'
|
||||
|
||||
interface RoutesStatus {
|
||||
isInitAuthRoute: boolean
|
||||
menus: MenuOption[]
|
||||
rowRoutes: AppRoute.RowRoute[]
|
||||
activeMenu: string | null
|
||||
cacheRoutes: string[]
|
||||
}
|
||||
export const useRouteStore = defineStore('route-store', {
|
||||
state: (): RoutesStatus => {
|
||||
return {
|
||||
isInitAuthRoute: false,
|
||||
activeMenu: null,
|
||||
menus: [],
|
||||
rowRoutes: [],
|
||||
cacheRoutes: [],
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
resetRouteStore() {
|
||||
this.resetRoutes()
|
||||
this.$reset()
|
||||
},
|
||||
resetRoutes() {
|
||||
if (router.hasRoute('appRoot'))
|
||||
router.removeRoute('appRoot')
|
||||
},
|
||||
// set the currently highlighted menu key
|
||||
setActiveMenu(key: string) {
|
||||
this.activeMenu = key
|
||||
},
|
||||
|
||||
async initRouteInfo() {
|
||||
if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') {
|
||||
const userInfo = local.get('userInfo')
|
||||
|
||||
if (!userInfo || !userInfo.id) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
return
|
||||
}
|
||||
|
||||
// Get user's route
|
||||
const { data } = await fetchUserRoutes({
|
||||
id: userInfo.id,
|
||||
})
|
||||
|
||||
if (!data)
|
||||
return
|
||||
|
||||
return data
|
||||
}
|
||||
else {
|
||||
this.rowRoutes = staticRoutes
|
||||
return staticRoutes
|
||||
}
|
||||
},
|
||||
async initAuthRoute() {
|
||||
this.isInitAuthRoute = false
|
||||
|
||||
// Initialize route information
|
||||
const rowRoutes = await this.initRouteInfo()
|
||||
if (!rowRoutes) {
|
||||
window.$message.error($t(`app.getRouteError`))
|
||||
return
|
||||
}
|
||||
this.rowRoutes = rowRoutes
|
||||
|
||||
// Generate actual route and insert
|
||||
const routes = createRoutes(rowRoutes)
|
||||
router.addRoute(routes)
|
||||
|
||||
// Generate side menu
|
||||
this.menus = createMenus(rowRoutes)
|
||||
|
||||
// Generate the route cache
|
||||
this.cacheRoutes = generateCacheRoutes(rowRoutes)
|
||||
|
||||
this.isInitAuthRoute = true
|
||||
},
|
||||
},
|
||||
})
|
@ -24,7 +24,7 @@ export const useTabStore = defineStore('tab-store', {
|
||||
return
|
||||
|
||||
// 如果标签名称已存在则不添加
|
||||
if (this.hasExistTab(route.path as string))
|
||||
if (this.hasExistTab(route.fullPath as string))
|
||||
return
|
||||
|
||||
// 根据meta.pinTab传递到不同的分组中
|
||||
@ -33,42 +33,42 @@ export const useTabStore = defineStore('tab-store', {
|
||||
else
|
||||
this.tabs.push(route)
|
||||
},
|
||||
async closeTab(path: string) {
|
||||
async closeTab(fullPath: string) {
|
||||
const tabsLength = this.tabs.length
|
||||
// 如果动态标签大于一个,才会标签跳转
|
||||
if (this.tabs.length > 1) {
|
||||
// 获取关闭的标签索引
|
||||
const index = this.getTabIndex(path)
|
||||
const index = this.getTabIndex(fullPath)
|
||||
const isLast = index + 1 === tabsLength
|
||||
// 如果是关闭的当前页面,路由跳转到原先标签的后一个标签
|
||||
if (this.currentTabPath === path && !isLast) {
|
||||
if (this.currentTabPath === fullPath && !isLast) {
|
||||
// 跳转到后一个标签
|
||||
router.push(this.tabs[index + 1].path)
|
||||
router.push(this.tabs[index + 1].fullPath)
|
||||
}
|
||||
else if (this.currentTabPath === path && isLast) {
|
||||
else if (this.currentTabPath === fullPath && isLast) {
|
||||
// 已经是最后一个了,就跳转前一个
|
||||
router.push(this.tabs[index - 1].path)
|
||||
router.push(this.tabs[index - 1].fullPath)
|
||||
}
|
||||
}
|
||||
// 删除标签
|
||||
this.tabs = this.tabs.filter((item) => {
|
||||
return item.path !== path
|
||||
return item.fullPath !== fullPath
|
||||
})
|
||||
// 删除后如果清空了,就跳转到默认首页
|
||||
if (tabsLength - 1 === 0)
|
||||
router.push('/')
|
||||
},
|
||||
|
||||
closeOtherTabs(path: string) {
|
||||
const index = this.getTabIndex(path)
|
||||
closeOtherTabs(fullPath: string) {
|
||||
const index = this.getTabIndex(fullPath)
|
||||
this.tabs = this.tabs.filter((item, i) => i === index)
|
||||
},
|
||||
closeLeftTabs(path: string) {
|
||||
const index = this.getTabIndex(path)
|
||||
closeLeftTabs(fullPath: string) {
|
||||
const index = this.getTabIndex(fullPath)
|
||||
this.tabs = this.tabs.filter((item, i) => i >= index)
|
||||
},
|
||||
closeRightTabs(path: string) {
|
||||
const index = this.getTabIndex(path)
|
||||
closeRightTabs(fullPath: string) {
|
||||
const index = this.getTabIndex(fullPath)
|
||||
this.tabs = this.tabs.filter((item, i) => i <= index)
|
||||
},
|
||||
clearAllTabs() {
|
||||
@ -80,28 +80,27 @@ export const useTabStore = defineStore('tab-store', {
|
||||
router.push('/')
|
||||
},
|
||||
|
||||
hasExistTab(path: string) {
|
||||
hasExistTab(fullPath: string) {
|
||||
const _tabs = [...this.tabs, ...this.pinTabs]
|
||||
return _tabs.some((item) => {
|
||||
return item.path === path
|
||||
return item.fullPath === fullPath
|
||||
})
|
||||
},
|
||||
/* 设置当前激活的标签 */
|
||||
setCurrentTab(path: string) {
|
||||
this.currentTabPath = path
|
||||
setCurrentTab(fullPath: string) {
|
||||
this.currentTabPath = fullPath
|
||||
},
|
||||
getTabIndex(path: string) {
|
||||
getTabIndex(fullPath: string) {
|
||||
return this.tabs.findIndex((item) => {
|
||||
return item.path === path
|
||||
return item.fullPath === fullPath
|
||||
})
|
||||
},
|
||||
modifyTab(fullPath: string, modifyFn: (route: RouteLocationNormalized) => void) {
|
||||
const index = this.getTabIndex(fullPath)
|
||||
modifyFn(this.tabs[index])
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
storage: sessionStorage,
|
||||
},
|
||||
],
|
||||
storage: sessionStorage,
|
||||
},
|
||||
})
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import './reset.css';
|
||||
@import './transition.css';
|
||||
@import './naive.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
@ -13,3 +14,7 @@ body,
|
||||
.gray-mode {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
}
|
||||
|
14
src/styles/naive.css
Normal file
14
src/styles/naive.css
Normal file
@ -0,0 +1,14 @@
|
||||
.n-modal-mask {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* 解决tabs组件不贴合下边缘问题 */
|
||||
.v-x-scroll {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 解决二维码尺寸问题 */
|
||||
.n-qr-code{
|
||||
height: unset !important;
|
||||
width: unset !important;;
|
||||
}
|
40
src/typings/api.d.ts
vendored
40
src/typings/api.d.ts
vendored
@ -1,40 +0,0 @@
|
||||
/* 接口类型数据 */
|
||||
|
||||
/** 后端返回的用户相关类型 */
|
||||
declare namespace ApiAuth {
|
||||
/* 登录返回的用户字段 */
|
||||
interface loginInfo {
|
||||
/** 用户id */
|
||||
id: number
|
||||
/** 用户名 */
|
||||
userName: string
|
||||
/* 用户头像 */
|
||||
avatar?: string
|
||||
/* 用户邮箱 */
|
||||
email?: string
|
||||
/* 用户昵称 */
|
||||
nickname?: string
|
||||
/* 用户电话 */
|
||||
tel?: string
|
||||
/** 用户角色类型 */
|
||||
role: Auth.RoleType
|
||||
/** 访问toekn */
|
||||
accessToken: string
|
||||
/** 刷新toekn */
|
||||
refreshToken: string
|
||||
}
|
||||
}
|
||||
declare namespace CommonList {
|
||||
/* 返回的性别类型 */
|
||||
type GenderType = '0' | '1' | null
|
||||
interface UserList {
|
||||
id: number
|
||||
name: string
|
||||
age: number
|
||||
gender: GenderType
|
||||
email: string
|
||||
address: string
|
||||
role: Auth.RoleType
|
||||
disabled: boolean
|
||||
}
|
||||
}
|
17
src/typings/api/login.d.ts
vendored
Normal file
17
src/typings/api/login.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
namespace Api {
|
||||
namespace Login {
|
||||
/* 登录返回的用户字段, 该数据是根据用户表扩展而来, 部分字段可能需要覆盖,例如id */
|
||||
interface Info extends Entity.User {
|
||||
/** 用户id */
|
||||
id: number
|
||||
/** 用户角色类型 */
|
||||
role: Entity.RoleType[]
|
||||
/** 访问toekn */
|
||||
accessToken: string
|
||||
/** 刷新toekn */
|
||||
refreshToken: string
|
||||
}
|
||||
}
|
||||
}
|
13
src/typings/entities/dict.d.ts
vendored
Normal file
13
src/typings/entities/dict.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/* 字典数据库表字段 */
|
||||
namespace Entity {
|
||||
|
||||
interface Dict {
|
||||
id?: number
|
||||
isRoot?: 0 | 1
|
||||
code: string
|
||||
label: string
|
||||
value?: number
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
/** 用户相关模块 */
|
||||
declare namespace Auth {
|
||||
/** 用户角色类型 */
|
||||
type RoleType = 'super' | 'admin' | 'user'
|
||||
}
|
||||
/* 系统消息 */
|
||||
declare namespace Message {
|
||||
interface List {
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/* 角色数据库表字段 */
|
||||
namespace Entity {
|
||||
interface Message {
|
||||
id: number
|
||||
type: 0 | 1 | 2
|
||||
title: string
|
13
src/typings/entities/role.d.ts
vendored
Normal file
13
src/typings/entities/role.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/* 角色数据库表字段 */
|
||||
namespace Entity {
|
||||
type RoleType = 'super' | 'admin' | 'user'
|
||||
|
||||
interface Role {
|
||||
/** 用户id */
|
||||
id?: number
|
||||
/** 用户名 */
|
||||
role?: RoleType
|
||||
}
|
||||
}
|
28
src/typings/entities/user.d.ts
vendored
Normal file
28
src/typings/entities/user.d.ts
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
/// <reference path="../global.d.ts"/>
|
||||
|
||||
/** 用户数据库表字段 */
|
||||
namespace Entity {
|
||||
interface User {
|
||||
/** 用户id */
|
||||
id?: number
|
||||
/** 用户名 */
|
||||
userName?: string
|
||||
/* 用户头像 */
|
||||
avatar?: string
|
||||
/* 用户性别 */
|
||||
gender?: 0 | 1
|
||||
/* 用户邮箱 */
|
||||
email?: string
|
||||
/* 用户昵称 */
|
||||
nickname?: string
|
||||
/* 用户电话 */
|
||||
tel?: string
|
||||
/** 用户角色类型 */
|
||||
role?: Entity.RoleType[]
|
||||
/** 用户状态 */
|
||||
status?: 0 | 1
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
}
|
||||
|
||||
}
|
11
src/typings/env.d.ts
vendored
11
src/typings/env.d.ts
vendored
@ -21,13 +21,18 @@ interface ImportMetaEnv {
|
||||
| 'brotliCompress'
|
||||
| 'deflate'
|
||||
| 'deflateRaw'
|
||||
/** hash路由模式 */
|
||||
/** 路由模式 */
|
||||
readonly VITE_ROUTE_MODE?: 'hash' | 'web'
|
||||
/** 路由加载模式 */
|
||||
readonly VITE_AUTH_ROUTE_MODE: 'static' | 'dynamic'
|
||||
readonly VITE_ROUTE_LOAD_MODE: 'static' | 'dynamic'
|
||||
/** 首次加载页面 */
|
||||
readonly VITE_HOME_PATH: string
|
||||
|
||||
/** 版权信息 */
|
||||
readonly VITE_COPYRIGHT_INFO: string
|
||||
/** 是否自动刷新token */
|
||||
readonly VITE_AUTO_REFRESH_TOKEN: 'Y' | 'N'
|
||||
/** 默认语言 */
|
||||
readonly VITE_DEFAULT_LANG: App.lang
|
||||
/** 后端服务的环境类型 */
|
||||
readonly MODE: ServiceEnvType
|
||||
}
|
||||
|
17
src/typings/global.d.ts
vendored
17
src/typings/global.d.ts
vendored
@ -1,3 +1,12 @@
|
||||
/* 存放数据库实体表类型, 具体内容在 ./entities */
|
||||
declare namespace Entity {
|
||||
}
|
||||
|
||||
/* 各类接口返回的数据类型, 具体内容在 ./api */
|
||||
declare namespace Api {
|
||||
|
||||
}
|
||||
|
||||
interface Window {
|
||||
$loadingBar: import('naive-ui').LoadingBarApi
|
||||
$dialog: import('naive-ui').DialogApi
|
||||
@ -21,12 +30,12 @@ declare namespace NaiveUI {
|
||||
|
||||
declare namespace Storage {
|
||||
interface Session {
|
||||
demoKey: string
|
||||
dict: DictMap
|
||||
}
|
||||
|
||||
interface Local {
|
||||
/* 存储用户信息 */
|
||||
userInfo: ApiAuth.loginInfo
|
||||
userInfo: Api.Login.Info
|
||||
/* 存储访问token */
|
||||
accessToken: string
|
||||
/* 存储刷新token */
|
||||
@ -41,3 +50,7 @@ declare namespace Storage {
|
||||
declare namespace App {
|
||||
type lang = 'zhCN' | 'enUS'
|
||||
}
|
||||
|
||||
interface DictMap {
|
||||
[key: string]: Entity.Dict[]
|
||||
}
|
||||
|
12
src/typings/route.d.ts
vendored
12
src/typings/route.d.ts
vendored
@ -10,7 +10,7 @@ declare namespace AppRoute {
|
||||
/* 是否需要登录权限。 */
|
||||
requiresAuth?: boolean
|
||||
/* 可以访问的角色 */
|
||||
roles?: Auth.RoleType[]
|
||||
roles?: Entity.RoleType[]
|
||||
/* 是否开启页面缓存 */
|
||||
keepAlive?: boolean
|
||||
/* 有些路由我们并不想在菜单中显示,比如某些编辑页面。 */
|
||||
@ -18,7 +18,7 @@ declare namespace AppRoute {
|
||||
/* 菜单排序。 */
|
||||
order?: number
|
||||
/* 嵌套外链 */
|
||||
herf?: string
|
||||
href?: string
|
||||
/** 当前路由不在左侧菜单显示,但需要高亮某个菜单的情况 */
|
||||
activeMenu?: string
|
||||
/** 当前路由是否会被添加到Tab中 */
|
||||
@ -29,6 +29,8 @@ declare namespace AppRoute {
|
||||
menuType?: MenuType
|
||||
}
|
||||
|
||||
type MetaKeys = keyof RouteMeta
|
||||
|
||||
interface baseRoute {
|
||||
/** 路由名称(路由唯一标识) */
|
||||
name: string
|
||||
@ -39,15 +41,13 @@ declare namespace AppRoute {
|
||||
/* 页面组件地址 */
|
||||
componentPath?: string | null
|
||||
/* 路由id */
|
||||
id: numnber
|
||||
id: number
|
||||
/* 父级路由id,顶级页面为null */
|
||||
pid: number | null
|
||||
}
|
||||
|
||||
/** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */
|
||||
type RowRoute = {
|
||||
[K in keyof RouteMeta as `meta.${K}`]?: RouteMeta[K]
|
||||
} & baseRoute
|
||||
type RowRoute = RouteMeta & baseRoute
|
||||
|
||||
/**
|
||||
* 挂载到项目上的真实路由结构
|
||||
|
@ -1,20 +1,37 @@
|
||||
import { arrayToTree as _arrayToTree } from 'performant-array-to-tree'
|
||||
import { omit } from 'radash'
|
||||
/**
|
||||
* 将给定的数组转换为树形结构。
|
||||
* @param arr - 原始数组,其中每个元素包含id和pid属性,pid表示父级id。
|
||||
* @returns 返回转换后的树形结构数组。
|
||||
*/
|
||||
export function arrayToTree(arr: any[]) {
|
||||
// 初始化结果数组
|
||||
const res: any = []
|
||||
// 使用Map存储数组元素,以id为键,元素本身为值
|
||||
const map = new Map()
|
||||
|
||||
export function arrayToTree(data: any) {
|
||||
const rowTree = _arrayToTree(data, {
|
||||
parentId: 'pid',
|
||||
dataField: null,
|
||||
// 遍历数组,将每个元素以id为键存储到Map中
|
||||
arr.forEach((item) => {
|
||||
map.set(item.id, item)
|
||||
})
|
||||
|
||||
const transform = (node: any) => {
|
||||
if (node.children.length > 0) {
|
||||
return ({
|
||||
...node,
|
||||
children: node.children.map(transform),
|
||||
})
|
||||
// 再次遍历数组,根据pid将元素组织成树形结构
|
||||
arr.forEach((item) => {
|
||||
// 获取当前元素的父级元素
|
||||
const parent = item.pid && map.get(item.pid)
|
||||
// 如果有父级元素
|
||||
if (parent) {
|
||||
// 如果父级元素已有子元素,则将当前元素追加到子元素数组中
|
||||
if (parent?.children)
|
||||
parent.children.push(item)
|
||||
// 如果父级元素没有子元素,则创建子元素数组,并将当前元素作为第一个元素
|
||||
else
|
||||
parent.children = [item]
|
||||
}
|
||||
return omit(node, ['children'])
|
||||
}
|
||||
return rowTree.map(transform)
|
||||
// 如果没有父级元素,则将当前元素直接添加到结果数组中
|
||||
else {
|
||||
res.push(item)
|
||||
}
|
||||
})
|
||||
// 返回组织好的树形结构数组
|
||||
return res
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { NDateLocale, NLocale } from 'naive-ui'
|
||||
import { dateZhCN, zhCN } from 'naive-ui'
|
||||
import { i18n } from '@/modules/i18n'
|
||||
import { dateZhCN, zhCN } from 'naive-ui'
|
||||
|
||||
export function setLocale(locale: App.lang) {
|
||||
i18n.global.locale.value = locale
|
||||
|
@ -1,6 +1,32 @@
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
|
||||
export function renderIcon(icon: string, props?: import('naive-ui').IconProps) {
|
||||
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
|
||||
export function renderIcon(icon?: string, props?: import('naive-ui').IconProps) {
|
||||
if (!icon)
|
||||
return
|
||||
|
||||
return () => createIcon(icon, props)
|
||||
}
|
||||
|
||||
export function createIcon(icon?: string, props?: import('naive-ui').IconProps) {
|
||||
if (!icon)
|
||||
return
|
||||
|
||||
const isLocal = icon.startsWith('local:')
|
||||
let innerIcon: any
|
||||
if (isLocal) {
|
||||
const svgName = icon.replace('local:', '')
|
||||
const svg = import.meta.glob('@/assets/svg-icons/*.svg', {
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
eager: true,
|
||||
})
|
||||
const target = svg[`/src/assets/svg-icons/${svgName}.svg`]
|
||||
innerIcon = h(NIcon, { ...props, innerHTML: target })
|
||||
}
|
||||
else {
|
||||
innerIcon = h(NIcon, props, { default: () => h(Icon, { icon }) })
|
||||
}
|
||||
|
||||
return innerIcon
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from './icon'
|
||||
export * from './storage'
|
||||
export * from './array'
|
||||
export * from './i18n'
|
||||
export * from './icon'
|
||||
|
@ -39,9 +39,8 @@ function createLocalStorage<T extends Storage.Local>() {
|
||||
window.localStorage.removeItem(`${STORAGE_PREFIX}${String(key)}`)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
window.localStorage.clear()
|
||||
}
|
||||
const clear = window.localStorage.clear
|
||||
|
||||
return {
|
||||
set,
|
||||
get,
|
||||
@ -73,9 +72,7 @@ function createSessionStorage<T extends Storage.Session>() {
|
||||
function remove(key: keyof T) {
|
||||
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${String(key)}`)
|
||||
}
|
||||
function clear() {
|
||||
window.sessionStorage.clear()
|
||||
}
|
||||
const clear = window.sessionStorage.clear
|
||||
|
||||
return {
|
||||
set,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { graphic } from 'echarts'
|
||||
import { type ECOption, useEcharts } from '@/hooks'
|
||||
import { graphic } from 'echarts'
|
||||
|
||||
const lineOptions = ref<ECOption>({
|
||||
tooltip: {
|
||||
@ -113,7 +113,8 @@ const lineOptions = ref<ECOption>({
|
||||
data: [20, 71, 8, 50, 57, 32],
|
||||
}],
|
||||
}) as Ref<ECOption>
|
||||
const { domRef: lineRef } = useEcharts(lineOptions)
|
||||
|
||||
useEcharts('lineRef', lineOptions)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { graphic } from 'echarts'
|
||||
import { type ECOption, useEcharts } from '@/hooks'
|
||||
import { graphic } from 'echarts'
|
||||
|
||||
const chartData = [
|
||||
{ name: '1', value: 300 },
|
||||
@ -90,7 +90,8 @@ const option = ref<ECOption>({
|
||||
},
|
||||
}],
|
||||
}) as Ref<ECOption>
|
||||
const { domRef: lineRef } = useEcharts(option)
|
||||
|
||||
useEcharts('lineRef', option)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -50,7 +50,7 @@ const option = ref<ECOption>({
|
||||
},
|
||||
],
|
||||
}) as Ref<ECOption>
|
||||
const { domRef: lineRef } = useEcharts(option)
|
||||
useEcharts('lineRef', option)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user