mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-12 21:03:42 +08:00
Compare commits
60 Commits
e6044d0fc7
...
v2.0-route
Author | SHA1 | Date | |
---|---|---|---|
|
3d0daae43c | ||
|
5be864a80b | ||
|
b041fdd864 | ||
|
3d72f954ed | ||
|
1213531bef | ||
|
805c338141 | ||
|
3c0a52825d | ||
|
100e0ea55d | ||
|
257f1183fc | ||
|
d73111116a | ||
|
29a2a5c66a | ||
|
4005763c00 | ||
|
a55b4dc073 | ||
|
be8f915a0c | ||
|
358e129765 | ||
|
9ea56c9b82 | ||
|
87adc35f2e | ||
|
ee4341457a | ||
|
8a7cd5934b | ||
|
c962f7b2c5 | ||
|
8cc5177cda | ||
|
3a343eea33 | ||
|
f83eefbc3e | ||
|
936b834e62 | ||
|
c965140b87 | ||
|
32b8f99071 | ||
|
abaaa4a068 | ||
|
b4e125300e | ||
|
50a5cba088 | ||
|
d6c8142bb4 | ||
|
8146858b96 | ||
|
8b8a2083bb | ||
|
6207292d81 | ||
|
b4e5c6d990 | ||
|
b6ac3106ce | ||
|
d37ce04606 | ||
|
8439a60070 | ||
|
f238fcbd47 | ||
|
8ba71a0857 | ||
|
e89b86ce56 | ||
|
03dd64c543 | ||
|
aeb6369005 | ||
|
133196f337 | ||
|
41191d54fb | ||
|
5cb1cebd88 | ||
|
a5c4b4e3b7 | ||
|
87a675bf62 | ||
|
4d42dcbea8 | ||
|
276d836c87 | ||
|
7d84062e2c | ||
|
afd604212b | ||
|
fcb89883fa | ||
|
dc674ce870 | ||
|
dbd995c12c | ||
|
7b2e510a2f | ||
|
da149e5bbd | ||
|
7c3dac4212 | ||
|
39b89a1234 | ||
|
c57f88aad2 | ||
|
3e4e17abd8 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -33,3 +33,5 @@ package-lock.json
|
|||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
|
|
||||||
|
.temp
|
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
@@ -4,15 +4,28 @@
|
|||||||
"source.organizeImports": "never"
|
"source.organizeImports": "never"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
|
"eslint.validate": [
|
||||||
|
"html",
|
||||||
|
"css",
|
||||||
|
"scss",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
"i18n-ally.displayLanguage": "zh-cn",
|
"i18n-ally.displayLanguage": "zh-cn",
|
||||||
"i18n-ally.enabledParsers": ["ts"],
|
"i18n-ally.enabledParsers": ["ts"],
|
||||||
"i18n-ally.enabledFrameworks": ["vue"],
|
"i18n-ally.enabledFrameworks": ["vue"],
|
||||||
"i18n-ally.editor.preferEditor": true,
|
"i18n-ally.editor.preferEditor": true,
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"i18n-ally.localesPaths": ["src/locales/langs"],
|
"i18n-ally.localesPaths": ["src/locales/langs"],
|
||||||
|
"i18n-ally.parsers.typescript.compilerOptions": {
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
"prettier.enable": false,
|
"prettier.enable": false,
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"unocss.root": ["./"],
|
"unocss.root": ["./"]
|
||||||
"vue.server.hybridMode": true
|
|
||||||
}
|
}
|
||||||
|
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,6 +1,102 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
|
||||||
|
## [v1.3.15](https://github.com/soybeanjs/soybean-admin/compare/v1.3.14...v1.3.15) (2025-06-24)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- **projects**: add configurable user name watermark option - by @wenyuanw [<samp>(7c3da)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7c3dac42)
|
||||||
|
|
||||||
|
### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- **app**: replace console.error with window.console.error for consistency - by @soybeanjs [<samp>(7d840)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7d84062e)
|
||||||
|
- **projects**: ensure proper text color when themes are inverted - by @wenyuanw [<samp>(afd60)</samp>](https://github.com/soybeanjs/soybean-admin/commit/afd60421)
|
||||||
|
- **types**: The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. - by **chenziwen** [<samp>(da149)</samp>](https://github.com/soybeanjs/soybean-admin/commit/da149e5b)
|
||||||
|
|
||||||
|
### 🛠 Optimizations
|
||||||
|
|
||||||
|
- **components**: optimize spacing for lang-switch dropdown options - by @wenyuanw [<samp>(fcb89)</samp>](https://github.com/soybeanjs/soybean-admin/commit/fcb89883)
|
||||||
|
|
||||||
|
### 💅 Refactors
|
||||||
|
|
||||||
|
- **iframe-page**: remove unused lifecycle hooks and clean up script setup - by @soybeanjs [<samp>(276d8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/276d836c)
|
||||||
|
|
||||||
|
### 📖 Documentation
|
||||||
|
|
||||||
|
- **other**: update docs with video tutorial link. - by **Azir** [<samp>(7b2e5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7b2e510a)
|
||||||
|
- **readme**: add warning about upcoming `V2` version and link to plan list - by @soybeanjs [<samp>(4d42d)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4d42dcbe)
|
||||||
|
|
||||||
|
### 🏡 Chore
|
||||||
|
|
||||||
|
- **deps**: update deps - by @soybeanjs [<samp>(dc674)</samp>](https://github.com/soybeanjs/soybean-admin/commit/dc674ce8)
|
||||||
|
- **projects**: update deps & fix `moduleResolution` - by @soybeanjs [<samp>(dbd99)</samp>](https://github.com/soybeanjs/soybean-admin/commit/dbd995c1)
|
||||||
|
|
||||||
|
### ❤️ Contributors
|
||||||
|
|
||||||
|
[](https://github.com/soybeanjs) [](https://github.com/wenyuanw)
|
||||||
|
[Azir](mailto:2075125282@qq.com), [chenziwen](mailto:chenziwen@qesong.com)
|
||||||
|
|
||||||
|
## [v1.3.14](https://github.com/soybeanjs/soybean-admin/compare/v1.3.13...v1.3.14) (2025-06-09)
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- **docs**:
|
||||||
|
- add GitCode star badge to README files - by @soybeanjs [<samp>(05dc1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/05dc11e2)
|
||||||
|
- add DartNode sponsorship badge to README files - by @soybeanjs [<samp>(2ed0b)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2ed0b648)
|
||||||
|
- **projects**:
|
||||||
|
- support vite devtools specify the editor by launchEditor option. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/730 [<samp>(29698)</samp>](https://github.com/soybeanjs/soybean-admin/commit/29698bef)
|
||||||
|
- clear tabs cache when switching users. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/744 [<samp>(1ff4d)</samp>](https://github.com/soybeanjs/soybean-admin/commit/1ff4d82d)
|
||||||
|
- **theme**:
|
||||||
|
- global search button toggle - by **t8y2** [<samp>(75455)</samp>](https://github.com/soybeanjs/soybean-admin/commit/75455b00)
|
||||||
|
- **types**:
|
||||||
|
- enhance Option type to support customizable label types - by @WgoW and @testbrate in https://github.com/soybeanjs/soybean-admin/issues/735 [<samp>(123d2)</samp>](https://github.com/soybeanjs/soybean-admin/commit/123d2c90)
|
||||||
|
- **utils**:
|
||||||
|
- support quick generation of code templates. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/733 [<samp>(8527a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8527aa80)
|
||||||
|
|
||||||
|
### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- **auth**:
|
||||||
|
- remove redundant authStore declaration in resetStore function - by @soybeanjs [<samp>(c57f8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c57f88aa)
|
||||||
|
- **hooks**:
|
||||||
|
- fixed the issue where loading was not properly closed in some cases. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/737 [<samp>(85e40)</samp>](https://github.com/soybeanjs/soybean-admin/commit/85e40b19)
|
||||||
|
- refactor useCountDown hook for improved countdown logic and clarity. - by **Azir** [<samp>(dfb64)</samp>](https://github.com/soybeanjs/soybean-admin/commit/dfb647a8)
|
||||||
|
- **projects**:
|
||||||
|
- tab closure did not remove cache correctly. - by **Azir** [<samp>(7fb5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7fb5c72f)
|
||||||
|
|
||||||
|
### 🛠 Optimizations
|
||||||
|
|
||||||
|
- **hooks**:
|
||||||
|
- remove obsolete disabling cache. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/729 [<samp>(4e1b6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4e1b65b6)
|
||||||
|
- update detection function to cover the exceptions that occur when the request fails. - by **恕瑞玛的皇帝** [<samp>(22218)</samp>](https://github.com/soybeanjs/soybean-admin/commit/222187d3)
|
||||||
|
- **projects**:
|
||||||
|
- optimize tab deletion logic. closed #755 - by @wenyuanw in https://github.com/soybeanjs/soybean-admin/issues/755 [<samp>(e6044)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e6044d0f)
|
||||||
|
|
||||||
|
### 📖 Documentation
|
||||||
|
|
||||||
|
- **README**:
|
||||||
|
- Add supporting ecosystem tools to the open-source repository - by @WgoW and @testbrate in https://github.com/soybeanjs/soybean-admin/issues/740 [<samp>(a013e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a013ea2c)
|
||||||
|
- **deps**:
|
||||||
|
- update the Vite version of the project description. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/732 [<samp>(80486)</samp>](https://github.com/soybeanjs/soybean-admin/commit/80486099)
|
||||||
|
- **projects**:
|
||||||
|
- update README - by @xiatianYa in https://github.com/soybeanjs/soybean-admin/issues/726 [<samp>(3cbaf)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3cbaf4f4)
|
||||||
|
- add gitcode link - by @soybeanjs [<samp>(f35c2)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f35c250a)
|
||||||
|
|
||||||
|
### 🏡 Chore
|
||||||
|
|
||||||
|
- **deps**:
|
||||||
|
- add vscode recommend plugin close #738 - by @tu6ge in https://github.com/soybeanjs/soybean-admin/issues/739 and https://github.com/soybeanjs/soybean-admin/issues/738 [<samp>(61244)</samp>](https://github.com/soybeanjs/soybean-admin/commit/61244f0f)
|
||||||
|
- update deps - by @soybeanjs [<samp>(41b5f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/41b5f493)
|
||||||
|
- update deps - by @soybeanjs [<samp>(3e4e1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3e4e17ab)
|
||||||
|
|
||||||
|
### 🤖 CI
|
||||||
|
|
||||||
|
- **hooks**: remove lint-staged in git hook. close #724 - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/743 and https://github.com/soybeanjs/soybean-admin/issues/724 [<samp>(c3abc)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c3abc3df)
|
||||||
|
|
||||||
|
### ❤️ Contributors
|
||||||
|
|
||||||
|
[](https://github.com/soybeanjs) [](https://github.com/wenyuanw) [](https://github.com/Azir-11) [](https://github.com/WgoW) [](https://github.com/testbrate) [](https://github.com/tu6ge) [](https://github.com/xiatianYa)
|
||||||
|
[恕瑞玛的皇帝](mailto:2075125282@qq.com), [t8y2](mailto:1156263951@qq.com),
|
||||||
|
|
||||||
## [v1.3.13](https://github.com/soybeanjs/soybean-admin/compare/v1.3.12...v1.3.13) (2025-03-19)
|
## [v1.3.13](https://github.com/soybeanjs/soybean-admin/compare/v1.3.12...v1.3.13) (2025-03-19)
|
||||||
|
|
||||||
### 🐞 Bug Fixes
|
### 🐞 Bug Fixes
|
||||||
|
@@ -19,14 +19,20 @@
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you think `SoybeanAdmin` is helpful to you, or you like our project, please give us a ⭐️ on GitHub. Your support is the driving force for us to continue to improve and add new features! Thank you for your support!
|
> If you think `SoybeanAdmin` is helpful to you, or you like our project, please give us a ⭐️ on GitHub. Your support is the driving force for us to continue to improve and add new features! Thank you for your support!
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The `SoybeanAdmin` quick start series videos have been uploaded to [Bilibili](https://www.bilibili.com/video/BV1YKdRYXELC) Go online [click here](https://www.bilibili.com/video/BV1YKdRYXELC) Go check it out
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `SoybeanAdmin` is planning to develop a `V2` version, see [plan list](https://github.com/soybeanjs/soybean-admin/issues/767)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) is a clean, elegant, beautiful and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite6, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `SoybeanAdmin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
|
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) is a clean, elegant, beautiful and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite7, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `SoybeanAdmin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Cutting-edge technology application**: using the latest popular technology stack such as Vue3, Vite6, TypeScript, Pinia and UnoCSS.
|
- **Cutting-edge technology application**: using the latest popular technology stack such as Vue3, Vite7, TypeScript, Pinia and UnoCSS.
|
||||||
- **Clear project architecture**: using pnpm monorepo architecture, clear structure, elegant and easy to understand.
|
- **Clear project architecture**: using pnpm monorepo architecture, clear structure, elegant and easy to understand.
|
||||||
- **Strict code specifications**: follow the [SoybeanJS specification](https://docs.soybeanjs.cn/standard), integrate eslint, prettier and simple-git-hooks to ensure the code is standardized.
|
- **Strict code specifications**: follow the [SoybeanJS specification](https://docs.soybeanjs.cn/standard), integrate eslint, prettier and simple-git-hooks to ensure the code is standardized.
|
||||||
- **TypeScript**: support strict type checking to improve code maintainability.
|
- **TypeScript**: support strict type checking to improve code maintainability.
|
||||||
@@ -93,8 +99,8 @@
|
|||||||
Make sure your environment meets the following requirements:
|
Make sure your environment meets the following requirements:
|
||||||
|
|
||||||
- **git**: you need git to clone and manage project versions.
|
- **git**: you need git to clone and manage project versions.
|
||||||
- **NodeJS**: >=18.12.0, recommended 18.19.0 or higher.
|
- **NodeJS**: >=20.19.0, recommended 20.19.0 or higher.
|
||||||
- **pnpm**: >= 8.7.0, recommended 8.14.0 or higher.
|
- **pnpm**: >= 10.5.0, recommended 10.5.0 or higher.
|
||||||
|
|
||||||
**Clone Project**
|
**Clone Project**
|
||||||
|
|
||||||
|
14
README.md
14
README.md
@@ -18,13 +18,19 @@
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 如果您觉得 `SoybeanAdmin`对您有所帮助,或者您喜欢我们的项目,请在 GitHub 上给我们一个 ⭐️。您的支持是我们持续改进和增加新功能的动力!感谢您的支持!
|
> 如果您觉得 `SoybeanAdmin`对您有所帮助,或者您喜欢我们的项目,请在 GitHub 上给我们一个 ⭐️。您的支持是我们持续改进和增加新功能的动力!感谢您的支持!
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `SoybeanAdmin` 快速上手系列视频已在 [Bilibili](https://www.bilibili.com/video/BV1YKdRYXELC) 上线 [点击这里](https://www.bilibili.com/video/BV1YKdRYXELC) 前往查看
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `SoybeanAdmin` 正在计划开发 `V2` 版本,详情见[计划清单](https://github.com/soybeanjs/soybean-admin/issues/767)
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite6, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- **前沿技术应用**:采用 Vue3, Vite6, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
|
- **前沿技术应用**:采用 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
|
||||||
- **清晰的项目架构**:采用 pnpm monorepo 架构,结构清晰,优雅易懂。
|
- **清晰的项目架构**:采用 pnpm monorepo 架构,结构清晰,优雅易懂。
|
||||||
- **严格的代码规范**:遵循 [SoybeanJS 规范](https://docs.soybeanjs.cn/zh/standard),集成了eslint, prettier 和 simple-git-hooks,保证代码的规范性。
|
- **严格的代码规范**:遵循 [SoybeanJS 规范](https://docs.soybeanjs.cn/zh/standard),集成了eslint, prettier 和 simple-git-hooks,保证代码的规范性。
|
||||||
- **TypeScript**: 支持严格的类型检查,提高代码的可维护性。
|
- **TypeScript**: 支持严格的类型检查,提高代码的可维护性。
|
||||||
@@ -118,8 +124,8 @@
|
|||||||
确保你的环境满足以下要求:
|
确保你的环境满足以下要求:
|
||||||
|
|
||||||
- **git**: 你需要git来克隆和管理项目版本。
|
- **git**: 你需要git来克隆和管理项目版本。
|
||||||
- **NodeJS**: >=18.12.0,推荐 18.19.0 或更高。
|
- **NodeJS**: >=20.19.0,推荐 20.19.0 或更高。
|
||||||
- **pnpm**: >= 8.7.0,推荐 8.14.0 或更高。
|
- **pnpm**: >= 10.5.0,推荐 10.5.0 或更高。
|
||||||
|
|
||||||
**克隆项目**
|
**克隆项目**
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import type { HttpProxy, ProxyOptions } from 'vite';
|
import type { ProxyOptions } from 'vite';
|
||||||
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
||||||
import { consola } from 'consola';
|
import { consola } from 'consola';
|
||||||
import { createServiceConfig } from '../../src/utils/service';
|
import { createServiceConfig } from '../../src/utils/service';
|
||||||
@@ -33,7 +33,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
|
|||||||
proxy[item.proxyPattern] = {
|
proxy[item.proxyPattern] = {
|
||||||
target: item.baseURL,
|
target: item.baseURL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
|
configure: (_proxy, options) => {
|
||||||
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||||
if (!enableLog) return;
|
if (!enableLog) return;
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ import type { PluginOption } from 'vite';
|
|||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import progress from 'vite-plugin-progress';
|
import progress from 'vite-plugin-progress';
|
||||||
import { setupElegantRouter } from './router';
|
import elegantRouter from 'elegant-router/vite';
|
||||||
import { setupUnocss } from './unocss';
|
import { setupUnocss } from './unocss';
|
||||||
import { setupUnplugin } from './unplugin';
|
import { setupUnplugin } from './unplugin';
|
||||||
import { setupHtmlPlugin } from './html';
|
import { setupHtmlPlugin } from './html';
|
||||||
@@ -13,7 +13,7 @@ export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
|||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
setupDevtoolsPlugin(viteEnv),
|
setupDevtoolsPlugin(viteEnv),
|
||||||
setupElegantRouter(),
|
elegantRouter(),
|
||||||
setupUnocss(viteEnv),
|
setupUnocss(viteEnv),
|
||||||
...setupUnplugin(viteEnv),
|
...setupUnplugin(viteEnv),
|
||||||
progress(),
|
progress(),
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
import type { RouteMeta } from 'vue-router';
|
|
||||||
import ElegantVueRouter from '@elegant-router/vue/vite';
|
|
||||||
import type { RouteKey } from '@elegant-router/types';
|
|
||||||
|
|
||||||
export function setupElegantRouter() {
|
|
||||||
return ElegantVueRouter({
|
|
||||||
layouts: {
|
|
||||||
base: 'src/layouts/base-layout/index.vue',
|
|
||||||
blank: 'src/layouts/blank-layout/index.vue'
|
|
||||||
},
|
|
||||||
routePathTransformer(routeName, routePath) {
|
|
||||||
const key = routeName as RouteKey;
|
|
||||||
|
|
||||||
if (key === 'login') {
|
|
||||||
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
|
|
||||||
|
|
||||||
const moduleReg = modules.join('|');
|
|
||||||
|
|
||||||
return `/login/:module(${moduleReg})?`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return routePath;
|
|
||||||
},
|
|
||||||
onRouteMetaGen(routeName) {
|
|
||||||
const key = routeName as RouteKey;
|
|
||||||
|
|
||||||
const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
|
|
||||||
|
|
||||||
const meta: Partial<RouteMeta> = {
|
|
||||||
title: key,
|
|
||||||
i18nKey: `route.${key}` as App.I18n.I18nKey
|
|
||||||
};
|
|
||||||
|
|
||||||
if (constantRoutes.includes(key)) {
|
|
||||||
meta.constant = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
37
er.config.ts
Normal file
37
er.config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { RouteMeta } from 'vue-router';
|
||||||
|
import { defineConfig } from 'elegant-router';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
pageDir: ['src/views'],
|
||||||
|
layouts: {
|
||||||
|
base: 'src/layouts/base-layout/index.vue',
|
||||||
|
blank: 'src/layouts/blank-layout/index.vue'
|
||||||
|
},
|
||||||
|
getRoutePath: node => {
|
||||||
|
if (node.name === 'Login') {
|
||||||
|
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
|
||||||
|
|
||||||
|
const moduleReg = modules.join('|');
|
||||||
|
|
||||||
|
return `/login/:module(${moduleReg})?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.path;
|
||||||
|
},
|
||||||
|
getRouteMeta: node => {
|
||||||
|
const constantRoutes: RouteKey[] = ['Login', '403', '404', '500'];
|
||||||
|
|
||||||
|
const name = node.name as RouteKey;
|
||||||
|
|
||||||
|
const meta: Partial<RouteMeta> = {
|
||||||
|
title: name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (constantRoutes.includes(name)) {
|
||||||
|
meta.constant = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
});
|
73
package.json
73
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "soybean-admin",
|
"name": "soybean-admin",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"description": "A fresh and elegant admin template, based on Vue3、Vite6、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite6、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
"description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Soybean",
|
"name": "Soybean",
|
||||||
"email": "soybeanjs@outlook.com",
|
"email": "soybeanjs@outlook.com",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"Vue3 admin ",
|
"Vue3 admin ",
|
||||||
"vue-admin-template",
|
"vue-admin-template",
|
||||||
"Vite6",
|
"Vite7",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"naive-ui",
|
"naive-ui",
|
||||||
"naive-ui-admin",
|
"naive-ui-admin",
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
"UnoCSS"
|
"UnoCSS"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.20.0",
|
"node": ">=20.19.0",
|
||||||
"pnpm": ">=8.7.0"
|
"pnpm": ">=10.5.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build --mode prod",
|
"build": "vite build --mode prod",
|
||||||
@@ -54,53 +54,54 @@
|
|||||||
"@sa/hooks": "workspace:*",
|
"@sa/hooks": "workspace:*",
|
||||||
"@sa/materials": "workspace:*",
|
"@sa/materials": "workspace:*",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"@vueuse/core": "13.1.0",
|
"@vueuse/core": "13.9.0",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.18",
|
||||||
"defu": "6.1.4",
|
"defu": "6.1.4",
|
||||||
"echarts": "5.6.0",
|
"echarts": "6.0.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"naive-ui": "2.41.0",
|
"naive-ui": "2.43.1",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"pinia": "3.0.2",
|
"pinia": "3.0.3",
|
||||||
"tailwind-merge": "3.2.0",
|
"tailwind-merge": "3.3.1",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.21",
|
||||||
"vue-draggable-plus": "0.6.0",
|
"vue-draggable-plus": "0.6.0",
|
||||||
"vue-i18n": "11.1.3",
|
"vue-i18n": "11.1.12",
|
||||||
"vue-router": "4.5.1"
|
"vue-router": "4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@elegant-router/vue": "0.3.8",
|
"@elegant-router/vue": "0.3.8",
|
||||||
"@iconify/json": "2.2.337",
|
"@iconify/json": "2.2.385",
|
||||||
"@sa/scripts": "workspace:*",
|
"@sa/scripts": "workspace:*",
|
||||||
"@sa/uno-preset": "workspace:*",
|
"@sa/uno-preset": "workspace:*",
|
||||||
"@soybeanjs/eslint-config": "1.6.0",
|
"@soybeanjs/eslint-config": "1.7.1",
|
||||||
"@types/node": "22.15.17",
|
"@types/node": "24.5.1",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@unocss/eslint-config": "66.1.1",
|
"@unocss/eslint-config": "66.5.1",
|
||||||
"@unocss/preset-icons": "66.1.1",
|
"@unocss/preset-icons": "66.5.1",
|
||||||
"@unocss/preset-uno": "66.1.1",
|
"@unocss/preset-uno": "66.5.1",
|
||||||
"@unocss/transformer-directives": "66.1.1",
|
"@unocss/transformer-directives": "66.5.1",
|
||||||
"@unocss/transformer-variant-group": "66.1.1",
|
"@unocss/transformer-variant-group": "66.5.1",
|
||||||
"@unocss/vite": "66.1.1",
|
"@unocss/vite": "66.5.1",
|
||||||
"@vitejs/plugin-vue": "5.2.4",
|
"@vitejs/plugin-vue": "6.0.1",
|
||||||
"@vitejs/plugin-vue-jsx": "4.1.2",
|
"@vitejs/plugin-vue-jsx": "5.1.1",
|
||||||
"consola": "3.4.2",
|
"consola": "3.4.2",
|
||||||
"eslint": "9.26.0",
|
"elegant-router": "1.0.4",
|
||||||
"eslint-plugin-vue": "10.1.0",
|
"eslint": "9.35.0",
|
||||||
|
"eslint-plugin-vue": "10.4.0",
|
||||||
"kolorist": "1.8.0",
|
"kolorist": "1.8.0",
|
||||||
"sass": "1.88.0",
|
"sass": "1.92.1",
|
||||||
"simple-git-hooks": "2.13.0",
|
"simple-git-hooks": "2.13.1",
|
||||||
"tsx": "4.19.4",
|
"tsx": "4.20.5",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.9.2",
|
||||||
"unplugin-icons": "22.1.0",
|
"unplugin-icons": "22.3.0",
|
||||||
"unplugin-vue-components": "28.5.0",
|
"unplugin-vue-components": "29.0.0",
|
||||||
"vite": "6.3.5",
|
"vite": "7.1.5",
|
||||||
"vite-plugin-progress": "0.0.7",
|
"vite-plugin-progress": "0.0.7",
|
||||||
"vite-plugin-svg-icons": "2.0.1",
|
"vite-plugin-svg-icons": "2.0.1",
|
||||||
"vite-plugin-vue-devtools": "7.7.6",
|
"vite-plugin-vue-devtools": "8.0.2",
|
||||||
"vue-eslint-parser": "10.1.3",
|
"vue-eslint-parser": "10.2.0",
|
||||||
"vue-tsc": "2.2.10"
|
"vue-tsc": "3.0.7"
|
||||||
},
|
},
|
||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"commit-msg": "pnpm sa git-commit-verify",
|
"commit-msg": "pnpm sa git-commit-verify",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/alova",
|
"name": "@sa/alova",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./fetch": "./src/fetch.ts",
|
"./fetch": "./src/fetch.ts",
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alova/mock": "2.0.14",
|
"@alova/mock": "2.0.17",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"alova": "3.2.10"
|
"alova": "3.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/axios",
|
"name": "@sa/axios",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"axios": "1.9.0",
|
"axios": "1.12.2",
|
||||||
"axios-retry": "4.5.0",
|
"axios-retry": "4.5.0",
|
||||||
"qs": "6.14.0"
|
"qs": "6.14.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/qs": "6.9.18"
|
"@types/qs": "6.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } f
|
|||||||
import axiosRetry from 'axios-retry';
|
import axiosRetry from 'axios-retry';
|
||||||
import { nanoid } from '@sa/utils';
|
import { nanoid } from '@sa/utils';
|
||||||
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
|
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
|
||||||
|
import { transformResponse } from './shared';
|
||||||
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
|
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
|
||||||
import type {
|
import type {
|
||||||
CustomAxiosRequestConfig,
|
CustomAxiosRequestConfig,
|
||||||
@@ -13,11 +14,12 @@ import type {
|
|||||||
ResponseType
|
ResponseType
|
||||||
} from './type';
|
} from './type';
|
||||||
|
|
||||||
function createCommonRequest<ResponseData = any>(
|
function createCommonRequest<
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
ResponseData,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
ApiData = ResponseData,
|
||||||
) {
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
const opts = createDefaultOptions<ResponseData>(options);
|
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||||
|
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
|
||||||
|
|
||||||
const axiosConf = createAxiosConfig(axiosConfig);
|
const axiosConf = createAxiosConfig(axiosConfig);
|
||||||
const instance = axios.create(axiosConf);
|
const instance = axios.create(axiosConf);
|
||||||
@@ -52,6 +54,8 @@ function createCommonRequest<ResponseData = any>(
|
|||||||
async response => {
|
async response => {
|
||||||
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
|
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
|
||||||
|
|
||||||
|
await transformResponse(response);
|
||||||
|
|
||||||
if (responseType !== 'json' || opts.isBackendSuccess(response)) {
|
if (responseType !== 'json' || opts.isBackendSuccess(response)) {
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
}
|
}
|
||||||
@@ -80,14 +84,6 @@ function createCommonRequest<ResponseData = any>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function cancelRequest(requestId: string) {
|
|
||||||
const abortController = abortControllerMap.get(requestId);
|
|
||||||
if (abortController) {
|
|
||||||
abortController.abort();
|
|
||||||
abortControllerMap.delete(requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelAllRequest() {
|
function cancelAllRequest() {
|
||||||
abortControllerMap.forEach(abortController => {
|
abortControllerMap.forEach(abortController => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
@@ -98,7 +94,6 @@ function createCommonRequest<ResponseData = any>(
|
|||||||
return {
|
return {
|
||||||
instance,
|
instance,
|
||||||
opts,
|
opts,
|
||||||
cancelRequest,
|
|
||||||
cancelAllRequest
|
cancelAllRequest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -109,27 +104,27 @@ function createCommonRequest<ResponseData = any>(
|
|||||||
* @param axiosConfig axios config
|
* @param axiosConfig axios config
|
||||||
* @param options request options
|
* @param options request options
|
||||||
*/
|
*/
|
||||||
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
|
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
) {
|
) {
|
||||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||||
|
|
||||||
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
|
const request: RequestInstance<ApiData, State> = async function request<
|
||||||
config: CustomAxiosRequestConfig
|
T extends ApiData = ApiData,
|
||||||
) {
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
const response: AxiosResponse<ResponseData> = await instance(config);
|
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||||
|
|
||||||
const responseType = response.config?.responseType || 'json';
|
const responseType = response.config?.responseType || 'json';
|
||||||
|
|
||||||
if (responseType === 'json') {
|
if (responseType === 'json') {
|
||||||
return opts.transformBackendResponse(response);
|
return opts.transform(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data as MappedType<R, T>;
|
return response.data as MappedType<R, T>;
|
||||||
} as RequestInstance<State>;
|
} as RequestInstance<ApiData, State>;
|
||||||
|
|
||||||
request.cancelRequest = cancelRequest;
|
|
||||||
request.cancelAllRequest = cancelAllRequest;
|
request.cancelAllRequest = cancelAllRequest;
|
||||||
request.state = {} as State;
|
request.state = {} as State;
|
||||||
|
|
||||||
@@ -144,14 +139,14 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
|
|||||||
* @param axiosConfig axios config
|
* @param axiosConfig axios config
|
||||||
* @param options request options
|
* @param options request options
|
||||||
*/
|
*/
|
||||||
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
|
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
) {
|
) {
|
||||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||||
|
|
||||||
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
|
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
|
||||||
T = any,
|
T extends ApiData = ApiData,
|
||||||
R extends ResponseType = 'json'
|
R extends ResponseType = 'json'
|
||||||
>(config: CustomAxiosRequestConfig) {
|
>(config: CustomAxiosRequestConfig) {
|
||||||
try {
|
try {
|
||||||
@@ -160,20 +155,21 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
|
|||||||
const responseType = response.config?.responseType || 'json';
|
const responseType = response.config?.responseType || 'json';
|
||||||
|
|
||||||
if (responseType === 'json') {
|
if (responseType === 'json') {
|
||||||
const data = opts.transformBackendResponse(response);
|
const data = await opts.transform(response);
|
||||||
|
|
||||||
return { data, error: null, response };
|
return { data, error: null, response };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: response.data as MappedType<R, T>, error: null };
|
return { data: response.data as MappedType<R, T>, error: null, response };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||||
}
|
}
|
||||||
} as FlatRequestInstance<State, ResponseData>;
|
} as FlatRequestInstance<ResponseData, ApiData, State>;
|
||||||
|
|
||||||
flatRequest.cancelRequest = cancelRequest;
|
|
||||||
flatRequest.cancelAllRequest = cancelAllRequest;
|
flatRequest.cancelAllRequest = cancelAllRequest;
|
||||||
flatRequest.state = {} as State;
|
flatRequest.state = {
|
||||||
|
...opts.defaultState
|
||||||
|
} as State;
|
||||||
|
|
||||||
return flatRequest;
|
return flatRequest;
|
||||||
}
|
}
|
||||||
|
@@ -4,15 +4,27 @@ import { stringify } from 'qs';
|
|||||||
import { isHttpSuccess } from './shared';
|
import { isHttpSuccess } from './shared';
|
||||||
import type { RequestOption } from './type';
|
import type { RequestOption } from './type';
|
||||||
|
|
||||||
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
|
export function createDefaultOptions<
|
||||||
const opts: RequestOption<ResponseData> = {
|
ResponseData,
|
||||||
|
ApiData = ResponseData,
|
||||||
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||||
|
const opts: RequestOption<ResponseData, ApiData, State> = {
|
||||||
|
defaultState: {} as State,
|
||||||
|
transform: async response => response.data as unknown as ApiData,
|
||||||
|
transformBackendResponse: async response => response.data as unknown as ApiData,
|
||||||
onRequest: async config => config,
|
onRequest: async config => config,
|
||||||
isBackendSuccess: _response => true,
|
isBackendSuccess: _response => true,
|
||||||
onBackendFail: async () => {},
|
onBackendFail: async () => {},
|
||||||
transformBackendResponse: async response => response.data,
|
|
||||||
onError: async () => {}
|
onError: async () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options?.transform) {
|
||||||
|
opts.transform = options.transform;
|
||||||
|
} else {
|
||||||
|
opts.transform = options?.transformBackendResponse || opts.transform;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(opts, options);
|
Object.assign(opts, options);
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import type { ResponseType } from './type';
|
||||||
|
|
||||||
export function getContentType(config: InternalAxiosRequestConfig) {
|
export function getContentType(config: InternalAxiosRequestConfig) {
|
||||||
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
|
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
|
||||||
@@ -26,3 +27,53 @@ export function isResponseJson(response: AxiosResponse) {
|
|||||||
|
|
||||||
return responseType === 'json' || responseType === undefined;
|
return responseType === 'json' || responseType === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function transformResponse(response: AxiosResponse) {
|
||||||
|
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
|
||||||
|
if (responseType === 'json') return;
|
||||||
|
|
||||||
|
const isJson = response.headers['content-type']?.includes('application/json');
|
||||||
|
if (!isJson) return;
|
||||||
|
|
||||||
|
if (responseType === 'blob') {
|
||||||
|
await transformBlobToJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseType === 'arrayBuffer') {
|
||||||
|
await transformArrayBufferToJson(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transformBlobToJson(response: AxiosResponse) {
|
||||||
|
try {
|
||||||
|
let data = response.data;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.toString.call(data) === '[object Blob]') {
|
||||||
|
const json = await data.text();
|
||||||
|
data = JSON.parse(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.data = data;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transformArrayBufferToJson(response: AxiosResponse) {
|
||||||
|
try {
|
||||||
|
let data = response.data;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.toString.call(data) === '[object ArrayBuffer]') {
|
||||||
|
const json = new TextDecoder().decode(data);
|
||||||
|
data = JSON.parse(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.data = data;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
@@ -8,7 +8,30 @@ export type ContentType =
|
|||||||
| 'application/x-www-form-urlencoded'
|
| 'application/x-www-form-urlencoded'
|
||||||
| 'application/octet-stream';
|
| 'application/octet-stream';
|
||||||
|
|
||||||
export interface RequestOption<ResponseData = any> {
|
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
|
||||||
|
|
||||||
|
export interface RequestOption<
|
||||||
|
ResponseData,
|
||||||
|
ApiData = ResponseData,
|
||||||
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The default state
|
||||||
|
*/
|
||||||
|
defaultState?: State;
|
||||||
|
/**
|
||||||
|
* transform the response data to the api data
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||||
|
/**
|
||||||
|
* transform the response data to the api data
|
||||||
|
*
|
||||||
|
* @deprecated use `transform` instead, will be removed in the next major version v3
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||||
/**
|
/**
|
||||||
* The hook before request
|
* The hook before request
|
||||||
*
|
*
|
||||||
@@ -35,12 +58,6 @@ export interface RequestOption<ResponseData = any> {
|
|||||||
response: AxiosResponse<ResponseData>,
|
response: AxiosResponse<ResponseData>,
|
||||||
instance: AxiosInstance
|
instance: AxiosInstance
|
||||||
) => Promise<AxiosResponse | null> | Promise<void>;
|
) => Promise<AxiosResponse | null> | Promise<void>;
|
||||||
/**
|
|
||||||
* transform backend response when the responseType is json
|
|
||||||
*
|
|
||||||
* @param response Axios response
|
|
||||||
*/
|
|
||||||
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
|
|
||||||
/**
|
/**
|
||||||
* The hook to handle error
|
* The hook to handle error
|
||||||
*
|
*
|
||||||
@@ -68,15 +85,7 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
|
|||||||
responseType?: R;
|
responseType?: R;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RequestInstanceCommon<T> {
|
export interface RequestInstanceCommon<State extends Record<string, unknown>> {
|
||||||
/**
|
|
||||||
* cancel the request by request id
|
|
||||||
*
|
|
||||||
* if the request provide abort controller sign from config, it will not collect in the abort controller map
|
|
||||||
*
|
|
||||||
* @param requestId
|
|
||||||
*/
|
|
||||||
cancelRequest: (requestId: string) => void;
|
|
||||||
/**
|
/**
|
||||||
* cancel all request
|
* cancel all request
|
||||||
*
|
*
|
||||||
@@ -84,32 +93,35 @@ export interface RequestInstanceCommon<T> {
|
|||||||
*/
|
*/
|
||||||
cancelAllRequest: () => void;
|
cancelAllRequest: () => void;
|
||||||
/** you can set custom state in the request instance */
|
/** you can set custom state in the request instance */
|
||||||
state: T;
|
state: State;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The request instance */
|
/** The request instance */
|
||||||
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
|
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
|
||||||
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig<R>
|
||||||
|
): Promise<MappedType<R, T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
|
export type FlatResponseSuccessData<ResponseData, ApiData> = {
|
||||||
data: T;
|
data: ApiData;
|
||||||
error: null;
|
error: null;
|
||||||
response: AxiosResponse<ResponseData>;
|
response: AxiosResponse<ResponseData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FlatResponseFailData<ResponseData = any> = {
|
export type FlatResponseFailData<ResponseData> = {
|
||||||
data: null;
|
data: null;
|
||||||
error: AxiosError<ResponseData>;
|
error: AxiosError<ResponseData>;
|
||||||
response: AxiosResponse<ResponseData>;
|
response: AxiosResponse<ResponseData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FlatResponseData<T = any, ResponseData = any> =
|
export type FlatResponseData<ResponseData, ApiData> =
|
||||||
| FlatResponseSuccessData<T, ResponseData>
|
| FlatResponseSuccessData<ResponseData, ApiData>
|
||||||
| FlatResponseFailData<ResponseData>;
|
| FlatResponseFailData<ResponseData>;
|
||||||
|
|
||||||
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
|
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||||
<T = any, R extends ResponseType = 'json'>(
|
extends RequestInstanceCommon<State> {
|
||||||
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
config: CustomAxiosRequestConfig<R>
|
config: CustomAxiosRequestConfig<R>
|
||||||
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
|
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/color",
|
"name": "@sa/color",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/hooks",
|
"name": "@sa/hooks",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
|
@@ -3,9 +3,7 @@ import useLoading from './use-loading';
|
|||||||
import useCountDown from './use-count-down';
|
import useCountDown from './use-count-down';
|
||||||
import useContext from './use-context';
|
import useContext from './use-context';
|
||||||
import useSvgIconRender from './use-svg-icon-render';
|
import useSvgIconRender from './use-svg-icon-render';
|
||||||
import useHookTable from './use-table';
|
import useTable from './use-table';
|
||||||
|
|
||||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
|
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable };
|
||||||
|
export type * from './use-table';
|
||||||
export * from './use-signal';
|
|
||||||
export * from './use-table';
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { inject, provide } from 'vue';
|
import { inject, provide } from 'vue';
|
||||||
import type { InjectionKey } from 'vue';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use context
|
* Use context
|
||||||
@@ -12,7 +11,7 @@ import type { InjectionKey } from 'vue';
|
|||||||
* import { ref } from 'vue';
|
* import { ref } from 'vue';
|
||||||
* import { useContext } from '@sa/hooks';
|
* import { useContext } from '@sa/hooks';
|
||||||
*
|
*
|
||||||
* export const { setupStore, useStore } = useContext('demo', () => {
|
* export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
|
||||||
* const count = ref(0);
|
* const count = ref(0);
|
||||||
*
|
*
|
||||||
* function increment() {
|
* function increment() {
|
||||||
@@ -35,10 +34,10 @@ import type { InjectionKey } from 'vue';
|
|||||||
* <div>A</div>
|
* <div>A</div>
|
||||||
* </template>
|
* </template>
|
||||||
* <script setup lang="ts">
|
* <script setup lang="ts">
|
||||||
* import { setupStore } from './context';
|
* import { provideDemoContext } from './context';
|
||||||
*
|
*
|
||||||
* setupStore();
|
* provideDemoContext();
|
||||||
* // const { increment } = setupStore(); // also can control the store in the parent component
|
* // const { increment } = provideDemoContext(); // also can control the store in the parent component
|
||||||
* </script>
|
* </script>
|
||||||
* ``` // B.vue
|
* ``` // B.vue
|
||||||
* ```vue
|
* ```vue
|
||||||
@@ -46,9 +45,9 @@ import type { InjectionKey } from 'vue';
|
|||||||
* <div>B</div>
|
* <div>B</div>
|
||||||
* </template>
|
* </template>
|
||||||
* <script setup lang="ts">
|
* <script setup lang="ts">
|
||||||
* import { useStore } from './context';
|
* import { useDemoContext } from './context';
|
||||||
*
|
*
|
||||||
* const { count, increment } = useStore();
|
* const { count, increment } = useDemoContext();
|
||||||
* </script>
|
* </script>
|
||||||
* ```;
|
* ```;
|
||||||
*
|
*
|
||||||
@@ -57,40 +56,41 @@ import type { InjectionKey } from 'vue';
|
|||||||
* @param contextName Context name
|
* @param contextName Context name
|
||||||
* @param fn Context function
|
* @param fn Context function
|
||||||
*/
|
*/
|
||||||
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
|
export default function useContext<Arguments extends Array<any>, T>(
|
||||||
type Context = ReturnType<T>;
|
contextName: string,
|
||||||
|
composable: (...args: Arguments) => T
|
||||||
|
) {
|
||||||
|
const key = Symbol(contextName);
|
||||||
|
|
||||||
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
|
/**
|
||||||
|
* Injects the context value.
|
||||||
|
*
|
||||||
|
* @param consumerName - The name of the component that is consuming the context. If provided, the component must be
|
||||||
|
* used within the context provider.
|
||||||
|
* @param defaultValue - The default value to return if the context is not provided.
|
||||||
|
* @returns The context value.
|
||||||
|
*/
|
||||||
|
const useInject = <N extends string | null | undefined = undefined>(
|
||||||
|
consumerName?: N,
|
||||||
|
defaultValue?: T
|
||||||
|
): N extends null | undefined ? T | null : T => {
|
||||||
|
const value = inject(key, defaultValue);
|
||||||
|
|
||||||
function setupStore(...args: Parameters<T>) {
|
if (consumerName && !value) {
|
||||||
const context: Context = fn(...args);
|
throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
|
||||||
return useProvide(context);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
// @ts-expect-error - we want to return null if the value is undefined or null
|
||||||
/** Setup store in the parent component */
|
return value || null;
|
||||||
setupStore,
|
|
||||||
/** Use store in the child component */
|
|
||||||
useStore
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/** Create context */
|
const useProvide = (...args: Arguments) => {
|
||||||
function createContext<T>(contextName: string) {
|
const value = composable(...args);
|
||||||
const injectKey: InjectionKey<T> = Symbol(contextName);
|
|
||||||
|
|
||||||
function useProvide(context: T) {
|
provide(key, value);
|
||||||
provide(injectKey, context);
|
|
||||||
|
|
||||||
return context;
|
return value;
|
||||||
}
|
|
||||||
|
|
||||||
function useInject() {
|
|
||||||
return inject(injectKey) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
useProvide,
|
|
||||||
useInject
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return [useProvide, useInject] as const;
|
||||||
}
|
}
|
||||||
|
@@ -6,31 +6,31 @@ import type {
|
|||||||
CreateAxiosDefaults,
|
CreateAxiosDefaults,
|
||||||
CustomAxiosRequestConfig,
|
CustomAxiosRequestConfig,
|
||||||
MappedType,
|
MappedType,
|
||||||
|
RequestInstanceCommon,
|
||||||
RequestOption,
|
RequestOption,
|
||||||
ResponseType
|
ResponseType
|
||||||
} from '@sa/axios';
|
} from '@sa/axios';
|
||||||
import useLoading from './use-loading';
|
import useLoading from './use-loading';
|
||||||
|
|
||||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
export type HookRequestInstanceResponseSuccessData<ApiData> = {
|
||||||
data: Ref<T>;
|
data: Ref<ApiData>;
|
||||||
error: Ref<null>;
|
error: Ref<null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
export type HookRequestInstanceResponseFailData<ResponseData> = {
|
||||||
data: Ref<null>;
|
data: Ref<null>;
|
||||||
error: Ref<AxiosError<ResponseData>>;
|
error: Ref<AxiosError<ResponseData>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
|
||||||
loading: Ref<boolean>;
|
loading: Ref<boolean>;
|
||||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||||
|
|
||||||
export interface HookRequestInstance<ResponseData = any> {
|
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||||
<T = any, R extends ResponseType = 'json'>(
|
extends RequestInstanceCommon<State> {
|
||||||
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
config: CustomAxiosRequestConfig
|
config: CustomAxiosRequestConfig
|
||||||
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
|
||||||
cancelRequest: (requestId: string) => void;
|
|
||||||
cancelAllRequest: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,25 +39,26 @@ export interface HookRequestInstance<ResponseData = any> {
|
|||||||
* @param axiosConfig
|
* @param axiosConfig
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
export default function createHookRequest<ResponseData = any>(
|
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
) {
|
) {
|
||||||
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||||
|
|
||||||
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
|
||||||
config: CustomAxiosRequestConfig
|
T extends ApiData = ApiData,
|
||||||
) {
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
const data = ref(null) as Ref<MappedType<R, T>>;
|
||||||
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
|
||||||
|
|
||||||
startLoading();
|
startLoading();
|
||||||
|
|
||||||
request(config).then(res => {
|
request(config).then(res => {
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
data.value = res.data;
|
data.value = res.data as MappedType<R, T>;
|
||||||
} else {
|
} else {
|
||||||
error.value = res.error;
|
error.value = res.error;
|
||||||
}
|
}
|
||||||
@@ -70,9 +71,8 @@ export default function createHookRequest<ResponseData = any>(
|
|||||||
data,
|
data,
|
||||||
error
|
error
|
||||||
};
|
};
|
||||||
} as HookRequestInstance<ResponseData>;
|
} as HookRequestInstance<ResponseData, ApiData, State>;
|
||||||
|
|
||||||
hookRequest.cancelRequest = request.cancelRequest;
|
|
||||||
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||||
|
|
||||||
return hookRequest;
|
return hookRequest;
|
||||||
|
@@ -1,144 +0,0 @@
|
|||||||
import { computed, ref, shallowRef, triggerRef } from 'vue';
|
|
||||||
import type {
|
|
||||||
ComputedGetter,
|
|
||||||
DebuggerOptions,
|
|
||||||
Ref,
|
|
||||||
ShallowRef,
|
|
||||||
WritableComputedOptions,
|
|
||||||
WritableComputedRef
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
type Updater<T> = (value: T) => T;
|
|
||||||
type Mutator<T> = (value: T) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signal is a reactive value that can be set, updated or mutated
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const count = useSignal(0);
|
|
||||||
*
|
|
||||||
* // `watchEffect`
|
|
||||||
* watchEffect(() => {
|
|
||||||
* console.log(count());
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // watch
|
|
||||||
* watch(count, value => {
|
|
||||||
* console.log(value);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // useComputed
|
|
||||||
* const double = useComputed(() => count() * 2);
|
|
||||||
* const writeableDouble = useComputed({
|
|
||||||
* get: () => count() * 2,
|
|
||||||
* set: value => count.set(value / 2)
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export interface Signal<T> {
|
|
||||||
(): Readonly<T>;
|
|
||||||
/**
|
|
||||||
* Set the value of the signal
|
|
||||||
*
|
|
||||||
* It recommend use `set` for primitive values
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
set(value: T): void;
|
|
||||||
/**
|
|
||||||
* Update the value of the signal using an updater function
|
|
||||||
*
|
|
||||||
* It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
|
|
||||||
*
|
|
||||||
* @param updater
|
|
||||||
*/
|
|
||||||
update(updater: Updater<T>): void;
|
|
||||||
/**
|
|
||||||
* Mutate the value of the signal using a mutator function
|
|
||||||
*
|
|
||||||
* this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
|
|
||||||
*
|
|
||||||
* It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
|
|
||||||
*
|
|
||||||
* @param mutator
|
|
||||||
*/
|
|
||||||
mutate(mutator: Mutator<T>): void;
|
|
||||||
/**
|
|
||||||
* Get the reference of the signal
|
|
||||||
*
|
|
||||||
* Sometimes it can be useful to make `v-model` work with the signal
|
|
||||||
*
|
|
||||||
* ```vue
|
|
||||||
* <template>
|
|
||||||
* <input v-model="model.count" />
|
|
||||||
* </template>;
|
|
||||||
*
|
|
||||||
* <script setup lang="ts">
|
|
||||||
* const state = useSignal({ count: 0 }, { useRef: true });
|
|
||||||
*
|
|
||||||
* const model = state.getRef();
|
|
||||||
* </script>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
getRef(): Readonly<ShallowRef<Readonly<T>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReadonlySignal<T> {
|
|
||||||
(): Readonly<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SignalOptions {
|
|
||||||
/**
|
|
||||||
* Whether to use `ref` to store the value
|
|
||||||
*
|
|
||||||
* @default false use `sharedRef` to store the value
|
|
||||||
*/
|
|
||||||
useRef?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
|
|
||||||
const { useRef } = options || {};
|
|
||||||
|
|
||||||
const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
|
|
||||||
|
|
||||||
return createSignal(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
|
|
||||||
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
|
|
||||||
export function useComputed<T>(
|
|
||||||
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
|
|
||||||
debugOptions?: DebuggerOptions
|
|
||||||
) {
|
|
||||||
const isGetter = typeof getterOrOptions === 'function';
|
|
||||||
|
|
||||||
const computedValue = computed(getterOrOptions as any, debugOptions);
|
|
||||||
|
|
||||||
if (isGetter) {
|
|
||||||
return () => computedValue.value as ReadonlySignal<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createSignal(computedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
|
|
||||||
const signal = () => state.value;
|
|
||||||
|
|
||||||
signal.set = (value: T) => {
|
|
||||||
state.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
signal.update = (updater: Updater<T>) => {
|
|
||||||
state.value = updater(state.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
signal.mutate = (mutator: Mutator<T>) => {
|
|
||||||
mutator(state.value);
|
|
||||||
triggerRef(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
|
|
||||||
|
|
||||||
return signal;
|
|
||||||
}
|
|
@@ -1,12 +1,20 @@
|
|||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { Ref, VNodeChild } from 'vue';
|
import type { Ref, VNodeChild } from 'vue';
|
||||||
import { jsonClone } from '@sa/utils';
|
|
||||||
import useBoolean from './use-boolean';
|
import useBoolean from './use-boolean';
|
||||||
import useLoading from './use-loading';
|
import useLoading from './use-loading';
|
||||||
|
|
||||||
export type MaybePromise<T> = T | Promise<T>;
|
export interface PaginationData<T> {
|
||||||
|
data: T[];
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiFn = (args: any) => Promise<unknown>;
|
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
|
||||||
|
|
||||||
|
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
|
||||||
|
response: ResponseData
|
||||||
|
) => GetApiData<ApiData, Pagination>;
|
||||||
|
|
||||||
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
||||||
|
|
||||||
@@ -14,76 +22,64 @@ export type TableColumnCheck = {
|
|||||||
key: string;
|
key: string;
|
||||||
title: TableColumnCheckTitle;
|
title: TableColumnCheckTitle;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
visible: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TableDataWithIndex<T> = T & { index: number };
|
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
|
||||||
|
/**
|
||||||
export type TransformedData<T> = {
|
* api function to get table data
|
||||||
data: TableDataWithIndex<T>[];
|
*/
|
||||||
pageNum: number;
|
api: () => Promise<ResponseData>;
|
||||||
pageSize: number;
|
/**
|
||||||
total: number;
|
* whether to enable pagination
|
||||||
};
|
*/
|
||||||
|
pagination?: Pagination;
|
||||||
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
|
/**
|
||||||
|
* transform api response to table data
|
||||||
export type TableConfig<A extends ApiFn, T, C> = {
|
*/
|
||||||
/** api function to get table data */
|
transform: Transform<ResponseData, ApiData, Pagination>;
|
||||||
apiFn: A;
|
/**
|
||||||
/** api params */
|
* columns factory
|
||||||
apiParams?: Parameters<A>[0];
|
*/
|
||||||
/** transform api response to table data */
|
columns: () => Column[];
|
||||||
transformer: Transformer<T, Awaited<ReturnType<A>>>;
|
|
||||||
/** columns factory */
|
|
||||||
columns: () => C[];
|
|
||||||
/**
|
/**
|
||||||
* get column checks
|
* get column checks
|
||||||
*
|
|
||||||
* @param columns
|
|
||||||
*/
|
*/
|
||||||
getColumnChecks: (columns: C[]) => TableColumnCheck[];
|
getColumnChecks: (columns: Column[]) => TableColumnCheck[];
|
||||||
/**
|
/**
|
||||||
* get columns
|
* get columns
|
||||||
*
|
|
||||||
* @param columns
|
|
||||||
*/
|
*/
|
||||||
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
|
getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
|
||||||
/**
|
/**
|
||||||
* callback when response fetched
|
* callback when response fetched
|
||||||
*
|
|
||||||
* @param transformed transformed data
|
|
||||||
*/
|
*/
|
||||||
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
|
onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
|
||||||
/**
|
/**
|
||||||
* whether to get data immediately
|
* whether to get data immediately
|
||||||
*
|
*
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
immediate?: boolean;
|
immediate?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
|
||||||
|
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
|
||||||
|
) {
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
const { bool: empty, setBool: setEmpty } = useBoolean();
|
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||||
|
|
||||||
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
|
const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options;
|
||||||
|
|
||||||
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
|
const data = ref([]) as Ref<ApiData[]>;
|
||||||
|
|
||||||
const allColumns = ref(config.columns()) as Ref<C[]>;
|
const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
|
||||||
|
|
||||||
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
|
const $columns = computed(() => getColumns(columns(), columnChecks.value));
|
||||||
|
|
||||||
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
|
|
||||||
|
|
||||||
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
|
|
||||||
|
|
||||||
function reloadColumns() {
|
function reloadColumns() {
|
||||||
allColumns.value = config.columns();
|
|
||||||
|
|
||||||
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
||||||
|
|
||||||
const defaultChecks = getColumnChecks(allColumns.value);
|
const defaultChecks = getColumnChecks(columns());
|
||||||
|
|
||||||
columnChecks.value = defaultChecks.map(col => ({
|
columnChecks.value = defaultChecks.map(col => ({
|
||||||
...col,
|
...col,
|
||||||
@@ -92,47 +88,21 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getData() {
|
async function getData() {
|
||||||
startLoading();
|
try {
|
||||||
|
startLoading();
|
||||||
|
|
||||||
const formattedParams = formatSearchParams(searchParams);
|
const response = await api();
|
||||||
|
|
||||||
const response = await apiFn(formattedParams);
|
const transformed = transform(response);
|
||||||
|
|
||||||
const transformed = transformer(response as Awaited<ReturnType<A>>);
|
data.value = getTableData(transformed, pagination);
|
||||||
|
|
||||||
data.value = transformed.data;
|
setEmpty(data.value.length === 0);
|
||||||
|
|
||||||
setEmpty(transformed.data.length === 0);
|
await onFetched?.(transformed);
|
||||||
|
} finally {
|
||||||
await config.onFetched?.(transformed);
|
endLoading();
|
||||||
|
}
|
||||||
endLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSearchParams(params: Record<string, unknown>) {
|
|
||||||
const formattedParams: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
|
||||||
if (value !== null && value !== undefined) {
|
|
||||||
formattedParams[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return formattedParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update search params
|
|
||||||
*
|
|
||||||
* @param params
|
|
||||||
*/
|
|
||||||
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
|
|
||||||
Object.assign(searchParams, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** reset search params */
|
|
||||||
function resetSearchParams() {
|
|
||||||
Object.assign(searchParams, jsonClone(apiParams));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
@@ -143,12 +113,20 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
|||||||
loading,
|
loading,
|
||||||
empty,
|
empty,
|
||||||
data,
|
data,
|
||||||
columns,
|
columns: $columns,
|
||||||
columnChecks,
|
columnChecks,
|
||||||
reloadColumns,
|
reloadColumns,
|
||||||
getData,
|
getData
|
||||||
searchParams,
|
|
||||||
updateSearchParams,
|
|
||||||
resetSearchParams
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTableData<ApiData, Pagination extends boolean>(
|
||||||
|
data: GetApiData<ApiData, Pagination>,
|
||||||
|
pagination?: Pagination
|
||||||
|
) {
|
||||||
|
if (pagination) {
|
||||||
|
return (data as PaginationData<ApiData>).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as ApiData[];
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/materials",
|
"name": "@sa/materials",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"simplebar-vue": "2.4.1"
|
"simplebar-vue": "2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typed-css-modules": "0.9.1"
|
"typed-css-modules": "0.9.1"
|
||||||
|
@@ -127,7 +127,6 @@ function handleClickMask() {
|
|||||||
:class="[
|
:class="[
|
||||||
style['layout-header'],
|
style['layout-header'],
|
||||||
commonClass,
|
commonClass,
|
||||||
headerClass,
|
|
||||||
headerLeftGapClass,
|
headerLeftGapClass,
|
||||||
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
||||||
]"
|
]"
|
||||||
|
@@ -6,12 +6,6 @@ interface AdminLayoutHeaderConfig {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
headerVisible?: boolean;
|
headerVisible?: boolean;
|
||||||
/**
|
|
||||||
* Header class
|
|
||||||
*
|
|
||||||
* @default ''
|
|
||||||
*/
|
|
||||||
headerClass?: string;
|
|
||||||
/**
|
/**
|
||||||
* Header height
|
* Header height
|
||||||
*
|
*
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@sa/fetch",
|
|
||||||
"version": "1.3.13",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"typesVersions": {
|
|
||||||
"*": {
|
|
||||||
"*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ofetch": "1.4.1"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
import { ofetch } from 'ofetch';
|
|
||||||
import type { FetchOptions } from 'ofetch';
|
|
||||||
|
|
||||||
export function createRequest(options: FetchOptions) {
|
|
||||||
const request = ofetch.create(options);
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createRequest;
|
|
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"jsx": "preserve",
|
|
||||||
"lib": ["DOM", "ESNext"],
|
|
||||||
"baseUrl": ".",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"types": ["node"],
|
|
||||||
"strict": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/scripts",
|
"name": "@sa/scripts",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sa": "./bin.ts"
|
"sa": "./bin.ts"
|
||||||
},
|
},
|
||||||
@@ -13,15 +13,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@soybeanjs/changelog": "0.3.24",
|
"@soybeanjs/changelog": "0.3.25",
|
||||||
"bumpp": "10.1.0",
|
"bumpp": "10.2.3",
|
||||||
"c12": "3.0.3",
|
"c12": "3.3.0",
|
||||||
"cac": "6.7.14",
|
"cac": "6.7.14",
|
||||||
"consola": "3.4.2",
|
"consola": "3.4.2",
|
||||||
"enquirer": "2.4.1",
|
"enquirer": "2.4.1",
|
||||||
"execa": "9.5.3",
|
"execa": "9.6.0",
|
||||||
"kolorist": "1.8.0",
|
"kolorist": "1.8.0",
|
||||||
"npm-check-updates": "18.0.1",
|
"npm-check-updates": "18.1.1",
|
||||||
|
"picomatch": "4.0.3",
|
||||||
"rimraf": "6.0.1"
|
"rimraf": "6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/uno-preset",
|
"name": "@sa/uno-preset",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/utils",
|
"name": "@sa/utils",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
|
@@ -32,7 +32,8 @@ export function createStorage<T extends object>(type: StorageType, storagePrefix
|
|||||||
storageData = JSON.parse(json);
|
storageData = JSON.parse(json);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
if (storageData) {
|
// storageData may be `false` if it is boolean type
|
||||||
|
if (storageData !== null) {
|
||||||
return storageData as T[K];
|
return storageData as T[K];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4438
pnpm-lock.yaml
generated
4438
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ const naiveDateLocale = computed(() => {
|
|||||||
|
|
||||||
const watermarkProps = computed<WatermarkProps>(() => {
|
const watermarkProps = computed<WatermarkProps>(() => {
|
||||||
return {
|
return {
|
||||||
content: themeStore.watermark.text,
|
content: themeStore.watermarkContent,
|
||||||
cross: true,
|
cross: true,
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
@@ -22,7 +22,12 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
|
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
|
||||||
<div v-for="item in columns" :key="item.key" class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)">
|
<div
|
||||||
|
v-for="item in columns"
|
||||||
|
:key="item.key"
|
||||||
|
class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
|
||||||
|
:class="{ hidden: !item.visible }"
|
||||||
|
>
|
||||||
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
|
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
|
||||||
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
|
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
|
||||||
<template v-if="typeof item.title === 'function'">
|
<template v-if="typeof item.title === 'function'">
|
||||||
|
42
src/components/common/icon-tooltip.vue
Normal file
42
src/components/common/icon-tooltip.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, useSlots } from 'vue';
|
||||||
|
import type { PopoverPlacement } from 'naive-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'IconTooltip' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: string;
|
||||||
|
localIcon?: string;
|
||||||
|
desc?: string;
|
||||||
|
placement?: PopoverPlacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
icon: 'mdi-help-circle',
|
||||||
|
localIcon: '',
|
||||||
|
desc: '',
|
||||||
|
placement: 'top'
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
|
||||||
|
|
||||||
|
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
|
||||||
|
throw new Error('icon or localIcon is required when no custom trigger slot is provided');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NTooltip :placement="placement">
|
||||||
|
<template #trigger>
|
||||||
|
<slot name="trigger">
|
||||||
|
<div class="cursor-pointer">
|
||||||
|
<SvgIcon :icon="icon" :local-icon="localIcon" />
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<slot>
|
||||||
|
<span>{{ desc }}</span>
|
||||||
|
</slot>
|
||||||
|
</NTooltip>
|
||||||
|
</template>
|
@@ -31,13 +31,25 @@ const tooltipContent = computed(() => {
|
|||||||
return $t('icon.lang');
|
return $t('icon.lang');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Add bottom margin to all options except the last one for proper visual separation */
|
||||||
|
const dropdownOptions = computed(() => {
|
||||||
|
const lastIndex = props.langOptions.length - 1;
|
||||||
|
|
||||||
|
return props.langOptions.map((option, index) => ({
|
||||||
|
...option,
|
||||||
|
props: {
|
||||||
|
class: index < lastIndex ? 'mb-1' : undefined
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
function changeLang(lang: App.I18n.LangType) {
|
function changeLang(lang: App.I18n.LangType) {
|
||||||
emit('changeLang', lang);
|
emit('changeLang', lang);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NDropdown :value="lang" :options="langOptions" trigger="hover" @select="changeLang">
|
<NDropdown :value="lang" :options="dropdownOptions" trigger="hover" @select="changeLang">
|
||||||
<div>
|
<div>
|
||||||
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
|
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
|
||||||
<SvgIcon icon="heroicons:language" />
|
<SvgIcon icon="heroicons:language" />
|
||||||
|
@@ -5,9 +5,9 @@ export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
|
|||||||
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
|
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
|
||||||
|
|
||||||
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
|
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
|
||||||
light: 'theme.themeSchema.light',
|
light: 'theme.appearance.themeSchema.light',
|
||||||
dark: 'theme.themeSchema.dark',
|
dark: 'theme.appearance.themeSchema.dark',
|
||||||
auto: 'theme.themeSchema.auto'
|
auto: 'theme.appearance.themeSchema.auto'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
|
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
|
||||||
@@ -21,45 +21,57 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
|
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
|
||||||
vertical: 'theme.layoutMode.vertical',
|
vertical: 'theme.layout.layoutMode.vertical',
|
||||||
'vertical-mix': 'theme.layoutMode.vertical-mix',
|
'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
|
||||||
horizontal: 'theme.layoutMode.horizontal',
|
'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
|
||||||
'horizontal-mix': 'theme.layoutMode.horizontal-mix'
|
horizontal: 'theme.layout.layoutMode.horizontal',
|
||||||
|
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
|
||||||
|
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
|
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
|
||||||
|
|
||||||
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
|
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
|
||||||
wrapper: 'theme.scrollMode.wrapper',
|
wrapper: 'theme.layout.content.scrollMode.wrapper',
|
||||||
content: 'theme.scrollMode.content'
|
content: 'theme.layout.content.scrollMode.content'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
|
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
|
||||||
|
|
||||||
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
|
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
|
||||||
chrome: 'theme.tab.mode.chrome',
|
chrome: 'theme.layout.tab.mode.chrome',
|
||||||
button: 'theme.tab.mode.button'
|
button: 'theme.layout.tab.mode.button'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
|
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
|
||||||
|
|
||||||
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
|
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
|
||||||
'fade-slide': 'theme.page.mode.fade-slide',
|
'fade-slide': 'theme.layout.content.page.mode.fade-slide',
|
||||||
fade: 'theme.page.mode.fade',
|
fade: 'theme.layout.content.page.mode.fade',
|
||||||
'fade-bottom': 'theme.page.mode.fade-bottom',
|
'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
|
||||||
'fade-scale': 'theme.page.mode.fade-scale',
|
'fade-scale': 'theme.layout.content.page.mode.fade-scale',
|
||||||
'zoom-fade': 'theme.page.mode.zoom-fade',
|
'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
|
||||||
'zoom-out': 'theme.page.mode.zoom-out',
|
'zoom-out': 'theme.layout.content.page.mode.zoom-out',
|
||||||
none: 'theme.page.mode.none'
|
none: 'theme.layout.content.page.mode.none'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
|
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
|
||||||
|
|
||||||
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
|
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
|
||||||
close: 'theme.resetCacheStrategy.close',
|
refresh: 'theme.layout.resetCacheStrategy.refresh',
|
||||||
refresh: 'theme.resetCacheStrategy.refresh'
|
close: 'theme.layout.resetCacheStrategy.close'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
|
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
|
||||||
|
|
||||||
export const DARK_CLASS = 'dark';
|
export const DARK_CLASS = 'dark';
|
||||||
|
|
||||||
|
export const watermarkTimeFormatOptions = [
|
||||||
|
{ label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
|
||||||
|
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
|
||||||
|
{ label: 'YYYY/MM/DD HH:mm', value: 'YYYY/MM/DD HH:mm' },
|
||||||
|
{ label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
|
||||||
|
{ label: 'HH:mm', value: 'HH:mm' },
|
||||||
|
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
|
||||||
|
{ label: 'MM-DD HH:mm', value: 'MM-DD HH:mm' }
|
||||||
|
];
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
|
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
||||||
@@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const darkMode = computed(() => themeStore.darkMode);
|
const darkMode = computed(() => themeStore.darkMode);
|
||||||
|
|
||||||
const domRef = ref<HTMLElement | null>(null);
|
const domRef = shallowRef<HTMLElement | null>(null);
|
||||||
const initialSize = { width: 0, height: 0 };
|
const initialSize = { width: 0, height: 0 };
|
||||||
const { width, height } = useElementSize(domRef, initialSize);
|
const { width, height } = useElementSize(domRef, initialSize);
|
||||||
|
|
||||||
let chart: echarts.ECharts | null = null;
|
const chart = shallowRef<echarts.ECharts | null>(null);
|
||||||
const chartOptions: T = optionsFactory();
|
const chartOptions: T = optionsFactory();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -111,18 +111,9 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
onDestroy
|
onDestroy
|
||||||
} = hooks;
|
} = hooks;
|
||||||
|
|
||||||
/**
|
|
||||||
* whether can render chart
|
|
||||||
*
|
|
||||||
* when domRef is ready and initialSize is valid
|
|
||||||
*/
|
|
||||||
function canRender() {
|
|
||||||
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** is chart rendered */
|
/** is chart rendered */
|
||||||
function isRendered() {
|
function isRendered() {
|
||||||
return Boolean(domRef.value && chart);
|
return Boolean(domRef.value && chart.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,59 +122,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
* @param callback callback function
|
* @param callback callback function
|
||||||
*/
|
*/
|
||||||
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
||||||
if (!isRendered()) return;
|
|
||||||
|
|
||||||
const updatedOpts = callback(chartOptions, optionsFactory);
|
const updatedOpts = callback(chartOptions, optionsFactory);
|
||||||
|
|
||||||
Object.assign(chartOptions, updatedOpts);
|
Object.assign(chartOptions, updatedOpts);
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (!isRendered()) return;
|
||||||
|
|
||||||
if (isRendered()) {
|
if (isRendered()) {
|
||||||
chart?.clear();
|
chart.value?.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
|
chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
|
||||||
|
|
||||||
await onUpdated?.(chart!);
|
await onUpdated?.(chart.value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOptions(options: T) {
|
function setOptions(options: T) {
|
||||||
chart?.setOption(options);
|
chart.value?.setOption(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** render chart */
|
/** render chart */
|
||||||
async function render() {
|
async function render() {
|
||||||
if (!isRendered()) {
|
if (isRendered()) return;
|
||||||
const chartTheme = darkMode.value ? 'dark' : 'light';
|
|
||||||
|
|
||||||
await nextTick();
|
const chartTheme = darkMode.value ? 'dark' : 'light';
|
||||||
|
|
||||||
chart = echarts.init(domRef.value, chartTheme);
|
chart.value = echarts.init(domRef.value, chartTheme);
|
||||||
|
|
||||||
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
|
chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' });
|
||||||
|
|
||||||
await onRender?.(chart);
|
await onRender?.(chart.value!);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** resize chart */
|
/** resize chart */
|
||||||
function resize() {
|
function resize() {
|
||||||
chart?.resize();
|
chart.value?.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** destroy chart */
|
/** destroy chart */
|
||||||
async function destroy() {
|
async function destroy() {
|
||||||
if (!chart) return;
|
if (!chart.value) return;
|
||||||
|
|
||||||
await onDestroy?.(chart);
|
await onDestroy?.(chart.value);
|
||||||
chart?.dispose();
|
chart.value?.dispose();
|
||||||
chart = null;
|
chart.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** change chart theme */
|
/** change chart theme */
|
||||||
async function changeTheme() {
|
async function changeTheme() {
|
||||||
await destroy();
|
await destroy();
|
||||||
await render();
|
await render();
|
||||||
await onUpdated?.(chart!);
|
await onUpdated?.(chart.value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,30 +187,29 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
initialSize.width = w;
|
initialSize.width = w;
|
||||||
initialSize.height = h;
|
initialSize.height = h;
|
||||||
|
|
||||||
// size is abnormal, destroy chart
|
|
||||||
if (!canRender()) {
|
|
||||||
await destroy();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// resize chart
|
// resize chart
|
||||||
if (isRendered()) {
|
if (isRendered()) {
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// render chart
|
// render chart
|
||||||
await render();
|
await render();
|
||||||
|
|
||||||
if (chart) {
|
if (chart.value) {
|
||||||
await onUpdated?.(chart);
|
await onUpdated?.(chart.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.run(() => {
|
scope.run(() => {
|
||||||
watch([width, height], ([newWidth, newHeight]) => {
|
watch(
|
||||||
renderChartBySize(newWidth, newHeight);
|
[width, height],
|
||||||
});
|
([newWidth, newHeight]) => {
|
||||||
|
renderChartBySize(newWidth, newHeight);
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
);
|
||||||
|
|
||||||
watch(darkMode, () => {
|
watch(darkMode, () => {
|
||||||
changeTheme();
|
changeTheme();
|
||||||
@@ -233,6 +223,7 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
domRef,
|
domRef,
|
||||||
|
chart,
|
||||||
updateOptions,
|
updateOptions,
|
||||||
setOptions
|
setOptions
|
||||||
};
|
};
|
||||||
|
@@ -1,192 +1,55 @@
|
|||||||
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
|
import { computed, effectScope, onScopeDispose, reactive, shallowRef, watch } from 'vue';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { PaginationProps } from 'naive-ui';
|
import type { PaginationProps } from 'naive-ui';
|
||||||
|
import { useBoolean, useTable } from '@sa/hooks';
|
||||||
|
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
|
||||||
|
import type { FlatResponseData } from '@sa/axios';
|
||||||
import { jsonClone } from '@sa/utils';
|
import { jsonClone } from '@sa/utils';
|
||||||
import { useBoolean, useHookTable } from '@sa/hooks';
|
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
type TableData = NaiveUI.TableData;
|
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
|
||||||
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
|
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
|
||||||
type TableColumn<T> = NaiveUI.TableColumn<T>;
|
'pagination' | 'getColumnChecks' | 'getColumns'
|
||||||
|
> & {
|
||||||
|
/**
|
||||||
|
* get column visible
|
||||||
|
*
|
||||||
|
* @param column
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*
|
||||||
|
* @returns true if the column is visible, false otherwise
|
||||||
|
*/
|
||||||
|
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
|
const SELECTION_KEY = '__selection__';
|
||||||
|
|
||||||
|
const EXPAND_KEY = '__expand__';
|
||||||
|
|
||||||
|
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
|
||||||
const scope = effectScope();
|
const scope = effectScope();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const isMobile = computed(() => appStore.isMobile);
|
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
|
||||||
|
...options,
|
||||||
const { apiFn, apiParams, immediate, showTotal } = config;
|
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||||
|
getColumns
|
||||||
const SELECTION_KEY = '__selection__';
|
|
||||||
|
|
||||||
const EXPAND_KEY = '__expand__';
|
|
||||||
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
empty,
|
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
columnChecks,
|
|
||||||
reloadColumns,
|
|
||||||
getData,
|
|
||||||
searchParams,
|
|
||||||
updateSearchParams,
|
|
||||||
resetSearchParams
|
|
||||||
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
|
|
||||||
apiFn,
|
|
||||||
apiParams,
|
|
||||||
columns: config.columns,
|
|
||||||
transformer: res => {
|
|
||||||
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
|
|
||||||
|
|
||||||
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
|
|
||||||
const pageSize = size <= 0 ? 10 : size;
|
|
||||||
|
|
||||||
const recordsWithIndex = records.map((item, index) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
index: (current - 1) * pageSize + index + 1
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: recordsWithIndex,
|
|
||||||
pageNum: current,
|
|
||||||
pageSize,
|
|
||||||
total
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getColumnChecks: cols => {
|
|
||||||
const checks: NaiveUI.TableColumnCheck[] = [];
|
|
||||||
|
|
||||||
cols.forEach(column => {
|
|
||||||
if (isTableColumnHasKey(column)) {
|
|
||||||
checks.push({
|
|
||||||
key: column.key as string,
|
|
||||||
title: column.title!,
|
|
||||||
checked: true
|
|
||||||
});
|
|
||||||
} else if (column.type === 'selection') {
|
|
||||||
checks.push({
|
|
||||||
key: SELECTION_KEY,
|
|
||||||
title: $t('common.check'),
|
|
||||||
checked: true
|
|
||||||
});
|
|
||||||
} else if (column.type === 'expand') {
|
|
||||||
checks.push({
|
|
||||||
key: EXPAND_KEY,
|
|
||||||
title: $t('common.expandColumn'),
|
|
||||||
checked: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return checks;
|
|
||||||
},
|
|
||||||
getColumns: (cols, checks) => {
|
|
||||||
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
|
|
||||||
|
|
||||||
cols.forEach(column => {
|
|
||||||
if (isTableColumnHasKey(column)) {
|
|
||||||
columnMap.set(column.key as string, column);
|
|
||||||
} else if (column.type === 'selection') {
|
|
||||||
columnMap.set(SELECTION_KEY, column);
|
|
||||||
} else if (column.type === 'expand') {
|
|
||||||
columnMap.set(EXPAND_KEY, column);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredColumns = checks
|
|
||||||
.filter(item => item.checked)
|
|
||||||
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
|
|
||||||
|
|
||||||
return filteredColumns;
|
|
||||||
},
|
|
||||||
onFetched: async transformed => {
|
|
||||||
const { pageNum, pageSize, total } = transformed;
|
|
||||||
|
|
||||||
updatePagination({
|
|
||||||
page: pageNum,
|
|
||||||
pageSize,
|
|
||||||
itemCount: total
|
|
||||||
});
|
|
||||||
},
|
|
||||||
immediate
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pagination: PaginationProps = reactive({
|
// calculate the total width of the table this is used for horizontal scrolling
|
||||||
page: 1,
|
const scrollX = computed(() => {
|
||||||
pageSize: 10,
|
return result.columns.value.reduce((acc, column) => {
|
||||||
showSizePicker: true,
|
return acc + Number(column.width ?? column.minWidth ?? 120);
|
||||||
itemCount: 0,
|
}, 0);
|
||||||
pageSizes: [10, 15, 20, 25, 30],
|
|
||||||
onUpdatePage: async (page: number) => {
|
|
||||||
pagination.page = page;
|
|
||||||
|
|
||||||
updateSearchParams({
|
|
||||||
current: page,
|
|
||||||
size: pagination.pageSize!
|
|
||||||
});
|
|
||||||
|
|
||||||
getData();
|
|
||||||
},
|
|
||||||
onUpdatePageSize: async (pageSize: number) => {
|
|
||||||
pagination.pageSize = pageSize;
|
|
||||||
pagination.page = 1;
|
|
||||||
|
|
||||||
updateSearchParams({
|
|
||||||
current: pagination.page,
|
|
||||||
size: pageSize
|
|
||||||
});
|
|
||||||
|
|
||||||
getData();
|
|
||||||
},
|
|
||||||
...(showTotal
|
|
||||||
? {
|
|
||||||
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
|
|
||||||
const mobilePagination = computed(() => {
|
|
||||||
const p: PaginationProps = {
|
|
||||||
...pagination,
|
|
||||||
pageSlot: isMobile.value ? 3 : 9,
|
|
||||||
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
|
|
||||||
function updatePagination(update: Partial<PaginationProps>) {
|
|
||||||
Object.assign(pagination, update);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get data by page number
|
|
||||||
*
|
|
||||||
* @param pageNum the page number. default is 1
|
|
||||||
*/
|
|
||||||
async function getDataByPage(pageNum: number = 1) {
|
|
||||||
updatePagination({
|
|
||||||
page: pageNum
|
|
||||||
});
|
|
||||||
|
|
||||||
updateSearchParams({
|
|
||||||
current: pageNum,
|
|
||||||
size: pagination.pageSize!
|
|
||||||
});
|
|
||||||
|
|
||||||
await getData();
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.run(() => {
|
scope.run(() => {
|
||||||
watch(
|
watch(
|
||||||
() => appStore.locale,
|
() => appStore.locale,
|
||||||
() => {
|
() => {
|
||||||
reloadColumns();
|
result.reloadColumns();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -196,27 +59,126 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
...result,
|
||||||
empty,
|
scrollX
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
columnChecks,
|
|
||||||
reloadColumns,
|
|
||||||
pagination,
|
|
||||||
mobilePagination,
|
|
||||||
updatePagination,
|
|
||||||
getData,
|
|
||||||
getDataByPage,
|
|
||||||
searchParams,
|
|
||||||
updateSearchParams,
|
|
||||||
resetSearchParams
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
|
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
|
||||||
|
|
||||||
|
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
|
||||||
|
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
|
||||||
|
/**
|
||||||
|
* whether to show the total count of the table
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
showTotal?: boolean;
|
||||||
|
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useNaivePaginatedTable<ResponseData, ApiData>(
|
||||||
|
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
|
||||||
|
) {
|
||||||
|
const scope = effectScope();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const isMobile = computed(() => appStore.isMobile);
|
||||||
|
|
||||||
|
const showTotal = computed(() => options.showTotal ?? true);
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
itemCount: 0,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 15, 20, 25, 30],
|
||||||
|
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
|
||||||
|
onUpdatePage(page) {
|
||||||
|
pagination.page = page;
|
||||||
|
},
|
||||||
|
onUpdatePageSize(pageSize) {
|
||||||
|
pagination.pageSize = pageSize;
|
||||||
|
pagination.page = 1;
|
||||||
|
},
|
||||||
|
...options.paginationProps
|
||||||
|
}) as PaginationProps;
|
||||||
|
|
||||||
|
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
|
||||||
|
const mobilePagination = computed(() => {
|
||||||
|
const p: PaginationProps = {
|
||||||
|
...pagination,
|
||||||
|
pageSlot: isMobile.value ? 3 : 9,
|
||||||
|
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationParams = computed(() => {
|
||||||
|
const { page, pageSize } = pagination;
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
|
||||||
|
...options,
|
||||||
|
pagination: true,
|
||||||
|
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||||
|
getColumns,
|
||||||
|
onFetched: data => {
|
||||||
|
pagination.itemCount = data.total;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getDataByPage(page: number = 1) {
|
||||||
|
if (page !== pagination.page) {
|
||||||
|
pagination.page = page;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await result.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
watch(
|
||||||
|
() => appStore.locale,
|
||||||
|
() => {
|
||||||
|
result.reloadColumns();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(paginationParams, async newVal => {
|
||||||
|
await options.onPaginationParamsChange?.(newVal);
|
||||||
|
|
||||||
|
await result.getData();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onScopeDispose(() => {
|
||||||
|
scope.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
getDataByPage,
|
||||||
|
pagination,
|
||||||
|
mobilePagination
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTableOperate<TableData>(
|
||||||
|
data: Ref<TableData[]>,
|
||||||
|
idKey: keyof TableData,
|
||||||
|
getData: () => Promise<void>
|
||||||
|
) {
|
||||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
||||||
|
|
||||||
const operateType = ref<NaiveUI.TableOperateType>('add');
|
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
|
||||||
|
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
operateType.value = 'add';
|
operateType.value = 'add';
|
||||||
@@ -224,18 +186,18 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** the editing row data */
|
/** the editing row data */
|
||||||
const editingData: Ref<T | null> = ref(null);
|
const editingData = shallowRef<TableData | null>(null);
|
||||||
|
|
||||||
function handleEdit(id: T['id']) {
|
function handleEdit(id: TableData[keyof TableData]) {
|
||||||
operateType.value = 'edit';
|
operateType.value = 'edit';
|
||||||
const findItem = data.value.find(item => item.id === id) || null;
|
const findItem = data.value.find(item => item[idKey] === id) || null;
|
||||||
editingData.value = jsonClone(findItem);
|
editingData.value = jsonClone(findItem);
|
||||||
|
|
||||||
openDrawer();
|
openDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the checked row keys of table */
|
/** the checked row keys of table */
|
||||||
const checkedRowKeys = ref<string[]>([]);
|
const checkedRowKeys = shallowRef<string[]>([]);
|
||||||
|
|
||||||
/** the hook after the batch delete operation is completed */
|
/** the hook after the batch delete operation is completed */
|
||||||
async function onBatchDeleted() {
|
async function onBatchDeleted() {
|
||||||
@@ -267,6 +229,82 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
|
export function defaultTransform<ApiData>(
|
||||||
|
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
|
||||||
|
): PaginationData<ApiData> {
|
||||||
|
const { data, error } = response;
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
const { records, current, size, total } = data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: records,
|
||||||
|
pageNum: current,
|
||||||
|
pageSize: size,
|
||||||
|
total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
|
||||||
|
cols: Column[],
|
||||||
|
getColumnVisible?: (column: Column) => boolean
|
||||||
|
) {
|
||||||
|
const checks: TableColumnCheck[] = [];
|
||||||
|
|
||||||
|
cols.forEach(column => {
|
||||||
|
if (isTableColumnHasKey(column)) {
|
||||||
|
checks.push({
|
||||||
|
key: column.key as string,
|
||||||
|
title: column.title!,
|
||||||
|
checked: true,
|
||||||
|
visible: getColumnVisible?.(column) ?? true
|
||||||
|
});
|
||||||
|
} else if (column.type === 'selection') {
|
||||||
|
checks.push({
|
||||||
|
key: SELECTION_KEY,
|
||||||
|
title: $t('common.check'),
|
||||||
|
checked: true,
|
||||||
|
visible: getColumnVisible?.(column) ?? false
|
||||||
|
});
|
||||||
|
} else if (column.type === 'expand') {
|
||||||
|
checks.push({
|
||||||
|
key: EXPAND_KEY,
|
||||||
|
title: $t('common.expandColumn'),
|
||||||
|
checked: true,
|
||||||
|
visible: getColumnVisible?.(column) ?? false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
|
||||||
|
const columnMap = new Map<string, Column>();
|
||||||
|
|
||||||
|
cols.forEach(column => {
|
||||||
|
if (isTableColumnHasKey(column)) {
|
||||||
|
columnMap.set(column.key as string, column);
|
||||||
|
} else if (column.type === 'selection') {
|
||||||
|
columnMap.set(SELECTION_KEY, column);
|
||||||
|
} else if (column.type === 'expand') {
|
||||||
|
columnMap.set(EXPAND_KEY, column);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
|
||||||
|
|
||||||
|
return filteredColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
|
||||||
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
|
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import GlobalTab from '../modules/global-tab/index.vue';
|
|||||||
import GlobalContent from '../modules/global-content/index.vue';
|
import GlobalContent from '../modules/global-content/index.vue';
|
||||||
import GlobalFooter from '../modules/global-footer/index.vue';
|
import GlobalFooter from '../modules/global-footer/index.vue';
|
||||||
import ThemeDrawer from '../modules/theme-drawer/index.vue';
|
import ThemeDrawer from '../modules/theme-drawer/index.vue';
|
||||||
import { setupMixMenuContext } from '../context';
|
import { provideMixMenuContext } from '../modules/global-menu/context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'BaseLayout'
|
name: 'BaseLayout'
|
||||||
@@ -18,7 +18,7 @@ defineOptions({
|
|||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
|
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext();
|
||||||
|
|
||||||
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
|
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ const layoutMode = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const headerProps = computed(() => {
|
const headerProps = computed(() => {
|
||||||
const { mode, reverseHorizontalMix } = themeStore.layout;
|
const { mode } = themeStore.layout;
|
||||||
|
|
||||||
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
|
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
|
||||||
vertical: {
|
vertical: {
|
||||||
@@ -42,15 +42,25 @@ const headerProps = computed(() => {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
showMenuToggler: false
|
showMenuToggler: false
|
||||||
},
|
},
|
||||||
|
'vertical-hybrid-header-first': {
|
||||||
|
showLogo: !isActiveFirstLevelMenuHasChildren.value,
|
||||||
|
showMenu: true,
|
||||||
|
showMenuToggler: false
|
||||||
|
},
|
||||||
horizontal: {
|
horizontal: {
|
||||||
showLogo: true,
|
showLogo: true,
|
||||||
showMenu: true,
|
showMenu: true,
|
||||||
showMenuToggler: false
|
showMenuToggler: false
|
||||||
},
|
},
|
||||||
'horizontal-mix': {
|
'top-hybrid-sidebar-first': {
|
||||||
showLogo: true,
|
showLogo: true,
|
||||||
showMenu: true,
|
showMenu: true,
|
||||||
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
|
showMenuToggler: false
|
||||||
|
},
|
||||||
|
'top-hybrid-header-first': {
|
||||||
|
showLogo: true,
|
||||||
|
showMenu: true,
|
||||||
|
showMenuToggler: isActiveFirstLevelMenuHasChildren.value
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,44 +71,56 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
|
|||||||
|
|
||||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
||||||
|
|
||||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
|
const isVerticalHybridHeaderFirst = computed(() => themeStore.layout.mode === 'vertical-hybrid-header-first');
|
||||||
|
|
||||||
|
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
|
||||||
|
|
||||||
|
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
|
||||||
|
|
||||||
const siderWidth = computed(() => getSiderWidth());
|
const siderWidth = computed(() => getSiderWidth());
|
||||||
|
|
||||||
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
||||||
|
|
||||||
function getSiderWidth() {
|
function getSiderAndCollapsedWidth(isCollapsed: boolean) {
|
||||||
const { reverseHorizontalMix } = themeStore.layout;
|
const {
|
||||||
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
|
mixChildMenuWidth,
|
||||||
|
collapsedWidth,
|
||||||
|
width: themeWidth,
|
||||||
|
mixCollapsedWidth,
|
||||||
|
mixWidth: themeMixWidth
|
||||||
|
} = themeStore.sider;
|
||||||
|
|
||||||
if (isHorizontalMix.value && reverseHorizontalMix) {
|
const width = isCollapsed ? collapsedWidth : themeWidth;
|
||||||
|
const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
|
||||||
|
|
||||||
|
if (isTopHybridHeaderFirst.value) {
|
||||||
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
|
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
|
if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) {
|
||||||
|
return 0;
|
||||||
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
|
||||||
w += mixChildMenuWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return w;
|
const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
|
||||||
|
let finalWidth = isMixMode ? mixWidth : width;
|
||||||
|
|
||||||
|
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
||||||
|
finalWidth += mixChildMenuWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVerticalHybridHeaderFirst.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
||||||
|
finalWidth += mixChildMenuWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSiderWidth() {
|
||||||
|
return getSiderAndCollapsedWidth(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSiderCollapsedWidth() {
|
function getSiderCollapsedWidth() {
|
||||||
const { reverseHorizontalMix } = themeStore.layout;
|
return getSiderAndCollapsedWidth(true);
|
||||||
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
|
|
||||||
|
|
||||||
if (isHorizontalMix.value && reverseHorizontalMix) {
|
|
||||||
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
|
|
||||||
|
|
||||||
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
|
||||||
w += mixChildMenuWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
return w;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -1,83 +0,0 @@
|
|||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useContext } from '@sa/hooks';
|
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
|
||||||
|
|
||||||
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
|
|
||||||
|
|
||||||
function useMixMenu() {
|
|
||||||
const route = useRoute();
|
|
||||||
const routeStore = useRouteStore();
|
|
||||||
const { selectedKey } = useMenu();
|
|
||||||
|
|
||||||
const activeFirstLevelMenuKey = ref('');
|
|
||||||
|
|
||||||
function setActiveFirstLevelMenuKey(key: string) {
|
|
||||||
activeFirstLevelMenuKey.value = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveFirstLevelMenuKey() {
|
|
||||||
const [firstLevelRouteName] = selectedKey.value.split('_');
|
|
||||||
|
|
||||||
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
|
|
||||||
|
|
||||||
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
|
|
||||||
routeStore.menus.map(menu => {
|
|
||||||
const { children: _, ...rest } = menu;
|
|
||||||
|
|
||||||
return rest;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const childLevelMenus = computed<App.Global.Menu[]>(
|
|
||||||
() => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
|
|
||||||
);
|
|
||||||
|
|
||||||
const isActiveFirstLevelMenuHasChildren = computed(() => {
|
|
||||||
if (!activeFirstLevelMenuKey.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
|
|
||||||
|
|
||||||
return Boolean(findItem?.children?.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.name,
|
|
||||||
() => {
|
|
||||||
getActiveFirstLevelMenuKey();
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
allMenus,
|
|
||||||
firstLevelMenus,
|
|
||||||
childLevelMenus,
|
|
||||||
isActiveFirstLevelMenuHasChildren,
|
|
||||||
activeFirstLevelMenuKey,
|
|
||||||
setActiveFirstLevelMenuKey,
|
|
||||||
getActiveFirstLevelMenuKey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMenu() {
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const selectedKey = computed(() => {
|
|
||||||
const { hideInMenu, activeMenu } = route.meta;
|
|
||||||
const name = route.name as string;
|
|
||||||
|
|
||||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
|
||||||
|
|
||||||
return routeName;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedKey
|
|
||||||
};
|
|
||||||
}
|
|
@@ -3,6 +3,7 @@ import { computed } from 'vue';
|
|||||||
import { createReusableTemplate } from '@vueuse/core';
|
import { createReusableTemplate } from '@vueuse/core';
|
||||||
import { SimpleScrollbar } from '@sa/materials';
|
import { SimpleScrollbar } from '@sa/materials';
|
||||||
import { transformColorWithOpacity } from '@sa/color';
|
import { transformColorWithOpacity } from '@sa/color';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'FirstLevelMenu'
|
name: 'FirstLevelMenu'
|
||||||
@@ -20,7 +21,7 @@ interface Props {
|
|||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'select', menu: App.Global.Menu): boolean;
|
(e: 'select', menuKey: RouteKey): boolean;
|
||||||
(e: 'toggleSiderCollapse'): void;
|
(e: 'toggleSiderCollapse'): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +48,8 @@ const selectedBgColor = computed(() => {
|
|||||||
return darkMode ? dark : light;
|
return darkMode ? dark : light;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClickMixMenu(menu: App.Global.Menu) {
|
function handleClickMixMenu(menuKey: RouteKey) {
|
||||||
emit('select', menu);
|
emit('select', menuKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSiderCollapse() {
|
function toggleSiderCollapse() {
|
||||||
@@ -88,7 +89,7 @@ function toggleSiderCollapse() {
|
|||||||
:icon="menu.icon"
|
:icon="menu.icon"
|
||||||
:active="menu.key === activeMenuKey"
|
:active="menu.key === activeMenuKey"
|
||||||
:is-mini="siderCollapse"
|
:is-mini="siderCollapse"
|
||||||
@click="handleClickMixMenu(menu)"
|
@click="handleClickMixMenu(menu.routeKey)"
|
||||||
/>
|
/>
|
||||||
</SimpleScrollbar>
|
</SimpleScrollbar>
|
||||||
<MenuToggler
|
<MenuToggler
|
||||||
|
143
src/layouts/modules/global-menu/context/index.ts
Normal file
143
src/layouts/modules/global-menu/context/index.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useContext } from '@sa/hooks';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
|
|
||||||
|
export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu', useMixMenu);
|
||||||
|
|
||||||
|
function useMixMenu() {
|
||||||
|
const route = useRoute();
|
||||||
|
const routeStore = useRouteStore();
|
||||||
|
const { selectedKey } = useMenu();
|
||||||
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
|
|
||||||
|
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
|
||||||
|
|
||||||
|
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
|
||||||
|
routeStore.menus.map(menu => {
|
||||||
|
const { children: _, ...rest } = menu;
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeFirstLevelMenuKey = ref('');
|
||||||
|
|
||||||
|
function setActiveFirstLevelMenuKey(key: string) {
|
||||||
|
activeFirstLevelMenuKey.value = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveFirstLevelMenuKey() {
|
||||||
|
const [firstLevelRouteName] = selectedKey.value.split('_');
|
||||||
|
|
||||||
|
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActiveFirstLevelMenuHasChildren = computed(() => {
|
||||||
|
if (!activeFirstLevelMenuKey.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
|
||||||
|
|
||||||
|
return Boolean(findItem?.children?.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSelectFirstLevelMenu(key: RouteKey) {
|
||||||
|
setActiveFirstLevelMenuKey(key);
|
||||||
|
|
||||||
|
if (!isActiveFirstLevelMenuHasChildren.value) {
|
||||||
|
routerPushByKeyWithMetaQuery(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondLevelMenus = computed<App.Global.Menu[]>(
|
||||||
|
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeSecondLevelMenuKey = ref('');
|
||||||
|
|
||||||
|
function setActiveSecondLevelMenuKey(key: string) {
|
||||||
|
activeSecondLevelMenuKey.value = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveSecondLevelMenuKey() {
|
||||||
|
const keys = selectedKey.value.split('_');
|
||||||
|
|
||||||
|
if (keys.length < 2) {
|
||||||
|
setActiveSecondLevelMenuKey('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstLevelRouteName, level2SuffixName] = keys;
|
||||||
|
|
||||||
|
const secondLevelRouteName = `${firstLevelRouteName}_${level2SuffixName}`;
|
||||||
|
|
||||||
|
setActiveSecondLevelMenuKey(secondLevelRouteName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActiveSecondLevelMenuHasChildren = computed(() => {
|
||||||
|
if (!activeSecondLevelMenuKey.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findItem = secondLevelMenus.value.find(item => item.key === activeSecondLevelMenuKey.value);
|
||||||
|
|
||||||
|
return Boolean(findItem?.children?.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSelectSecondLevelMenu(key: RouteKey) {
|
||||||
|
setActiveSecondLevelMenuKey(key);
|
||||||
|
|
||||||
|
if (!isActiveSecondLevelMenuHasChildren.value) {
|
||||||
|
routerPushByKeyWithMetaQuery(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const childLevelMenus = computed<App.Global.Menu[]>(
|
||||||
|
() => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.name,
|
||||||
|
() => {
|
||||||
|
getActiveFirstLevelMenuKey();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstLevelMenus,
|
||||||
|
activeFirstLevelMenuKey,
|
||||||
|
setActiveFirstLevelMenuKey,
|
||||||
|
isActiveFirstLevelMenuHasChildren,
|
||||||
|
handleSelectFirstLevelMenu,
|
||||||
|
getActiveFirstLevelMenuKey,
|
||||||
|
secondLevelMenus,
|
||||||
|
activeSecondLevelMenuKey,
|
||||||
|
setActiveSecondLevelMenuKey,
|
||||||
|
isActiveSecondLevelMenuHasChildren,
|
||||||
|
handleSelectSecondLevelMenu,
|
||||||
|
getActiveSecondLevelMenuKey,
|
||||||
|
childLevelMenus
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMenu() {
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const selectedKey = computed(() => {
|
||||||
|
const { hideInMenu, activeMenu } = route.meta;
|
||||||
|
const name = route.name as string;
|
||||||
|
|
||||||
|
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||||
|
|
||||||
|
return routeName;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedKey
|
||||||
|
};
|
||||||
|
}
|
@@ -5,9 +5,10 @@ import { useAppStore } from '@/store/modules/app';
|
|||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import VerticalMenu from './modules/vertical-menu.vue';
|
import VerticalMenu from './modules/vertical-menu.vue';
|
||||||
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
|
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
|
||||||
|
import VerticalHybridHeaderFirst from './modules/vertical-hybrid-header-first.vue';
|
||||||
import HorizontalMenu from './modules/horizontal-menu.vue';
|
import HorizontalMenu from './modules/horizontal-menu.vue';
|
||||||
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
|
import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
|
||||||
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
|
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'GlobalMenu'
|
name: 'GlobalMenu'
|
||||||
@@ -20,8 +21,10 @@ const activeMenu = computed(() => {
|
|||||||
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
|
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
|
||||||
vertical: VerticalMenu,
|
vertical: VerticalMenu,
|
||||||
'vertical-mix': VerticalMixMenu,
|
'vertical-mix': VerticalMixMenu,
|
||||||
|
'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
|
||||||
horizontal: HorizontalMenu,
|
horizontal: HorizontalMenu,
|
||||||
'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
|
'top-hybrid-sidebar-first': TopHybridSidebarFirst,
|
||||||
|
'top-hybrid-header-first': TopHybridHeaderFirst
|
||||||
};
|
};
|
||||||
|
|
||||||
return menuMap[themeStore.layout.mode];
|
return menuMap[themeStore.layout.mode];
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import { useMenu } from '../../../context';
|
import { useMenu } from '../context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'HorizontalMenu'
|
name: 'HorizontalMenu'
|
||||||
|
@@ -1,17 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import type { RouteKey } from '@elegant-router/types';
|
|
||||||
import { SimpleScrollbar } from '@sa/materials';
|
import { SimpleScrollbar } from '@sa/materials';
|
||||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import { useMenu, useMixMenuContext } from '../../../context';
|
import { useMenu, useMixMenuContext } from '../context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ReversedHorizontalMixMenu'
|
name: 'TopHybridHeaderFirst'
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -19,23 +18,10 @@ const appStore = useAppStore();
|
|||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
const {
|
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
|
||||||
firstLevelMenus,
|
useMixMenuContext('TopHybridHeaderFirst');
|
||||||
childLevelMenus,
|
|
||||||
activeFirstLevelMenuKey,
|
|
||||||
setActiveFirstLevelMenuKey,
|
|
||||||
isActiveFirstLevelMenuHasChildren
|
|
||||||
} = useMixMenuContext();
|
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
function handleSelectMixMenu(key: RouteKey) {
|
|
||||||
setActiveFirstLevelMenuKey(key);
|
|
||||||
|
|
||||||
if (!isActiveFirstLevelMenuHasChildren.value) {
|
|
||||||
routerPushByKeyWithMetaQuery(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandedKeys = ref<string[]>([]);
|
const expandedKeys = ref<string[]>([]);
|
||||||
|
|
||||||
function updateExpandedKeys() {
|
function updateExpandedKeys() {
|
||||||
@@ -63,7 +49,7 @@ watch(
|
|||||||
:options="firstLevelMenus"
|
:options="firstLevelMenus"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
responsive
|
responsive
|
||||||
@update:value="handleSelectMixMenu"
|
@update:value="handleSelectFirstLevelMenu"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
@@ -75,7 +61,7 @@ watch(
|
|||||||
:collapsed="appStore.siderCollapse"
|
:collapsed="appStore.siderCollapse"
|
||||||
:collapsed-width="themeStore.sider.collapsedWidth"
|
:collapsed-width="themeStore.sider.collapsedWidth"
|
||||||
:collapsed-icon-size="22"
|
:collapsed-icon-size="22"
|
||||||
:options="childLevelMenus"
|
:options="secondLevelMenus"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
@update:value="routerPushByKeyWithMetaQuery"
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
/>
|
/>
|
@@ -4,25 +4,18 @@ import { useAppStore } from '@/store/modules/app';
|
|||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||||
import { useMenu, useMixMenuContext } from '../../../context';
|
import { useMenu, useMixMenuContext } from '../context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'HorizontalMixMenu'
|
name: 'TopHybridSidebarFirst'
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
|
||||||
|
useMixMenuContext('TopHybridSidebarFirst');
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
|
||||||
setActiveFirstLevelMenuKey(menu.key);
|
|
||||||
|
|
||||||
if (!menu.children?.length) {
|
|
||||||
routerPushByKeyWithMetaQuery(menu.routeKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -30,22 +23,24 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
|
|||||||
<NMenu
|
<NMenu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
:value="selectedKey"
|
:value="selectedKey"
|
||||||
:options="childLevelMenus"
|
:options="secondLevelMenus"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
responsive
|
responsive
|
||||||
@update:value="routerPushByKeyWithMetaQuery"
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<FirstLevelMenu
|
<div class="h-full pt-2">
|
||||||
:menus="allMenus"
|
<FirstLevelMenu
|
||||||
:active-menu-key="activeFirstLevelMenuKey"
|
:menus="firstLevelMenus"
|
||||||
:sider-collapse="appStore.siderCollapse"
|
:active-menu-key="activeFirstLevelMenuKey"
|
||||||
:dark-mode="themeStore.darkMode"
|
:sider-collapse="appStore.siderCollapse"
|
||||||
:theme-color="themeStore.themeColor"
|
:dark-mode="themeStore.darkMode"
|
||||||
@select="handleSelectMixMenu"
|
:theme-color="themeStore.themeColor"
|
||||||
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
@select="handleSelectFirstLevelMenu"
|
||||||
/>
|
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
@@ -0,0 +1,149 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
import { SimpleScrollbar } from '@sa/materials';
|
||||||
|
import { useBoolean } from '@sa/hooks';
|
||||||
|
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||||
|
import { useAppStore } from '@/store/modules/app';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
|
import { useMenu, useMixMenuContext } from '../context';
|
||||||
|
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||||
|
import GlobalLogo from '../../global-logo/index.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'VerticalHybridHeaderFirst'
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const routeStore = useRouteStore();
|
||||||
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
|
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
|
||||||
|
const {
|
||||||
|
firstLevelMenus,
|
||||||
|
activeFirstLevelMenuKey,
|
||||||
|
handleSelectFirstLevelMenu,
|
||||||
|
getActiveFirstLevelMenuKey,
|
||||||
|
secondLevelMenus,
|
||||||
|
activeSecondLevelMenuKey,
|
||||||
|
isActiveSecondLevelMenuHasChildren,
|
||||||
|
handleSelectSecondLevelMenu,
|
||||||
|
getActiveSecondLevelMenuKey,
|
||||||
|
childLevelMenus
|
||||||
|
} = useMixMenuContext('VerticalHybridHeaderFirst');
|
||||||
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
|
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||||
|
|
||||||
|
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
|
||||||
|
|
||||||
|
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
|
||||||
|
|
||||||
|
function handleSelectMixMenu(key: RouteKey) {
|
||||||
|
handleSelectSecondLevelMenu(key);
|
||||||
|
|
||||||
|
if (isActiveSecondLevelMenuHasChildren.value) {
|
||||||
|
setDrawerVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectMenu(key: RouteKey) {
|
||||||
|
handleSelectFirstLevelMenu(key);
|
||||||
|
|
||||||
|
if (secondLevelMenus.value.length > 0) {
|
||||||
|
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResetActiveMenu() {
|
||||||
|
setDrawerVisible(false);
|
||||||
|
|
||||||
|
if (!appStore.mixSiderFixed) {
|
||||||
|
getActiveFirstLevelMenuKey();
|
||||||
|
getActiveSecondLevelMenuKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedKeys = ref<string[]>([]);
|
||||||
|
|
||||||
|
function updateExpandedKeys() {
|
||||||
|
if (appStore.siderCollapse || !selectedKey.value) {
|
||||||
|
expandedKeys.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.name,
|
||||||
|
() => {
|
||||||
|
updateExpandedKeys();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
|
<NMenu
|
||||||
|
mode="horizontal"
|
||||||
|
:value="activeFirstLevelMenuKey"
|
||||||
|
:options="firstLevelMenus"
|
||||||
|
:indent="18"
|
||||||
|
responsive
|
||||||
|
@update:value="handleSelectMenu"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
|
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
||||||
|
<FirstLevelMenu
|
||||||
|
:menus="secondLevelMenus"
|
||||||
|
:active-menu-key="activeSecondLevelMenuKey"
|
||||||
|
:inverted="inverted"
|
||||||
|
:sider-collapse="appStore.siderCollapse"
|
||||||
|
:dark-mode="themeStore.darkMode"
|
||||||
|
:theme-color="themeStore.themeColor"
|
||||||
|
@select="handleSelectMixMenu"
|
||||||
|
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
||||||
|
>
|
||||||
|
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
|
||||||
|
</FirstLevelMenu>
|
||||||
|
<div
|
||||||
|
class="relative h-full transition-width-300"
|
||||||
|
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
|
||||||
|
>
|
||||||
|
<DarkModeContainer
|
||||||
|
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
|
||||||
|
:inverted="inverted"
|
||||||
|
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
|
||||||
|
>
|
||||||
|
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
|
||||||
|
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
|
||||||
|
<PinToggler
|
||||||
|
:pin="appStore.mixSiderFixed"
|
||||||
|
:class="{ 'text-white:88 !hover:text-white': inverted }"
|
||||||
|
@click="appStore.toggleMixSiderFixed"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<SimpleScrollbar>
|
||||||
|
<NMenu
|
||||||
|
v-model:expanded-keys="expandedKeys"
|
||||||
|
mode="vertical"
|
||||||
|
:value="selectedKey"
|
||||||
|
:options="childLevelMenus"
|
||||||
|
:inverted="inverted"
|
||||||
|
:indent="18"
|
||||||
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
|
/>
|
||||||
|
</SimpleScrollbar>
|
||||||
|
</DarkModeContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -7,7 +7,7 @@ import { useAppStore } from '@/store/modules/app';
|
|||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import { useMenu } from '../../../context';
|
import { useMenu } from '../context';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'VerticalMenu'
|
name: 'VerticalMenu'
|
||||||
|
@@ -3,13 +3,14 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { SimpleScrollbar } from '@sa/materials';
|
import { SimpleScrollbar } from '@sa/materials';
|
||||||
import { useBoolean } from '@sa/hooks';
|
import { useBoolean } from '@sa/hooks';
|
||||||
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouteStore } from '@/store/modules/route';
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { useMenu, useMixMenuContext } from '../../../context';
|
import { useMenu, useMixMenuContext } from '../context';
|
||||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||||
import GlobalLogo from '../../global-logo/index.vue';
|
import GlobalLogo from '../../global-logo/index.vue';
|
||||||
|
|
||||||
@@ -24,28 +25,26 @@ const routeStore = useRouteStore();
|
|||||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
|
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
|
||||||
const {
|
const {
|
||||||
allMenus,
|
firstLevelMenus,
|
||||||
childLevelMenus,
|
secondLevelMenus,
|
||||||
activeFirstLevelMenuKey,
|
activeFirstLevelMenuKey,
|
||||||
setActiveFirstLevelMenuKey,
|
isActiveFirstLevelMenuHasChildren,
|
||||||
getActiveFirstLevelMenuKey
|
getActiveFirstLevelMenuKey,
|
||||||
//
|
handleSelectFirstLevelMenu
|
||||||
} = useMixMenuContext();
|
} = useMixMenuContext('VerticalMixMenu');
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||||
|
|
||||||
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
|
const hasChildMenus = computed(() => secondLevelMenus.value.length > 0);
|
||||||
|
|
||||||
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
|
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
|
||||||
|
|
||||||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
function handleSelectMenu(key: RouteKey) {
|
||||||
setActiveFirstLevelMenuKey(menu.key);
|
handleSelectFirstLevelMenu(key);
|
||||||
|
|
||||||
if (menu.children?.length) {
|
if (isActiveFirstLevelMenuHasChildren.value) {
|
||||||
setDrawerVisible(true);
|
setDrawerVisible(true);
|
||||||
} else {
|
|
||||||
routerPushByKeyWithMetaQuery(menu.routeKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +79,13 @@ watch(
|
|||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="firstLevelMenus"
|
||||||
:active-menu-key="activeFirstLevelMenuKey"
|
:active-menu-key="activeFirstLevelMenuKey"
|
||||||
:inverted="inverted"
|
:inverted="inverted"
|
||||||
:sider-collapse="appStore.siderCollapse"
|
:sider-collapse="appStore.siderCollapse"
|
||||||
:dark-mode="themeStore.darkMode"
|
:dark-mode="themeStore.darkMode"
|
||||||
:theme-color="themeStore.themeColor"
|
:theme-color="themeStore.themeColor"
|
||||||
@select="handleSelectMixMenu"
|
@select="handleSelectMenu"
|
||||||
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
||||||
>
|
>
|
||||||
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
|
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
|
||||||
@@ -113,7 +112,7 @@ watch(
|
|||||||
v-model:expanded-keys="expandedKeys"
|
v-model:expanded-keys="expandedKeys"
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
:value="selectedKey"
|
:value="selectedKey"
|
||||||
:options="childLevelMenus"
|
:options="secondLevelMenus"
|
||||||
:inverted="inverted"
|
:inverted="inverted"
|
||||||
:indent="18"
|
:indent="18"
|
||||||
@update:value="routerPushByKeyWithMetaQuery"
|
@update:value="routerPushByKeyWithMetaQuery"
|
||||||
|
@@ -12,10 +12,13 @@ defineOptions({
|
|||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
|
||||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
|
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
|
||||||
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
|
const darkMenu = computed(
|
||||||
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
|
() =>
|
||||||
|
!themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted
|
||||||
|
);
|
||||||
|
const showLogo = computed(() => themeStore.layout.mode === 'vertical');
|
||||||
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
|
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -27,7 +27,6 @@ type LayoutConfig = Record<
|
|||||||
UnionKey.ThemeLayoutMode,
|
UnionKey.ThemeLayoutMode,
|
||||||
{
|
{
|
||||||
placement: PopoverPlacement;
|
placement: PopoverPlacement;
|
||||||
headerClass: string;
|
|
||||||
menuClass: string;
|
menuClass: string;
|
||||||
mainClass: string;
|
mainClass: string;
|
||||||
}
|
}
|
||||||
@@ -36,25 +35,31 @@ type LayoutConfig = Record<
|
|||||||
const layoutConfig: LayoutConfig = {
|
const layoutConfig: LayoutConfig = {
|
||||||
vertical: {
|
vertical: {
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
headerClass: '',
|
|
||||||
menuClass: 'w-1/3 h-full',
|
menuClass: 'w-1/3 h-full',
|
||||||
mainClass: 'w-2/3 h-3/4'
|
mainClass: 'w-2/3 h-3/4'
|
||||||
},
|
},
|
||||||
'vertical-mix': {
|
'vertical-mix': {
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
headerClass: '',
|
menuClass: 'w-1/4 h-full',
|
||||||
|
mainClass: 'w-2/3 h-3/4'
|
||||||
|
},
|
||||||
|
'vertical-hybrid-header-first': {
|
||||||
|
placement: 'bottom',
|
||||||
menuClass: 'w-1/4 h-full',
|
menuClass: 'w-1/4 h-full',
|
||||||
mainClass: 'w-2/3 h-3/4'
|
mainClass: 'w-2/3 h-3/4'
|
||||||
},
|
},
|
||||||
horizontal: {
|
horizontal: {
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
headerClass: '',
|
|
||||||
menuClass: 'w-full h-1/4',
|
menuClass: 'w-full h-1/4',
|
||||||
mainClass: 'w-full h-3/4'
|
mainClass: 'w-full h-3/4'
|
||||||
},
|
},
|
||||||
'horizontal-mix': {
|
'top-hybrid-sidebar-first': {
|
||||||
|
placement: 'bottom',
|
||||||
|
menuClass: 'w-full h-1/4',
|
||||||
|
mainClass: 'w-2/3 h-3/4'
|
||||||
|
},
|
||||||
|
'top-hybrid-header-first': {
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
headerClass: '',
|
|
||||||
menuClass: 'w-full h-1/4',
|
menuClass: 'w-full h-1/4',
|
||||||
mainClass: 'w-2/3 h-3/4'
|
mainClass: 'w-2/3 h-3/4'
|
||||||
}
|
}
|
||||||
@@ -68,25 +73,27 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-center flex-wrap gap-x-32px gap-y-16px">
|
<div class="grid grid-cols-2 gap-x-16px gap-y-12px md:grid-cols-3">
|
||||||
<div
|
<div
|
||||||
v-for="(item, key) in layoutConfig"
|
v-for="(item, key) in layoutConfig"
|
||||||
:key="key"
|
:key="key"
|
||||||
class="flex cursor-pointer border-2px rounded-6px hover:border-primary"
|
class="flex-col-center cursor-pointer"
|
||||||
:class="[mode === key ? 'border-primary' : 'border-transparent']"
|
|
||||||
@click="handleChangeMode(key)"
|
@click="handleChangeMode(key)"
|
||||||
>
|
>
|
||||||
<NTooltip :placement="item.placement">
|
<IconTooltip :placement="item.placement">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div
|
<div
|
||||||
class="h-64px w-96px gap-6px rd-4px p-6px shadow dark:shadow-coolGray-5"
|
class="h-64px w-96px gap-6px rd-4px p-6px shadow ring-2 ring-transparent transition-all hover:ring-primary"
|
||||||
:class="[key.includes('vertical') ? 'flex' : 'flex-col']"
|
:class="{ '!ring-primary': mode === key }"
|
||||||
>
|
>
|
||||||
<slot :name="key"></slot>
|
<div class="h-full w-full gap-1" :class="[key.includes('vertical') ? 'flex' : 'flex-col']">
|
||||||
|
<slot :name="key"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
{{ $t(themeLayoutModeRecord[key]) }}
|
{{ $t(`theme.layout.layoutMode.${key}_detail`) }}
|
||||||
</NTooltip>
|
</IconTooltip>
|
||||||
|
<p class="mt-8px text-12px">{{ $t(themeLayoutModeRecord[key]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -13,7 +13,7 @@ defineProps<Props>();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full flex-y-center justify-between">
|
<div class="w-full flex-y-center justify-between">
|
||||||
<div>
|
<div class="flex-y-center">
|
||||||
<span class="pr-8px text-base-text">{{ label }}</span>
|
<span class="pr-8px text-base-text">{{ label }}</span>
|
||||||
<slot name="suffix"></slot>
|
<slot name="suffix"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,26 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import DarkMode from './modules/dark-mode.vue';
|
import AppearanceSettings from './modules/appearance/index.vue';
|
||||||
import LayoutMode from './modules/layout-mode.vue';
|
import LayoutSettings from './modules/layout/index.vue';
|
||||||
import ThemeColor from './modules/theme-color.vue';
|
import GeneralSettings from './modules/general/index.vue';
|
||||||
import PageFun from './modules/page-fun.vue';
|
|
||||||
import ConfigOperation from './modules/config-operation.vue';
|
import ConfigOperation from './modules/config-operation.vue';
|
||||||
|
import PresetSettings from './modules/preset/index.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ThemeDrawer'
|
name: 'ThemeDrawer'
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const activeTab = ref('appearance');
|
||||||
|
|
||||||
|
const drawerWidth = computed(() => {
|
||||||
|
const width = 400;
|
||||||
|
|
||||||
|
// On mobile devices, use 90% of viewport width with a maximum of 400px
|
||||||
|
if (appStore.isMobile) {
|
||||||
|
return `min(90vw, ${width}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="360">
|
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="drawerWidth">
|
||||||
<NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable>
|
<NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable>
|
||||||
<DarkMode />
|
<NTabs v-model:value="activeTab" type="segment" size="medium" class="mb-16px">
|
||||||
<LayoutMode />
|
<NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
|
||||||
<ThemeColor />
|
<NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
|
||||||
<PageFun />
|
<NTab name="general" :tab="$t('theme.tabs.general')"></NTab>
|
||||||
|
<NTab name="preset" :tab="$t('theme.tabs.preset')"></NTab>
|
||||||
|
</NTabs>
|
||||||
|
|
||||||
|
<div class="min-h-400px">
|
||||||
|
<KeepAlive>
|
||||||
|
<AppearanceSettings v-if="activeTab === 'appearance'" />
|
||||||
|
<LayoutSettings v-else-if="activeTab === 'layout'" />
|
||||||
|
<GeneralSettings v-else-if="activeTab === 'general'" />
|
||||||
|
<PresetSettings v-else-if="activeTab === 'preset'" />
|
||||||
|
</KeepAlive>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ConfigOperation />
|
<ConfigOperation />
|
||||||
</template>
|
</template>
|
||||||
@@ -28,4 +53,14 @@ const appStore = useAppStore();
|
|||||||
</NDrawer>
|
</NDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
:deep(.n-tab) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tab-pane) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ThemeSchema from './modules/theme-schema.vue';
|
||||||
|
import ThemeColor from './modules/theme-color.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AppearanceSettings'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px">
|
||||||
|
<ThemeSchema />
|
||||||
|
<ThemeColor />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import SettingItem from '../components/setting-item.vue';
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ThemeColor'
|
name: 'ThemeColor'
|
||||||
@@ -34,33 +34,38 @@ const swatches: string[] = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NDivider>{{ $t('theme.themeColor.title') }}</NDivider>
|
<NDivider>{{ $t('theme.appearance.themeColor.title') }}</NDivider>
|
||||||
<div class="flex-col-stretch gap-12px">
|
<div class="flex-col-stretch gap-12px">
|
||||||
<NTooltip placement="top-start">
|
<SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
|
||||||
<template #trigger>
|
<template #suffix>
|
||||||
<SettingItem key="recommend-color" :label="$t('theme.recommendColor')">
|
<IconTooltip>
|
||||||
<NSwitch v-model:value="themeStore.recommendColor" />
|
<p>
|
||||||
</SettingItem>
|
<span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
|
||||||
|
<br />
|
||||||
|
<NButton
|
||||||
|
text
|
||||||
|
tag="a"
|
||||||
|
href="https://uicolors.app/create"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-gray"
|
||||||
|
>
|
||||||
|
https://uicolors.app/create
|
||||||
|
</NButton>
|
||||||
|
</p>
|
||||||
|
</IconTooltip>
|
||||||
</template>
|
</template>
|
||||||
<p>
|
<NSwitch v-model:value="themeStore.recommendColor" />
|
||||||
<span class="pr-12px">{{ $t('theme.recommendColorDesc') }}</span>
|
</SettingItem>
|
||||||
<br />
|
|
||||||
<NButton
|
<SettingItem
|
||||||
text
|
v-for="(_, key) in themeStore.themeColors"
|
||||||
tag="a"
|
:key="key"
|
||||||
href="https://uicolors.app/create"
|
:label="$t(`theme.appearance.themeColor.${key}`)"
|
||||||
target="_blank"
|
>
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-gray"
|
|
||||||
>
|
|
||||||
https://uicolors.app/create
|
|
||||||
</NButton>
|
|
||||||
</p>
|
|
||||||
</NTooltip>
|
|
||||||
<SettingItem v-for="(_, key) in themeStore.themeColors" :key="key" :label="$t(`theme.themeColor.${key}`)">
|
|
||||||
<template v-if="key === 'info'" #suffix>
|
<template v-if="key === 'info'" #suffix>
|
||||||
<NCheckbox v-model:checked="themeStore.isInfoFollowPrimary">
|
<NCheckbox v-model:checked="themeStore.isInfoFollowPrimary">
|
||||||
{{ $t('theme.themeColor.followPrimary') }}
|
{{ $t('theme.appearance.themeColor.followPrimary') }}
|
||||||
</NCheckbox>
|
</NCheckbox>
|
||||||
</template>
|
</template>
|
||||||
<NColorPicker
|
<NColorPicker
|
@@ -3,10 +3,10 @@ import { computed } from 'vue';
|
|||||||
import { themeSchemaRecord } from '@/constants/app';
|
import { themeSchemaRecord } from '@/constants/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import SettingItem from '../components/setting-item.vue';
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'DarkMode'
|
name: 'ThemeSchema'
|
||||||
});
|
});
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -33,7 +33,7 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NDivider>{{ $t('theme.themeSchema.title') }}</NDivider>
|
<NDivider>{{ $t('theme.appearance.themeSchema.title') }}</NDivider>
|
||||||
<div class="flex-col-stretch gap-16px">
|
<div class="flex-col-stretch gap-16px">
|
||||||
<div class="i-flex-center">
|
<div class="i-flex-center">
|
||||||
<NTabs
|
<NTabs
|
||||||
@@ -50,14 +50,14 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
|
|||||||
</NTabs>
|
</NTabs>
|
||||||
</div>
|
</div>
|
||||||
<Transition name="sider-inverted">
|
<Transition name="sider-inverted">
|
||||||
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
|
<SettingItem v-if="showSiderInverted" :label="$t('theme.layout.sider.inverted')">
|
||||||
<NSwitch v-model:value="themeStore.sider.inverted" />
|
<NSwitch v-model:value="themeStore.sider.inverted" />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</Transition>
|
</Transition>
|
||||||
<SettingItem :label="$t('theme.grayscale')">
|
<SettingItem :label="$t('theme.appearance.grayscale')">
|
||||||
<NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" />
|
<NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem :label="$t('theme.colourWeakness')">
|
<SettingItem :label="$t('theme.appearance.colourWeakness')">
|
||||||
<NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" />
|
<NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</div>
|
</div>
|
17
src/layouts/modules/theme-drawer/modules/general/index.vue
Normal file
17
src/layouts/modules/theme-drawer/modules/general/index.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import GlobalSettings from './modules/global-settings.vue';
|
||||||
|
import WatermarkSettings from './modules/watermark-settings.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'GeneralSettings'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px">
|
||||||
|
<GlobalSettings />
|
||||||
|
<WatermarkSettings />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'GlobalSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.general.title') }}</NDivider>
|
||||||
|
<SettingItem :label="$t('theme.general.multilingual.visible')">
|
||||||
|
<NSwitch v-model:value="themeStore.header.multilingual.visible" />
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem :label="$t('theme.general.globalSearch.visible')">
|
||||||
|
<NSwitch v-model:value="themeStore.header.globalSearch.visible" />
|
||||||
|
</SettingItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-list-move,
|
||||||
|
.setting-list-enter-active,
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: transition-all-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-enter-from,
|
||||||
|
.setting-list-leave-to {
|
||||||
|
--uno: opacity-0 -translate-x-30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { watermarkTimeFormatOptions } from '@/constants/app';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'WatermarkSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
const isWatermarkTextVisible = computed(
|
||||||
|
() => themeStore.watermark.visible && !themeStore.watermark.enableUserName && !themeStore.watermark.enableTime
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.general.watermark.title') }}</NDivider>
|
||||||
|
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||||
|
<SettingItem key="1" :label="$t('theme.general.watermark.visible')">
|
||||||
|
<NSwitch v-model:value="themeStore.watermark.visible" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="themeStore.watermark.visible" key="2" :label="$t('theme.general.watermark.enableUserName')">
|
||||||
|
<NSwitch :value="themeStore.watermark.enableUserName" @update:value="themeStore.setWatermarkEnableUserName" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="themeStore.watermark.visible" key="3" :label="$t('theme.general.watermark.enableTime')">
|
||||||
|
<NSwitch :value="themeStore.watermark.enableTime" @update:value="themeStore.setWatermarkEnableTime" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem
|
||||||
|
v-if="themeStore.watermark.visible && themeStore.watermark.enableTime"
|
||||||
|
key="4"
|
||||||
|
:label="$t('theme.general.watermark.timeFormat')"
|
||||||
|
>
|
||||||
|
<NSelect
|
||||||
|
v-model:value="themeStore.watermark.timeFormat"
|
||||||
|
:options="watermarkTimeFormatOptions"
|
||||||
|
size="small"
|
||||||
|
class="w-210px"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="isWatermarkTextVisible" key="5" :label="$t('theme.general.watermark.text')">
|
||||||
|
<NInput
|
||||||
|
v-model:value="themeStore.watermark.text"
|
||||||
|
autosize
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="w-120px"
|
||||||
|
placeholder="SoybeanAdmin"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-list-move,
|
||||||
|
.setting-list-enter-active,
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: transition-all-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-enter-from,
|
||||||
|
.setting-list-leave-to {
|
||||||
|
--uno: opacity-0 -translate-x-30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
29
src/layouts/modules/theme-drawer/modules/layout/index.vue
Normal file
29
src/layouts/modules/theme-drawer/modules/layout/index.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import LayoutMode from './modules/layout-mode.vue';
|
||||||
|
import TabSettings from './modules/tab-settings.vue';
|
||||||
|
import HeaderSettings from './modules/header-settings.vue';
|
||||||
|
import SiderSettings from './modules/sider-settings.vue';
|
||||||
|
import FooterSettings from './modules/footer-settings.vue';
|
||||||
|
import ContentSettings from './modules/content-settings.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'LayoutSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px">
|
||||||
|
<LayoutMode />
|
||||||
|
<TabSettings />
|
||||||
|
<HeaderSettings />
|
||||||
|
<!-- The top menu mode does not have a sidebar -->
|
||||||
|
<SiderSettings v-if="themeStore.layout.mode !== 'horizontal'" />
|
||||||
|
<FooterSettings />
|
||||||
|
<ContentSettings />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { themePageAnimationModeOptions, themeScrollModeOptions } from '@/constants/app';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { translateOptions } from '@/utils/common';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ContentSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.layout.content.title') }}</NDivider>
|
||||||
|
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||||
|
<SettingItem key="1" :label="$t('theme.layout.content.scrollMode.title')">
|
||||||
|
<template #suffix>
|
||||||
|
<IconTooltip :desc="$t('theme.layout.content.scrollMode.tip')" />
|
||||||
|
</template>
|
||||||
|
<NSelect
|
||||||
|
v-model:value="themeStore.layout.scrollMode"
|
||||||
|
:options="translateOptions(themeScrollModeOptions)"
|
||||||
|
size="small"
|
||||||
|
class="w-120px"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem key="2" :label="$t('theme.layout.content.page.animate')">
|
||||||
|
<NSwitch v-model:value="themeStore.page.animate" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="themeStore.page.animate" key="3" :label="$t('theme.layout.content.page.mode.title')">
|
||||||
|
<NSelect
|
||||||
|
v-model:value="themeStore.page.animateMode"
|
||||||
|
:options="translateOptions(themePageAnimationModeOptions)"
|
||||||
|
size="small"
|
||||||
|
class="w-120px"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="isWrapperScrollMode" key="4" :label="$t('theme.layout.content.fixedHeaderAndTab')">
|
||||||
|
<NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
|
||||||
|
</SettingItem>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-list-move,
|
||||||
|
.setting-list-enter-active,
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: transition-all-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-enter-from,
|
||||||
|
.setting-list-leave-to {
|
||||||
|
--uno: opacity-0 -translate-x-30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FooterSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
const layoutMode = computed(() => themeStore.layout.mode);
|
||||||
|
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
|
||||||
|
const isMixHorizontalMode = computed(() =>
|
||||||
|
['top-hybrid-sidebar-first', 'top-hybrid-header-first'].includes(layoutMode.value)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.layout.footer.title') }}</NDivider>
|
||||||
|
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||||
|
<SettingItem key="1" :label="$t('theme.layout.footer.visible')">
|
||||||
|
<NSwitch v-model:value="themeStore.footer.visible" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem
|
||||||
|
v-if="themeStore.footer.visible && isWrapperScrollMode"
|
||||||
|
key="2"
|
||||||
|
:label="$t('theme.layout.footer.fixed')"
|
||||||
|
>
|
||||||
|
<NSwitch v-model:value="themeStore.footer.fixed" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="themeStore.footer.visible" key="3" :label="$t('theme.layout.footer.height')">
|
||||||
|
<NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem
|
||||||
|
v-if="themeStore.footer.visible && isMixHorizontalMode"
|
||||||
|
key="4"
|
||||||
|
:label="$t('theme.layout.footer.right')"
|
||||||
|
>
|
||||||
|
<NSwitch v-model:value="themeStore.footer.right" />
|
||||||
|
</SettingItem>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-list-move,
|
||||||
|
.setting-list-enter-active,
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: transition-all-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-enter-from,
|
||||||
|
.setting-list-leave-to {
|
||||||
|
--uno: opacity-0 -translate-x-30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'HeaderSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.layout.header.title') }}</NDivider>
|
||||||
|
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||||
|
<SettingItem key="1" :label="$t('theme.layout.header.height')">
|
||||||
|
<NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem key="2" :label="$t('theme.layout.header.breadcrumb.visible')">
|
||||||
|
<NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem
|
||||||
|
v-if="themeStore.header.breadcrumb.visible"
|
||||||
|
key="3"
|
||||||
|
:label="$t('theme.layout.header.breadcrumb.showIcon')"
|
||||||
|
>
|
||||||
|
<NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
|
||||||
|
</SettingItem>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-list-move,
|
||||||
|
.setting-list-enter-active,
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: transition-all-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-enter-from,
|
||||||
|
.setting-list-leave-to {
|
||||||
|
--uno: opacity-0 -translate-x-30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -2,8 +2,7 @@
|
|||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import LayoutModeCard from '../components/layout-mode-card.vue';
|
import LayoutModeCard from '../../../components/layout-mode-card.vue';
|
||||||
import SettingItem from '../components/setting-item.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LayoutMode'
|
name: 'LayoutMode'
|
||||||
@@ -11,56 +10,60 @@ defineOptions({
|
|||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
function handleReverseHorizontalMixChange(value: boolean) {
|
|
||||||
themeStore.setLayoutReverseHorizontalMix(value);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NDivider>{{ $t('theme.layoutMode.title') }}</NDivider>
|
<NDivider>{{ $t('theme.layout.layoutMode.title') }}</NDivider>
|
||||||
<LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile">
|
<LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile">
|
||||||
<template #vertical>
|
<template #vertical>
|
||||||
<div class="layout-sider h-full w-18px"></div>
|
<div class="layout-sider h-full w-18px !bg-primary"></div>
|
||||||
<div class="vertical-wrapper">
|
<div class="vertical-wrapper">
|
||||||
<div class="layout-header"></div>
|
<div class="layout-header bg-primary-200"></div>
|
||||||
<div class="layout-main"></div>
|
<div class="layout-main"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #vertical-mix>
|
<template #vertical-mix>
|
||||||
<div class="layout-sider h-full w-8px"></div>
|
<div class="layout-sider h-full w-8px !bg-primary"></div>
|
||||||
<div class="layout-sider h-full w-16px"></div>
|
<div class="layout-sider h-full w-16px !bg-primary-300"></div>
|
||||||
<div class="vertical-wrapper">
|
<div class="vertical-wrapper">
|
||||||
<div class="layout-header"></div>
|
<div class="layout-header bg-primary-200"></div>
|
||||||
|
<div class="layout-main"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #vertical-hybrid-header-first>
|
||||||
|
<div class="layout-sider h-full w-8px !bg-primary"></div>
|
||||||
|
<div class="layout-sider h-full w-16px !bg-primary-300"></div>
|
||||||
|
<div class="vertical-wrapper">
|
||||||
|
<div class="layout-header bg-primary"></div>
|
||||||
<div class="layout-main"></div>
|
<div class="layout-main"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #horizontal>
|
<template #horizontal>
|
||||||
<div class="layout-header"></div>
|
<div class="layout-header !bg-primary"></div>
|
||||||
<div class="horizontal-wrapper">
|
<div class="horizontal-wrapper">
|
||||||
<div class="layout-main"></div>
|
<div class="layout-main"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #horizontal-mix>
|
<template #top-hybrid-sidebar-first>
|
||||||
<div class="layout-header"></div>
|
<div class="layout-header !bg-primary-300"></div>
|
||||||
|
<div class="horizontal-wrapper">
|
||||||
|
<div class="layout-sider w-18px !bg-primary"></div>
|
||||||
|
<div class="layout-main"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #top-hybrid-header-first>
|
||||||
|
<div class="layout-header bg-primary"></div>
|
||||||
<div class="horizontal-wrapper">
|
<div class="horizontal-wrapper">
|
||||||
<div class="layout-sider w-18px"></div>
|
<div class="layout-sider w-18px"></div>
|
||||||
<div class="layout-main"></div>
|
<div class="layout-main"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</LayoutModeCard>
|
</LayoutModeCard>
|
||||||
<SettingItem
|
|
||||||
v-if="themeStore.layout.mode === 'horizontal-mix'"
|
|
||||||
:label="$t('theme.layoutMode.reverseHorizontalMix')"
|
|
||||||
class="mt-16px"
|
|
||||||
>
|
|
||||||
<NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />
|
|
||||||
</SettingItem>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.layout-header {
|
.layout-header {
|
||||||
--uno: h-16px bg-primary rd-4px;
|
--uno: h-16px rd-4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-sider {
|
.layout-sider {
|
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SiderSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
const layoutMode = computed(() => themeStore.layout.mode);
|
||||||
|
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layoutMode.value.includes('hybrid'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.layout.sider.title') }}</NDivider>
|
||||||
|
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||||
|
<SettingItem v-if="layoutMode === 'vertical'" key="1" :label="$t('theme.layout.sider.width')">
|
||||||
|
<NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="layoutMode === 'vertical'" key="2" :label="$t('theme.layout.sider.collapsedWidth')">
|
||||||
|
<NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="isMixLayoutMode" key="3" :label="$t('theme.layout.sider.mixWidth')">
|
||||||
|
<NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="isMixLayoutMode" key="4" :label="$t('theme.layout.sider.mixCollapsedWidth')">
|
||||||
|
<NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="layoutMode === 'vertical-mix'" key="5" :label="$t('theme.layout.sider.mixChildMenuWidth')">
|
||||||
|
<NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-list-move,
|
||||||
|
.setting-list-enter-active,
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: transition-all-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-enter-from,
|
||||||
|
.setting-list-leave-to {
|
||||||
|
--uno: opacity-0 -translate-x-30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { resetCacheStrategyOptions, themeTabModeOptions } from '@/constants/app';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { translateOptions } from '@/utils/common';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
import SettingItem from '../../../components/setting-item.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'TabSettings'
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.layout.tab.title') }}</NDivider>
|
||||||
|
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||||
|
<SettingItem key="0" :label="$t('theme.layout.resetCacheStrategy.title')">
|
||||||
|
<NSelect
|
||||||
|
v-model:value="themeStore.resetCacheStrategy"
|
||||||
|
:options="translateOptions(resetCacheStrategyOptions)"
|
||||||
|
size="small"
|
||||||
|
class="w-120px"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem key="1" :label="$t('theme.layout.tab.visible')">
|
||||||
|
<NSwitch v-model:value="themeStore.tab.visible" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="themeStore.tab.visible" key="2" :label="$t('theme.layout.tab.cache')">
|
||||||
|
<template #suffix>
|
||||||
|
<IconTooltip :desc="$t('theme.layout.tab.cacheTip')" />
|
||||||
|
</template>
|
||||||
|
<NSwitch v-model:value="themeStore.tab.cache" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="themeStore.tab.visible" key="3" :label="$t('theme.layout.tab.height')">
|
||||||
|
<NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="themeStore.tab.visible" key="4" :label="$t('theme.layout.tab.mode.title')">
|
||||||
|
<NSelect
|
||||||
|
v-model:value="themeStore.tab.mode"
|
||||||
|
:options="translateOptions(themeTabModeOptions)"
|
||||||
|
size="small"
|
||||||
|
class="w-120px"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-list-move,
|
||||||
|
.setting-list-enter-active,
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: transition-all-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-enter-from,
|
||||||
|
.setting-list-leave-to {
|
||||||
|
--uno: opacity-0 -translate-x-30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list-leave-active {
|
||||||
|
--uno: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,154 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import {
|
|
||||||
resetCacheStrategyOptions,
|
|
||||||
themePageAnimationModeOptions,
|
|
||||||
themeScrollModeOptions,
|
|
||||||
themeTabModeOptions
|
|
||||||
} from '@/constants/app';
|
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
|
||||||
import { translateOptions } from '@/utils/common';
|
|
||||||
import { $t } from '@/locales';
|
|
||||||
import SettingItem from '../components/setting-item.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: 'PageFun'
|
|
||||||
});
|
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
|
||||||
|
|
||||||
const layoutMode = computed(() => themeStore.layout.mode);
|
|
||||||
|
|
||||||
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
|
|
||||||
|
|
||||||
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NDivider>{{ $t('theme.pageFunTitle') }}</NDivider>
|
|
||||||
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
|
||||||
<SettingItem key="0" :label="$t('theme.resetCacheStrategy.title')">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="themeStore.resetCacheStrategy"
|
|
||||||
:options="translateOptions(resetCacheStrategyOptions)"
|
|
||||||
size="small"
|
|
||||||
class="w-120px"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="1" :label="$t('theme.scrollMode.title')">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="themeStore.layout.scrollMode"
|
|
||||||
:options="translateOptions(themeScrollModeOptions)"
|
|
||||||
size="small"
|
|
||||||
class="w-120px"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="1-1" :label="$t('theme.page.animate')">
|
|
||||||
<NSwitch v-model:value="themeStore.page.animate" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.page.animate" key="1-2" :label="$t('theme.page.mode.title')">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="themeStore.page.animateMode"
|
|
||||||
:options="translateOptions(themePageAnimationModeOptions)"
|
|
||||||
size="small"
|
|
||||||
class="w-120px"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="isWrapperScrollMode" key="2" :label="$t('theme.fixedHeaderAndTab')">
|
|
||||||
<NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="3" :label="$t('theme.header.height')">
|
|
||||||
<NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="4" :label="$t('theme.header.breadcrumb.visible')">
|
|
||||||
<NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.header.breadcrumb.visible" key="4-1" :label="$t('theme.header.breadcrumb.showIcon')">
|
|
||||||
<NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="5" :label="$t('theme.tab.visible')">
|
|
||||||
<NSwitch v-model:value="themeStore.tab.visible" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.tab.visible" key="5-1" :label="$t('theme.tab.cache')">
|
|
||||||
<NSwitch v-model:value="themeStore.tab.cache" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.tab.visible" key="5-2" :label="$t('theme.tab.height')">
|
|
||||||
<NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.tab.visible" key="5-3" :label="$t('theme.tab.mode.title')">
|
|
||||||
<NSelect
|
|
||||||
v-model:value="themeStore.tab.mode"
|
|
||||||
:options="translateOptions(themeTabModeOptions)"
|
|
||||||
size="small"
|
|
||||||
class="w-120px"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="layoutMode === 'vertical'" key="6-1" :label="$t('theme.sider.width')">
|
|
||||||
<NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="layoutMode === 'vertical'" key="6-2" :label="$t('theme.sider.collapsedWidth')">
|
|
||||||
<NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="isMixLayoutMode" key="6-3" :label="$t('theme.sider.mixWidth')">
|
|
||||||
<NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="isMixLayoutMode" key="6-4" :label="$t('theme.sider.mixCollapsedWidth')">
|
|
||||||
<NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="layoutMode === 'vertical-mix'" key="6-5" :label="$t('theme.sider.mixChildMenuWidth')">
|
|
||||||
<NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="7" :label="$t('theme.footer.visible')">
|
|
||||||
<NSwitch v-model:value="themeStore.footer.visible" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.footer.visible && isWrapperScrollMode" key="7-1" :label="$t('theme.footer.fixed')">
|
|
||||||
<NSwitch v-model:value="themeStore.footer.fixed" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.footer.visible" key="7-2" :label="$t('theme.footer.height')">
|
|
||||||
<NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem
|
|
||||||
v-if="themeStore.footer.visible && layoutMode === 'horizontal-mix'"
|
|
||||||
key="7-3"
|
|
||||||
:label="$t('theme.footer.right')"
|
|
||||||
>
|
|
||||||
<NSwitch v-model:value="themeStore.footer.right" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="8" :label="$t('theme.watermark.visible')">
|
|
||||||
<NSwitch v-model:value="themeStore.watermark.visible" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.text')">
|
|
||||||
<NInput
|
|
||||||
v-model:value="themeStore.watermark.text"
|
|
||||||
autosize
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
class="w-120px"
|
|
||||||
placeholder="SoybeanAdmin"
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="9" :label="$t('theme.header.multilingual.visible')">
|
|
||||||
<NSwitch v-model:value="themeStore.header.multilingual.visible" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem key="10" :label="$t('theme.header.globalSearch.visible')">
|
|
||||||
<NSwitch v-model:value="themeStore.header.globalSearch.visible" />
|
|
||||||
</SettingItem>
|
|
||||||
</TransitionGroup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.setting-list-move,
|
|
||||||
.setting-list-enter-active,
|
|
||||||
.setting-list-leave-active {
|
|
||||||
--uno: transition-all-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-list-enter-from,
|
|
||||||
.setting-list-leave-to {
|
|
||||||
--uno: opacity-0 -translate-x-30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-list-leave-active {
|
|
||||||
--uno: absolute;
|
|
||||||
}
|
|
||||||
</style>
|
|
15
src/layouts/modules/theme-drawer/modules/preset/index.vue
Normal file
15
src/layouts/modules/theme-drawer/modules/preset/index.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ThemePreset from './modules/theme-preset.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PresetSettings'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px">
|
||||||
|
<ThemePreset />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -0,0 +1,148 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ThemePreset'
|
||||||
|
});
|
||||||
|
|
||||||
|
type ThemePreset = Pick<
|
||||||
|
App.Theme.ThemeSetting,
|
||||||
|
| 'themeScheme'
|
||||||
|
| 'grayscale'
|
||||||
|
| 'colourWeakness'
|
||||||
|
| 'recommendColor'
|
||||||
|
| 'themeColor'
|
||||||
|
| 'otherColor'
|
||||||
|
| 'isInfoFollowPrimary'
|
||||||
|
| 'resetCacheStrategy'
|
||||||
|
| 'layout'
|
||||||
|
| 'page'
|
||||||
|
| 'header'
|
||||||
|
| 'tab'
|
||||||
|
| 'fixedHeaderAndTab'
|
||||||
|
| 'sider'
|
||||||
|
| 'footer'
|
||||||
|
| 'watermark'
|
||||||
|
| 'tokens'
|
||||||
|
> & {
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
i18nkey?: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
// Extract preset data
|
||||||
|
const presets = computed(() =>
|
||||||
|
Object.entries(presetModules)
|
||||||
|
.map(([path, presetData]) => {
|
||||||
|
const fileName = path.split('/').pop()?.replace('.json', '') || '';
|
||||||
|
return {
|
||||||
|
id: fileName,
|
||||||
|
...(presetData as ThemePreset)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.name === 'default') return -1;
|
||||||
|
if (b.name === 'default') return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const getPresetName = (preset: ThemePreset): string => {
|
||||||
|
if (!preset.i18nkey) return preset.name;
|
||||||
|
try {
|
||||||
|
const key = `${preset.i18nkey}.name` as App.I18n.I18nKey;
|
||||||
|
const translated = $t(key);
|
||||||
|
return translated !== key ? translated : preset.name;
|
||||||
|
} catch {
|
||||||
|
return preset.name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPresetDesc = (preset: ThemePreset): string => {
|
||||||
|
if (!preset.i18nkey) return preset.desc;
|
||||||
|
try {
|
||||||
|
const key = `${preset.i18nkey}.desc` as App.I18n.I18nKey;
|
||||||
|
const translated = $t(key);
|
||||||
|
return translated !== key ? translated : preset.desc;
|
||||||
|
} catch {
|
||||||
|
return preset.desc;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
|
||||||
|
themeStore.setThemeScheme(themeScheme);
|
||||||
|
themeStore.setGrayscale(grayscale);
|
||||||
|
themeStore.setColourWeakness(colourWeakness);
|
||||||
|
themeStore.setThemeLayout(layout.mode);
|
||||||
|
themeStore.setWatermarkEnableUserName(watermark.enableUserName);
|
||||||
|
themeStore.setWatermarkEnableTime(watermark.enableTime);
|
||||||
|
|
||||||
|
Object.assign(themeStore, {
|
||||||
|
...rest,
|
||||||
|
layout: { ...themeStore.layout, scrollMode: layout.scrollMode },
|
||||||
|
page: { ...rest.page },
|
||||||
|
header: { ...rest.header },
|
||||||
|
tab: { ...rest.tab },
|
||||||
|
sider: { ...rest.sider },
|
||||||
|
footer: { ...rest.footer },
|
||||||
|
watermark: { ...watermark },
|
||||||
|
tokens: { ...rest.tokens }
|
||||||
|
});
|
||||||
|
|
||||||
|
window.$message?.success($t('theme.appearance.preset.applySuccess'));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDivider>{{ $t('theme.appearance.preset.title') }}</NDivider>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
v-for="preset in presets"
|
||||||
|
:key="preset.id"
|
||||||
|
class="border border-primary/10 rounded-lg border-solid bg-white/5 p-3 backdrop-blur-10 transition-all duration-300 hover:(shadow-md -translate-y-0.5)"
|
||||||
|
>
|
||||||
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<div class="min-w-0 w-full flex flex-1 items-center justify-between gap-2">
|
||||||
|
<h5 class="m-0 truncate text-sm text-primary font-600">
|
||||||
|
{{ getPresetName(preset) }}
|
||||||
|
</h5>
|
||||||
|
<NBadge :value="`v${preset.version}`" type="info" size="small" class="flex-shrink-0 opacity-80" />
|
||||||
|
</div>
|
||||||
|
<NButton type="primary" size="tiny" ghost round class="ml-2 flex-shrink-0" @click="applyPreset(preset)">
|
||||||
|
{{ $t('theme.appearance.preset.apply') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="line-clamp-2 mb-3 text-xs text-gray-500 leading-4">{{ getPresetDesc(preset) }}</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<div
|
||||||
|
v-for="(color, key) in { primary: preset.themeColor, ...preset.otherColor }"
|
||||||
|
:key="key"
|
||||||
|
class="h-3 w-3 cursor-pointer border border-white/30 rounded-full transition-transform hover:scale-110"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
:class="{ 'ring-1 ring-primary/50': key === 'primary' }"
|
||||||
|
:title="key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="text-lg">
|
||||||
|
{{ preset.themeScheme === 'dark' ? '🌙' : '☀️' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-lg">
|
||||||
|
{{ preset.grayscale ? '🎨' : '' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -58,100 +58,159 @@ const local: App.I18n.Schema = {
|
|||||||
tokenExpired: 'The requested token has expired'
|
tokenExpired: 'The requested token has expired'
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
themeSchema: {
|
themeDrawerTitle: 'Theme Configuration',
|
||||||
title: 'Theme Schema',
|
tabs: {
|
||||||
light: 'Light',
|
appearance: 'Appearance',
|
||||||
dark: 'Dark',
|
layout: 'Layout',
|
||||||
auto: 'Follow System'
|
general: 'General',
|
||||||
|
preset: 'Preset'
|
||||||
},
|
},
|
||||||
grayscale: 'Grayscale',
|
appearance: {
|
||||||
colourWeakness: 'Colour Weakness',
|
themeSchema: {
|
||||||
layoutMode: {
|
title: 'Theme Schema',
|
||||||
title: 'Layout Mode',
|
light: 'Light',
|
||||||
vertical: 'Vertical Menu Mode',
|
dark: 'Dark',
|
||||||
horizontal: 'Horizontal Menu Mode',
|
auto: 'Follow System'
|
||||||
'vertical-mix': 'Vertical Mix Menu Mode',
|
},
|
||||||
'horizontal-mix': 'Horizontal Mix menu Mode',
|
grayscale: 'Grayscale',
|
||||||
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
|
colourWeakness: 'Colour Weakness',
|
||||||
},
|
themeColor: {
|
||||||
recommendColor: 'Apply Recommended Color Algorithm',
|
title: 'Theme Color',
|
||||||
recommendColorDesc: 'The recommended color algorithm refers to',
|
primary: 'Primary',
|
||||||
themeColor: {
|
info: 'Info',
|
||||||
title: 'Theme Color',
|
success: 'Success',
|
||||||
primary: 'Primary',
|
warning: 'Warning',
|
||||||
info: 'Info',
|
error: 'Error',
|
||||||
success: 'Success',
|
followPrimary: 'Follow Primary'
|
||||||
warning: 'Warning',
|
},
|
||||||
error: 'Error',
|
recommendColor: 'Apply Recommended Color Algorithm',
|
||||||
followPrimary: 'Follow Primary'
|
recommendColorDesc: 'The recommended color algorithm refers to',
|
||||||
},
|
preset: {
|
||||||
scrollMode: {
|
title: 'Theme Presets',
|
||||||
title: 'Scroll Mode',
|
apply: 'Apply',
|
||||||
wrapper: 'Wrapper',
|
applySuccess: 'Preset applied successfully',
|
||||||
content: 'Content'
|
default: {
|
||||||
},
|
name: 'Default Preset',
|
||||||
page: {
|
desc: 'Default theme preset with balanced settings'
|
||||||
animate: 'Page Animate',
|
},
|
||||||
mode: {
|
dark: {
|
||||||
title: 'Page Animate Mode',
|
name: 'Dark Preset',
|
||||||
fade: 'Fade',
|
desc: 'Dark theme preset for night time usage'
|
||||||
'fade-slide': 'Slide',
|
},
|
||||||
'fade-bottom': 'Fade Zoom',
|
compact: {
|
||||||
'fade-scale': 'Fade Scale',
|
name: 'Compact Preset',
|
||||||
'zoom-fade': 'Zoom Fade',
|
desc: 'Compact layout preset for small screens'
|
||||||
'zoom-out': 'Zoom Out',
|
},
|
||||||
none: 'None'
|
azir: {
|
||||||
|
name: "Azir's Preset",
|
||||||
|
desc: 'It is a cold and elegant preset that Azir likes'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fixedHeaderAndTab: 'Fixed Header And Tab',
|
layout: {
|
||||||
header: {
|
layoutMode: {
|
||||||
height: 'Header Height',
|
title: 'Layout Mode',
|
||||||
breadcrumb: {
|
vertical: 'Vertical Mode',
|
||||||
visible: 'Breadcrumb Visible',
|
horizontal: 'Horizontal Mode',
|
||||||
showIcon: 'Breadcrumb Icon Visible'
|
'vertical-mix': 'Vertical Mix Mode',
|
||||||
|
'vertical-hybrid-header-first': 'Left Hybrid Header-First',
|
||||||
|
'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-First',
|
||||||
|
'top-hybrid-header-first': 'Top-Hybrid Header-First',
|
||||||
|
vertical_detail: 'Vertical menu layout, with the menu on the left and content on the right.',
|
||||||
|
'vertical-mix_detail':
|
||||||
|
'Vertical mix-menu layout, with the primary menu on the dark left side and the secondary menu on the lighter left side.',
|
||||||
|
'vertical-hybrid-header-first_detail':
|
||||||
|
'Left hybrid layout, with the primary menu at the top, the secondary menu on the dark left side, and the tertiary menu on the lighter left side.',
|
||||||
|
horizontal_detail: 'Horizontal menu layout, with the menu at the top and content below.',
|
||||||
|
'top-hybrid-sidebar-first_detail':
|
||||||
|
'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.',
|
||||||
|
'top-hybrid-header-first_detail':
|
||||||
|
'Top hybrid layout, with the primary menu at the top and the secondary menu on the left.'
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
title: 'Tab Settings',
|
||||||
|
visible: 'Tab Visible',
|
||||||
|
cache: 'Tag Bar Info Cache',
|
||||||
|
cacheTip: 'One-click to open/close global keepalive',
|
||||||
|
height: 'Tab Height',
|
||||||
|
mode: {
|
||||||
|
title: 'Tab Mode',
|
||||||
|
chrome: 'Chrome',
|
||||||
|
button: 'Button'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: 'Header Settings',
|
||||||
|
height: 'Header Height',
|
||||||
|
breadcrumb: {
|
||||||
|
visible: 'Breadcrumb Visible',
|
||||||
|
showIcon: 'Breadcrumb Icon Visible'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sider: {
|
||||||
|
title: 'Sider Settings',
|
||||||
|
inverted: 'Dark Sider',
|
||||||
|
width: 'Sider Width',
|
||||||
|
collapsedWidth: 'Sider Collapsed Width',
|
||||||
|
mixWidth: 'Mix Sider Width',
|
||||||
|
mixCollapsedWidth: 'Mix Sider Collapse Width',
|
||||||
|
mixChildMenuWidth: 'Mix Child Menu Width'
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
title: 'Footer Settings',
|
||||||
|
visible: 'Footer Visible',
|
||||||
|
fixed: 'Fixed Footer',
|
||||||
|
height: 'Footer Height',
|
||||||
|
right: 'Right Footer'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
title: 'Content Area Settings',
|
||||||
|
scrollMode: {
|
||||||
|
title: 'Scroll Mode',
|
||||||
|
tip: 'The theme scroll only scrolls the main part, the outer scroll can carry the header and footer together',
|
||||||
|
wrapper: 'Wrapper',
|
||||||
|
content: 'Content'
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
animate: 'Page Animate',
|
||||||
|
mode: {
|
||||||
|
title: 'Page Animate Mode',
|
||||||
|
fade: 'Fade',
|
||||||
|
'fade-slide': 'Slide',
|
||||||
|
'fade-bottom': 'Fade Zoom',
|
||||||
|
'fade-scale': 'Fade Scale',
|
||||||
|
'zoom-fade': 'Zoom Fade',
|
||||||
|
'zoom-out': 'Zoom Out',
|
||||||
|
none: 'None'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fixedHeaderAndTab: 'Fixed Header And Tab'
|
||||||
|
},
|
||||||
|
resetCacheStrategy: {
|
||||||
|
title: 'Reset Cache Strategy',
|
||||||
|
close: 'Close Page',
|
||||||
|
refresh: 'Refresh Page'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: 'General Settings',
|
||||||
|
watermark: {
|
||||||
|
title: 'Watermark Settings',
|
||||||
|
visible: 'Watermark Full Screen Visible',
|
||||||
|
text: 'Custom Watermark Text',
|
||||||
|
enableUserName: 'Enable User Name Watermark',
|
||||||
|
enableTime: 'Show Current Time',
|
||||||
|
timeFormat: 'Time Format'
|
||||||
},
|
},
|
||||||
multilingual: {
|
multilingual: {
|
||||||
|
title: 'Multilingual Settings',
|
||||||
visible: 'Display multilingual button'
|
visible: 'Display multilingual button'
|
||||||
},
|
},
|
||||||
globalSearch: {
|
globalSearch: {
|
||||||
|
title: 'Global Search Settings',
|
||||||
visible: 'Display GlobalSearch button'
|
visible: 'Display GlobalSearch button'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tab: {
|
|
||||||
visible: 'Tab Visible',
|
|
||||||
cache: 'Tag Bar Info Cache',
|
|
||||||
height: 'Tab Height',
|
|
||||||
mode: {
|
|
||||||
title: 'Tab Mode',
|
|
||||||
chrome: 'Chrome',
|
|
||||||
button: 'Button'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sider: {
|
|
||||||
inverted: 'Dark Sider',
|
|
||||||
width: 'Sider Width',
|
|
||||||
collapsedWidth: 'Sider Collapsed Width',
|
|
||||||
mixWidth: 'Mix Sider Width',
|
|
||||||
mixCollapsedWidth: 'Mix Sider Collapse Width',
|
|
||||||
mixChildMenuWidth: 'Mix Child Menu Width'
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
visible: 'Footer Visible',
|
|
||||||
fixed: 'Fixed Footer',
|
|
||||||
height: 'Footer Height',
|
|
||||||
right: 'Right Footer'
|
|
||||||
},
|
|
||||||
watermark: {
|
|
||||||
visible: 'Watermark Full Screen Visible',
|
|
||||||
text: 'Watermark Text'
|
|
||||||
},
|
|
||||||
themeDrawerTitle: 'Theme Configuration',
|
|
||||||
pageFunTitle: 'Page Function',
|
|
||||||
resetCacheStrategy: {
|
|
||||||
title: 'Reset Cache Strategy',
|
|
||||||
close: 'Close Page',
|
|
||||||
refresh: 'Refresh Page'
|
|
||||||
},
|
|
||||||
configOperation: {
|
configOperation: {
|
||||||
copyConfig: 'Copy Config',
|
copyConfig: 'Copy Config',
|
||||||
copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"',
|
copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"',
|
||||||
|
@@ -58,100 +58,156 @@ const local: App.I18n.Schema = {
|
|||||||
tokenExpired: 'token已过期'
|
tokenExpired: 'token已过期'
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
themeSchema: {
|
themeDrawerTitle: '主题配置',
|
||||||
title: '主题模式',
|
tabs: {
|
||||||
light: '亮色模式',
|
appearance: '外观',
|
||||||
dark: '暗黑模式',
|
layout: '布局',
|
||||||
auto: '跟随系统'
|
general: '通用',
|
||||||
|
preset: '预设'
|
||||||
},
|
},
|
||||||
grayscale: '灰色模式',
|
appearance: {
|
||||||
colourWeakness: '色弱模式',
|
themeSchema: {
|
||||||
layoutMode: {
|
title: '主题模式',
|
||||||
title: '布局模式',
|
light: '亮色模式',
|
||||||
vertical: '左侧菜单模式',
|
dark: '暗黑模式',
|
||||||
'vertical-mix': '左侧菜单混合模式',
|
auto: '跟随系统'
|
||||||
horizontal: '顶部菜单模式',
|
},
|
||||||
'horizontal-mix': '顶部菜单混合模式',
|
grayscale: '灰色模式',
|
||||||
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
|
colourWeakness: '色弱模式',
|
||||||
},
|
themeColor: {
|
||||||
recommendColor: '应用推荐算法的颜色',
|
title: '主题颜色',
|
||||||
recommendColorDesc: '推荐颜色的算法参照',
|
primary: '主色',
|
||||||
themeColor: {
|
info: '信息色',
|
||||||
title: '主题颜色',
|
success: '成功色',
|
||||||
primary: '主色',
|
warning: '警告色',
|
||||||
info: '信息色',
|
error: '错误色',
|
||||||
success: '成功色',
|
followPrimary: '跟随主色'
|
||||||
warning: '警告色',
|
},
|
||||||
error: '错误色',
|
recommendColor: '应用推荐算法的颜色',
|
||||||
followPrimary: '跟随主色'
|
recommendColorDesc: '推荐颜色的算法参照',
|
||||||
},
|
preset: {
|
||||||
scrollMode: {
|
title: '主题预设',
|
||||||
title: '滚动模式',
|
apply: '应用',
|
||||||
wrapper: '外层滚动',
|
applySuccess: '预设应用成功',
|
||||||
content: '主体滚动'
|
default: {
|
||||||
},
|
name: '默认预设',
|
||||||
page: {
|
desc: 'Soybean 默认主题预设'
|
||||||
animate: '页面切换动画',
|
},
|
||||||
mode: {
|
dark: {
|
||||||
title: '页面切换动画类型',
|
name: '暗色预设',
|
||||||
'fade-slide': '滑动',
|
desc: '适用于夜间使用的暗色主题预设'
|
||||||
fade: '淡入淡出',
|
},
|
||||||
'fade-bottom': '底部消退',
|
compact: {
|
||||||
'fade-scale': '缩放消退',
|
name: '紧凑型',
|
||||||
'zoom-fade': '渐变',
|
desc: '适用于小屏幕的紧凑布局预设'
|
||||||
'zoom-out': '闪现',
|
},
|
||||||
none: '无'
|
azir: {
|
||||||
|
name: 'Azir的预设',
|
||||||
|
desc: '是 Azir 比较喜欢的莫兰迪色系冷淡风'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fixedHeaderAndTab: '固定头部和标签栏',
|
layout: {
|
||||||
header: {
|
layoutMode: {
|
||||||
height: '头部高度',
|
title: '布局模式',
|
||||||
breadcrumb: {
|
vertical: '左侧菜单模式',
|
||||||
visible: '显示面包屑',
|
'vertical-mix': '左侧菜单混合模式',
|
||||||
showIcon: '显示面包屑图标'
|
'vertical-hybrid-header-first': '左侧混合-顶部优先',
|
||||||
|
horizontal: '顶部菜单模式',
|
||||||
|
'top-hybrid-sidebar-first': '顶部混合-侧边优先',
|
||||||
|
'top-hybrid-header-first': '顶部混合-顶部优先',
|
||||||
|
vertical_detail: '左侧菜单布局,菜单在左,内容在右。',
|
||||||
|
'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在左侧浅色区域。',
|
||||||
|
'vertical-hybrid-header-first_detail':
|
||||||
|
'左侧混合布局,一级菜单在顶部,二级菜单在左侧深色区域,三级菜单在左侧浅色区域。',
|
||||||
|
horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。',
|
||||||
|
'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。',
|
||||||
|
'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。'
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
title: '标签栏设置',
|
||||||
|
visible: '显示标签栏',
|
||||||
|
cache: '标签栏信息缓存',
|
||||||
|
cacheTip: '一键开启/关闭全局 keepalive',
|
||||||
|
height: '标签栏高度',
|
||||||
|
mode: {
|
||||||
|
title: '标签栏风格',
|
||||||
|
chrome: '谷歌风格',
|
||||||
|
button: '按钮风格'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: '头部设置',
|
||||||
|
height: '头部高度',
|
||||||
|
breadcrumb: {
|
||||||
|
visible: '显示面包屑',
|
||||||
|
showIcon: '显示面包屑图标'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sider: {
|
||||||
|
title: '侧边栏设置',
|
||||||
|
inverted: '深色侧边栏',
|
||||||
|
width: '侧边栏宽度',
|
||||||
|
collapsedWidth: '侧边栏折叠宽度',
|
||||||
|
mixWidth: '混合布局侧边栏宽度',
|
||||||
|
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
|
||||||
|
mixChildMenuWidth: '混合布局子菜单宽度'
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
title: '底部设置',
|
||||||
|
visible: '显示底部',
|
||||||
|
fixed: '固定底部',
|
||||||
|
height: '底部高度',
|
||||||
|
right: '底部局右'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
title: '内容区域设置',
|
||||||
|
scrollMode: {
|
||||||
|
title: '滚动模式',
|
||||||
|
tip: '主题滚动仅 main 部分滚动,外层滚动可携带头部底部一起滚动',
|
||||||
|
wrapper: '外层滚动',
|
||||||
|
content: '主体滚动'
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
animate: '页面切换动画',
|
||||||
|
mode: {
|
||||||
|
title: '页面切换动画类型',
|
||||||
|
'fade-slide': '滑动',
|
||||||
|
fade: '淡入淡出',
|
||||||
|
'fade-bottom': '底部消退',
|
||||||
|
'fade-scale': '缩放消退',
|
||||||
|
'zoom-fade': '渐变',
|
||||||
|
'zoom-out': '闪现',
|
||||||
|
none: '无'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fixedHeaderAndTab: '固定头部和标签栏'
|
||||||
|
},
|
||||||
|
resetCacheStrategy: {
|
||||||
|
title: '重置缓存策略',
|
||||||
|
close: '关闭页面',
|
||||||
|
refresh: '刷新页面'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: '通用设置',
|
||||||
|
watermark: {
|
||||||
|
title: '水印设置',
|
||||||
|
visible: '显示全屏水印',
|
||||||
|
text: '自定义水印文本',
|
||||||
|
enableUserName: '启用用户名水印',
|
||||||
|
enableTime: '显示当前时间',
|
||||||
|
timeFormat: '时间格式'
|
||||||
},
|
},
|
||||||
multilingual: {
|
multilingual: {
|
||||||
|
title: '多语言设置',
|
||||||
visible: '显示多语言按钮'
|
visible: '显示多语言按钮'
|
||||||
},
|
},
|
||||||
globalSearch: {
|
globalSearch: {
|
||||||
|
title: '全局搜索设置',
|
||||||
visible: '显示全局搜索按钮'
|
visible: '显示全局搜索按钮'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tab: {
|
|
||||||
visible: '显示标签栏',
|
|
||||||
cache: '标签栏信息缓存',
|
|
||||||
height: '标签栏高度',
|
|
||||||
mode: {
|
|
||||||
title: '标签栏风格',
|
|
||||||
chrome: '谷歌风格',
|
|
||||||
button: '按钮风格'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sider: {
|
|
||||||
inverted: '深色侧边栏',
|
|
||||||
width: '侧边栏宽度',
|
|
||||||
collapsedWidth: '侧边栏折叠宽度',
|
|
||||||
mixWidth: '混合布局侧边栏宽度',
|
|
||||||
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
|
|
||||||
mixChildMenuWidth: '混合布局子菜单宽度'
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
visible: '显示底部',
|
|
||||||
fixed: '固定底部',
|
|
||||||
height: '底部高度',
|
|
||||||
right: '底部局右'
|
|
||||||
},
|
|
||||||
watermark: {
|
|
||||||
visible: '显示全屏水印',
|
|
||||||
text: '水印文本'
|
|
||||||
},
|
|
||||||
themeDrawerTitle: '主题配置',
|
|
||||||
pageFunTitle: '页面功能',
|
|
||||||
resetCacheStrategy: {
|
|
||||||
title: '重置缓存策略',
|
|
||||||
close: '关闭页面',
|
|
||||||
refresh: '刷新页面'
|
|
||||||
},
|
|
||||||
configOperation: {
|
configOperation: {
|
||||||
copyConfig: '复制配置',
|
copyConfig: '复制配置',
|
||||||
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
|
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
|
||||||
|
@@ -95,7 +95,6 @@ async function getHtmlBuildTime(): Promise<string | null> {
|
|||||||
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
|
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error('getHtmlBuildTime error:', res.status, res.statusText);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ async function getHtmlBuildTime(): Promise<string | null> {
|
|||||||
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
||||||
return match?.[1] || null;
|
return match?.[1] || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getHtmlBuildTime error:', error);
|
window.console.error('getHtmlBuildTime error:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
src/router/_generated/imports.ts
Normal file
31
src/router/_generated/imports.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
/* oxlint-disable */
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// Generated by elegant-router
|
||||||
|
// Read more: https://github.com/soybeanjs/elegant-router
|
||||||
|
|
||||||
|
import type { RouteFileKey, RouteLayoutKey, RawRouteComponent } from "@elegant-router/types";
|
||||||
|
|
||||||
|
export const layouts: Record<RouteLayoutKey, RawRouteComponent> = {
|
||||||
|
base: () => import("@/layouts/base-layout/index.vue"),
|
||||||
|
blank: () => import("@/layouts/blank-layout/index.vue"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const views: Record<RouteFileKey, RawRouteComponent> = {
|
||||||
|
403: () => import("@/views/(builtin)/403/index.vue"),
|
||||||
|
404: () => import("@/views/(builtin)/404/index.vue"),
|
||||||
|
500: () => import("@/views/(builtin)/500/index.vue"),
|
||||||
|
Home: () => import("@/views/home/index.vue"),
|
||||||
|
IframeUrl: () => import("@/views/(builtin)/iframe/[url].vue"),
|
||||||
|
Login: () => import("@/views/(builtin)/login/index.vue"),
|
||||||
|
ManageApi: () => import("@/views/manage/api/index.vue"),
|
||||||
|
ManageDictionary: () => import("@/views/manage/dictionary/index.vue"),
|
||||||
|
ManageMenu: () => import("@/views/manage/menu/index.vue"),
|
||||||
|
ManageOrganization: () => import("@/views/manage/organization/index.vue"),
|
||||||
|
ManagePermission: () => import("@/views/manage/permission/index.vue"),
|
||||||
|
ManageRole: () => import("@/views/manage/role/index.vue"),
|
||||||
|
ManageRoute: () => import("@/views/manage/route/index.vue"),
|
||||||
|
ManageUser: () => import("@/views/manage/user/index.vue"),
|
||||||
|
Wip: () => import("@/views/(builtin)/wip/index.vue"),
|
||||||
|
};
|
166
src/router/_generated/routes.ts
Normal file
166
src/router/_generated/routes.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// Generated by elegant-router
|
||||||
|
// Read more: https://github.com/soybeanjs/elegant-router
|
||||||
|
|
||||||
|
import type { AutoRouterRoute } from '@elegant-router/types';
|
||||||
|
|
||||||
|
export const routes: AutoRouterRoute[] = [
|
||||||
|
{
|
||||||
|
name: 'Root',
|
||||||
|
path: '/',
|
||||||
|
redirect: '/home',
|
||||||
|
meta: {
|
||||||
|
title: "Root"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NotFound',
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
layout: 'base',
|
||||||
|
component: '404',
|
||||||
|
meta: {
|
||||||
|
title: "NotFound"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '403',
|
||||||
|
path: '/403',
|
||||||
|
layout: 'base',
|
||||||
|
component: '403',
|
||||||
|
meta: {
|
||||||
|
title: "403",
|
||||||
|
constant: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '404',
|
||||||
|
path: '/404',
|
||||||
|
layout: 'base',
|
||||||
|
component: '404',
|
||||||
|
meta: {
|
||||||
|
title: "404",
|
||||||
|
constant: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '500',
|
||||||
|
path: '/500',
|
||||||
|
layout: 'base',
|
||||||
|
component: '500',
|
||||||
|
meta: {
|
||||||
|
title: "500",
|
||||||
|
constant: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
path: '/home',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'Home',
|
||||||
|
meta: {
|
||||||
|
title: "Home"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IframeUrl',
|
||||||
|
path: '/iframe/:url',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'IframeUrl',
|
||||||
|
meta: {
|
||||||
|
title: "IframeUrl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Login',
|
||||||
|
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'Login',
|
||||||
|
meta: {
|
||||||
|
title: "Login",
|
||||||
|
constant: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManageApi',
|
||||||
|
path: '/manage/api',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManageApi',
|
||||||
|
meta: {
|
||||||
|
title: "ManageApi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManageDictionary',
|
||||||
|
path: '/manage/dictionary',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManageDictionary',
|
||||||
|
meta: {
|
||||||
|
title: "ManageDictionary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManageMenu',
|
||||||
|
path: '/manage/menu',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManageMenu',
|
||||||
|
meta: {
|
||||||
|
title: "ManageMenu"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManageOrganization',
|
||||||
|
path: '/manage/organization',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManageOrganization',
|
||||||
|
meta: {
|
||||||
|
title: "ManageOrganization"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManagePermission',
|
||||||
|
path: '/manage/permission',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManagePermission',
|
||||||
|
meta: {
|
||||||
|
title: "ManagePermission"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManageRole',
|
||||||
|
path: '/manage/role',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManageRole',
|
||||||
|
meta: {
|
||||||
|
title: "ManageRole"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManageRoute',
|
||||||
|
path: '/manage/route',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManageRoute',
|
||||||
|
meta: {
|
||||||
|
title: "ManageRoute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ManageUser',
|
||||||
|
path: '/manage/user',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'ManageUser',
|
||||||
|
meta: {
|
||||||
|
title: "ManageUser"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Wip',
|
||||||
|
path: '/wip',
|
||||||
|
layout: 'base',
|
||||||
|
component: 'Wip',
|
||||||
|
meta: {
|
||||||
|
title: "Wip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
32
src/router/_generated/shared.ts
Normal file
32
src/router/_generated/shared.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
/* oxlint-disable */
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// Generated by elegant-router
|
||||||
|
// Read more: https://github.com/soybeanjs/elegant-router
|
||||||
|
|
||||||
|
import type { RouteKey, RoutePathMap } from '@elegant-router/types';
|
||||||
|
|
||||||
|
const routePathMap: RoutePathMap = {
|
||||||
|
"Root": "/",
|
||||||
|
"NotFound": "/:pathMatch(.*)*",
|
||||||
|
"403": "/403",
|
||||||
|
"404": "/404",
|
||||||
|
"500": "/500",
|
||||||
|
"Home": "/home",
|
||||||
|
"IframeUrl": "/iframe/:url",
|
||||||
|
"Login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
|
||||||
|
"ManageApi": "/manage/api",
|
||||||
|
"ManageDictionary": "/manage/dictionary",
|
||||||
|
"ManageMenu": "/manage/menu",
|
||||||
|
"ManageOrganization": "/manage/organization",
|
||||||
|
"ManagePermission": "/manage/permission",
|
||||||
|
"ManageRole": "/manage/role",
|
||||||
|
"ManageRoute": "/manage/route",
|
||||||
|
"ManageUser": "/manage/user",
|
||||||
|
"Wip": "/wip",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getRoutePath(key: RouteKey) {
|
||||||
|
return routePathMap[key];
|
||||||
|
}
|
70
src/router/_generated/transformer.ts
Normal file
70
src/router/_generated/transformer.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
/* oxlint-disable */
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// Generated by elegant-router
|
||||||
|
// Read more: https://github.com/soybeanjs/elegant-router
|
||||||
|
|
||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
import type {
|
||||||
|
AutoRouterRedirect,
|
||||||
|
AutoRouterRoute,
|
||||||
|
AutoRouterSingleView,
|
||||||
|
RawRouteComponent,
|
||||||
|
RouteFileKey,
|
||||||
|
RouteLayoutKey
|
||||||
|
} from '@elegant-router/types';
|
||||||
|
|
||||||
|
export function transformToVueRoutes(
|
||||||
|
routes: AutoRouterRoute[],
|
||||||
|
layouts: Record<RouteLayoutKey, RawRouteComponent>,
|
||||||
|
views: Record<RouteFileKey, RawRouteComponent>
|
||||||
|
) {
|
||||||
|
const { redirects, groupedRoutes } = getFormattedRoutes(routes);
|
||||||
|
|
||||||
|
const vueRoutes: RouteRecordRaw[] = [...redirects];
|
||||||
|
|
||||||
|
groupedRoutes.forEach((items, layout) => {
|
||||||
|
const layoutRoute: RouteRecordRaw = {
|
||||||
|
path: `/${layout}-layout`,
|
||||||
|
component: layouts[layout],
|
||||||
|
children: items.map(item => {
|
||||||
|
const { layout: _, component, ...rest } = item;
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: views[component],
|
||||||
|
...rest
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
vueRoutes.push(layoutRoute);
|
||||||
|
});
|
||||||
|
|
||||||
|
return vueRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedRoutes(routes: AutoRouterRoute[]) {
|
||||||
|
const groupedRoutes = new Map<RouteLayoutKey, AutoRouterSingleView[]>();
|
||||||
|
const redirects: AutoRouterRedirect[] = [];
|
||||||
|
|
||||||
|
routes.forEach(route => {
|
||||||
|
if (isAutoRouterRedirect(route)) {
|
||||||
|
redirects.push(route);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = groupedRoutes.get(route.layout) || [];
|
||||||
|
items.push(route);
|
||||||
|
groupedRoutes.set(route.layout, items);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirects,
|
||||||
|
groupedRoutes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAutoRouterRedirect(route: AutoRouterRoute): route is AutoRouterRedirect {
|
||||||
|
return 'redirect' in route;
|
||||||
|
}
|
@@ -1,24 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// Generated by elegant-router
|
|
||||||
// Read more: https://github.com/soybeanjs/elegant-router
|
|
||||||
|
|
||||||
import type { RouteComponent } from "vue-router";
|
|
||||||
import type { LastLevelRouteKey, RouteLayout } from "@elegant-router/types";
|
|
||||||
|
|
||||||
import BaseLayout from "@/layouts/base-layout/index.vue";
|
|
||||||
import BlankLayout from "@/layouts/blank-layout/index.vue";
|
|
||||||
|
|
||||||
export const layouts: Record<RouteLayout, RouteComponent | (() => Promise<RouteComponent>)> = {
|
|
||||||
base: BaseLayout,
|
|
||||||
blank: BlankLayout,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<RouteComponent>)> = {
|
|
||||||
403: () => import("@/views/_builtin/403/index.vue"),
|
|
||||||
404: () => import("@/views/_builtin/404/index.vue"),
|
|
||||||
500: () => import("@/views/_builtin/500/index.vue"),
|
|
||||||
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
|
||||||
login: () => import("@/views/_builtin/login/index.vue"),
|
|
||||||
home: () => import("@/views/home/index.vue"),
|
|
||||||
};
|
|
@@ -1,78 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// Generated by elegant-router
|
|
||||||
// Read more: https://github.com/soybeanjs/elegant-router
|
|
||||||
|
|
||||||
import type { GeneratedRoute } from '@elegant-router/types';
|
|
||||||
|
|
||||||
export const generatedRoutes: GeneratedRoute[] = [
|
|
||||||
{
|
|
||||||
name: '403',
|
|
||||||
path: '/403',
|
|
||||||
component: 'layout.blank$view.403',
|
|
||||||
meta: {
|
|
||||||
title: '403',
|
|
||||||
i18nKey: 'route.403',
|
|
||||||
constant: true,
|
|
||||||
hideInMenu: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '404',
|
|
||||||
path: '/404',
|
|
||||||
component: 'layout.blank$view.404',
|
|
||||||
meta: {
|
|
||||||
title: '404',
|
|
||||||
i18nKey: 'route.404',
|
|
||||||
constant: true,
|
|
||||||
hideInMenu: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '500',
|
|
||||||
path: '/500',
|
|
||||||
component: 'layout.blank$view.500',
|
|
||||||
meta: {
|
|
||||||
title: '500',
|
|
||||||
i18nKey: 'route.500',
|
|
||||||
constant: true,
|
|
||||||
hideInMenu: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'home',
|
|
||||||
path: '/home',
|
|
||||||
component: 'layout.base$view.home',
|
|
||||||
meta: {
|
|
||||||
title: 'home',
|
|
||||||
i18nKey: 'route.home',
|
|
||||||
icon: 'mdi:monitor-dashboard',
|
|
||||||
order: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'iframe-page',
|
|
||||||
path: '/iframe-page/:url',
|
|
||||||
component: 'layout.base$view.iframe-page',
|
|
||||||
props: true,
|
|
||||||
meta: {
|
|
||||||
title: 'iframe-page',
|
|
||||||
i18nKey: 'route.iframe-page',
|
|
||||||
constant: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'login',
|
|
||||||
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
|
|
||||||
component: 'layout.blank$view.login',
|
|
||||||
props: true,
|
|
||||||
meta: {
|
|
||||||
title: 'login',
|
|
||||||
i18nKey: 'route.login',
|
|
||||||
constant: true,
|
|
||||||
hideInMenu: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
@@ -1,192 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// Generated by elegant-router
|
|
||||||
// Read more: https://github.com/soybeanjs/elegant-router
|
|
||||||
|
|
||||||
import type { RouteRecordRaw, RouteComponent } from 'vue-router';
|
|
||||||
import type { ElegantConstRoute } from '@elegant-router/vue';
|
|
||||||
import type { RouteMap, RouteKey, RoutePath } from '@elegant-router/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform elegant const routes to vue routes
|
|
||||||
* @param routes elegant const routes
|
|
||||||
* @param layouts layout components
|
|
||||||
* @param views view components
|
|
||||||
*/
|
|
||||||
export function transformElegantRoutesToVueRoutes(
|
|
||||||
routes: ElegantConstRoute[],
|
|
||||||
layouts: Record<string, RouteComponent | (() => Promise<RouteComponent>)>,
|
|
||||||
views: Record<string, RouteComponent | (() => Promise<RouteComponent>)>
|
|
||||||
) {
|
|
||||||
return routes.flatMap(route => transformElegantRouteToVueRoute(route, layouts, views));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform elegant route to vue route
|
|
||||||
* @param route elegant const route
|
|
||||||
* @param layouts layout components
|
|
||||||
* @param views view components
|
|
||||||
*/
|
|
||||||
function transformElegantRouteToVueRoute(
|
|
||||||
route: ElegantConstRoute,
|
|
||||||
layouts: Record<string, RouteComponent | (() => Promise<RouteComponent>)>,
|
|
||||||
views: Record<string, RouteComponent | (() => Promise<RouteComponent>)>
|
|
||||||
) {
|
|
||||||
const LAYOUT_PREFIX = 'layout.';
|
|
||||||
const VIEW_PREFIX = 'view.';
|
|
||||||
const ROUTE_DEGREE_SPLITTER = '_';
|
|
||||||
const FIRST_LEVEL_ROUTE_COMPONENT_SPLIT = '$';
|
|
||||||
|
|
||||||
function isLayout(component: string) {
|
|
||||||
return component.startsWith(LAYOUT_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayoutName(component: string) {
|
|
||||||
const layout = component.replace(LAYOUT_PREFIX, '');
|
|
||||||
|
|
||||||
if(!layouts[layout]) {
|
|
||||||
throw new Error(`Layout component "${layout}" not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isView(component: string) {
|
|
||||||
return component.startsWith(VIEW_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getViewName(component: string) {
|
|
||||||
const view = component.replace(VIEW_PREFIX, '');
|
|
||||||
|
|
||||||
if(!views[view]) {
|
|
||||||
throw new Error(`View component "${view}" not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFirstLevelRoute(item: ElegantConstRoute) {
|
|
||||||
return !item.name.includes(ROUTE_DEGREE_SPLITTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSingleLevelRoute(item: ElegantConstRoute) {
|
|
||||||
return isFirstLevelRoute(item) && !item.children?.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSingleLevelRouteComponent(component: string) {
|
|
||||||
const [layout, view] = component.split(FIRST_LEVEL_ROUTE_COMPONENT_SPLIT);
|
|
||||||
|
|
||||||
return {
|
|
||||||
layout: getLayoutName(layout),
|
|
||||||
view: getViewName(view)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const vueRoutes: RouteRecordRaw[] = [];
|
|
||||||
|
|
||||||
// add props: true to route
|
|
||||||
if (route.path.includes(':') && !route.props) {
|
|
||||||
route.props = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, path, component, children, ...rest } = route;
|
|
||||||
|
|
||||||
const vueRoute = { name, path, ...rest } as RouteRecordRaw;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (component) {
|
|
||||||
if (isSingleLevelRoute(route)) {
|
|
||||||
const { layout, view } = getSingleLevelRouteComponent(component);
|
|
||||||
|
|
||||||
const singleLevelRoute: RouteRecordRaw = {
|
|
||||||
path,
|
|
||||||
component: layouts[layout],
|
|
||||||
meta: {
|
|
||||||
title: route.meta?.title || ''
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
path: '',
|
|
||||||
component: views[view],
|
|
||||||
...rest
|
|
||||||
} as RouteRecordRaw
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return [singleLevelRoute];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLayout(component)) {
|
|
||||||
const layoutName = getLayoutName(component);
|
|
||||||
|
|
||||||
vueRoute.component = layouts[layoutName];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isView(component)) {
|
|
||||||
const viewName = getViewName(component);
|
|
||||||
|
|
||||||
vueRoute.component = views[viewName];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error transforming route "${route.name}": ${error.toString()}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// add redirect to child
|
|
||||||
if (children?.length && !vueRoute.redirect) {
|
|
||||||
vueRoute.redirect = {
|
|
||||||
name: children[0].name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (children?.length) {
|
|
||||||
const childRoutes = children.flatMap(child => transformElegantRouteToVueRoute(child, layouts, views));
|
|
||||||
|
|
||||||
if(isFirstLevelRoute(route)) {
|
|
||||||
vueRoute.children = childRoutes;
|
|
||||||
} else {
|
|
||||||
vueRoutes.push(...childRoutes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vueRoutes.unshift(vueRoute);
|
|
||||||
|
|
||||||
return vueRoutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* map of route name and route path
|
|
||||||
*/
|
|
||||||
const routeMap: RouteMap = {
|
|
||||||
"root": "/",
|
|
||||||
"not-found": "/:pathMatch(.*)*",
|
|
||||||
"403": "/403",
|
|
||||||
"404": "/404",
|
|
||||||
"500": "/500",
|
|
||||||
"home": "/home",
|
|
||||||
"iframe-page": "/iframe-page/:url",
|
|
||||||
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get route path by route name
|
|
||||||
* @param name route name
|
|
||||||
*/
|
|
||||||
export function getRoutePath<T extends RouteKey>(name: T) {
|
|
||||||
return routeMap[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get route name by route path
|
|
||||||
* @param path route path
|
|
||||||
*/
|
|
||||||
export function getRouteName(path: RoutePath) {
|
|
||||||
const routeEntries = Object.entries(routeMap) as [RouteKey, RoutePath][];
|
|
||||||
|
|
||||||
const routeName: RouteKey | null = routeEntries.find(([, routePath]) => routePath === path)?.[0] || null;
|
|
||||||
|
|
||||||
return routeName;
|
|
||||||
}
|
|
@@ -1,12 +1,6 @@
|
|||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
import {
|
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
|
||||||
type RouterHistory,
|
import type { RouterHistory } from 'vue-router';
|
||||||
createMemoryHistory,
|
|
||||||
createRouter,
|
|
||||||
createWebHashHistory,
|
|
||||||
createWebHistory
|
|
||||||
} from 'vue-router';
|
|
||||||
import { createBuiltinVueRoutes } from './routes/builtin';
|
|
||||||
import { createRouterGuard } from './guard';
|
import { createRouterGuard } from './guard';
|
||||||
|
|
||||||
const { VITE_ROUTER_HISTORY_MODE = 'history', VITE_BASE_URL } = import.meta.env;
|
const { VITE_ROUTER_HISTORY_MODE = 'history', VITE_BASE_URL } = import.meta.env;
|
||||||
@@ -19,7 +13,7 @@ const historyCreatorMap: Record<Env.RouterHistoryMode, (base?: string) => Router
|
|||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL),
|
history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL),
|
||||||
routes: createBuiltinVueRoutes()
|
routes: []
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Setup Vue Router */
|
/** Setup Vue Router */
|
||||||
|
@@ -1,31 +0,0 @@
|
|||||||
import type { CustomRoute } from '@elegant-router/types';
|
|
||||||
import { layouts, views } from '../elegant/imports';
|
|
||||||
import { getRoutePath, transformElegantRoutesToVueRoutes } from '../elegant/transform';
|
|
||||||
|
|
||||||
export const ROOT_ROUTE: CustomRoute = {
|
|
||||||
name: 'root',
|
|
||||||
path: '/',
|
|
||||||
redirect: getRoutePath(import.meta.env.VITE_ROUTE_HOME) || '/home',
|
|
||||||
meta: {
|
|
||||||
title: 'root',
|
|
||||||
constant: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const NOT_FOUND_ROUTE: CustomRoute = {
|
|
||||||
name: 'not-found',
|
|
||||||
path: '/:pathMatch(.*)*',
|
|
||||||
component: 'layout.blank$view.404',
|
|
||||||
meta: {
|
|
||||||
title: 'not-found',
|
|
||||||
constant: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** builtin routes, it must be constant and setup in vue-router */
|
|
||||||
const builtinRoutes: CustomRoute[] = [ROOT_ROUTE, NOT_FOUND_ROUTE];
|
|
||||||
|
|
||||||
/** create builtin vue routes */
|
|
||||||
export function createBuiltinVueRoutes() {
|
|
||||||
return transformElegantRoutesToVueRoutes(builtinRoutes, layouts, views);
|
|
||||||
}
|
|
@@ -1,22 +1,14 @@
|
|||||||
import type { CustomRoute, ElegantConstRoute, ElegantRoute } from '@elegant-router/types';
|
import type { AutoRouterRoute } from '@elegant-router/types';
|
||||||
import { generatedRoutes } from '../elegant/routes';
|
import { routes } from '../_generated/routes';
|
||||||
import { layouts, views } from '../elegant/imports';
|
import { layouts, views } from '../_generated/imports';
|
||||||
import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
|
import { transformToVueRoutes } from '../_generated/transformer';
|
||||||
|
|
||||||
/**
|
|
||||||
* custom routes
|
|
||||||
*
|
|
||||||
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
|
|
||||||
*/
|
|
||||||
const customRoutes: CustomRoute[] = [];
|
|
||||||
|
|
||||||
/** create routes when the auth route mode is static */
|
/** create routes when the auth route mode is static */
|
||||||
export function createStaticRoutes() {
|
export function createStaticRoutes() {
|
||||||
const constantRoutes: ElegantRoute[] = [];
|
const constantRoutes: AutoRouterRoute[] = [];
|
||||||
|
const authRoutes: AutoRouterRoute[] = [];
|
||||||
|
|
||||||
const authRoutes: ElegantRoute[] = [];
|
routes.forEach(item => {
|
||||||
|
|
||||||
[...customRoutes, ...generatedRoutes].forEach(item => {
|
|
||||||
if (item.meta?.constant) {
|
if (item.meta?.constant) {
|
||||||
constantRoutes.push(item);
|
constantRoutes.push(item);
|
||||||
} else {
|
} else {
|
||||||
@@ -33,8 +25,8 @@ export function createStaticRoutes() {
|
|||||||
/**
|
/**
|
||||||
* Get auth vue routes
|
* Get auth vue routes
|
||||||
*
|
*
|
||||||
* @param routes Elegant routes
|
* @param authRoutes Elegant routes
|
||||||
*/
|
*/
|
||||||
export function getAuthVueRoutes(routes: ElegantConstRoute[]) {
|
export function getAuthVueRoutes(authRoutes: AutoRouterRoute[]) {
|
||||||
return transformElegantRoutesToVueRoutes(routes, layouts, views);
|
return transformToVueRoutes(authRoutes, layouts, views);
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import type { RequestInstanceState } from './type';
|
|||||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||||
|
|
||||||
export const request = createFlatRequest<App.Service.Response, RequestInstanceState>(
|
export const request = createFlatRequest(
|
||||||
{
|
{
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -18,6 +18,13 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
defaultState: {
|
||||||
|
errMsgStack: [],
|
||||||
|
refreshTokenPromise: null
|
||||||
|
} as RequestInstanceState,
|
||||||
|
transform(response: AxiosResponse<App.Service.Response<any>>) {
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
async onRequest(config) {
|
async onRequest(config) {
|
||||||
const Authorization = getAuthorization();
|
const Authorization = getAuthorization();
|
||||||
Object.assign(config.headers, { Authorization });
|
Object.assign(config.headers, { Authorization });
|
||||||
@@ -91,9 +98,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
transformBackendResponse(response) {
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
onError(error) {
|
onError(error) {
|
||||||
// when the request is fail, you can show error message
|
// when the request is fail, you can show error message
|
||||||
|
|
||||||
@@ -123,11 +127,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const demoRequest = createRequest<App.Service.DemoResponse>(
|
export const demoRequest = createRequest(
|
||||||
{
|
{
|
||||||
baseURL: otherBaseURL.demo
|
baseURL: otherBaseURL.demo
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
transform(response: AxiosResponse<App.Service.DemoResponse>) {
|
||||||
|
return response.data.result;
|
||||||
|
},
|
||||||
async onRequest(config) {
|
async onRequest(config) {
|
||||||
const { headers } = config;
|
const { headers } = config;
|
||||||
|
|
||||||
@@ -147,9 +154,6 @@ export const demoRequest = createRequest<App.Service.DemoResponse>(
|
|||||||
// when the backend response code is not "200", it means the request is fail
|
// when the backend response code is not "200", it means the request is fail
|
||||||
// for example: the token is expired, refresh token and retry request
|
// for example: the token is expired, refresh token and retry request
|
||||||
},
|
},
|
||||||
transformBackendResponse(response) {
|
|
||||||
return response.data.result;
|
|
||||||
},
|
|
||||||
onError(error) {
|
onError(error) {
|
||||||
// when the request is fail, you can show error message
|
// when the request is fail, you can show error message
|
||||||
|
|
||||||
|
@@ -28,14 +28,14 @@ async function handleRefreshToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleExpiredRequest(state: RequestInstanceState) {
|
export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||||
if (!state.refreshTokenFn) {
|
if (!state.refreshTokenPromise) {
|
||||||
state.refreshTokenFn = handleRefreshToken();
|
state.refreshTokenPromise = handleRefreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await state.refreshTokenFn;
|
const success = await state.refreshTokenPromise;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
state.refreshTokenFn = null;
|
state.refreshTokenPromise = null;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
export interface RequestInstanceState {
|
export interface RequestInstanceState {
|
||||||
/** whether the request is refreshing token */
|
/** the promise of refreshing token */
|
||||||
refreshTokenFn: Promise<boolean> | null;
|
refreshTokenPromise: Promise<boolean> | null;
|
||||||
/** the request error message stack */
|
/** the request error message stack */
|
||||||
errMsgStack: string[];
|
errMsgStack: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import { clearAuthStorage, getToken } from './shared';
|
|||||||
|
|
||||||
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const authStore = useAuthStore();
|
||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
const tabStore = useTabStore();
|
const tabStore = useTabStore();
|
||||||
const { toLogin, redirectFromLogin } = useRouterPush(false);
|
const { toLogin, redirectFromLogin } = useRouterPush(false);
|
||||||
@@ -39,8 +40,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
|
|
||||||
/** Reset auth store */
|
/** Reset auth store */
|
||||||
async function resetStore() {
|
async function resetStore() {
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
recordUserId();
|
recordUserId();
|
||||||
|
|
||||||
clearAuthStorage();
|
clearAuthStorage();
|
||||||
|
@@ -318,7 +318,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onRouteSwitchWhenLoggedIn() {
|
async function onRouteSwitchWhenLoggedIn() {
|
||||||
await authStore.initUserInfo();
|
// some global init logic when logged in and switch route
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRouteSwitchWhenNotLoggedIn() {
|
async function onRouteSwitchWhenNotLoggedIn() {
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
|
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { useEventListener, usePreferredColorScheme } from '@vueuse/core';
|
import { useDateFormat, useEventListener, useNow, usePreferredColorScheme } from '@vueuse/core';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { getPaletteColorByNumber } from '@sa/color';
|
import { getPaletteColorByNumber } from '@sa/color';
|
||||||
import { localStg } from '@/utils/storage';
|
import { localStg } from '@/utils/storage';
|
||||||
import { SetupStoreId } from '@/enum';
|
import { SetupStoreId } from '@/enum';
|
||||||
|
import { useAuthStore } from '../auth';
|
||||||
import {
|
import {
|
||||||
addThemeVarsToGlobal,
|
addThemeVarsToGlobal,
|
||||||
createThemeToken,
|
createThemeToken,
|
||||||
@@ -18,10 +19,14 @@ import {
|
|||||||
export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||||
const scope = effectScope();
|
const scope = effectScope();
|
||||||
const osTheme = usePreferredColorScheme();
|
const osTheme = usePreferredColorScheme();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
/** Theme settings */
|
/** Theme settings */
|
||||||
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
|
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
|
||||||
|
|
||||||
|
/** Watermark time instance with controls */
|
||||||
|
const { now: watermarkTime, pause: pauseWatermarkTime, resume: resumeWatermarkTime } = useNow({ controls: true });
|
||||||
|
|
||||||
/** Dark mode */
|
/** Dark mode */
|
||||||
const darkMode = computed(() => {
|
const darkMode = computed(() => {
|
||||||
if (settings.value.themeScheme === 'auto') {
|
if (settings.value.themeScheme === 'auto') {
|
||||||
@@ -57,6 +62,28 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
*/
|
*/
|
||||||
const settingsJson = computed(() => JSON.stringify(settings.value));
|
const settingsJson = computed(() => JSON.stringify(settings.value));
|
||||||
|
|
||||||
|
/** Watermark time date formatter */
|
||||||
|
const formattedWatermarkTime = computed(() => {
|
||||||
|
const { watermark } = settings.value;
|
||||||
|
const date = useDateFormat(watermarkTime, watermark.timeFormat);
|
||||||
|
return date.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Watermark content */
|
||||||
|
const watermarkContent = computed(() => {
|
||||||
|
const { watermark } = settings.value;
|
||||||
|
|
||||||
|
if (watermark.enableUserName && authStore.userInfo.userName) {
|
||||||
|
return authStore.userInfo.userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watermark.enableTime) {
|
||||||
|
return formattedWatermarkTime.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return watermark.text;
|
||||||
|
});
|
||||||
|
|
||||||
/** Reset store */
|
/** Reset store */
|
||||||
function resetStore() {
|
function resetStore() {
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -144,13 +171,43 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
);
|
);
|
||||||
addThemeVarsToGlobal(themeTokens, darkThemeTokens);
|
addThemeVarsToGlobal(themeTokens, darkThemeTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set layout reverse horizontal mix
|
* Set watermark enable user name
|
||||||
*
|
*
|
||||||
* @param reverse Reverse horizontal mix
|
* @param enable Whether to enable user name watermark
|
||||||
*/
|
*/
|
||||||
function setLayoutReverseHorizontalMix(reverse: boolean) {
|
function setWatermarkEnableUserName(enable: boolean) {
|
||||||
settings.value.layout.reverseHorizontalMix = reverse;
|
settings.value.watermark.enableUserName = enable;
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
settings.value.watermark.enableTime = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set watermark enable time
|
||||||
|
*
|
||||||
|
* @param enable Whether to enable time watermark
|
||||||
|
*/
|
||||||
|
function setWatermarkEnableTime(enable: boolean) {
|
||||||
|
settings.value.watermark.enableTime = enable;
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
settings.value.watermark.enableUserName = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only run timer when watermark is visible and time display is enabled */
|
||||||
|
function updateWatermarkTimer() {
|
||||||
|
const { watermark } = settings.value;
|
||||||
|
const shouldRunTimer = watermark.visible && watermark.enableTime;
|
||||||
|
|
||||||
|
if (shouldRunTimer) {
|
||||||
|
resumeWatermarkTime();
|
||||||
|
} else {
|
||||||
|
pauseWatermarkTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cache theme settings */
|
/** Cache theme settings */
|
||||||
@@ -196,6 +253,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// watch watermark settings to control timer
|
||||||
|
watch(
|
||||||
|
() => [settings.value.watermark.visible, settings.value.watermark.enableTime],
|
||||||
|
() => {
|
||||||
|
updateWatermarkTimer();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** On scope dispose */
|
/** On scope dispose */
|
||||||
@@ -209,6 +275,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
themeColors,
|
themeColors,
|
||||||
naiveTheme,
|
naiveTheme,
|
||||||
settingsJson,
|
settingsJson,
|
||||||
|
watermarkContent,
|
||||||
setGrayscale,
|
setGrayscale,
|
||||||
setColourWeakness,
|
setColourWeakness,
|
||||||
resetStore,
|
resetStore,
|
||||||
@@ -216,6 +283,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
toggleThemeScheme,
|
toggleThemeScheme,
|
||||||
updateThemeColors,
|
updateThemeColors,
|
||||||
setThemeLayout,
|
setThemeLayout,
|
||||||
setLayoutReverseHorizontalMix
|
setWatermarkEnableUserName,
|
||||||
|
setWatermarkEnableTime
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -10,4 +10,5 @@ body,
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
color: rgb(var(--base-text-color));
|
||||||
}
|
}
|
||||||
|
90
src/theme/preset/azir.json
Normal file
90
src/theme/preset/azir.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"name": "Azir's Preset",
|
||||||
|
"desc": "It is a cold and elegant preset that Azir likes",
|
||||||
|
"i18nkey": "theme.appearance.preset.azir",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"themeScheme": "light",
|
||||||
|
"grayscale": false,
|
||||||
|
"colourWeakness": false,
|
||||||
|
"recommendColor": true,
|
||||||
|
"themeColor": "#78a878",
|
||||||
|
"otherColor": {
|
||||||
|
"info": "#89b989",
|
||||||
|
"success": "#99c299",
|
||||||
|
"warning": "#d4bb9d",
|
||||||
|
"error": "#c49a9a"
|
||||||
|
},
|
||||||
|
"isInfoFollowPrimary": true,
|
||||||
|
"resetCacheStrategy": "refresh",
|
||||||
|
"layout": {
|
||||||
|
"mode": "vertical-mix",
|
||||||
|
"scrollMode": "wrapper"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"animate": true,
|
||||||
|
"animateMode": "zoom-fade"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"height": 64,
|
||||||
|
"breadcrumb": {
|
||||||
|
"visible": true,
|
||||||
|
"showIcon": true
|
||||||
|
},
|
||||||
|
"multilingual": {
|
||||||
|
"visible": true
|
||||||
|
},
|
||||||
|
"globalSearch": {
|
||||||
|
"visible": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tab": {
|
||||||
|
"visible": true,
|
||||||
|
"cache": true,
|
||||||
|
"height": 48,
|
||||||
|
"mode": "chrome"
|
||||||
|
},
|
||||||
|
"fixedHeaderAndTab": true,
|
||||||
|
"sider": {
|
||||||
|
"inverted": false,
|
||||||
|
"width": 220,
|
||||||
|
"collapsedWidth": 64,
|
||||||
|
"mixWidth": 90,
|
||||||
|
"mixCollapsedWidth": 64,
|
||||||
|
"mixChildMenuWidth": 200
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"visible": true,
|
||||||
|
"fixed": true,
|
||||||
|
"height": 56,
|
||||||
|
"right": true
|
||||||
|
},
|
||||||
|
"watermark": {
|
||||||
|
"visible": false,
|
||||||
|
"text": "SoybeanAdmin",
|
||||||
|
"enableUserName": false,
|
||||||
|
"enableTime": true,
|
||||||
|
"timeFormat": "YYYY-MM-DD HH:mm:ss"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"light": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(255, 255, 255)",
|
||||||
|
"layout": "rgb(247, 250, 252)",
|
||||||
|
"inverted": "rgb(0, 20, 40)",
|
||||||
|
"base-text": "rgb(31, 31, 31)"
|
||||||
|
},
|
||||||
|
"boxShadow": {
|
||||||
|
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||||
|
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||||
|
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(28, 28, 28)",
|
||||||
|
"layout": "rgb(18, 18, 18)",
|
||||||
|
"base-text": "rgb(224, 224, 224)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
src/theme/preset/compact.json
Normal file
90
src/theme/preset/compact.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"name": "Compact Preset",
|
||||||
|
"desc": "Compact layout preset for small screens",
|
||||||
|
"i18nkey": "theme.appearance.preset.compact",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"themeScheme": "light",
|
||||||
|
"grayscale": false,
|
||||||
|
"colourWeakness": false,
|
||||||
|
"recommendColor": false,
|
||||||
|
"themeColor": "#646cff",
|
||||||
|
"otherColor": {
|
||||||
|
"info": "#2080f0",
|
||||||
|
"success": "#52c41a",
|
||||||
|
"warning": "#faad14",
|
||||||
|
"error": "#f5222d"
|
||||||
|
},
|
||||||
|
"isInfoFollowPrimary": true,
|
||||||
|
"resetCacheStrategy": "close",
|
||||||
|
"layout": {
|
||||||
|
"mode": "vertical",
|
||||||
|
"scrollMode": "content"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"animate": true,
|
||||||
|
"animateMode": "fade-slide"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"height": 48,
|
||||||
|
"breadcrumb": {
|
||||||
|
"visible": true,
|
||||||
|
"showIcon": true
|
||||||
|
},
|
||||||
|
"multilingual": {
|
||||||
|
"visible": false
|
||||||
|
},
|
||||||
|
"globalSearch": {
|
||||||
|
"visible": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tab": {
|
||||||
|
"visible": true,
|
||||||
|
"cache": true,
|
||||||
|
"height": 36,
|
||||||
|
"mode": "button"
|
||||||
|
},
|
||||||
|
"fixedHeaderAndTab": true,
|
||||||
|
"sider": {
|
||||||
|
"inverted": false,
|
||||||
|
"width": 180,
|
||||||
|
"collapsedWidth": 48,
|
||||||
|
"mixWidth": 80,
|
||||||
|
"mixCollapsedWidth": 48,
|
||||||
|
"mixChildMenuWidth": 180
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"visible": false,
|
||||||
|
"fixed": false,
|
||||||
|
"height": 40,
|
||||||
|
"right": true
|
||||||
|
},
|
||||||
|
"watermark": {
|
||||||
|
"visible": false,
|
||||||
|
"text": "SoybeanAdmin",
|
||||||
|
"enableUserName": false,
|
||||||
|
"enableTime": false,
|
||||||
|
"timeFormat": "YYYY-MM-DD HH:mm"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"light": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(255, 255, 255)",
|
||||||
|
"layout": "rgb(247, 250, 252)",
|
||||||
|
"inverted": "rgb(0, 20, 40)",
|
||||||
|
"base-text": "rgb(31, 31, 31)"
|
||||||
|
},
|
||||||
|
"boxShadow": {
|
||||||
|
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||||
|
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||||
|
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(28, 28, 28)",
|
||||||
|
"layout": "rgb(18, 18, 18)",
|
||||||
|
"base-text": "rgb(224, 224, 224)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
src/theme/preset/dark.json
Normal file
90
src/theme/preset/dark.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"name": "Dark Preset",
|
||||||
|
"desc": "Dark theme preset for night time usage",
|
||||||
|
"i18nkey": "theme.appearance.preset.dark",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"themeScheme": "dark",
|
||||||
|
"grayscale": false,
|
||||||
|
"colourWeakness": false,
|
||||||
|
"recommendColor": false,
|
||||||
|
"themeColor": "#409eff",
|
||||||
|
"otherColor": {
|
||||||
|
"info": "#2080f0",
|
||||||
|
"success": "#52c41a",
|
||||||
|
"warning": "#faad14",
|
||||||
|
"error": "#f5222d"
|
||||||
|
},
|
||||||
|
"isInfoFollowPrimary": true,
|
||||||
|
"resetCacheStrategy": "close",
|
||||||
|
"layout": {
|
||||||
|
"mode": "vertical",
|
||||||
|
"scrollMode": "content"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"animate": true,
|
||||||
|
"animateMode": "fade-slide"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"height": 56,
|
||||||
|
"breadcrumb": {
|
||||||
|
"visible": true,
|
||||||
|
"showIcon": true
|
||||||
|
},
|
||||||
|
"multilingual": {
|
||||||
|
"visible": true
|
||||||
|
},
|
||||||
|
"globalSearch": {
|
||||||
|
"visible": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tab": {
|
||||||
|
"visible": true,
|
||||||
|
"cache": true,
|
||||||
|
"height": 44,
|
||||||
|
"mode": "chrome"
|
||||||
|
},
|
||||||
|
"fixedHeaderAndTab": true,
|
||||||
|
"sider": {
|
||||||
|
"inverted": true,
|
||||||
|
"width": 220,
|
||||||
|
"collapsedWidth": 64,
|
||||||
|
"mixWidth": 90,
|
||||||
|
"mixCollapsedWidth": 64,
|
||||||
|
"mixChildMenuWidth": 200
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"visible": true,
|
||||||
|
"fixed": false,
|
||||||
|
"height": 48,
|
||||||
|
"right": true
|
||||||
|
},
|
||||||
|
"watermark": {
|
||||||
|
"visible": false,
|
||||||
|
"text": "SoybeanAdmin",
|
||||||
|
"enableUserName": false,
|
||||||
|
"enableTime": false,
|
||||||
|
"timeFormat": "YYYY-MM-DD HH:mm"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"light": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(255, 255, 255)",
|
||||||
|
"layout": "rgb(247, 250, 252)",
|
||||||
|
"inverted": "rgb(0, 20, 40)",
|
||||||
|
"base-text": "rgb(31, 31, 31)"
|
||||||
|
},
|
||||||
|
"boxShadow": {
|
||||||
|
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||||
|
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||||
|
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(28, 28, 28)",
|
||||||
|
"layout": "rgb(18, 18, 18)",
|
||||||
|
"base-text": "rgb(224, 224, 224)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
src/theme/preset/default.json
Normal file
90
src/theme/preset/default.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"desc": "Default theme preset with balanced settings",
|
||||||
|
"i18nkey": "theme.appearance.preset.default",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"themeScheme": "light",
|
||||||
|
"grayscale": false,
|
||||||
|
"colourWeakness": false,
|
||||||
|
"recommendColor": false,
|
||||||
|
"themeColor": "#646cff",
|
||||||
|
"otherColor": {
|
||||||
|
"info": "#2080f0",
|
||||||
|
"success": "#52c41a",
|
||||||
|
"warning": "#faad14",
|
||||||
|
"error": "#f5222d"
|
||||||
|
},
|
||||||
|
"isInfoFollowPrimary": true,
|
||||||
|
"resetCacheStrategy": "close",
|
||||||
|
"layout": {
|
||||||
|
"mode": "vertical",
|
||||||
|
"scrollMode": "content"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"animate": true,
|
||||||
|
"animateMode": "fade-slide"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"height": 56,
|
||||||
|
"breadcrumb": {
|
||||||
|
"visible": true,
|
||||||
|
"showIcon": true
|
||||||
|
},
|
||||||
|
"multilingual": {
|
||||||
|
"visible": true
|
||||||
|
},
|
||||||
|
"globalSearch": {
|
||||||
|
"visible": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tab": {
|
||||||
|
"visible": true,
|
||||||
|
"cache": true,
|
||||||
|
"height": 44,
|
||||||
|
"mode": "chrome"
|
||||||
|
},
|
||||||
|
"fixedHeaderAndTab": true,
|
||||||
|
"sider": {
|
||||||
|
"inverted": false,
|
||||||
|
"width": 220,
|
||||||
|
"collapsedWidth": 64,
|
||||||
|
"mixWidth": 90,
|
||||||
|
"mixCollapsedWidth": 64,
|
||||||
|
"mixChildMenuWidth": 200
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"visible": true,
|
||||||
|
"fixed": false,
|
||||||
|
"height": 48,
|
||||||
|
"right": true
|
||||||
|
},
|
||||||
|
"watermark": {
|
||||||
|
"visible": false,
|
||||||
|
"text": "SoybeanAdmin",
|
||||||
|
"enableUserName": false,
|
||||||
|
"enableTime": false,
|
||||||
|
"timeFormat": "YYYY-MM-DD HH:mm"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"light": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(255, 255, 255)",
|
||||||
|
"layout": "rgb(247, 250, 252)",
|
||||||
|
"inverted": "rgb(0, 20, 40)",
|
||||||
|
"base-text": "rgb(31, 31, 31)"
|
||||||
|
},
|
||||||
|
"boxShadow": {
|
||||||
|
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
|
||||||
|
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
|
||||||
|
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"colors": {
|
||||||
|
"container": "rgb(28, 28, 28)",
|
||||||
|
"layout": "rgb(18, 18, 18)",
|
||||||
|
"base-text": "rgb(224, 224, 224)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -12,11 +12,10 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
error: '#f5222d'
|
error: '#f5222d'
|
||||||
},
|
},
|
||||||
isInfoFollowPrimary: true,
|
isInfoFollowPrimary: true,
|
||||||
resetCacheStrategy: 'close',
|
resetCacheStrategy: 'refresh',
|
||||||
layout: {
|
layout: {
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
scrollMode: 'content',
|
scrollMode: 'content'
|
||||||
reverseHorizontalMix: false
|
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
animate: true,
|
animate: true,
|
||||||
@@ -58,7 +57,10 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
},
|
},
|
||||||
watermark: {
|
watermark: {
|
||||||
visible: false,
|
visible: false,
|
||||||
text: 'SoybeanAdmin'
|
text: 'SoybeanAdmin',
|
||||||
|
enableUserName: false,
|
||||||
|
enableTime: false,
|
||||||
|
timeFormat: 'YYYY-MM-DD HH:mm'
|
||||||
},
|
},
|
||||||
tokens: {
|
tokens: {
|
||||||
light: {
|
light: {
|
||||||
|
20
src/typings/api/auth.d.ts
vendored
Normal file
20
src/typings/api/auth.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace Auth
|
||||||
|
*
|
||||||
|
* backend api module: "auth"
|
||||||
|
*/
|
||||||
|
namespace Auth {
|
||||||
|
interface LoginToken {
|
||||||
|
token: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
roles: string[];
|
||||||
|
buttons: string[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -47,41 +47,4 @@ declare namespace Api {
|
|||||||
status: EnableStatus | null;
|
status: EnableStatus | null;
|
||||||
} & T;
|
} & T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* namespace Auth
|
|
||||||
*
|
|
||||||
* backend api module: "auth"
|
|
||||||
*/
|
|
||||||
namespace Auth {
|
|
||||||
interface LoginToken {
|
|
||||||
token: string;
|
|
||||||
refreshToken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserInfo {
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
roles: string[];
|
|
||||||
buttons: string[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* namespace Route
|
|
||||||
*
|
|
||||||
* backend api module: "route"
|
|
||||||
*/
|
|
||||||
namespace Route {
|
|
||||||
type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute;
|
|
||||||
|
|
||||||
interface MenuRoute extends ElegantConstRoute {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserRoute {
|
|
||||||
routes: MenuRoute[];
|
|
||||||
home: import('@elegant-router/types').LastLevelRouteKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
19
src/typings/api/route.d.ts
vendored
Normal file
19
src/typings/api/route.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace Route
|
||||||
|
*
|
||||||
|
* backend api module: "route"
|
||||||
|
*/
|
||||||
|
namespace Route {
|
||||||
|
type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute;
|
||||||
|
|
||||||
|
interface MenuRoute extends ElegantConstRoute {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserRoute {
|
||||||
|
routes: MenuRoute[];
|
||||||
|
home: import('@elegant-router/types').LastLevelRouteKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user