mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-10-11 20:33:41 +08:00
Compare commits
63 Commits
v1.3.10
...
206d5a4459
Author | SHA1 | Date | |
---|---|---|---|
|
206d5a4459 | ||
|
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 | ||
|
fa305146bc | ||
|
2e8cb35cfe | ||
|
a7c59adabc | ||
|
a6ecd3e083 | ||
|
3febb65d70 | ||
|
5d8b782d37 | ||
|
8b12ef9fd8 | ||
|
a6a47247ff | ||
|
4cc1487f46 | ||
|
b8112613ea | ||
|
a1a5c74c74 | ||
|
be6080ba0f | ||
|
3e0076d466 | ||
|
52c336d7e0 | ||
|
a03becdaed | ||
|
15163d7011 | ||
|
132e101243 | ||
|
9b9455d945 | ||
|
86da767e24 | ||
|
54e7d6d00a | ||
|
5cf3236475 | ||
|
6489ec46ae | ||
|
ac86247876 | ||
|
c9433e1710 | ||
|
60dd22624b | ||
|
56760245d4 | ||
|
d7aebb7dfa | ||
|
214341ee0b |
7
.env
7
.env
@@ -51,3 +51,10 @@ VITE_STORAGE_PREFIX=SOY_
|
||||
|
||||
# used to control whether the program automatically detects updates
|
||||
VITE_AUTOMATICALLY_DETECT_UPDATE=Y
|
||||
|
||||
# show proxy url log in terminal
|
||||
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",
|
||||
"vue.volar",
|
||||
"whtouche.vscode-js-console-utils",
|
||||
"zhuangtongfa.material-theme"
|
||||
"zhuangtongfa.material-theme",
|
||||
"tu6ge.naive-ui-intelligence"
|
||||
]
|
||||
}
|
||||
|
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -14,7 +14,9 @@
|
||||
"name": "TS Debugger",
|
||||
"runtimeExecutable": "tsx",
|
||||
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
|
||||
"program": "${file}"
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -3,7 +3,6 @@
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"eslint.useFlatConfig": true,
|
||||
"editor.formatOnSave": false,
|
||||
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
|
||||
"i18n-ally.displayLanguage": "zh-cn",
|
||||
|
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",
|
||||
],
|
||||
},
|
||||
}
|
187
CHANGELOG.md
187
CHANGELOG.md
@@ -1,6 +1,193 @@
|
||||
# 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)
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
- **projects**: fix active tab switch issue after removal - by @me-o in https://github.com/soybeanjs/soybean-admin/issues/723 [<samp>(a7c59)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a7c59ada)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **projects**: update README - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/718 [<samp>(3febb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3febb65d)
|
||||
|
||||
### 📦 Build
|
||||
|
||||
- **deps**: Restrict the minimum Node.js version. - by **一寸灰** in https://github.com/soybeanjs/soybean-admin/issues/720 [<samp>(a6ecd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a6ecd3e0)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps**:
|
||||
- update deps - by @soybeanjs [<samp>(5d8b7)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5d8b782d)
|
||||
- update deps - by @soybeanjs [<samp>(2e8cb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2e8cb35c)
|
||||
- **projects**:
|
||||
- update vscode settings and launch - by @soybeanjs [<samp>(8b12e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8b12ef9f)
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
[](https://github.com/soybeanjs) [](https://github.com/me-o) [](https://github.com/Azir-11)
|
||||
[一寸灰](mailto:webzhangfei@163.com),
|
||||
|
||||
## [v1.3.12](https://github.com/soybeanjs/soybean-admin/compare/v1.3.11...v1.3.12) (2025-03-12)
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- **projects**:
|
||||
- support loading page dark mode adaptation. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/702 [<samp>(9b945)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9b9455d9)
|
||||
- tab support touch event - by @soybeanjs [<samp>(a03be)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a03becda)
|
||||
- support proxy log in terminal - by @soybeanjs [<samp>(4cc14)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4cc1487f)
|
||||
- **projects): feat(projects**:
|
||||
- TableColumnCheck title support VNode - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/716 [<samp>(a1a5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a1a5c74c)
|
||||
- **utils**:
|
||||
- support replaceTab. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/713 [<samp>(be608)</samp>](https://github.com/soybeanjs/soybean-admin/commit/be6080ba)
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
- **projects**:
|
||||
- hidden multi-language button in login page. fix #694 - by **Azir** in https://github.com/soybeanjs/soybean-admin/issues/694 [<samp>(54e7d)</samp>](https://github.com/soybeanjs/soybean-admin/commit/54e7d6d0)
|
||||
- fix multiple calls to the login API when clicking quickly. fixed #697 - by @zsdycs in https://github.com/soybeanjs/soybean-admin/issues/698 and https://github.com/soybeanjs/soybean-admin/issues/697 [<samp>(86da7)</samp>](https://github.com/soybeanjs/soybean-admin/commit/86da767e)
|
||||
- fix multiple calls to the login API when clicking quickly. fixed #697 " - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/698 and https://github.com/soybeanjs/soybean-admin/issues/697 [<samp>(15163)</samp>](https://github.com/soybeanjs/soybean-admin/commit/15163d70)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps**:
|
||||
- update deps - by @soybeanjs [<samp>(132e1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/132e1012)
|
||||
- update deps - by **Azir** [<samp>(52c33)</samp>](https://github.com/soybeanjs/soybean-admin/commit/52c336d7)
|
||||
- update deps - by @soybeanjs [<samp>(b8112)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b8112613)
|
||||
- **projects**:
|
||||
- update unocss preset - by @Wangijun in https://github.com/soybeanjs/soybean-admin/issues/712 [<samp>(3e007)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3e0076d4)
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
[](https://github.com/soybeanjs) [](https://github.com/Azir-11) [](https://github.com/Wangijun) [](https://github.com/zsdycs)
|
||||
[Azir](mailto:2075125282@qq.com),
|
||||
|
||||
## [v1.3.11](https://github.com/soybeanjs/soybean-admin/compare/v1.3.10...v1.3.11) (2025-01-19)
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- **projects**: multi language buttons support hiding. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/683 [<samp>(d7aeb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d7aebb7)
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
- **hooks**:
|
||||
- The total number before assigning a value to the table is incorrect. - by @Azir-11 in https://github.com/soybeanjs/soybean-admin/issues/687 [<samp>(56760)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5676024)
|
||||
- **projects**:
|
||||
- fix login success notification. fixed #688 - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/688 [<samp>(60dd2)</samp>](https://github.com/soybeanjs/soybean-admin/commit/60dd226)
|
||||
- fix update notifications. fixed #691, fixed #692 - by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/691 and https://github.com/soybeanjs/soybean-admin/issues/692 [<samp>(ac862)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ac86247)
|
||||
|
||||
### 🛠 Optimizations
|
||||
|
||||
- **projects**: optimize code - by @soybeanjs [<samp>(6489e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/6489ec4)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **projects**: update README - by @soybeanjs [<samp>(21434)</samp>](https://github.com/soybeanjs/soybean-admin/commit/214341e)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps**: update deps - by @soybeanjs [<samp>(c9433)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c9433e1)
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
[](https://github.com/soybeanjs) [](https://github.com/Azir-11)
|
||||
|
||||
## [v1.3.10](https://github.com/honghuangdc/soybean-admin/compare/v1.3.9...v1.3.10) (2024-12-16)
|
||||
|
||||
### 🚀 Features
|
||||
|
@@ -10,6 +10,8 @@
|
||||
[](https://github.com/soybeanjs/soybean-admin)
|
||||
[](https://github.com/soybeanjs/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>
|
||||
|
||||
@@ -17,14 +19,20 @@
|
||||
> [!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!
|
||||
|
||||
> [!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
|
||||
|
||||
[`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, Vite6, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `SoybeanAdmin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
|
||||
|
||||
|
||||
## 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, Vite6, TypeScript, Pinia and UnoCSS.
|
||||
- **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.
|
||||
- **TypeScript**: support strict type checking to improve code maintainability.
|
||||
@@ -43,19 +51,25 @@
|
||||
- [Preview Link](https://naive.soybeanjs.cn/)
|
||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin)
|
||||
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin)
|
||||
- [Gitcode Repository](https://gitcode.com/soybeanjs/soybean-admin)
|
||||
|
||||
- **AntDesignVue Version:**
|
||||
- [Preview Link](https://antd.soybeanjs.cn/)
|
||||
- [Github Repository](https://github.com/soybeanjs/soybean-admin-antd)
|
||||
- [Gitee Repository](https://gitee.com/honghuangdc/soybean-admin-antd)
|
||||
- [Gitcode Repository](https://gitcode.com/soybeanjs/soybean-admin-antd)
|
||||
|
||||
- **ElementPlusVue Version:**
|
||||
- **ElementPlus Version:**
|
||||
- [Preview Link](https://elp.soybeanjs.cn/)
|
||||
- [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:**
|
||||
- [Preview Link](https://legacy.soybeanjs.cn/)
|
||||
- [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
|
||||
@@ -91,7 +105,12 @@ Make sure your environment meets the following requirements:
|
||||
**Clone Project**
|
||||
|
||||
```bash
|
||||
# github
|
||||
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**
|
||||
@@ -113,6 +132,10 @@ pnpm dev
|
||||
pnpm build
|
||||
```
|
||||
|
||||
**Code Synchronization**
|
||||
|
||||
Refer to the [Code Synchronization](https://docs.soybeanjs.cn/guide/sync) document.
|
||||
|
||||
## Ecosystem
|
||||
|
||||
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): SoybeanAdmin based version of React.
|
||||
@@ -124,6 +147,8 @@ pnpm build
|
||||
- [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!
|
||||
- [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
|
||||
@@ -160,7 +185,7 @@ Thanks the following people for their contributions. If you want to contribute t
|
||||
|
||||
<div>
|
||||
<p>QQ Group</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-3.jpg" style="width:200px" />
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-4.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>WeChat Group</p>
|
||||
|
37
README.md
37
README.md
@@ -7,22 +7,30 @@
|
||||
---
|
||||
|
||||
[](./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://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>
|
||||
|
||||
> [!NOTE]
|
||||
> 如果您觉得 `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, Vite6, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
||||
|
||||
## 特性
|
||||
|
||||
- **前沿技术应用**:采用 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
|
||||
- **前沿技术应用**:采用 Vue3, Vite6, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
|
||||
- **清晰的项目架构**:采用 pnpm monorepo 架构,结构清晰,优雅易懂。
|
||||
- **严格的代码规范**:遵循 [SoybeanJS 规范](https://docs.soybeanjs.cn/zh/standard),集成了eslint, prettier 和 simple-git-hooks,保证代码的规范性。
|
||||
- **TypeScript**: 支持严格的类型检查,提高代码的可维护性。
|
||||
@@ -41,16 +49,22 @@
|
||||
- [预览地址](https://naive.soybeanjs.cn/)
|
||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin)
|
||||
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin)
|
||||
- [Gitcode 仓库](https://gitcode.com/soybeanjs/soybean-admin)
|
||||
- **AntDesignVue 版本:**
|
||||
- [预览地址](https://antd.soybeanjs.cn/)
|
||||
- [Github 仓库](https://github.com/soybeanjs/soybean-admin-antd)
|
||||
- [Gitee 仓库](https://gitee.com/honghuangdc/soybean-admin-antd)
|
||||
- **ElementPlusVue 版本:**
|
||||
- [Gitcode 仓库](https://gitcode.com/soybeanjs/soybean-admin-antd)
|
||||
- **ElementPlus 版本:**
|
||||
- [预览地址](https://elp.soybeanjs.cn/)
|
||||
- [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/)
|
||||
- [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)
|
||||
|
||||
|
||||
## 文档
|
||||
@@ -116,7 +130,12 @@
|
||||
**克隆项目**
|
||||
|
||||
```bash
|
||||
# github
|
||||
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
|
||||
```
|
||||
|
||||
**安装依赖**
|
||||
@@ -138,6 +157,10 @@ pnpm dev
|
||||
pnpm build
|
||||
```
|
||||
|
||||
**代码同步**
|
||||
|
||||
参考 [代码同步](https://docs.soybeanjs.cn/zh/guide/sync) 文档。
|
||||
|
||||
## 周边生态
|
||||
|
||||
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): 基于SoybeanAdmin的React版本.
|
||||
@@ -149,6 +172,8 @@ pnpm build
|
||||
- [snail-job](https://github.com/aizuda/snail-job): 一款兼具 “高性能、高颜值、高活跃” 的分布式任务重试和分布式任务调度平台。
|
||||
- [SuperApi](https://github.com/TmmTop/SuperApi): 快速将你的 idea 变成线上稳定运行的产品! 无实体建库建表,对无实体库表进行增删改查,支持 15 种条件查询,以及分页,列表,无限级树形列表 等功能的 API 部署! 拥有接口文档,Auth 授权,接口限流,获取客户端真实 IP,先进的服务器缓存组件,动态 API 等功能,期待您的体验!
|
||||
- [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分支,适配动态路由,接口鉴权限。
|
||||
|
||||
|
||||
## 如何贡献
|
||||
@@ -187,7 +212,7 @@ pnpm build
|
||||
|
||||
<div>
|
||||
<p>QQ交流群</p>
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-3.jpg" style="width:200px" />
|
||||
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-4.jpg" style="width:200px" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<p>微信群</p>
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import type { ProxyOptions } from 'vite';
|
||||
import type { HttpProxy, ProxyOptions } from 'vite';
|
||||
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
||||
import { consola } from 'consola';
|
||||
import { createServiceConfig } from '../../src/utils/service';
|
||||
|
||||
/**
|
||||
@@ -12,23 +14,40 @@ export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
|
||||
|
||||
if (!isEnableHttpProxy) return undefined;
|
||||
|
||||
const isEnableProxyLog = env.VITE_PROXY_LOG === 'Y';
|
||||
|
||||
const { baseURL, proxyPattern, other } = createServiceConfig(env);
|
||||
|
||||
const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern });
|
||||
const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern }, isEnableProxyLog);
|
||||
|
||||
other.forEach(item => {
|
||||
Object.assign(proxy, createProxyItem(item));
|
||||
Object.assign(proxy, createProxyItem(item, isEnableProxyLog));
|
||||
});
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function createProxyItem(item: App.Service.ServiceConfigItem) {
|
||||
function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean) {
|
||||
const proxy: Record<string, ProxyOptions> = {};
|
||||
|
||||
proxy[item.proxyPattern] = {
|
||||
target: item.baseURL,
|
||||
changeOrigin: true,
|
||||
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
|
||||
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||
if (!enableLog) return;
|
||||
|
||||
const requestUrl = `${lightBlue('[proxy url]')}: ${bgYellow(` ${req.method} `)} ${green(`${item.proxyPattern}${req.url}`)}`;
|
||||
|
||||
const proxyUrl = `${lightBlue('[real request url]')}: ${green(`${options.target}${req.url}`)}`;
|
||||
|
||||
consola.log(`${requestUrl}\n${proxyUrl}`);
|
||||
});
|
||||
_proxy.on('error', (_err, req, _res) => {
|
||||
if (!enableLog) return;
|
||||
consola.log(bgRed(`Error: ${req.method} `), green(`${options.target}${req.url}`));
|
||||
});
|
||||
},
|
||||
rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
|
||||
};
|
||||
|
||||
|
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 vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||
import progress from 'vite-plugin-progress';
|
||||
import { setupElegantRouter } from './router';
|
||||
import { setupUnocss } from './unocss';
|
||||
import { setupUnplugin } from './unplugin';
|
||||
import { setupHtmlPlugin } from './html';
|
||||
import { setupDevtoolsPlugin } from './devtools';
|
||||
|
||||
export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
||||
const plugins: PluginOption = [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
VueDevtools(),
|
||||
setupDevtoolsPlugin(viteEnv),
|
||||
setupElegantRouter(),
|
||||
setupUnocss(viteEnv),
|
||||
...setupUnplugin(viteEnv),
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import process from 'node:process';
|
||||
import path from 'node:path';
|
||||
import type { PluginOption } from 'vite';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import Icons from 'unplugin-icons/vite';
|
||||
import IconsResolver from 'unplugin-icons/resolver';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
|
||||
export function setupUnplugin(viteEnv: Env.ImportMeta) {
|
||||
const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
|
||||
|
80
package.json
80
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "soybean-admin",
|
||||
"type": "module",
|
||||
"version": "1.3.10",
|
||||
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
||||
"version": "1.3.15",
|
||||
"description": "A fresh and elegant admin template, based on Vue3、Vite6、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite6、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
||||
"author": {
|
||||
"name": "Soybean",
|
||||
"email": "soybeanjs@outlook.com",
|
||||
@@ -19,7 +19,7 @@
|
||||
"keywords": [
|
||||
"Vue3 admin ",
|
||||
"vue-admin-template",
|
||||
"Vite5",
|
||||
"Vite6",
|
||||
"TypeScript",
|
||||
"naive-ui",
|
||||
"naive-ui-admin",
|
||||
@@ -27,7 +27,7 @@
|
||||
"UnoCSS"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.12.0",
|
||||
"node": ">=18.20.0",
|
||||
"pnpm": ">=8.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -48,65 +48,63 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@iconify/vue": "4.2.0",
|
||||
"@iconify/vue": "5.0.0",
|
||||
"@sa/axios": "workspace:*",
|
||||
"@sa/color": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@vueuse/core": "12.0.0",
|
||||
"@vueuse/core": "13.4.0",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.13",
|
||||
"defu": "6.1.4",
|
||||
"echarts": "5.5.1",
|
||||
"echarts": "5.6.0",
|
||||
"json5": "2.2.3",
|
||||
"naive-ui": "2.40.3",
|
||||
"naive-ui": "2.42.0",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "2.3.0",
|
||||
"tailwind-merge": "2.5.5",
|
||||
"vue": "3.5.13",
|
||||
"pinia": "3.0.3",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"vue": "3.5.17",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "10.0.5",
|
||||
"vue-router": "4.5.0"
|
||||
"vue-i18n": "11.1.7",
|
||||
"vue-router": "4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.283",
|
||||
"@iconify/json": "2.2.352",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.4.4",
|
||||
"@types/node": "22.10.2",
|
||||
"@soybeanjs/eslint-config": "1.6.1",
|
||||
"@types/node": "24.0.3",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "0.65.1",
|
||||
"@unocss/preset-icons": "0.65.1",
|
||||
"@unocss/preset-uno": "0.65.1",
|
||||
"@unocss/transformer-directives": "0.65.1",
|
||||
"@unocss/transformer-variant-group": "0.65.1",
|
||||
"@unocss/vite": "0.65.1",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "4.1.1",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"lint-staged": "15.2.11",
|
||||
"sass": "1.83.0",
|
||||
"simple-git-hooks": "2.11.1",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.7.2",
|
||||
"unplugin-icons": "0.21.0",
|
||||
"unplugin-vue-components": "0.28.0",
|
||||
"vite": "6.0.3",
|
||||
"@unocss/eslint-config": "66.2.3",
|
||||
"@unocss/preset-icons": "66.2.3",
|
||||
"@unocss/preset-uno": "66.2.3",
|
||||
"@unocss/transformer-directives": "66.2.3",
|
||||
"@unocss/transformer-variant-group": "66.2.3",
|
||||
"@unocss/vite": "66.2.3",
|
||||
"@vitejs/plugin-vue": "6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "5.0.0",
|
||||
"consola": "3.4.2",
|
||||
"eslint": "9.29.0",
|
||||
"eslint-plugin-vue": "10.2.0",
|
||||
"kolorist": "1.8.0",
|
||||
"sass": "1.89.2",
|
||||
"simple-git-hooks": "2.13.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "5.8.3",
|
||||
"unplugin-icons": "22.1.0",
|
||||
"unplugin-vue-components": "28.7.0",
|
||||
"vite": "7.0.0",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "7.6.8",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
"vue-tsc": "2.1.10"
|
||||
"vite-plugin-vue-devtools": "7.7.7",
|
||||
"vue-eslint-parser": "10.1.4",
|
||||
"vue-tsc": "2.2.10"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
"pre-commit": "pnpm typecheck && pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
"pre-commit": "pnpm typecheck && pnpm lint && git diff --exit-code"
|
||||
},
|
||||
"website": "https://admin.soybeanjs.cn"
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/alova",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./fetch": "./src/fetch.ts",
|
||||
@@ -13,8 +13,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/mock": "2.0.10",
|
||||
"@alova/mock": "2.0.17",
|
||||
"@sa/utils": "workspace:*",
|
||||
"alova": "3.2.6"
|
||||
"alova": "3.3.3"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/axios",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
@@ -11,11 +11,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"axios": "1.7.9",
|
||||
"axios": "1.10.0",
|
||||
"axios-retry": "4.5.0",
|
||||
"qs": "6.13.1"
|
||||
"qs": "6.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "6.9.17"
|
||||
"@types/qs": "6.14.0"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/color",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/hooks",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@@ -2,40 +2,59 @@ import { computed, onScopeDispose, ref } from 'vue';
|
||||
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) {
|
||||
const FPS_PER_SECOND = 60;
|
||||
export default function useCountDown(initialSeconds: number) {
|
||||
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(() => fps.value > 0);
|
||||
const isCounting = computed(() => remainingSeconds.value > 0);
|
||||
|
||||
const { pause, resume } = useRafFn(
|
||||
() => {
|
||||
if (fps.value > 0) {
|
||||
fps.value -= 1;
|
||||
} else {
|
||||
({ delta }) => {
|
||||
// delta: milliseconds elapsed since the last frame.
|
||||
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
{ 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();
|
||||
}
|
||||
|
||||
/** Stops the countdown and resets the remaining time to 0. */
|
||||
function stop() {
|
||||
fps.value = 0;
|
||||
remainingSeconds.value = 0;
|
||||
pause();
|
||||
}
|
||||
|
||||
// Ensure the rAF loop is cleaned up when the component is unmounted.
|
||||
onScopeDispose(() => {
|
||||
pause();
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { Ref, VNodeChild } from 'vue';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
@@ -8,9 +8,11 @@ export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export type ApiFn = (args: any) => Promise<unknown>;
|
||||
|
||||
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
||||
|
||||
export type TableColumnCheck = {
|
||||
key: string;
|
||||
title: string;
|
||||
title: TableColumnCheckTitle;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/materials",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"simplebar-vue": "2.4.0"
|
||||
"simplebar-vue": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typed-css-modules": "0.9.1"
|
||||
|
@@ -63,7 +63,7 @@ function handleClose() {
|
||||
<slot></slot>
|
||||
<template #suffix>
|
||||
<slot name="suffix">
|
||||
<SvgClose v-if="closable" :class="[style['svg-close']]" @click.stop="handleClose" />
|
||||
<SvgClose v-if="closable" :class="[style['svg-close']]" @pointerdown.stop="handleClose" />
|
||||
</slot>
|
||||
</template>
|
||||
</component>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/fetch",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/scripts",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"bin": {
|
||||
"sa": "./bin.ts"
|
||||
},
|
||||
@@ -14,14 +14,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.3.24",
|
||||
"bumpp": "9.9.1",
|
||||
"c12": "2.0.1",
|
||||
"bumpp": "10.2.0",
|
||||
"c12": "3.0.4",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.2.3",
|
||||
"consola": "3.4.2",
|
||||
"enquirer": "2.4.1",
|
||||
"execa": "9.5.2",
|
||||
"execa": "9.6.0",
|
||||
"kolorist": "1.8.0",
|
||||
"npm-check-updates": "17.1.11",
|
||||
"npm-check-updates": "18.0.1",
|
||||
"rimraf": "6.0.1"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/uno-preset",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/utils",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"crypto-js": "4.2.0",
|
||||
"klona": "2.0.6",
|
||||
"localforage": "1.10.0",
|
||||
"nanoid": "5.0.9"
|
||||
"nanoid": "5.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "4.2.2"
|
||||
|
5146
pnpm-lock.yaml
generated
5146
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
|
||||
import type { WatermarkProps } from 'naive-ui';
|
||||
import { useAppStore } from './store/modules/app';
|
||||
import { useThemeStore } from './store/modules/theme';
|
||||
import { useAuthStore } from './store/modules/auth';
|
||||
import { naiveDateLocales, naiveLocales } from './locales/naive';
|
||||
|
||||
defineOptions({
|
||||
@@ -12,6 +13,7 @@ defineOptions({
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
|
||||
|
||||
@@ -24,8 +26,13 @@ const naiveDateLocale = computed(() => {
|
||||
});
|
||||
|
||||
const watermarkProps = computed<WatermarkProps>(() => {
|
||||
const content =
|
||||
themeStore.watermark.enableUserName && authStore.userInfo.userName
|
||||
? authStore.userInfo.userName
|
||||
: themeStore.watermark.text;
|
||||
|
||||
return {
|
||||
content: themeStore.watermark.text,
|
||||
content,
|
||||
cross: true,
|
||||
fullscreen: true,
|
||||
fontSize: 16,
|
||||
|
@@ -25,7 +25,10 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
|
||||
<div v-for="item in columns" :key="item.key" class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)">
|
||||
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
|
||||
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
|
||||
{{ item.title }}
|
||||
<template v-if="typeof item.title === 'function'">
|
||||
<component :is="item.title" />
|
||||
</template>
|
||||
<template v-else>{{ item.title }}</template>
|
||||
</NCheckbox>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'ExceptionBase' });
|
||||
|
||||
|
@@ -31,13 +31,25 @@ const tooltipContent = computed(() => {
|
||||
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) {
|
||||
emit('changeLang', lang);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDropdown :value="lang" :options="langOptions" trigger="hover" @select="changeLang">
|
||||
<NDropdown :value="lang" :options="dropdownOptions" trigger="hover" @select="changeLang">
|
||||
<div>
|
||||
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
|
||||
<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 themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
|
||||
light: 'theme.themeSchema.light',
|
||||
dark: 'theme.themeSchema.dark',
|
||||
auto: 'theme.themeSchema.auto'
|
||||
light: 'theme.appearance.themeSchema.light',
|
||||
dark: 'theme.appearance.themeSchema.dark',
|
||||
auto: 'theme.appearance.themeSchema.auto'
|
||||
};
|
||||
|
||||
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
|
||||
@@ -21,43 +21,45 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
|
||||
};
|
||||
|
||||
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
|
||||
vertical: 'theme.layoutMode.vertical',
|
||||
'vertical-mix': 'theme.layoutMode.vertical-mix',
|
||||
horizontal: 'theme.layoutMode.horizontal',
|
||||
'horizontal-mix': 'theme.layoutMode.horizontal-mix'
|
||||
vertical: 'theme.layout.layoutMode.vertical',
|
||||
'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
|
||||
horizontal: 'theme.layout.layoutMode.horizontal',
|
||||
'horizontal-mix': 'theme.layout.layoutMode.horizontal-mix'
|
||||
};
|
||||
|
||||
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
|
||||
|
||||
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
|
||||
wrapper: 'theme.scrollMode.wrapper',
|
||||
content: 'theme.scrollMode.content'
|
||||
wrapper: 'theme.layout.content.scrollMode.wrapper',
|
||||
content: 'theme.layout.content.scrollMode.content'
|
||||
};
|
||||
|
||||
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
|
||||
|
||||
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
|
||||
chrome: 'theme.tab.mode.chrome',
|
||||
button: 'theme.tab.mode.button'
|
||||
chrome: 'theme.layout.tab.mode.chrome',
|
||||
button: 'theme.layout.tab.mode.button'
|
||||
};
|
||||
|
||||
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
|
||||
|
||||
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
|
||||
'fade-slide': 'theme.page.mode.fade-slide',
|
||||
fade: 'theme.page.mode.fade',
|
||||
'fade-bottom': 'theme.page.mode.fade-bottom',
|
||||
'fade-scale': 'theme.page.mode.fade-scale',
|
||||
'zoom-fade': 'theme.page.mode.zoom-fade',
|
||||
'zoom-out': 'theme.page.mode.zoom-out',
|
||||
none: 'theme.page.mode.none'
|
||||
'fade-slide': 'theme.layout.content.page.mode.fade-slide',
|
||||
fade: 'theme.layout.content.page.mode.fade',
|
||||
'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
|
||||
'fade-scale': 'theme.layout.content.page.mode.fade-scale',
|
||||
'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
|
||||
'zoom-out': 'theme.layout.content.page.mode.zoom-out',
|
||||
none: 'theme.layout.content.page.mode.none'
|
||||
};
|
||||
|
||||
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
|
||||
|
||||
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
|
||||
close: 'theme.resetCacheStrategy.close',
|
||||
refresh: 'theme.resetCacheStrategy.refresh'
|
||||
close: 'theme.layout.resetCacheStrategy.close',
|
||||
refresh: 'theme.layout.resetCacheStrategy.refresh'
|
||||
};
|
||||
|
||||
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
|
||||
|
||||
export const DARK_CLASS = 'dark';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { useCountDown, useLoading } from '@sa/hooks';
|
||||
import { $t } from '@/locales';
|
||||
import { REG_PHONE } from '@/constants/reg';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function useCaptcha() {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
||||
import type {
|
||||
@@ -29,7 +30,6 @@ import type {
|
||||
} from 'echarts/components';
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
export type ECOption = echarts.ComposeOption<
|
||||
@@ -210,6 +210,10 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
||||
|
||||
// render chart
|
||||
await render();
|
||||
|
||||
if (chart) {
|
||||
await onUpdated?.(chart);
|
||||
}
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
|
@@ -18,12 +18,7 @@ export function useRouterPush(inSetup = true) {
|
||||
|
||||
const routerBack = router.back;
|
||||
|
||||
interface RouterPushOptions {
|
||||
query?: Record<string, string>;
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
|
||||
async function routerPushByKey(key: RouteKey, options?: RouterPushOptions) {
|
||||
async function routerPushByKey(key: RouteKey, options?: App.Global.RouterPushOptions) {
|
||||
const { query, params } = options || {};
|
||||
|
||||
const routeLocation: RouteLocationRaw = {
|
||||
@@ -67,7 +62,7 @@ export function useRouterPush(inSetup = true) {
|
||||
async function toLogin(loginModule?: UnionKey.LoginModule, redirectUrl?: string) {
|
||||
const module = loginModule || 'pwd-login';
|
||||
|
||||
const options: RouterPushOptions = {
|
||||
const options: App.Global.RouterPushOptions = {
|
||||
params: {
|
||||
module
|
||||
}
|
||||
@@ -102,9 +97,9 @@ export function useRouterPush(inSetup = true) {
|
||||
const redirect = route.value.query?.redirect as string;
|
||||
|
||||
if (needRedirect && redirect) {
|
||||
routerPush(redirect);
|
||||
await routerPush(redirect);
|
||||
} else {
|
||||
toHome();
|
||||
await toHome();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -64,7 +64,7 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
if (isTableColumnHasKey(column)) {
|
||||
checks.push({
|
||||
key: column.key as string,
|
||||
title: column.title as string,
|
||||
title: column.title!,
|
||||
checked: true
|
||||
});
|
||||
} else if (column.type === 'selection') {
|
||||
@@ -119,6 +119,7 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
itemCount: 0,
|
||||
pageSizes: [10, 15, 20, 25, 30],
|
||||
onUpdatePage: async (page: number) => {
|
||||
pagination.page = page;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
||||
import GlobalLogo from '../global-logo/index.vue';
|
||||
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
|
||||
import GlobalSearch from '../global-search/index.vue';
|
||||
@@ -38,9 +38,14 @@ const { isFullscreen, toggle } = useFullscreen();
|
||||
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
|
||||
</div>
|
||||
<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" />
|
||||
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
:lang="appStore.locale"
|
||||
:lang-options="appStore.localeOptions"
|
||||
@change-lang="appStore.changeLocale"
|
||||
/>
|
||||
<ThemeSchemaSwitch
|
||||
:theme-schema="themeStore.themeScheme"
|
||||
:is-dark="themeStore.darkMode"
|
||||
|
@@ -2,11 +2,11 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
import { 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 { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useMenu } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
|
@@ -3,12 +3,12 @@ import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { 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 { $t } from '@/locales';
|
||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import GlobalLogo from '../../global-logo/index.vue';
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import GlobalLogo from '../global-logo/index.vue';
|
||||
|
||||
defineOptions({
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { VNode } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useTabStore } from '@/store/modules/tab';
|
||||
import { useSvgIcon } from '@/hooks/common/icon';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ContextMenu'
|
||||
|
@@ -3,12 +3,11 @@ import { nextTick, reactive, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useElementBounding } from '@vueuse/core';
|
||||
import { PageTab } from '@sa/materials';
|
||||
import BetterScroll from '@/components/custom/better-scroll.vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useTabStore } from '@/store/modules/tab';
|
||||
import { isPC } from '@/utils/agent';
|
||||
import BetterScroll from '@/components/custom/better-scroll.vue';
|
||||
import ContextMenu from './context-menu.vue';
|
||||
|
||||
defineOptions({
|
||||
@@ -18,7 +17,6 @@ defineOptions({
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const tabStore = useTabStore();
|
||||
|
||||
const bsWrapper = ref<HTMLElement>();
|
||||
@@ -82,12 +80,8 @@ function getContextMenuDisabledKeys(tabId: string) {
|
||||
return disabledKeys;
|
||||
}
|
||||
|
||||
async function handleCloseTab(tab: App.Global.Tab) {
|
||||
await tabStore.removeTab(tab.id);
|
||||
|
||||
if (themeStore.resetCacheStrategy === 'close') {
|
||||
routeStore.resetRouteCache(tab.routeKey);
|
||||
}
|
||||
function handleCloseTab(tab: App.Global.Tab) {
|
||||
tabStore.removeTab(tab.id);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
@@ -114,7 +108,7 @@ function setDropdown(config: Partial<DropdownConfig>) {
|
||||
|
||||
let isClickContextMenu = false;
|
||||
|
||||
function handleDropdownVisible(visible: boolean) {
|
||||
function handleDropdownVisible(visible: boolean | undefined) {
|
||||
if (!isClickContextMenu) {
|
||||
setDropdown({ visible });
|
||||
}
|
||||
@@ -186,7 +180,7 @@ init();
|
||||
:active="tab.id === tabStore.activeTabId"
|
||||
:active-color="themeStore.themeColor"
|
||||
:closable="!tabStore.isTabRetain(tab.id)"
|
||||
@click="tabStore.switchRouteByTab(tab)"
|
||||
@pointerdown="tabStore.switchRouteByTab(tab)"
|
||||
@close="handleCloseTab(tab)"
|
||||
@contextmenu="handleContextMenu($event, tab.id)"
|
||||
>
|
||||
|
@@ -68,11 +68,11 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center flex-wrap gap-x-32px gap-y-16px">
|
||||
<div class="grid grid-cols-3 gap-x-32px gap-y-16px">
|
||||
<div
|
||||
v-for="(item, key) in layoutConfig"
|
||||
:key="key"
|
||||
class="flex cursor-pointer border-2px rounded-6px hover:border-primary"
|
||||
class="flex-center cursor-pointer border-2px rounded-6px hover:border-primary"
|
||||
:class="[mode === key ? 'border-primary' : 'border-transparent']"
|
||||
@click="handleChangeMode(key)"
|
||||
>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { $t } from '@/locales';
|
||||
import DarkMode from './modules/dark-mode.vue';
|
||||
import LayoutMode from './modules/layout-mode.vue';
|
||||
import ThemeColor from './modules/theme-color.vue';
|
||||
import PageFun from './modules/page-fun.vue';
|
||||
import AppearanceSettings from './modules/appearance/index.vue';
|
||||
import LayoutSettings from './modules/layout/index.vue';
|
||||
import GeneralSettings from './modules/general/index.vue';
|
||||
import ConfigOperation from './modules/config-operation.vue';
|
||||
|
||||
defineOptions({
|
||||
@@ -12,15 +12,26 @@ defineOptions({
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
const activeTab = ref('appearance');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="360">
|
||||
<NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="460">
|
||||
<NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable>
|
||||
<DarkMode />
|
||||
<LayoutMode />
|
||||
<ThemeColor />
|
||||
<PageFun />
|
||||
<NTabs v-model:value="activeTab" type="segment" size="medium" class="mb-16px">
|
||||
<NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
|
||||
<NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
|
||||
<NTab name="general" :tab="$t('theme.tabs.general')"></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'" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ConfigOperation />
|
||||
</template>
|
||||
@@ -28,4 +39,14 @@ const appStore = useAppStore();
|
||||
</NDrawer>
|
||||
</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">
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
import SettingItem from '../components/setting-item.vue';
|
||||
import SettingItem from '../../../components/setting-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ThemeColor'
|
||||
@@ -34,16 +34,16 @@ const swatches: string[] = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.themeColor.title') }}</NDivider>
|
||||
<NDivider>{{ $t('theme.appearance.themeColor.title') }}</NDivider>
|
||||
<div class="flex-col-stretch gap-12px">
|
||||
<NTooltip placement="top-start">
|
||||
<template #trigger>
|
||||
<SettingItem key="recommend-color" :label="$t('theme.recommendColor')">
|
||||
<SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
|
||||
<NSwitch v-model:value="themeStore.recommendColor" />
|
||||
</SettingItem>
|
||||
</template>
|
||||
<p>
|
||||
<span class="pr-12px">{{ $t('theme.recommendColorDesc') }}</span>
|
||||
<span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
|
||||
<br />
|
||||
<NButton
|
||||
text
|
||||
@@ -57,10 +57,14 @@ const swatches: string[] = [
|
||||
</NButton>
|
||||
</p>
|
||||
</NTooltip>
|
||||
<SettingItem v-for="(_, key) in themeStore.themeColors" :key="key" :label="$t(`theme.themeColor.${key}`)">
|
||||
<SettingItem
|
||||
v-for="(_, key) in themeStore.themeColors"
|
||||
:key="key"
|
||||
:label="$t(`theme.appearance.themeColor.${key}`)"
|
||||
>
|
||||
<template v-if="key === 'info'" #suffix>
|
||||
<NCheckbox v-model:checked="themeStore.isInfoFollowPrimary">
|
||||
{{ $t('theme.themeColor.followPrimary') }}
|
||||
{{ $t('theme.appearance.themeColor.followPrimary') }}
|
||||
</NCheckbox>
|
||||
</template>
|
||||
<NColorPicker
|
@@ -3,10 +3,10 @@ import { computed } from 'vue';
|
||||
import { themeSchemaRecord } from '@/constants/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
import SettingItem from '../components/setting-item.vue';
|
||||
import SettingItem from '../../../components/setting-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'DarkMode'
|
||||
name: 'ThemeSchema'
|
||||
});
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
@@ -33,7 +33,7 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.themeSchema.title') }}</NDivider>
|
||||
<NDivider>{{ $t('theme.appearance.themeSchema.title') }}</NDivider>
|
||||
<div class="flex-col-stretch gap-16px">
|
||||
<div class="i-flex-center">
|
||||
<NTabs
|
||||
@@ -50,14 +50,14 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
|
||||
</NTabs>
|
||||
</div>
|
||||
<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" />
|
||||
</SettingItem>
|
||||
</Transition>
|
||||
<SettingItem :label="$t('theme.grayscale')">
|
||||
<SettingItem :label="$t('theme.appearance.grayscale')">
|
||||
<NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" />
|
||||
</SettingItem>
|
||||
<SettingItem :label="$t('theme.colourWeakness')">
|
||||
<SettingItem :label="$t('theme.appearance.colourWeakness')">
|
||||
<NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" />
|
||||
</SettingItem>
|
||||
</div>
|
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Clipboard from 'clipboard';
|
||||
import { $t } from '@/locales';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ConfigOperation'
|
||||
|
15
src/layouts/modules/theme-drawer/modules/general/index.vue
Normal file
15
src/layouts/modules/theme-drawer/modules/general/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import GlobalSettings from './modules/global-settings.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'GeneralSettings'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px">
|
||||
<GlobalSettings />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@@ -0,0 +1,59 @@
|
||||
<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>
|
||||
<div class="flex-col-stretch gap-16px">
|
||||
<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>
|
||||
|
||||
<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 v-model:value="themeStore.watermark.enableUserName" />
|
||||
</SettingItem>
|
||||
<SettingItem v-if="themeStore.watermark.visible" key="3" :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>
|
||||
</div>
|
||||
</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>
|
25
src/layouts/modules/theme-drawer/modules/layout/index.vue
Normal file
25
src/layouts/modules/theme-drawer/modules/layout/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px">
|
||||
<LayoutMode />
|
||||
<TabSettings />
|
||||
<HeaderSettings />
|
||||
<SiderSettings />
|
||||
<FooterSettings />
|
||||
<ContentSettings />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@@ -0,0 +1,61 @@
|
||||
<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')">
|
||||
<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,58 @@
|
||||
<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');
|
||||
</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 && layoutMode === 'horizontal-mix'"
|
||||
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,8 @@
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
import LayoutModeCard from '../components/layout-mode-card.vue';
|
||||
import SettingItem from '../components/setting-item.vue';
|
||||
import LayoutModeCard from '../../../components/layout-mode-card.vue';
|
||||
import SettingItem from '../../../components/setting-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutMode'
|
||||
@@ -18,7 +18,7 @@ function handleReverseHorizontalMixChange(value: boolean) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.layoutMode.title') }}</NDivider>
|
||||
<NDivider>{{ $t('theme.layout.layoutMode.title') }}</NDivider>
|
||||
<LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile">
|
||||
<template #vertical>
|
||||
<div class="layout-sider h-full w-18px"></div>
|
||||
@@ -51,7 +51,7 @@ function handleReverseHorizontalMixChange(value: boolean) {
|
||||
</LayoutModeCard>
|
||||
<SettingItem
|
||||
v-if="themeStore.layout.mode === 'horizontal-mix'"
|
||||
:label="$t('theme.layoutMode.reverseHorizontalMix')"
|
||||
:label="$t('theme.layout.layoutMode.reverseHorizontalMix')"
|
||||
class="mt-16px"
|
||||
>
|
||||
<NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />
|
@@ -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'));
|
||||
</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,61 @@
|
||||
<script setup lang="ts">
|
||||
import { resetCacheStrategyOptions, themeTabModeOptions } from '@/constants/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { translateOptions } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
import SettingItem from '../../../components/setting-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'TabSettings'
|
||||
});
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.layout.tab.title') }}</NDivider>
|
||||
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||
<SettingItem key="0" :label="$t('theme.layout.resetCacheStrategy.title')">
|
||||
<NSelect
|
||||
v-model:value="themeStore.resetCacheStrategy"
|
||||
:options="translateOptions(resetCacheStrategyOptions)"
|
||||
size="small"
|
||||
class="w-120px"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem key="1" :label="$t('theme.layout.tab.visible')">
|
||||
<NSwitch v-model:value="themeStore.tab.visible" />
|
||||
</SettingItem>
|
||||
<SettingItem v-if="themeStore.tab.visible" key="2" :label="$t('theme.layout.tab.cache')">
|
||||
<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,148 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import {
|
||||
resetCacheStrategyOptions,
|
||||
themePageAnimationModeOptions,
|
||||
themeScrollModeOptions,
|
||||
themeTabModeOptions
|
||||
} from '@/constants/app';
|
||||
import { translateOptions } from '@/utils/common';
|
||||
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>
|
||||
</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>
|
@@ -58,93 +58,119 @@ const local: App.I18n.Schema = {
|
||||
tokenExpired: 'The requested token has expired'
|
||||
},
|
||||
theme: {
|
||||
themeSchema: {
|
||||
title: 'Theme Schema',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
auto: 'Follow System'
|
||||
},
|
||||
grayscale: 'Grayscale',
|
||||
colourWeakness: 'Colour Weakness',
|
||||
layoutMode: {
|
||||
title: 'Layout Mode',
|
||||
vertical: 'Vertical Menu Mode',
|
||||
horizontal: 'Horizontal Menu Mode',
|
||||
'vertical-mix': 'Vertical Mix Menu Mode',
|
||||
'horizontal-mix': 'Horizontal Mix menu Mode',
|
||||
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
|
||||
},
|
||||
recommendColor: 'Apply Recommended Color Algorithm',
|
||||
recommendColorDesc: 'The recommended color algorithm refers to',
|
||||
themeColor: {
|
||||
title: 'Theme Color',
|
||||
primary: 'Primary',
|
||||
info: 'Info',
|
||||
success: 'Success',
|
||||
warning: 'Warning',
|
||||
error: 'Error',
|
||||
followPrimary: 'Follow Primary'
|
||||
},
|
||||
scrollMode: {
|
||||
title: 'Scroll Mode',
|
||||
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',
|
||||
header: {
|
||||
height: 'Header Height',
|
||||
breadcrumb: {
|
||||
visible: 'Breadcrumb Visible',
|
||||
showIcon: 'Breadcrumb Icon Visible'
|
||||
}
|
||||
},
|
||||
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'
|
||||
tabs: {
|
||||
appearance: 'Appearance',
|
||||
layout: 'Layout',
|
||||
general: 'General'
|
||||
},
|
||||
appearance: {
|
||||
themeSchema: {
|
||||
title: 'Theme Schema',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
auto: 'Follow System'
|
||||
},
|
||||
grayscale: 'Grayscale',
|
||||
colourWeakness: 'Colour Weakness',
|
||||
themeColor: {
|
||||
title: 'Theme Color',
|
||||
primary: 'Primary',
|
||||
info: 'Info',
|
||||
success: 'Success',
|
||||
warning: 'Warning',
|
||||
error: 'Error',
|
||||
followPrimary: 'Follow Primary'
|
||||
},
|
||||
recommendColor: 'Apply Recommended Color Algorithm',
|
||||
recommendColorDesc: 'The recommended color algorithm refers to'
|
||||
},
|
||||
layout: {
|
||||
layoutMode: {
|
||||
title: 'Layout Mode',
|
||||
vertical: 'Vertical Menu Mode',
|
||||
horizontal: 'Horizontal Menu Mode',
|
||||
'vertical-mix': 'Vertical Mix Menu Mode',
|
||||
'horizontal-mix': 'Horizontal Mix menu Mode',
|
||||
reverseHorizontalMix: 'Reverse first level menus and child level menus position'
|
||||
},
|
||||
tab: {
|
||||
title: 'Tab Settings',
|
||||
visible: 'Tab Visible',
|
||||
cache: 'Tag Bar Info Cache',
|
||||
height: 'Tab Height',
|
||||
mode: {
|
||||
title: 'Tab Mode',
|
||||
chrome: 'Chrome',
|
||||
button: 'Button'
|
||||
}
|
||||
},
|
||||
header: {
|
||||
title: 'Header Settings',
|
||||
height: 'Header Height',
|
||||
breadcrumb: {
|
||||
visible: 'Breadcrumb Visible',
|
||||
showIcon: 'Breadcrumb Icon Visible'
|
||||
}
|
||||
},
|
||||
sider: {
|
||||
title: 'Sider Settings',
|
||||
inverted: 'Dark Sider',
|
||||
width: 'Sider Width',
|
||||
collapsedWidth: 'Sider Collapsed Width',
|
||||
mixWidth: 'Mix Sider Width',
|
||||
mixCollapsedWidth: 'Mix Sider Collapse Width',
|
||||
mixChildMenuWidth: 'Mix Child Menu Width'
|
||||
},
|
||||
footer: {
|
||||
title: 'Footer Settings',
|
||||
visible: 'Footer Visible',
|
||||
fixed: 'Fixed Footer',
|
||||
height: 'Footer Height',
|
||||
right: 'Right Footer'
|
||||
},
|
||||
content: {
|
||||
title: 'Content Area Settings',
|
||||
scrollMode: {
|
||||
title: 'Scroll Mode',
|
||||
wrapper: 'Wrapper',
|
||||
content: 'Content'
|
||||
},
|
||||
page: {
|
||||
animate: 'Page Animate',
|
||||
mode: {
|
||||
title: 'Page Animate Mode',
|
||||
fade: 'Fade',
|
||||
'fade-slide': 'Slide',
|
||||
'fade-bottom': 'Fade Zoom',
|
||||
'fade-scale': 'Fade Scale',
|
||||
'zoom-fade': 'Zoom Fade',
|
||||
'zoom-out': 'Zoom Out',
|
||||
none: 'None'
|
||||
}
|
||||
},
|
||||
fixedHeaderAndTab: 'Fixed Header And Tab'
|
||||
},
|
||||
resetCacheStrategy: {
|
||||
title: 'Reset Cache Strategy',
|
||||
close: 'Close Page',
|
||||
refresh: 'Refresh Page'
|
||||
}
|
||||
},
|
||||
general: {
|
||||
watermark: {
|
||||
visible: 'Watermark Full Screen Visible',
|
||||
text: 'Watermark Text',
|
||||
enableUserName: 'Enable User Name Watermark'
|
||||
},
|
||||
multilingual: {
|
||||
title: 'Multilingual Settings',
|
||||
visible: 'Display multilingual button'
|
||||
},
|
||||
globalSearch: {
|
||||
title: 'Global Search Settings',
|
||||
visible: 'Display GlobalSearch button'
|
||||
}
|
||||
},
|
||||
configOperation: {
|
||||
copyConfig: 'Copy Config',
|
||||
|
@@ -58,93 +58,119 @@ const local: App.I18n.Schema = {
|
||||
tokenExpired: 'token已过期'
|
||||
},
|
||||
theme: {
|
||||
themeSchema: {
|
||||
title: '主题模式',
|
||||
light: '亮色模式',
|
||||
dark: '暗黑模式',
|
||||
auto: '跟随系统'
|
||||
},
|
||||
grayscale: '灰色模式',
|
||||
colourWeakness: '色弱模式',
|
||||
layoutMode: {
|
||||
title: '布局模式',
|
||||
vertical: '左侧菜单模式',
|
||||
'vertical-mix': '左侧菜单混合模式',
|
||||
horizontal: '顶部菜单模式',
|
||||
'horizontal-mix': '顶部菜单混合模式',
|
||||
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
|
||||
},
|
||||
recommendColor: '应用推荐算法的颜色',
|
||||
recommendColorDesc: '推荐颜色的算法参照',
|
||||
themeColor: {
|
||||
title: '主题颜色',
|
||||
primary: '主色',
|
||||
info: '信息色',
|
||||
success: '成功色',
|
||||
warning: '警告色',
|
||||
error: '错误色',
|
||||
followPrimary: '跟随主色'
|
||||
},
|
||||
scrollMode: {
|
||||
title: '滚动模式',
|
||||
wrapper: '外层滚动',
|
||||
content: '主体滚动'
|
||||
},
|
||||
page: {
|
||||
animate: '页面切换动画',
|
||||
mode: {
|
||||
title: '页面切换动画类型',
|
||||
'fade-slide': '滑动',
|
||||
fade: '淡入淡出',
|
||||
'fade-bottom': '底部消退',
|
||||
'fade-scale': '缩放消退',
|
||||
'zoom-fade': '渐变',
|
||||
'zoom-out': '闪现',
|
||||
none: '无'
|
||||
}
|
||||
},
|
||||
fixedHeaderAndTab: '固定头部和标签栏',
|
||||
header: {
|
||||
height: '头部高度',
|
||||
breadcrumb: {
|
||||
visible: '显示面包屑',
|
||||
showIcon: '显示面包屑图标'
|
||||
}
|
||||
},
|
||||
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: '刷新页面'
|
||||
tabs: {
|
||||
appearance: '外观',
|
||||
layout: '布局',
|
||||
general: '通用'
|
||||
},
|
||||
appearance: {
|
||||
themeSchema: {
|
||||
title: '主题模式',
|
||||
light: '亮色模式',
|
||||
dark: '暗黑模式',
|
||||
auto: '跟随系统'
|
||||
},
|
||||
grayscale: '灰色模式',
|
||||
colourWeakness: '色弱模式',
|
||||
themeColor: {
|
||||
title: '主题颜色',
|
||||
primary: '主色',
|
||||
info: '信息色',
|
||||
success: '成功色',
|
||||
warning: '警告色',
|
||||
error: '错误色',
|
||||
followPrimary: '跟随主色'
|
||||
},
|
||||
recommendColor: '应用推荐算法的颜色',
|
||||
recommendColorDesc: '推荐颜色的算法参照'
|
||||
},
|
||||
layout: {
|
||||
layoutMode: {
|
||||
title: '布局模式',
|
||||
vertical: '左侧菜单模式',
|
||||
'vertical-mix': '左侧菜单混合模式',
|
||||
horizontal: '顶部菜单模式',
|
||||
'horizontal-mix': '顶部菜单混合模式',
|
||||
reverseHorizontalMix: '一级菜单与子级菜单位置反转'
|
||||
},
|
||||
tab: {
|
||||
title: '标签栏设置',
|
||||
visible: '显示标签栏',
|
||||
cache: '标签栏信息缓存',
|
||||
height: '标签栏高度',
|
||||
mode: {
|
||||
title: '标签栏风格',
|
||||
chrome: '谷歌风格',
|
||||
button: '按钮风格'
|
||||
}
|
||||
},
|
||||
header: {
|
||||
title: '头部设置',
|
||||
height: '头部高度',
|
||||
breadcrumb: {
|
||||
visible: '显示面包屑',
|
||||
showIcon: '显示面包屑图标'
|
||||
}
|
||||
},
|
||||
sider: {
|
||||
title: '侧边栏设置',
|
||||
inverted: '深色侧边栏',
|
||||
width: '侧边栏宽度',
|
||||
collapsedWidth: '侧边栏折叠宽度',
|
||||
mixWidth: '混合布局侧边栏宽度',
|
||||
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
|
||||
mixChildMenuWidth: '混合布局子菜单宽度'
|
||||
},
|
||||
footer: {
|
||||
title: '底部设置',
|
||||
visible: '显示底部',
|
||||
fixed: '固定底部',
|
||||
height: '底部高度',
|
||||
right: '底部局右'
|
||||
},
|
||||
content: {
|
||||
title: '内容区域设置',
|
||||
scrollMode: {
|
||||
title: '滚动模式',
|
||||
wrapper: '外层滚动',
|
||||
content: '主体滚动'
|
||||
},
|
||||
page: {
|
||||
animate: '页面切换动画',
|
||||
mode: {
|
||||
title: '页面切换动画类型',
|
||||
'fade-slide': '滑动',
|
||||
fade: '淡入淡出',
|
||||
'fade-bottom': '底部消退',
|
||||
'fade-scale': '缩放消退',
|
||||
'zoom-fade': '渐变',
|
||||
'zoom-out': '闪现',
|
||||
none: '无'
|
||||
}
|
||||
},
|
||||
fixedHeaderAndTab: '固定头部和标签栏'
|
||||
},
|
||||
resetCacheStrategy: {
|
||||
title: '重置缓存策略',
|
||||
close: '关闭页面',
|
||||
refresh: '刷新页面'
|
||||
}
|
||||
},
|
||||
general: {
|
||||
watermark: {
|
||||
visible: '显示全屏水印',
|
||||
text: '水印文本',
|
||||
enableUserName: '启用用户名水印'
|
||||
},
|
||||
multilingual: {
|
||||
title: '多语言设置',
|
||||
visible: '显示多语言按钮'
|
||||
},
|
||||
globalSearch: {
|
||||
title: '全局搜索设置',
|
||||
visible: '显示全局搜索按钮'
|
||||
}
|
||||
},
|
||||
configOperation: {
|
||||
copyConfig: '复制配置',
|
||||
|
@@ -10,27 +10,23 @@ export function setupAppErrorHandle(app: App) {
|
||||
};
|
||||
}
|
||||
|
||||
// Update check interval in milliseconds
|
||||
const UPDATE_CHECK_INTERVAL = 3 * 60 * 1000;
|
||||
|
||||
export function setupAppVersionNotification() {
|
||||
const canAutoUpdateApp = import.meta.env.VITE_AUTOMATICALLY_DETECT_UPDATE === 'Y';
|
||||
// Update check interval in milliseconds
|
||||
const UPDATE_CHECK_INTERVAL = 3 * 60 * 1000;
|
||||
|
||||
const canAutoUpdateApp = import.meta.env.VITE_AUTOMATICALLY_DETECT_UPDATE === 'Y' && import.meta.env.PROD;
|
||||
if (!canAutoUpdateApp) return;
|
||||
|
||||
let isShow = false;
|
||||
let updateInterval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
// Check if updates should be checked
|
||||
const shouldCheckForUpdates = [!isShow, document.visibilityState === 'visible', !import.meta.env.DEV].every(Boolean);
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
if (!shouldCheckForUpdates) return;
|
||||
if (isShow) return;
|
||||
|
||||
const buildTime = await getHtmlBuildTime();
|
||||
|
||||
// If build time hasn't changed, no update is needed
|
||||
if (buildTime === BUILD_TIME) {
|
||||
// If failed to get build time or build time hasn't changed, no update is needed.
|
||||
if (!buildTime || buildTime === BUILD_TIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,6 +43,7 @@ export function setupAppVersionNotification() {
|
||||
{
|
||||
onClick() {
|
||||
n?.destroy();
|
||||
isShow = false;
|
||||
}
|
||||
},
|
||||
() => $t('system.updateCancel')
|
||||
@@ -77,7 +74,7 @@ export function setupAppVersionNotification() {
|
||||
};
|
||||
|
||||
// If updates should be checked, set up the visibility change listener and start the update interval
|
||||
if (shouldCheckForUpdates) {
|
||||
if (!isShow && document.visibilityState === 'visible') {
|
||||
// Check for updates when the document is visible
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
@@ -91,16 +88,21 @@ export function setupAppVersionNotification() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getHtmlBuildTime() {
|
||||
async function getHtmlBuildTime(): Promise<string | null> {
|
||||
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 buildTime = match?.[1] || '';
|
||||
|
||||
return buildTime;
|
||||
const html = await res.text();
|
||||
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
||||
return match?.[1] || null;
|
||||
} catch (error) {
|
||||
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 */
|
||||
export function setupIconifyOffline() {
|
||||
@@ -6,7 +6,5 @@ export function setupIconifyOffline() {
|
||||
|
||||
if (VITE_ICONIFY_URL) {
|
||||
addAPIProvider('', { resources: [VITE_ICONIFY_URL] });
|
||||
|
||||
disableCache('all');
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,22 @@
|
||||
// @unocss-include
|
||||
import { getRgb } from '@sa/color';
|
||||
import { $t } from '@/locales';
|
||||
import { DARK_CLASS } from '@/constants/app';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { toggleHtmlClass } from '@/utils/common';
|
||||
import systemLogo from '@/assets/svg-icon/logo.svg?raw';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function setupLoading() {
|
||||
const themeColor = localStg.get('themeColor') || '#646cff';
|
||||
|
||||
const darkMode = localStg.get('darkMode') || false;
|
||||
const { r, g, b } = getRgb(themeColor);
|
||||
|
||||
const primaryColor = `--primary-color: ${r} ${g} ${b}`;
|
||||
|
||||
if (darkMode) {
|
||||
toggleHtmlClass(DARK_CLASS).add();
|
||||
}
|
||||
|
||||
const loadingClasses = [
|
||||
'left-0 top-0',
|
||||
'left-0 bottom-0 animate-delay-500',
|
||||
@@ -27,14 +33,14 @@ export function setupLoading() {
|
||||
.join('\n');
|
||||
|
||||
const loading = `
|
||||
<div class="fixed-center flex-col" style="${primaryColor}">
|
||||
<div class="fixed-center flex-col bg-layout" style="${primaryColor}">
|
||||
${logoWithClass}
|
||||
<div class="w-56px h-56px my-36px">
|
||||
<div class="relative h-full animate-spin">
|
||||
${dot}
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-28px font-500 text-#646464">${$t('system.title')}</h2>
|
||||
<h2 class="text-28px font-500 text-primary">${$t('system.title')}</h2>
|
||||
</div>`;
|
||||
|
||||
const app = document.getElementById('app');
|
||||
|
@@ -6,10 +6,10 @@ import type {
|
||||
Router
|
||||
} from 'vue-router';
|
||||
import type { RouteKey, RoutePath } from '@elegant-router/types';
|
||||
import { getRouteName } from '@/router/elegant/transform';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getRouteName } from '@/router/elegant/transform';
|
||||
|
||||
/**
|
||||
* create route guard
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import type { Router } from 'vue-router';
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function createDocumentTitleGuard(router: Router) {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { $t } from '@/locales';
|
||||
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { breakpointsTailwind, useBreakpoints, useEventListener, useTitle } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { router } from '@/router';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { $t, setLocale } from '@/locales';
|
||||
import { setDayjsLocale } from '@/locales/dayjs';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { useRouteStore } from '../route';
|
||||
import { useTabStore } from '../tab';
|
||||
import { useThemeStore } from '../theme';
|
||||
|
@@ -2,10 +2,10 @@ import { computed, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { fetchGetUserInfo, fetchLogin } from '@/service/api';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouteStore } from '../route';
|
||||
import { useTabStore } from '../tab';
|
||||
@@ -13,6 +13,7 @@ import { clearAuthStorage, getToken } from './shared';
|
||||
|
||||
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const routeStore = useRouteStore();
|
||||
const tabStore = useTabStore();
|
||||
const { toLogin, redirectFromLogin } = useRouterPush(false);
|
||||
@@ -39,7 +40,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
/** Reset auth store */
|
||||
async function resetStore() {
|
||||
const authStore = useAuthStore();
|
||||
recordUserId();
|
||||
|
||||
clearAuthStorage();
|
||||
|
||||
@@ -53,6 +54,41 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
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
|
||||
*
|
||||
@@ -69,15 +105,21 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const pass = await loginByToken(loginToken);
|
||||
|
||||
if (pass) {
|
||||
await redirectFromLogin(redirect);
|
||||
// Check if the tab needs to be cleared
|
||||
const isClear = checkTabClear();
|
||||
let needRedirect = redirect;
|
||||
|
||||
if (routeStore.isInitAuthRoute) {
|
||||
window.$notification?.success({
|
||||
title: $t('page.login.common.loginSuccess'),
|
||||
content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }),
|
||||
duration: 4500
|
||||
});
|
||||
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({
|
||||
title: $t('page.login.common.loginSuccess'),
|
||||
content: $t('page.login.common.welcomeBack', { userName: userInfo.userName }),
|
||||
duration: 4500
|
||||
});
|
||||
}
|
||||
} else {
|
||||
resetStore();
|
||||
|
@@ -3,12 +3,12 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import type { CustomRoute, ElegantConstRoute, LastLevelRouteKey, RouteKey, RouteMap } from '@elegant-router/types';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { router } from '@/router';
|
||||
import { fetchGetConstantRoutes, fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { createStaticRoutes, getAuthVueRoutes } from '@/router/routes';
|
||||
import { ROOT_ROUTE } from '@/router/routes/builtin';
|
||||
import { getRouteName, getRoutePath } from '@/router/elegant/transform';
|
||||
import { fetchGetConstantRoutes, fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api';
|
||||
import { useAuthStore } from '../auth';
|
||||
import { useTabStore } from '../tab';
|
||||
import {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { RouteLocationNormalizedLoaded, RouteRecordRaw, _RouteRecordBase } from 'vue-router';
|
||||
import type { ElegantConstRoute, LastLevelRouteKey, RouteKey, RouteMap } from '@elegant-router/types';
|
||||
import { $t } from '@/locales';
|
||||
import { useSvgIcon } from '@/hooks/common/icon';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
/**
|
||||
* Filter auth routes by roles
|
||||
|
@@ -1,16 +1,15 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import { router } from '@/router';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { useThemeStore } from '../theme';
|
||||
import {
|
||||
extractTabsByAllRoutes,
|
||||
filterTabsById,
|
||||
filterTabsByIds,
|
||||
findTabByRouteName,
|
||||
getAllTabs,
|
||||
@@ -96,23 +95,26 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
* @param tabId Tab id
|
||||
*/
|
||||
async function removeTab(tabId: string) {
|
||||
const removeTabIndex = tabs.value.findIndex(tab => tab.id === tabId);
|
||||
if (removeTabIndex === -1) return;
|
||||
|
||||
const removedTabRouteKey = tabs.value[removeTabIndex].routeKey;
|
||||
const isRemoveActiveTab = activeTabId.value === tabId;
|
||||
const updatedTabs = filterTabsById(tabId, tabs.value);
|
||||
|
||||
function update() {
|
||||
tabs.value = updatedTabs;
|
||||
// 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);
|
||||
|
||||
// if current tab is removed, then switch to next tab
|
||||
if (isRemoveActiveTab && nextTab) {
|
||||
await switchRouteByTab(nextTab);
|
||||
}
|
||||
|
||||
if (!isRemoveActiveTab) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTab = updatedTabs.at(-1) || homeTab.value;
|
||||
|
||||
if (activeTab) {
|
||||
await switchRouteByTab(activeTab);
|
||||
update();
|
||||
// reset route cache if cache strategy is close
|
||||
if (themeStore.resetCacheStrategy === 'close') {
|
||||
routeStore.resetRouteCache(removedTabRouteKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +142,26 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
*/
|
||||
async function clearTabs(excludes: string[] = []) {
|
||||
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[] = [];
|
||||
|
||||
if (themeStore.resetCacheStrategy === 'close') {
|
||||
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);
|
||||
// filterTabsByIds returns tabs NOT in removedTabsIds, so these are the tabs that will remain
|
||||
const updatedTabs = filterTabsByIds(removedTabsIds, tabs.value);
|
||||
|
||||
function update() {
|
||||
@@ -151,13 +170,40 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
|
||||
if (!isRemoveActiveTab) {
|
||||
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) {
|
||||
routeStore.resetRouteCache(routeKey);
|
||||
}
|
||||
}
|
||||
|
||||
await switchRouteByTab(activeTab);
|
||||
update();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
/**
|
||||
* Replace tab
|
||||
*
|
||||
* @param key Route key
|
||||
* @param options Router push options
|
||||
*/
|
||||
async function replaceTab(key: RouteKey, options?: App.Global.RouterPushOptions) {
|
||||
const oldTabId = activeTabId.value;
|
||||
|
||||
// push new route
|
||||
await routerPushByKey(key, options);
|
||||
|
||||
// remove old tab (exclude fixed tab)
|
||||
if (!isTabRetain(oldTabId)) {
|
||||
await removeTab(oldTabId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,6 +328,7 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
removeTab,
|
||||
removeActiveTab,
|
||||
removeTabByRouteName,
|
||||
replaceTab,
|
||||
clearTabs,
|
||||
clearLeftTabs,
|
||||
clearRightTabs,
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useEventListener, usePreferredColorScheme } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import { getPaletteColorByNumber } from '@sa/color';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import {
|
||||
addThemeVarsToGlobal,
|
||||
createThemeToken,
|
||||
@@ -174,6 +174,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||
darkMode,
|
||||
val => {
|
||||
toggleCssDarkMode(val);
|
||||
localStg.set('darkMode', val);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||
import { defu } from 'defu';
|
||||
import { addColorAlpha, getColorPalette, getPaletteColorByNumber, getRgb } from '@sa/color';
|
||||
import { overrideThemeSettings, themeSettings } from '@/theme/settings';
|
||||
import { themeVars } from '@/theme/vars';
|
||||
import { DARK_CLASS } from '@/constants/app';
|
||||
import { toggleHtmlClass } from '@/utils/common';
|
||||
import { localStg } from '@/utils/storage';
|
||||
|
||||
const DARK_CLASS = 'dark';
|
||||
import { overrideThemeSettings, themeSettings } from '@/theme/settings';
|
||||
import { themeVars } from '@/theme/vars';
|
||||
|
||||
/** Init theme settings */
|
||||
export function initThemeSettings() {
|
||||
|
@@ -10,4 +10,5 @@ body,
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
color: rgb(var(--base-text-color));
|
||||
}
|
||||
|
@@ -113,7 +113,8 @@ code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,12 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
breadcrumb: {
|
||||
visible: true,
|
||||
showIcon: true
|
||||
},
|
||||
multilingual: {
|
||||
visible: true
|
||||
},
|
||||
globalSearch: {
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
@@ -52,7 +58,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
},
|
||||
watermark: {
|
||||
visible: false,
|
||||
text: 'SoybeanAdmin'
|
||||
text: 'SoybeanAdmin',
|
||||
enableUserName: false
|
||||
},
|
||||
tokens: {
|
||||
light: {
|
||||
|
135
src/typings/app.d.ts
vendored
135
src/typings/app.d.ts
vendored
@@ -53,6 +53,15 @@ declare namespace App {
|
||||
/** Whether to show the breadcrumb icon */
|
||||
showIcon: boolean;
|
||||
};
|
||||
/** Multilingual */
|
||||
multilingual: {
|
||||
/** Whether to show the multilingual */
|
||||
visible: boolean;
|
||||
};
|
||||
globalSearch: {
|
||||
/** Whether to show the GlobalSearch */
|
||||
visible: boolean;
|
||||
};
|
||||
};
|
||||
/** Tab */
|
||||
tab: {
|
||||
@@ -103,6 +112,8 @@ declare namespace App {
|
||||
visible: boolean;
|
||||
/** Watermark text */
|
||||
text: string;
|
||||
/** Whether to use user name as watermark text */
|
||||
enableUserName: boolean;
|
||||
};
|
||||
/** define some theme settings tokens, will transform to css variables */
|
||||
tokens: {
|
||||
@@ -170,6 +181,12 @@ declare namespace App {
|
||||
type RoutePath = import('@elegant-router/types').RoutePath;
|
||||
type LastLevelRouteKey = import('@elegant-router/types').LastLevelRouteKey;
|
||||
|
||||
/** The router push options */
|
||||
type RouterPushOptions = {
|
||||
query?: Record<string, string>;
|
||||
params?: Record<string, string>;
|
||||
};
|
||||
|
||||
/** The global header props */
|
||||
interface HeaderProps {
|
||||
/** Whether to show the logo */
|
||||
@@ -341,56 +358,82 @@ declare namespace App {
|
||||
tokenExpired: string;
|
||||
};
|
||||
theme: {
|
||||
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
|
||||
grayscale: string;
|
||||
colourWeakness: string;
|
||||
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
|
||||
recommendColor: string;
|
||||
recommendColorDesc: string;
|
||||
themeColor: {
|
||||
title: string;
|
||||
followPrimary: string;
|
||||
} & Theme.ThemeColor;
|
||||
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
|
||||
page: {
|
||||
animate: string;
|
||||
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
|
||||
themeDrawerTitle: string;
|
||||
tabs: {
|
||||
appearance: string;
|
||||
layout: string;
|
||||
general: string;
|
||||
};
|
||||
fixedHeaderAndTab: string;
|
||||
header: {
|
||||
height: string;
|
||||
breadcrumb: {
|
||||
appearance: {
|
||||
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
|
||||
grayscale: string;
|
||||
colourWeakness: string;
|
||||
themeColor: {
|
||||
title: string;
|
||||
followPrimary: string;
|
||||
} & Theme.ThemeColor;
|
||||
recommendColor: string;
|
||||
recommendColorDesc: string;
|
||||
};
|
||||
layout: {
|
||||
layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
|
||||
tab: {
|
||||
title: string;
|
||||
visible: string;
|
||||
cache: string;
|
||||
height: string;
|
||||
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
|
||||
};
|
||||
header: {
|
||||
title: string;
|
||||
height: string;
|
||||
breadcrumb: {
|
||||
visible: string;
|
||||
showIcon: string;
|
||||
};
|
||||
};
|
||||
sider: {
|
||||
title: string;
|
||||
inverted: string;
|
||||
width: string;
|
||||
collapsedWidth: string;
|
||||
mixWidth: string;
|
||||
mixCollapsedWidth: string;
|
||||
mixChildMenuWidth: string;
|
||||
};
|
||||
footer: {
|
||||
title: string;
|
||||
visible: string;
|
||||
fixed: string;
|
||||
height: string;
|
||||
right: string;
|
||||
};
|
||||
content: {
|
||||
title: string;
|
||||
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
|
||||
page: {
|
||||
animate: string;
|
||||
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
|
||||
};
|
||||
fixedHeaderAndTab: string;
|
||||
};
|
||||
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
|
||||
};
|
||||
general: {
|
||||
watermark: {
|
||||
visible: string;
|
||||
text: string;
|
||||
enableUserName: string;
|
||||
};
|
||||
multilingual: {
|
||||
title: string;
|
||||
visible: string;
|
||||
};
|
||||
globalSearch: {
|
||||
title: string;
|
||||
visible: string;
|
||||
showIcon: string;
|
||||
};
|
||||
};
|
||||
tab: {
|
||||
visible: string;
|
||||
cache: string;
|
||||
height: string;
|
||||
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
|
||||
};
|
||||
sider: {
|
||||
inverted: string;
|
||||
width: string;
|
||||
collapsedWidth: string;
|
||||
mixWidth: string;
|
||||
mixCollapsedWidth: string;
|
||||
mixChildMenuWidth: string;
|
||||
};
|
||||
footer: {
|
||||
visible: string;
|
||||
fixed: string;
|
||||
height: string;
|
||||
right: string;
|
||||
};
|
||||
watermark: {
|
||||
visible: string;
|
||||
text: string;
|
||||
};
|
||||
themeDrawerTitle: string;
|
||||
pageFunTitle: string;
|
||||
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
|
||||
configOperation: {
|
||||
copyConfig: string;
|
||||
copySuccessMsg: string;
|
||||
|
2
src/typings/common.d.ts
vendored
2
src/typings/common.d.ts
vendored
@@ -14,7 +14,7 @@ declare namespace CommonType {
|
||||
* @property value: The option value
|
||||
* @property label: The option label
|
||||
*/
|
||||
type Option<K = string> = { value: K; label: string };
|
||||
type Option<K = string, M = string> = { value: K; label: M };
|
||||
|
||||
type YesOrNo = 'Y' | 'N';
|
||||
|
||||
|
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
|
4
src/typings/storage.d.ts
vendored
4
src/typings/storage.d.ts
vendored
@@ -20,6 +20,8 @@ declare namespace StorageType {
|
||||
refreshToken: string;
|
||||
/** The theme color */
|
||||
themeColor: string;
|
||||
/** The dark mode */
|
||||
darkMode: boolean;
|
||||
/** The theme settings */
|
||||
themeSettings: App.Theme.ThemeSetting;
|
||||
/**
|
||||
@@ -35,5 +37,7 @@ declare namespace StorageType {
|
||||
layout: UnionKey.ThemeLayoutMode;
|
||||
siderCollapse: boolean;
|
||||
};
|
||||
/** The last login user id */
|
||||
lastLoginUserId: string;
|
||||
}
|
||||
}
|
||||
|
7
src/typings/vite-env.d.ts
vendored
7
src/typings/vite-env.d.ts
vendored
@@ -8,6 +8,7 @@ declare namespace Env {
|
||||
type RouterHistoryMode = 'hash' | 'history' | 'memory';
|
||||
|
||||
/** Interface for import.meta */
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
interface ImportMeta extends ImportMetaEnv {
|
||||
/** The base url of the application */
|
||||
readonly VITE_BASE_URL: string;
|
||||
@@ -24,7 +25,7 @@ declare namespace Env {
|
||||
*
|
||||
* This prefix is start with the icon prefix
|
||||
*/
|
||||
readonly VITE_ICON_LOCAL_PREFIX: 'local-icon';
|
||||
readonly VITE_ICON_LOCAL_PREFIX: 'icon-local';
|
||||
/** backend service base url */
|
||||
readonly VITE_SERVICE_BASE_URL: string;
|
||||
/**
|
||||
@@ -105,6 +106,10 @@ declare namespace Env {
|
||||
readonly VITE_STORAGE_PREFIX?: string;
|
||||
/** Whether to automatically detect updates after configuring application packaging */
|
||||
readonly VITE_AUTOMATICALLY_DETECT_UPDATE?: CommonType.YesOrNo;
|
||||
/** show proxy url log in terminal */
|
||||
readonly VITE_PROXY_LOG?: CommonType.YesOrNo;
|
||||
/** The launch editor */
|
||||
readonly VITE_DEVTOOLS_LAUNCH_EDITOR?: import('vite-plugin-vue-devtools').VitePluginVueDevToolsOptions['launchEditor'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -22,7 +22,7 @@ export function transformRecordToOption<T extends Record<string, string>>(record
|
||||
return Object.entries(record).map(([value, label]) => ({
|
||||
value,
|
||||
label
|
||||
})) as CommonType.Option<keyof T>[];
|
||||
})) as CommonType.Option<keyof T, T[keyof T]>[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,10 +30,10 @@ export function transformRecordToOption<T extends Record<string, string>>(record
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
export function translateOptions(options: CommonType.Option<string>[]) {
|
||||
export function translateOptions(options: CommonType.Option<string, App.I18n.I18nKey>[]) {
|
||||
return options.map(option => ({
|
||||
...option,
|
||||
label: $t(option.label as App.I18n.I18nKey)
|
||||
label: $t(option.label)
|
||||
}));
|
||||
}
|
||||
|
||||
|
@@ -1,19 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { onActivated, onMounted } from 'vue';
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
onMounted(() => {
|
||||
console.log('mounted');
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
console.log('activated');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -2,10 +2,10 @@
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { getPaletteColorByNumber, mixColor } from '@sa/color';
|
||||
import { $t } from '@/locales';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { $t } from '@/locales';
|
||||
import PwdLogin from './modules/pwd-login.vue';
|
||||
import CodeLogin from './modules/code-login.vue';
|
||||
import Register from './modules/register.vue';
|
||||
@@ -66,6 +66,7 @@ const bgColor = computed(() => {
|
||||
@switch="themeStore.toggleThemeScheme"
|
||||
/>
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
:lang="appStore.locale"
|
||||
:lang-options="appStore.localeOptions"
|
||||
:show-tooltip="false"
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { useCaptcha } from '@/hooks/business/captcha';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'CodeLogin'
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'PwdLogin'
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { useCaptcha } from '@/hooks/business/captcha';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'Register'
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ResetPwd'
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'HeaderBanner'
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'LineChart'
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'PieChart'
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from '@unocss/vite';
|
||||
import transformerDirectives from '@unocss/transformer-directives';
|
||||
import transformerVariantGroup from '@unocss/transformer-variant-group';
|
||||
import presetUno from '@unocss/preset-uno';
|
||||
import presetWind3 from '@unocss/preset-wind3';
|
||||
import type { Theme } from '@unocss/preset-uno';
|
||||
import { presetSoybeanAdmin } from '@sa/uno-preset';
|
||||
import { themeVars } from './src/theme/vars';
|
||||
@@ -26,5 +26,5 @@ export default defineConfig<Theme>({
|
||||
'card-wrapper': 'rd-8px shadow-sm'
|
||||
},
|
||||
transformers: [transformerDirectives(), transformerVariantGroup()],
|
||||
presets: [presetUno({ dark: 'class' }), presetSoybeanAdmin()]
|
||||
presets: [presetWind3({ dark: 'class' }), presetSoybeanAdmin()]
|
||||
});
|
||||
|
Reference in New Issue
Block a user