mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-14 22:03:41 +08:00
Compare commits
85 Commits
65891a94d2
...
v2.0
Author | SHA1 | Date | |
---|---|---|---|
|
d5a3a25d3d | ||
|
d567c057a8 | ||
|
91fae50260 | ||
|
61fa4b7f3b | ||
|
ef7acc626f | ||
|
2a0c9f1b41 | ||
|
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 | ||
|
e6044d0fc7 | ||
|
2ed0b6484c | ||
|
222187d3b0 | ||
|
75455b006c | ||
|
dfb647a82c | ||
|
7fb5c72f7e | ||
|
41b5f49341 | ||
|
f35c250a89 | ||
|
1ff4d82d19 | ||
|
c3abc3df09 | ||
|
a013ea2c46 | ||
|
61244f0f2a | ||
|
85e40b1943 | ||
|
123d2c9074 | ||
|
05dc11e258 | ||
|
8527aa80b8 | ||
|
804860994e | ||
|
29698bef69 | ||
|
4e1b65b6c4 | ||
|
3cbaf4f4bf |
4
.env
4
.env
@@ -54,3 +54,7 @@ VITE_AUTOMATICALLY_DETECT_UPDATE=Y
|
|||||||
|
|
||||||
# show proxy url log in terminal
|
# show proxy url log in terminal
|
||||||
VITE_PROXY_LOG=Y
|
VITE_PROXY_LOG=Y
|
||||||
|
|
||||||
|
# used to control whether to launch editor
|
||||||
|
# by the way, this plugin is only available in dev mode, not in build mode
|
||||||
|
VITE_DEVTOOLS_LAUNCH_EDITOR=code
|
||||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -14,6 +14,7 @@
|
|||||||
"sdras.vue-vscode-snippets",
|
"sdras.vue-vscode-snippets",
|
||||||
"vue.volar",
|
"vue.volar",
|
||||||
"whtouche.vscode-js-console-utils",
|
"whtouche.vscode-js-console-utils",
|
||||||
"zhuangtongfa.material-theme"
|
"zhuangtongfa.material-theme",
|
||||||
|
"tu6ge.naive-ui-intelligence"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
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
|
|
||||||
}
|
}
|
||||||
|
51
.vscode/vue3.code-snippets
vendored
Normal file
51
.vscode/vue3.code-snippets
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"Print soy Vue3 SFC page": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": ["v3","page","view"],
|
||||||
|
"body": [
|
||||||
|
"<script lang=\"ts\" setup>",
|
||||||
|
"//$1",
|
||||||
|
"</script>\n",
|
||||||
|
"<template>",
|
||||||
|
" <div class=\"\">",
|
||||||
|
" <!-- page only one root element -->",
|
||||||
|
" $2",
|
||||||
|
" </div>",
|
||||||
|
"</template>\n",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
"Print soy Vue3 SFC Component": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": ["component","comp"],
|
||||||
|
"body": [
|
||||||
|
"<script lang=\"ts\" setup>",
|
||||||
|
"//$1",
|
||||||
|
"</script>\n",
|
||||||
|
"<template>",
|
||||||
|
" <div class=\"\">",
|
||||||
|
" $2",
|
||||||
|
" </div>",
|
||||||
|
"</template>\n",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Print soy style": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "st",
|
||||||
|
"body": ["<style scoped>", "//", "</style>\n"],
|
||||||
|
},
|
||||||
|
"Print soy script": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "sc",
|
||||||
|
"body": ["<script lang=\"ts\" setup>", "//$3", "</script>\n"],
|
||||||
|
},
|
||||||
|
"Print soy template": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "te",
|
||||||
|
"body": [
|
||||||
|
"<template>",
|
||||||
|
" <div class=\"\">$1</div>",
|
||||||
|
"</template>\n",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
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
|
||||||
|
@@ -10,21 +10,31 @@
|
|||||||
[](https://github.com/soybeanjs/soybean-admin)
|
[](https://github.com/soybeanjs/soybean-admin)
|
||||||
[](https://github.com/soybeanjs/soybean-admin)
|
[](https://github.com/soybeanjs/soybean-admin)
|
||||||
[](https://gitee.com/honghuangdc/soybean-admin)
|
[](https://gitee.com/honghuangdc/soybean-admin)
|
||||||
|
[](https://gitcode.com/soybeanjs/soybean-admin)
|
||||||
|
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||||
|
|
||||||
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<div style="display: flex; gap: 12px; align-items: center;">
|
||||||
|
<a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
> [!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, Vite5, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `SoybeanAdmin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
|
[`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, Vite5, 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.
|
||||||
@@ -43,19 +53,25 @@
|
|||||||
- [Preview Link](https://naive.soybeanjs.cn/)
|
- [Preview Link](https://naive.soybeanjs.cn/)
|
||||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin)
|
- [Github Repository](https://github.com/soybeanjs/soybean-admin)
|
||||||
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin)
|
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin)
|
||||||
|
- [Gitcode Repository](https://gitcode.com/soybeanjs/soybean-admin)
|
||||||
|
|
||||||
- **AntDesignVue Version:**
|
- **AntDesignVue Version:**
|
||||||
- [Preview Link](https://antd.soybeanjs.cn/)
|
- [Preview Link](https://antd.soybeanjs.cn/)
|
||||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin-antd)
|
- [Github Repository](https://github.com/soybeanjs/soybean-admin-antd)
|
||||||
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin-antd)
|
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin-antd)
|
||||||
|
- [Gitcode Repository](https://gitcode.com/soybeanjs/soybean-admin-antd)
|
||||||
|
|
||||||
- **ElementPlus Version:**
|
- **ElementPlus Version:**
|
||||||
- [Preview Link](https://elp.soybeanjs.cn/)
|
- [Preview Link](https://elp.soybeanjs.cn/)
|
||||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin-element-plus)
|
- [Github Repository](https://github.com/soybeanjs/soybean-admin-element-plus)
|
||||||
|
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin-element-plus)
|
||||||
|
- [Gitcode Repository](https://gitcode.com/soybeanjs/soybean-admin-element-plus)
|
||||||
|
|
||||||
- **Legacy Version:**
|
- **Legacy Version:**
|
||||||
- [Preview Link](https://legacy.soybeanjs.cn/)
|
- [Preview Link](https://legacy.soybeanjs.cn/)
|
||||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin/tree/legacy)
|
- [Github Repository](https://github.com/soybeanjs/soybean-admin/tree/legacy)
|
||||||
|
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin/tree/legacy)
|
||||||
|
- [Gitcode Repository](https://gitcode.com/soybeanjs/soybean-admin/tree/legacy)
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@@ -85,13 +101,18 @@
|
|||||||
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**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# github
|
||||||
git clone https://github.com/soybeanjs/soybean-admin.git
|
git clone https://github.com/soybeanjs/soybean-admin.git
|
||||||
|
# gitee
|
||||||
|
git clone https://gitee.com/honghuangdc/soybean-admin.git
|
||||||
|
# gitcode
|
||||||
|
git clone https://gitcode.com/soybeanjs/soybean-admin.git
|
||||||
```
|
```
|
||||||
|
|
||||||
**Install Dependencies**
|
**Install Dependencies**
|
||||||
@@ -128,6 +149,8 @@ Refer to the [Code Synchronization](https://docs.soybeanjs.cn/guide/sync) docume
|
|||||||
- [snail-job](https://github.com/aizuda/snail-job): A distributed task retry and task scheduling platform with "high performance, high value and high activity".
|
- [snail-job](https://github.com/aizuda/snail-job): A distributed task retry and task scheduling platform with "high performance, high value and high activity".
|
||||||
- [SuperApi](https://github.com/TmmTop/SuperApi): Quickly turn your idea into an online stable product! Entity-less library and table building, add, delete, change and check entity-less library table, support 15 kinds of condition query, as well as paging, list, unlimited tree list and other functions of the API deployment! With interface documentation, Auth authorisation, interface flow restriction, access to the client's real IP, advanced server caching components, dynamic APIs and other features, we look forward to your experience!
|
- [SuperApi](https://github.com/TmmTop/SuperApi): Quickly turn your idea into an online stable product! Entity-less library and table building, add, delete, change and check entity-less library table, support 15 kinds of condition query, as well as paging, list, unlimited tree list and other functions of the API deployment! With interface documentation, Auth authorisation, interface flow restriction, access to the client's real IP, advanced server caching components, dynamic APIs and other features, we look forward to your experience!
|
||||||
- [FastSoyAdmin](https://github.com/sleep1223/fast-soy-admin): A modern Management Platform based on FastAPI+Vue3+Naive UI.
|
- [FastSoyAdmin](https://github.com/sleep1223/fast-soy-admin): A modern Management Platform based on FastAPI+Vue3+Naive UI.
|
||||||
|
- [ba](https://github.com/xiatianYa/Ba-Server): Backend service docking with soybean admin based on goFrame framework, adapted to dynamic routing, and interface authentication permissions.
|
||||||
|
- [soybean-admin-go](https://github.com/WgoW/soybean-admin-go):A Go backend service developed based on the Gin and GORM frameworks, integrated with the example branch of Soybean Admin. It supports dynamic routing and API permission authentication.
|
||||||
|
|
||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
|
38
README.md
38
README.md
@@ -7,22 +7,33 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
[](./LICENSE)
|
[](./LICENSE)
|
||||||
[](https://github.com/soybeanjs/soybean-admin)
|
[](https://github.com/soybeanjs/soybean-admin)
|
||||||
[](https://github.com/soybeanjs/soybean-admin)
|
[](https://github.com/soybeanjs/soybean-admin)
|
||||||
[](https://gitee.com/honghuangdc/soybean-admin)
|
[](https://gitee.com/honghuangdc/soybean-admin)
|
||||||
|
[](https://gitcode.com/soybeanjs/soybean-admin)
|
||||||
|
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||||
|
|
||||||
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<div style="display: flex; gap: 12px; align-items: center;">
|
||||||
|
<a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
> [!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, Vite5, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- **前沿技术应用**:采用 Vue3, Vite5, 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**: 支持严格的类型检查,提高代码的可维护性。
|
||||||
@@ -41,16 +52,22 @@
|
|||||||
- [预览地址](https://naive.soybeanjs.cn/)
|
- [预览地址](https://naive.soybeanjs.cn/)
|
||||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin)
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin)
|
||||||
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin)
|
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin)
|
||||||
|
- [Gitcode 仓库](https://gitcode.com/soybeanjs/soybean-admin)
|
||||||
- **AntDesignVue 版本:**
|
- **AntDesignVue 版本:**
|
||||||
- [预览地址](https://antd.soybeanjs.cn/)
|
- [预览地址](https://antd.soybeanjs.cn/)
|
||||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-antd)
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-antd)
|
||||||
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin-antd)
|
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin-antd)
|
||||||
|
- [Gitcode 仓库](https://gitcode.com/soybeanjs/soybean-admin-antd)
|
||||||
- **ElementPlus 版本:**
|
- **ElementPlus 版本:**
|
||||||
- [预览地址](https://elp.soybeanjs.cn/)
|
- [预览地址](https://elp.soybeanjs.cn/)
|
||||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-element-plus)
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-element-plus)
|
||||||
|
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin-element-plus)
|
||||||
|
- [Gitcode 仓库](https://gitcode.com/soybeanjs/soybean-admin-element-plus)
|
||||||
- **旧版:**
|
- **旧版:**
|
||||||
- [预览地址](https://legacy.soybeanjs.cn/)
|
- [预览地址](https://legacy.soybeanjs.cn/)
|
||||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin/tree/legacy)
|
- [Github 仓库](https://github.com/soybeanjs/soybean-admin/tree/legacy)
|
||||||
|
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin/tree/legacy)
|
||||||
|
- [Gitcode 仓库](https://gitcode.com/soybeanjs/soybean-admin/tree/legacy)
|
||||||
|
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
@@ -110,13 +127,18 @@
|
|||||||
确保你的环境满足以下要求:
|
确保你的环境满足以下要求:
|
||||||
|
|
||||||
- **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 或更高。
|
||||||
|
|
||||||
**克隆项目**
|
**克隆项目**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# github
|
||||||
git clone https://github.com/soybeanjs/soybean-admin.git
|
git clone https://github.com/soybeanjs/soybean-admin.git
|
||||||
|
# gitee
|
||||||
|
git clone https://gitee.com/honghuangdc/soybean-admin.git
|
||||||
|
# gitcode
|
||||||
|
git clone https://gitcode.com/soybeanjs/soybean-admin.git
|
||||||
```
|
```
|
||||||
|
|
||||||
**安装依赖**
|
**安装依赖**
|
||||||
@@ -153,6 +175,8 @@ pnpm build
|
|||||||
- [snail-job](https://github.com/aizuda/snail-job): 一款兼具 “高性能、高颜值、高活跃” 的分布式任务重试和分布式任务调度平台。
|
- [snail-job](https://github.com/aizuda/snail-job): 一款兼具 “高性能、高颜值、高活跃” 的分布式任务重试和分布式任务调度平台。
|
||||||
- [SuperApi](https://github.com/TmmTop/SuperApi): 快速将你的 idea 变成线上稳定运行的产品! 无实体建库建表,对无实体库表进行增删改查,支持 15 种条件查询,以及分页,列表,无限级树形列表 等功能的 API 部署! 拥有接口文档,Auth 授权,接口限流,获取客户端真实 IP,先进的服务器缓存组件,动态 API 等功能,期待您的体验!
|
- [SuperApi](https://github.com/TmmTop/SuperApi): 快速将你的 idea 变成线上稳定运行的产品! 无实体建库建表,对无实体库表进行增删改查,支持 15 种条件查询,以及分页,列表,无限级树形列表 等功能的 API 部署! 拥有接口文档,Auth 授权,接口限流,获取客户端真实 IP,先进的服务器缓存组件,动态 API 等功能,期待您的体验!
|
||||||
- [FastSoyAdmin](https://github.com/sleep1223/fast-soy-admin): 基于 FastAPI+Vue3+Naive UI 的现代化轻量管理平台.
|
- [FastSoyAdmin](https://github.com/sleep1223/fast-soy-admin): 基于 FastAPI+Vue3+Naive UI 的现代化轻量管理平台.
|
||||||
|
- [ba](https://github.com/xiatianYa/Ba-Server): 基于goFrame框架开发的后端服务对接soybean-admin,适配动态路由,接口鉴权限。
|
||||||
|
- [soybean-admin-go](https://github.com/WgoW/soybean-admin-go):基于gin+gorm框架开发的go语言后端服务对接soybean-admin的example分支,适配动态路由,接口鉴权限。
|
||||||
|
|
||||||
|
|
||||||
## 如何贡献
|
## 如何贡献
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
9
build/plugins/devtools.ts
Normal file
9
build/plugins/devtools.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||||
|
|
||||||
|
export function setupDevtoolsPlugin(viteEnv: Env.ImportMeta) {
|
||||||
|
const { VITE_DEVTOOLS_LAUNCH_EDITOR } = viteEnv;
|
||||||
|
|
||||||
|
return VueDevtools({
|
||||||
|
launchEditor: VITE_DEVTOOLS_LAUNCH_EDITOR
|
||||||
|
});
|
||||||
|
}
|
@@ -1,18 +1,18 @@
|
|||||||
import type { PluginOption } from 'vite';
|
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 VueDevtools from 'vite-plugin-vue-devtools';
|
|
||||||
import progress from 'vite-plugin-progress';
|
import progress from 'vite-plugin-progress';
|
||||||
import { setupElegantRouter } from './router';
|
import { setupElegantRouter } from './router';
|
||||||
import { setupUnocss } from './unocss';
|
import { setupUnocss } from './unocss';
|
||||||
import { setupUnplugin } from './unplugin';
|
import { setupUnplugin } from './unplugin';
|
||||||
import { setupHtmlPlugin } from './html';
|
import { setupHtmlPlugin } from './html';
|
||||||
|
import { setupDevtoolsPlugin } from './devtools';
|
||||||
|
|
||||||
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
||||||
const plugins: PluginOption = [
|
const plugins: PluginOption = [
|
||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
VueDevtools(),
|
setupDevtoolsPlugin(viteEnv),
|
||||||
setupElegantRouter(),
|
setupElegantRouter(),
|
||||||
setupUnocss(viteEnv),
|
setupUnocss(viteEnv),
|
||||||
...setupUnplugin(viteEnv),
|
...setupUnplugin(viteEnv),
|
||||||
|
82
package.json
82
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、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、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",
|
||||||
"Vite5",
|
"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",
|
||||||
@@ -48,67 +48,63 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-scroll/core": "2.5.1",
|
"@better-scroll/core": "2.5.1",
|
||||||
"@iconify/vue": "4.3.0",
|
"@iconify/vue": "5.0.0",
|
||||||
"@sa/axios": "workspace:*",
|
"@sa/axios": "workspace:*",
|
||||||
"@sa/color": "workspace:*",
|
"@sa/color": "workspace:*",
|
||||||
"@sa/hooks": "workspace:*",
|
"@sa/hooks": "workspace:*",
|
||||||
"@sa/materials": "workspace:*",
|
"@sa/materials": "workspace:*",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"@vueuse/core": "13.0.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.1",
|
"pinia": "3.0.3",
|
||||||
"tailwind-merge": "3.0.2",
|
"tailwind-merge": "3.3.1",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.22",
|
||||||
"vue-draggable-plus": "0.6.0",
|
"vue-draggable-plus": "0.6.0",
|
||||||
"vue-i18n": "11.1.2",
|
"vue-i18n": "11.1.12",
|
||||||
"vue-router": "4.5.0"
|
"vue-router": "4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@elegant-router/vue": "0.3.8",
|
"@elegant-router/vue": "0.3.8",
|
||||||
"@iconify/json": "2.2.318",
|
"@iconify/json": "2.2.395",
|
||||||
"@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.13.10",
|
"@types/node": "24.7.2",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@unocss/eslint-config": "66.0.0",
|
"@unocss/eslint-config": "66.5.3",
|
||||||
"@unocss/preset-icons": "66.0.0",
|
"@unocss/preset-icons": "66.5.3",
|
||||||
"@unocss/preset-uno": "66.0.0",
|
"@unocss/preset-uno": "66.5.3",
|
||||||
"@unocss/transformer-directives": "66.0.0",
|
"@unocss/transformer-directives": "66.5.3",
|
||||||
"@unocss/transformer-variant-group": "66.0.0",
|
"@unocss/transformer-variant-group": "66.5.3",
|
||||||
"@unocss/vite": "66.0.0",
|
"@unocss/vite": "66.5.3",
|
||||||
"@vitejs/plugin-vue": "5.2.3",
|
"@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.22.0",
|
"eslint": "9.37.0",
|
||||||
"eslint-plugin-vue": "10.0.0",
|
"eslint-plugin-vue": "10.5.0",
|
||||||
"kolorist": "1.8.0",
|
"kolorist": "1.8.0",
|
||||||
"lint-staged": "15.5.0",
|
"sass": "1.93.2",
|
||||||
"sass": "1.86.0",
|
"simple-git-hooks": "2.13.1",
|
||||||
"simple-git-hooks": "2.11.1",
|
"tsx": "4.20.6",
|
||||||
"tsx": "4.19.3",
|
"typescript": "5.9.3",
|
||||||
"typescript": "5.8.2",
|
"unplugin-icons": "22.4.2",
|
||||||
"unplugin-icons": "22.1.0",
|
"unplugin-vue-components": "29.1.0",
|
||||||
"unplugin-vue-components": "28.4.1",
|
"vite": "7.1.9",
|
||||||
"vite": "6.2.2",
|
|
||||||
"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.2",
|
"vite-plugin-vue-devtools": "8.0.2",
|
||||||
"vue-eslint-parser": "10.1.1",
|
"vue-eslint-parser": "10.2.0",
|
||||||
"vue-tsc": "2.2.8"
|
"vue-tsc": "3.1.1"
|
||||||
},
|
},
|
||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"commit-msg": "pnpm sa git-commit-verify",
|
"commit-msg": "pnpm sa git-commit-verify",
|
||||||
"pre-commit": "pnpm typecheck && pnpm lint-staged"
|
"pre-commit": "pnpm typecheck && pnpm lint && git diff --exit-code"
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*": "eslint --fix"
|
|
||||||
},
|
},
|
||||||
"website": "https://admin.soybeanjs.cn"
|
"website": "https://admin.soybeanjs.cn"
|
||||||
}
|
}
|
||||||
|
@@ -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.12",
|
"@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.8.3",
|
"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;
|
||||||
}
|
}
|
||||||
|
@@ -2,40 +2,59 @@ import { computed, onScopeDispose, ref } from 'vue';
|
|||||||
import { useRafFn } from '@vueuse/core';
|
import { useRafFn } from '@vueuse/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* count down
|
* A hook for implementing a countdown timer. It uses `requestAnimationFrame` for smooth and accurate timing,
|
||||||
|
* independent of the screen refresh rate.
|
||||||
*
|
*
|
||||||
* @param seconds - count down seconds
|
* @param initialSeconds - The total number of seconds for the countdown.
|
||||||
*/
|
*/
|
||||||
export default function useCountDown(seconds: number) {
|
export default function useCountDown(initialSeconds: number) {
|
||||||
const FPS_PER_SECOND = 60;
|
const remainingSeconds = ref(0);
|
||||||
|
|
||||||
const fps = ref(0);
|
const count = computed(() => Math.ceil(remainingSeconds.value));
|
||||||
|
|
||||||
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
|
const isCounting = computed(() => remainingSeconds.value > 0);
|
||||||
|
|
||||||
const isCounting = computed(() => fps.value > 0);
|
|
||||||
|
|
||||||
const { pause, resume } = useRafFn(
|
const { pause, resume } = useRafFn(
|
||||||
() => {
|
({ delta }) => {
|
||||||
if (fps.value > 0) {
|
// delta: milliseconds elapsed since the last frame.
|
||||||
fps.value -= 1;
|
|
||||||
} else {
|
// If countdown already reached zero or below, ensure it's 0 and stop.
|
||||||
|
if (remainingSeconds.value <= 0) {
|
||||||
|
remainingSeconds.value = 0;
|
||||||
|
pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate seconds passed since the last frame.
|
||||||
|
const secondsPassed = delta / 1000;
|
||||||
|
remainingSeconds.value -= secondsPassed;
|
||||||
|
|
||||||
|
// If countdown has finished after decrementing.
|
||||||
|
if (remainingSeconds.value <= 0) {
|
||||||
|
remainingSeconds.value = 0;
|
||||||
pause();
|
pause();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: false }
|
{ immediate: false } // The timer does not start automatically.
|
||||||
);
|
);
|
||||||
|
|
||||||
function start(updateSeconds: number = seconds) {
|
/**
|
||||||
fps.value = FPS_PER_SECOND * updateSeconds;
|
* Starts the countdown.
|
||||||
|
*
|
||||||
|
* @param [updatedSeconds=initialSeconds] - Optionally, start with a new duration. Default is `initialSeconds`
|
||||||
|
*/
|
||||||
|
function start(updatedSeconds: number = initialSeconds) {
|
||||||
|
remainingSeconds.value = updatedSeconds;
|
||||||
resume();
|
resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stops the countdown and resets the remaining time to 0. */
|
||||||
function stop() {
|
function stop() {
|
||||||
fps.value = 0;
|
remainingSeconds.value = 0;
|
||||||
pause();
|
pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the rAF loop is cleaned up when the component is unmounted.
|
||||||
onScopeDispose(() => {
|
onScopeDispose(() => {
|
||||||
pause();
|
pause();
|
||||||
});
|
});
|
||||||
|
@@ -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.0"
|
"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 }
|
||||||
]"
|
]"
|
||||||
|
@@ -95,3 +95,27 @@
|
|||||||
.chrome-tab_dark .chrome-tab-divider {
|
.chrome-tab_dark .chrome-tab-divider {
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slider-tab {
|
||||||
|
background-color: transparent;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab_dark {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab:hover {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab_active {
|
||||||
|
color: var(--soy-primary-color);
|
||||||
|
background-color: var(--soy-primary-color-opacity1);
|
||||||
|
border-bottom-color: var(--soy-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-tab_active_dark {
|
||||||
|
background-color: var(--soy-primary-color-opacity2);
|
||||||
|
}
|
||||||
|
@@ -10,6 +10,10 @@ declare const styles: {
|
|||||||
readonly 'chrome-tab_dark': string;
|
readonly 'chrome-tab_dark': string;
|
||||||
readonly 'chrome-tab-divider': string;
|
readonly 'chrome-tab-divider': string;
|
||||||
readonly 'svg-close': string;
|
readonly 'svg-close': string;
|
||||||
|
readonly 'slider-tab': string;
|
||||||
|
readonly 'slider-tab_active': string;
|
||||||
|
readonly 'slider-tab_active_dark': string;
|
||||||
|
readonly 'slider-tab_dark': string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default styles;
|
export default styles;
|
||||||
|
@@ -5,6 +5,7 @@ import type { PageTabMode, PageTabProps } from '../../types';
|
|||||||
import { ACTIVE_COLOR, createTabCssVars } from './shared';
|
import { ACTIVE_COLOR, createTabCssVars } from './shared';
|
||||||
import ChromeTab from './chrome-tab.vue';
|
import ChromeTab from './chrome-tab.vue';
|
||||||
import ButtonTab from './button-tab.vue';
|
import ButtonTab from './button-tab.vue';
|
||||||
|
import SliderTab from './slider-tab.vue';
|
||||||
import SvgClose from './svg-close.vue';
|
import SvgClose from './svg-close.vue';
|
||||||
import style from './index.module.css';
|
import style from './index.module.css';
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ interface Emits {
|
|||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
const activeTabComponent = computed(() => {
|
const activeTabComponent = computed(() => {
|
||||||
const { mode, chromeClass, buttonClass } = props;
|
const { mode, chromeClass, buttonClass, sliderClass } = props;
|
||||||
|
|
||||||
const tabComponentMap = {
|
const tabComponentMap = {
|
||||||
chrome: {
|
chrome: {
|
||||||
@@ -36,6 +37,10 @@ const activeTabComponent = computed(() => {
|
|||||||
button: {
|
button: {
|
||||||
component: ButtonTab,
|
component: ButtonTab,
|
||||||
class: buttonClass
|
class: buttonClass
|
||||||
|
},
|
||||||
|
slider: {
|
||||||
|
component: SliderTab,
|
||||||
|
class: sliderClass
|
||||||
}
|
}
|
||||||
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
|
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
|
||||||
|
|
||||||
@@ -45,7 +50,7 @@ const activeTabComponent = computed(() => {
|
|||||||
const cssVars = computed(() => createTabCssVars(props.activeColor));
|
const cssVars = computed(() => createTabCssVars(props.activeColor));
|
||||||
|
|
||||||
const bindProps = computed(() => {
|
const bindProps = computed(() => {
|
||||||
const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
|
const { chromeClass: _chromeCls, buttonClass: _btnCls, sliderClass: _sliderCls, ...rest } = props;
|
||||||
|
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
|
53
packages/materials/src/libs/page-tab/slider-tab.vue
Normal file
53
packages/materials/src/libs/page-tab/slider-tab.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageTabProps } from '../../types';
|
||||||
|
import style from './index.module.css';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SliderTab'
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps<PageTabProps>();
|
||||||
|
|
||||||
|
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||||
|
|
||||||
|
type Slots = {
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The center content of the tab
|
||||||
|
*/
|
||||||
|
default?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The left content of the tab
|
||||||
|
*/
|
||||||
|
prefix?: SlotFn;
|
||||||
|
/**
|
||||||
|
* Slot
|
||||||
|
*
|
||||||
|
* The right content of the tab
|
||||||
|
*/
|
||||||
|
suffix?: SlotFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineSlots<Slots>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-6px whitespace-nowrap px-12px py-4px"
|
||||||
|
:class="[
|
||||||
|
style['slider-tab'],
|
||||||
|
{ [style['slider-tab_dark']]: darkMode },
|
||||||
|
{ [style['slider-tab_active']]: active },
|
||||||
|
{ [style['slider-tab_active_dark']]: active && darkMode }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@@ -6,12 +6,6 @@ interface AdminLayoutHeaderConfig {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
headerVisible?: boolean;
|
headerVisible?: boolean;
|
||||||
/**
|
|
||||||
* Header class
|
|
||||||
*
|
|
||||||
* @default ''
|
|
||||||
*/
|
|
||||||
headerClass?: string;
|
|
||||||
/**
|
/**
|
||||||
* Header height
|
* Header height
|
||||||
*
|
*
|
||||||
@@ -245,7 +239,7 @@ export type LayoutCssVars = {
|
|||||||
*
|
*
|
||||||
* @default chrome
|
* @default chrome
|
||||||
*/
|
*/
|
||||||
export type PageTabMode = 'button' | 'chrome';
|
export type PageTabMode = 'button' | 'chrome' | 'slider';
|
||||||
|
|
||||||
export interface PageTabProps {
|
export interface PageTabProps {
|
||||||
/** Whether is dark mode */
|
/** Whether is dark mode */
|
||||||
@@ -268,6 +262,8 @@ export interface PageTabProps {
|
|||||||
buttonClass?: string;
|
buttonClass?: string;
|
||||||
/** The class of the chrome tab */
|
/** The class of the chrome tab */
|
||||||
chromeClass?: string;
|
chromeClass?: string;
|
||||||
|
/** The class of the title tab */
|
||||||
|
sliderClass?: string;
|
||||||
/** Whether the tab is active */
|
/** Whether the tab is active */
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
/** The color of the active tab */
|
/** The color of the active tab */
|
||||||
|
@@ -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.3.1",
|
||||||
"c12": "3.0.2",
|
"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.2",
|
"execa": "9.6.0",
|
||||||
"kolorist": "1.8.0",
|
"kolorist": "1.8.0",
|
||||||
"npm-check-updates": "17.1.15",
|
"npm-check-updates": "19.0.0",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"crypto-js": "4.2.0",
|
"crypto-js": "4.2.0",
|
||||||
"klona": "2.0.6",
|
"klona": "2.0.6",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"nanoid": "5.1.5"
|
"nanoid": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "4.2.2"
|
"@types/crypto-js": "4.2.2"
|
||||||
|
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4536
pnpm-lock.yaml
generated
4536
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,51 @@ 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',
|
||||||
|
slider: 'theme.layout.tab.mode.slider'
|
||||||
};
|
};
|
||||||
|
|
||||||
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> = {
|
|
||||||
close: 'theme.resetCacheStrategy.close',
|
|
||||||
refresh: 'theme.resetCacheStrategy.refresh'
|
|
||||||
};
|
|
||||||
|
|
||||||
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,26 +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
|
// resize chart
|
||||||
if (!canRender()) {
|
if (isRendered()) {
|
||||||
await destroy();
|
resize();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// resize chart
|
|
||||||
if (isRendered()) {
|
|
||||||
resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// render chart
|
// render chart
|
||||||
await render();
|
await render();
|
||||||
|
|
||||||
|
if (chart.value) {
|
||||||
|
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();
|
||||||
@@ -229,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
|
|
||||||
};
|
|
||||||
}
|
|
@@ -38,7 +38,7 @@ const { isFullscreen, toggle } = useFullscreen();
|
|||||||
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
|
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex-y-center justify-end">
|
<div class="h-full flex-y-center justify-end">
|
||||||
<GlobalSearch />
|
<GlobalSearch v-if="themeStore.header.globalSearch.visible" />
|
||||||
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
||||||
<LangSwitch
|
<LangSwitch
|
||||||
v-if="themeStore.header.multilingual.visible"
|
v-if="themeStore.header.multilingual.visible"
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -5,7 +5,6 @@ import { useElementBounding } from '@vueuse/core';
|
|||||||
import { PageTab } from '@sa/materials';
|
import { PageTab } from '@sa/materials';
|
||||||
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 { useTabStore } from '@/store/modules/tab';
|
import { useTabStore } from '@/store/modules/tab';
|
||||||
import { isPC } from '@/utils/agent';
|
import { isPC } from '@/utils/agent';
|
||||||
import BetterScroll from '@/components/custom/better-scroll.vue';
|
import BetterScroll from '@/components/custom/better-scroll.vue';
|
||||||
@@ -18,7 +17,6 @@ defineOptions({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const routeStore = useRouteStore();
|
|
||||||
const tabStore = useTabStore();
|
const tabStore = useTabStore();
|
||||||
|
|
||||||
const bsWrapper = ref<HTMLElement>();
|
const bsWrapper = ref<HTMLElement>();
|
||||||
@@ -82,12 +80,8 @@ function getContextMenuDisabledKeys(tabId: string) {
|
|||||||
return disabledKeys;
|
return disabledKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCloseTab(tab: App.Global.Tab) {
|
function handleCloseTab(tab: App.Global.Tab) {
|
||||||
await tabStore.removeTab(tab.id);
|
tabStore.removeTab(tab.id);
|
||||||
|
|
||||||
if (themeStore.resetCacheStrategy === 'close') {
|
|
||||||
routeStore.resetRouteCache(tab.routeKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
@@ -175,7 +169,9 @@ init();
|
|||||||
<div
|
<div
|
||||||
ref="tabRef"
|
ref="tabRef"
|
||||||
class="h-full flex pr-18px"
|
class="h-full flex pr-18px"
|
||||||
:class="[themeStore.tab.mode === 'chrome' ? 'items-end' : 'items-center gap-12px']"
|
:class="[
|
||||||
|
themeStore.tab.mode === 'chrome' || themeStore.tab.mode === 'slider' ? 'items-end' : 'items-center gap-12px'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<PageTab
|
<PageTab
|
||||||
v-for="tab in tabStore.tabs"
|
v-for="tab in tabStore.tabs"
|
||||||
|
@@ -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,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { 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="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,151 +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>
|
|
||||||
</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,147 @@
|
|||||||
|
<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'
|
||||||
|
| '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,97 +58,155 @@ 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',
|
||||||
|
slider: 'Slider',
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
title: 'Global Search Settings',
|
||||||
|
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,97 +58,152 @@ 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: '标签栏风格',
|
||||||
|
slider: '滑块风格',
|
||||||
|
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: '固定头部和标签栏'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: '通用设置',
|
||||||
|
watermark: {
|
||||||
|
title: '水印设置',
|
||||||
|
visible: '显示全屏水印',
|
||||||
|
text: '自定义水印文本',
|
||||||
|
enableUserName: '启用用户名水印',
|
||||||
|
enableTime: '显示当前时间',
|
||||||
|
timeFormat: '时间格式'
|
||||||
},
|
},
|
||||||
multilingual: {
|
multilingual: {
|
||||||
|
title: '多语言设置',
|
||||||
visible: '显示多语言按钮'
|
visible: '显示多语言按钮'
|
||||||
|
},
|
||||||
|
globalSearch: {
|
||||||
|
title: '全局搜索设置',
|
||||||
|
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',
|
||||||
|
@@ -25,8 +25,8 @@ export function setupAppVersionNotification() {
|
|||||||
|
|
||||||
const buildTime = await getHtmlBuildTime();
|
const buildTime = await getHtmlBuildTime();
|
||||||
|
|
||||||
// If build time hasn't changed, no update is needed
|
// If failed to get build time or build time hasn't changed, no update is needed.
|
||||||
if (buildTime === BUILD_TIME) {
|
if (!buildTime || buildTime === BUILD_TIME) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,16 +88,21 @@ export function setupAppVersionNotification() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getHtmlBuildTime() {
|
async function getHtmlBuildTime(): Promise<string | null> {
|
||||||
const baseUrl = import.meta.env.VITE_BASE_URL || '/';
|
const baseUrl = import.meta.env.VITE_BASE_URL || '/';
|
||||||
|
|
||||||
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
|
||||||
|
|
||||||
const html = await res.text();
|
if (!res.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
const html = await res.text();
|
||||||
|
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
||||||
const buildTime = match?.[1] || '';
|
return match?.[1] || null;
|
||||||
|
} catch (error) {
|
||||||
return buildTime;
|
window.console.error('getHtmlBuildTime error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { addAPIProvider, disableCache } from '@iconify/vue';
|
import { addAPIProvider } from '@iconify/vue';
|
||||||
|
|
||||||
/** Setup the iconify offline */
|
/** Setup the iconify offline */
|
||||||
export function setupIconifyOffline() {
|
export function setupIconifyOffline() {
|
||||||
@@ -6,7 +6,5 @@ export function setupIconifyOffline() {
|
|||||||
|
|
||||||
if (VITE_ICONIFY_URL) {
|
if (VITE_ICONIFY_URL) {
|
||||||
addAPIProvider('', { resources: [VITE_ICONIFY_URL] });
|
addAPIProvider('', { resources: [VITE_ICONIFY_URL] });
|
||||||
|
|
||||||
disableCache('all');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -46,10 +46,7 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setReloadFlag(true);
|
setReloadFlag(true);
|
||||||
|
routeStore.resetRouteCache();
|
||||||
if (themeStore.resetCacheStrategy === 'refresh') {
|
|
||||||
routeStore.resetRouteCache();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'zh-CN');
|
const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'zh-CN');
|
||||||
|
@@ -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,7 +40,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
|
|
||||||
/** Reset auth store */
|
/** Reset auth store */
|
||||||
async function resetStore() {
|
async function resetStore() {
|
||||||
const authStore = useAuthStore();
|
recordUserId();
|
||||||
|
|
||||||
clearAuthStorage();
|
clearAuthStorage();
|
||||||
|
|
||||||
@@ -53,6 +54,41 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
routeStore.resetStore();
|
routeStore.resetStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Record the user ID of the previous login session Used to compare with the current user ID on next login */
|
||||||
|
function recordUserId() {
|
||||||
|
if (!userInfo.userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store current user ID locally for next login comparison
|
||||||
|
localStg.set('lastLoginUserId', userInfo.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current login user is different from previous login user If different, clear all tabs
|
||||||
|
*
|
||||||
|
* @returns {boolean} Whether to clear all tabs
|
||||||
|
*/
|
||||||
|
function checkTabClear(): boolean {
|
||||||
|
if (!userInfo.userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastLoginUserId = localStg.get('lastLoginUserId');
|
||||||
|
|
||||||
|
// Clear all tabs if current user is different from previous user
|
||||||
|
if (!lastLoginUserId || lastLoginUserId !== userInfo.userId) {
|
||||||
|
localStg.remove('globalTabs');
|
||||||
|
tabStore.clearTabs();
|
||||||
|
|
||||||
|
localStg.remove('lastLoginUserId');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStg.remove('lastLoginUserId');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login
|
* Login
|
||||||
*
|
*
|
||||||
@@ -69,7 +105,15 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
const pass = await loginByToken(loginToken);
|
const pass = await loginByToken(loginToken);
|
||||||
|
|
||||||
if (pass) {
|
if (pass) {
|
||||||
await redirectFromLogin(redirect);
|
// Check if the tab needs to be cleared
|
||||||
|
const isClear = checkTabClear();
|
||||||
|
let needRedirect = redirect;
|
||||||
|
|
||||||
|
if (isClear) {
|
||||||
|
// If the tab needs to be cleared,it means we don't need to redirect.
|
||||||
|
needRedirect = false;
|
||||||
|
}
|
||||||
|
await redirectFromLogin(needRedirect);
|
||||||
|
|
||||||
window.$notification?.success({
|
window.$notification?.success({
|
||||||
title: $t('page.login.common.loginSuccess'),
|
title: $t('page.login.common.loginSuccess'),
|
||||||
|
@@ -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() {
|
||||||
|
@@ -98,13 +98,22 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
|||||||
const removeTabIndex = tabs.value.findIndex(tab => tab.id === tabId);
|
const removeTabIndex = tabs.value.findIndex(tab => tab.id === tabId);
|
||||||
if (removeTabIndex === -1) return;
|
if (removeTabIndex === -1) return;
|
||||||
|
|
||||||
|
const removedTabRouteKey = tabs.value[removeTabIndex].routeKey;
|
||||||
const isRemoveActiveTab = activeTabId.value === tabId;
|
const isRemoveActiveTab = activeTabId.value === tabId;
|
||||||
const nextTab = tabs.value[removeTabIndex + 1] || homeTab.value;
|
|
||||||
|
|
||||||
|
// if remove the last tab, then switch to the second last tab
|
||||||
|
const nextTab = tabs.value[removeTabIndex + 1] || tabs.value[removeTabIndex - 1] || homeTab.value;
|
||||||
|
|
||||||
|
// remove tab
|
||||||
tabs.value.splice(removeTabIndex, 1);
|
tabs.value.splice(removeTabIndex, 1);
|
||||||
|
|
||||||
|
// if current tab is removed, then switch to next tab
|
||||||
if (isRemoveActiveTab && nextTab) {
|
if (isRemoveActiveTab && nextTab) {
|
||||||
await switchRouteByTab(nextTab);
|
await switchRouteByTab(nextTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset route cache
|
||||||
|
routeStore.resetRouteCache(removedTabRouteKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** remove active tab */
|
/** remove active tab */
|
||||||
@@ -131,9 +140,24 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
|||||||
*/
|
*/
|
||||||
async function clearTabs(excludes: string[] = []) {
|
async function clearTabs(excludes: string[] = []) {
|
||||||
const remainTabIds = [...getFixedTabIds(tabs.value), ...excludes];
|
const remainTabIds = [...getFixedTabIds(tabs.value), ...excludes];
|
||||||
const removedTabsIds = tabs.value.map(tab => tab.id).filter(id => !remainTabIds.includes(id));
|
|
||||||
|
// Identify tabs to be removed and collect their routeKeys if strategy is 'close'
|
||||||
|
const tabsToRemove = tabs.value.filter(tab => !remainTabIds.includes(tab.id));
|
||||||
|
const routeKeysToReset: RouteKey[] = [];
|
||||||
|
|
||||||
|
for (const tab of tabsToRemove) {
|
||||||
|
routeKeysToReset.push(tab.routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedTabsIds = tabsToRemove.map(tab => tab.id);
|
||||||
|
|
||||||
|
// If no tabs are actually being removed based on excludes and fixed tabs, exit
|
||||||
|
if (removedTabsIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isRemoveActiveTab = removedTabsIds.includes(activeTabId.value);
|
const isRemoveActiveTab = removedTabsIds.includes(activeTabId.value);
|
||||||
|
// filterTabsByIds returns tabs NOT in removedTabsIds, so these are the tabs that will remain
|
||||||
const updatedTabs = filterTabsByIds(removedTabsIds, tabs.value);
|
const updatedTabs = filterTabsByIds(removedTabsIds, tabs.value);
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
@@ -142,13 +166,21 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
|||||||
|
|
||||||
if (!isRemoveActiveTab) {
|
if (!isRemoveActiveTab) {
|
||||||
update();
|
update();
|
||||||
return;
|
} else {
|
||||||
|
const activeTabCandidate = updatedTabs[updatedTabs.length - 1] || homeTab.value;
|
||||||
|
|
||||||
|
if (activeTabCandidate) {
|
||||||
|
// Ensure there's a tab to switch to
|
||||||
|
await switchRouteByTab(activeTabCandidate);
|
||||||
|
}
|
||||||
|
// Update the tabs array regardless of switch success or if a candidate was found
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTab = updatedTabs[updatedTabs.length - 1] || homeTab.value;
|
// After tabs are updated and route potentially switched, reset cache for removed tabs
|
||||||
|
for (const routeKey of routeKeysToReset) {
|
||||||
await switchRouteByTab(activeTab);
|
routeStore.resetRouteCache(routeKey);
|
||||||
update();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { routerPushByKey } = useRouterPush();
|
const { routerPushByKey } = useRouterPush();
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
|
89
src/theme/preset/azir.json
Normal file
89
src/theme/preset/azir.json
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
src/theme/preset/compact.json
Normal file
89
src/theme/preset/compact.json
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
src/theme/preset/dark.json
Normal file
89
src/theme/preset/dark.json
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
src/theme/preset/default.json
Normal file
89
src/theme/preset/default.json
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"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,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
error: '#f5222d'
|
error: '#f5222d'
|
||||||
},
|
},
|
||||||
isInfoFollowPrimary: true,
|
isInfoFollowPrimary: true,
|
||||||
resetCacheStrategy: 'close',
|
|
||||||
layout: {
|
layout: {
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
scrollMode: 'content',
|
scrollMode: 'content'
|
||||||
reverseHorizontalMix: false
|
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
animate: true,
|
animate: true,
|
||||||
@@ -30,6 +28,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
},
|
},
|
||||||
multilingual: {
|
multilingual: {
|
||||||
visible: true
|
visible: true
|
||||||
|
},
|
||||||
|
globalSearch: {
|
||||||
|
visible: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tab: {
|
tab: {
|
||||||
@@ -55,7 +56,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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user