Compare commits

..

17 Commits

Author SHA1 Message Date
Soybean
232e1ac40d chore(deps): update deps 2025-12-25 18:54:42 +08:00
Soybean
5aac540a4c feat(logo): use new logo 2025-12-25 18:52:14 +08:00
Soybean
5e40b85018 chore(projects): release v2.0.2 2025-12-23 11:19:18 +08:00
Soybean
ec9f9af9a2 chore(deps): update deps 2025-12-22 23:17:03 +08:00
Azir-11
62a43c3957 fix(projects): fix the incorrect judgment of home by pin tab. 2025-12-04 16:45:11 +08:00
Azir-11
64226d9bb8 fix(hooks): update pagination pageSize after data fetch. 2025-12-04 15:50:34 +08:00
Soybean
e675474b43 chore(projects): release v2.0.1 2025-12-04 14:07:46 +08:00
Soybean
098cd50e23 chore(styles): format code 2025-12-04 14:07:03 +08:00
Soybean
7cf4083b22 chore(deps): update deps 2025-12-04 14:06:44 +08:00
paynezhuang
9401925f41 feat(projects): hybrid layout mode auto select first deepest child menu 2025-12-04 14:01:07 +08:00
hooke
b8a767d704 feat(projects): support pinning and unpinning of tabs 2025-12-04 13:59:01 +08:00
Azir-11
605173a1cc feat(projects): support theme perset to override component library presets. 2025-12-02 20:13:51 +08:00
Azir-11
73e9a0fe0b chore(other): remove Prettier's recommendation. 2025-12-02 20:13:51 +08:00
Azir-11
c6d97dba21 optimize(projects): simplify some theme preset configurations. 2025-11-29 21:47:39 +08:00
Azir-11
9da847fb6f feat(projects): support theme presets to only set partial content. 2025-11-29 21:47:39 +08:00
Azir-11
c472a94395 docs(projects): add link to ecosystem document. 2025-11-24 20:29:56 +08:00
Azir-11
91a261c1ef style(projects): modify homepage prompt title to tip. 2025-11-24 19:41:43 +08:00
42 changed files with 1746 additions and 1185 deletions

View File

@@ -5,7 +5,6 @@
"antfu.unocss",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"lokalise.i18n-ally",
"mhutchie.git-graph",
"mikestead.dotenv",

View File

@@ -1,6 +1,66 @@
# Changelog
## [v2.0.2](https://github.com/soybeanjs/soybean-admin/compare/v2.0.1...v2.0.2) (2025-12-23)
###    🐞 Bug Fixes
- **hooks**: update pagination pageSize after data fetch. &nbsp;-&nbsp; by **Azir-11** [<samp>(64226)</samp>](https://github.com/soybeanjs/soybean-admin/commit/64226d9b)
- **projects**: fix the incorrect judgment of home by pin tab. &nbsp;-&nbsp; by **Azir-11** [<samp>(62a43)</samp>](https://github.com/soybeanjs/soybean-admin/commit/62a43c39)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**: update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(ec9f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ec9f9af9)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;
[Azir-11](mailto:2075125282@qq.com)
## [v2.0.1](https://github.com/soybeanjs/soybean-admin/compare/v2.0.0...v2.0.1) (2025-12-04)
### &nbsp;&nbsp;&nbsp;🚀 Features
- **docs**:
- update QQ group image in README &nbsp;-&nbsp; by @soybeanjs [<samp>(46081)</samp>](https://github.com/soybeanjs/soybean-admin/commit/46081c36)
- **projects**:
- support theme presets to only set partial content. &nbsp;-&nbsp; by **Azir-11** [<samp>(9da84)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9da847fb)
- support theme perset to override component library presets. &nbsp;-&nbsp; by **Azir-11** [<samp>(60517)</samp>](https://github.com/soybeanjs/soybean-admin/commit/605173a1)
- support pinning and unpinning of tabs &nbsp;-&nbsp; by **hooke** [<samp>(b8a76)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b8a767d7)
- hybrid layout mode auto select first deepest child menu &nbsp;-&nbsp; by @paynezhuang [<samp>(94019)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9401925f)
### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes
- **docs**: update project name in ecosystem section of README &nbsp;-&nbsp; by @soybeanjs [<samp>(bb232)</samp>](https://github.com/soybeanjs/soybean-admin/commit/bb232bf8)
- **types**: add missing property in theme presets &nbsp;-&nbsp; by **刘璐** [<samp>(4a9cf)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4a9cf6c3)
### &nbsp;&nbsp;&nbsp;🛠 Optimizations
- **projects**: simplify some theme preset configurations. &nbsp;-&nbsp; by **Azir-11** [<samp>(c6d97)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c6d97dba)
### &nbsp;&nbsp;&nbsp;📖 Documentation
- **projects**: add link to ecosystem document. &nbsp;-&nbsp; by **Azir-11** [<samp>(c472a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c472a943)
### &nbsp;&nbsp;&nbsp;🏡 Chore
- **deps**:
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(f8dc6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f8dc639e)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(7cf40)</samp>](https://github.com/soybeanjs/soybean-admin/commit/7cf4083b)
- **other**:
- remove Prettier's recommendation. &nbsp;-&nbsp; by **Azir-11** [<samp>(73e9a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/73e9a0fe)
- **styles**:
- format code &nbsp;-&nbsp; by @soybeanjs [<samp>(098cd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/098cd50e)
### &nbsp;&nbsp;&nbsp;🎨 Styles
- **projects**: modify homepage prompt title to tip. &nbsp;-&nbsp; by **Azir-11** [<samp>(91a26)</samp>](https://github.com/soybeanjs/soybean-admin/commit/91a261c1)
### &nbsp;&nbsp;&nbsp;❤️ Contributors
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![paynezhuang](https://github.com/paynezhuang.png?size=48)](https://github.com/paynezhuang)&nbsp;&nbsp;
[hooke](mailto:hellohooke@foxmail.com),&nbsp;[Azir-11](mailto:2075125282@qq.com),&nbsp;[刘璐](mailto:hi.alue@qq.com)
## [v2.0.0](https://github.com/soybeanjs/soybean-admin/compare/v1.3.15...v2.0.0) (2025-11-02)
### &nbsp;&nbsp;&nbsp;🚨 Breaking Changes

View File

@@ -151,6 +151,8 @@ Refer to the [Code Synchronization](https://docs.soybeanjs.cn/guide/sync) docume
- [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.
More ecosystem please refer to [Ecosystem](https://docs.soybeanjs.cn/awesome) document.
## How to Contribute

View File

@@ -177,6 +177,8 @@ pnpm build
- [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分支,适配动态路由,接口鉴权限。
更多周边生态请翻阅 [周边生态](https://docs.soybeanjs.cn/zh/awesome) 文档。
## 如何贡献

View File

@@ -1,7 +1,7 @@
{
"name": "soybean-admin",
"type": "module",
"version": "2.0.0",
"version": "2.0.2",
"description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
"author": {
"name": "Soybean",
@@ -54,53 +54,53 @@
"@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*",
"@sa/utils": "workspace:*",
"@vueuse/core": "14.0.0",
"@vueuse/core": "14.1.0",
"clipboard": "2.0.11",
"dayjs": "1.11.19",
"defu": "6.1.4",
"echarts": "6.0.0",
"json5": "2.2.3",
"naive-ui": "2.43.1",
"naive-ui": "2.43.2",
"nprogress": "0.2.0",
"pinia": "3.0.4",
"tailwind-merge": "3.4.0",
"vue": "3.5.24",
"vue": "3.5.26",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.12",
"vue-router": "4.6.3"
"vue-i18n": "11.2.7",
"vue-router": "4.6.4"
},
"devDependencies": {
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.407",
"@iconify/json": "2.2.417",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.3",
"@types/node": "24.10.1",
"@soybeanjs/eslint-config": "1.7.5",
"@types/node": "25.0.3",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.5.6",
"@unocss/preset-icons": "66.5.6",
"@unocss/preset-uno": "66.5.6",
"@unocss/transformer-directives": "66.5.6",
"@unocss/transformer-variant-group": "66.5.6",
"@unocss/vite": "66.5.6",
"@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "5.1.1",
"@unocss/eslint-config": "66.5.10",
"@unocss/preset-icons": "66.5.10",
"@unocss/preset-uno": "66.5.10",
"@unocss/transformer-directives": "66.5.10",
"@unocss/transformer-variant-group": "66.5.10",
"@unocss/vite": "66.5.10",
"@vitejs/plugin-vue": "6.0.3",
"@vitejs/plugin-vue-jsx": "5.1.3",
"consola": "3.4.2",
"eslint": "9.39.1",
"eslint-plugin-vue": "10.5.1",
"eslint": "9.39.2",
"eslint-plugin-vue": "10.6.2",
"kolorist": "1.8.0",
"sass": "1.94.0",
"sass": "1.97.1",
"simple-git-hooks": "2.13.1",
"tsx": "4.20.6",
"tsx": "4.21.0",
"typescript": "5.9.3",
"unplugin-icons": "22.5.0",
"unplugin-vue-components": "30.0.0",
"vite": "7.2.2",
"vite": "7.3.0",
"vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "8.0.3",
"vite-plugin-vue-devtools": "8.0.5",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.4"
"vue-tsc": "3.2.1"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/alova",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
".": "./src/index.ts",
"./fetch": "./src/fetch.ts",
@@ -13,8 +13,8 @@
}
},
"dependencies": {
"@alova/mock": "2.0.17",
"@alova/mock": "2.0.18",
"@sa/utils": "workspace:*",
"alova": "3.3.4"
"alova": "3.4.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/axios",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
".": "./src/index.ts"
},

View File

@@ -119,8 +119,11 @@ export type FlatResponseData<ResponseData, ApiData> =
| FlatResponseSuccessData<ResponseData, ApiData>
| FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
extends RequestInstanceCommon<State> {
export interface FlatRequestInstance<
ResponseData,
ApiData,
State extends Record<string, unknown>
> extends RequestInstanceCommon<State> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/color",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
".": "./src/index.ts"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/hooks",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
".": "./src/index.ts"
},

View File

@@ -26,8 +26,11 @@ export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
extends RequestInstanceCommon<State> {
export interface HookRequestInstance<
ResponseData,
ApiData,
State extends Record<string, unknown>
> extends RequestInstanceCommon<State> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/materials",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
".": "./src/index.ts"
},

View File

@@ -146,7 +146,8 @@ export type LayoutScrollMode = 'wrapper' | 'content';
/** Admin layout props */
export interface AdminLayoutProps
extends AdminLayoutHeaderConfig,
extends
AdminLayoutHeaderConfig,
AdminLayoutTabConfig,
AdminLayoutSiderConfig,
AdminLayoutContentConfig,

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/scripts",
"version": "2.0.0",
"version": "2.0.2",
"bin": {
"sa": "./bin.ts"
},
@@ -13,16 +13,16 @@
}
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.25",
"bumpp": "10.3.1",
"c12": "3.3.2",
"@soybeanjs/changelog": "0.4.3",
"bumpp": "10.3.2",
"c12": "3.3.3",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",
"execa": "9.6.0",
"execa": "9.6.1",
"kolorist": "1.8.0",
"npm-check-updates": "19.1.2",
"npm-check-updates": "19.2.0",
"picomatch": "4.0.3",
"rimraf": "6.1.0"
"rimraf": "6.1.2"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/uno-preset",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
".": "./src/index.ts"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@sa/utils",
"version": "2.0.0",
"version": "2.0.2",
"exports": {
".": "./src/index.ts"
},

1943
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,60 @@
<svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><path d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" fill="#646cff"/><path d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" fill="#646cff"/></svg>
<svg width="100%" height="100%" version="1.1" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<path
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient)" fill-rule="nonzero" opacity="1" stroke="none" />
<path
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient_2)" fill-rule="nonzero" opacity="1" stroke="none" />
<path d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
fill="url(#LinearGradient_3)" fill-rule="nonzero" opacity="1" stroke="none" />
<path d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
fill="url(#LinearGradient_4)" fill-rule="nonzero" opacity="1" stroke="none" />
<path d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
fill="url(#LinearGradient_5)" fill-rule="nonzero" opacity="1" stroke="none" />
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
fill="url(#LinearGradient_6)" fill-rule="nonzero" opacity="1" stroke="none" />
<path
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
fill="url(#LinearGradient_7)" fill-rule="nonzero" opacity="1" stroke="none" />
</g>
<defs>
<linearGradient gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
gradientUnits="userSpaceOnUse" id="LinearGradient" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#373ebf" />
<stop offset="1" stop-color="#5058e6" />
</linearGradient>
<linearGradient gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
gradientUnits="userSpaceOnUse" id="LinearGradient_2" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#c2d6ff" />
<stop offset="1" stop-color="#646cff" />
</linearGradient>
<linearGradient gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
gradientUnits="userSpaceOnUse" id="LinearGradient_3" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#5058e6" />
<stop offset="1" stop-color="#373ebf" />
</linearGradient>
<linearGradient gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
gradientUnits="userSpaceOnUse" id="LinearGradient_4" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#91a7ff" />
<stop offset="1" stop-color="#5058e6" />
</linearGradient>
<linearGradient gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
gradientUnits="userSpaceOnUse" id="LinearGradient_5" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#646cff" />
<stop offset="1" stop-color="#c2d6ff" />
</linearGradient>
<linearGradient gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
gradientUnits="userSpaceOnUse" id="LinearGradient_6" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#c2d6ff" />
<stop offset="1" stop-color="#646cff" />
</linearGradient>
<linearGradient gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
gradientUnits="userSpaceOnUse" id="LinearGradient_7" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#373ebf" />
<stop offset="1" stop-color="#5058e6" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,9 +1,160 @@
<script lang="ts" setup>
defineOptions({ name: 'SystemLogo' });
</script>
<template>
<icon-local-logo />
<div class="app-logo">
<svg
width="100%"
height="100%"
version="1.1"
viewBox="0 0 1000 1000"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g>
<path
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient_2)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
fill="url(#LinearGradient_3)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
fill="url(#LinearGradient_4)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
fill="url(#LinearGradient_5)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
fill="url(#LinearGradient_6)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
fill="url(#LinearGradient_7)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
</g>
<defs>
<linearGradient
id="LinearGradient"
gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-700)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
<linearGradient
id="LinearGradient_2"
gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-300)" />
<stop offset="1" stop-color="var(--logo-color-500)" />
</linearGradient>
<linearGradient
id="LinearGradient_3"
gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-600)" />
<stop offset="1" stop-color="var(--logo-color-700)" />
</linearGradient>
<linearGradient
id="LinearGradient_4"
gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-400)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
<linearGradient
id="LinearGradient_5"
gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-500)" />
<stop offset="1" stop-color="var(--logo-color-300)" />
</linearGradient>
<linearGradient
id="LinearGradient_6"
gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-300)" />
<stop offset="1" stop-color="var(--logo-color-500)" />
</linearGradient>
<linearGradient
id="LinearGradient_7"
gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-700)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
</defs>
</svg>
</div>
</template>
<style scoped></style>
<style scoped>
.app-logo {
--logo-color-300: rgb(var(--primary-300-color));
--logo-color-400: rgb(var(--primary-400-color));
--logo-color-500: rgb(var(--primary-500-color));
--logo-color-600: rgb(var(--primary-600-color));
--logo-color-700: rgb(var(--primary-700-color));
}
</style>

View File

@@ -131,6 +131,7 @@ export function useNaivePaginatedTable<ResponseData, ApiData>(
getColumns,
onFetched: data => {
pagination.itemCount = data.total;
pagination.pageSize = data.pageSize;
}
});

View File

@@ -17,7 +17,7 @@ withDefaults(defineProps<Props>(), {
<template>
<RouterLink to="/" class="w-full flex-center nowrap-hidden">
<SystemLogo class="text-32px text-primary" />
<SystemLogo class="size-32px" />
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold transition duration-300 ease-in-out">
{{ $t('system.title') }}
</h2>

View File

@@ -3,6 +3,7 @@ import { useRoute } from 'vue-router';
import { useContext } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { useRouteStore } from '@/store/modules/route';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu', useMixMenu);
@@ -10,6 +11,7 @@ export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu',
function useMixMenu() {
const route = useRoute();
const routeStore = useRouteStore();
const themeStore = useThemeStore();
const { selectedKey } = useMenu();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
@@ -100,10 +102,46 @@ function useMixMenu() {
() => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
);
const hasChildLevelMenus = computed(() => childLevelMenus.value.length > 0);
function getDeepestLevelMenuKey(): RouteKey | null {
if (!secondLevelMenus.value.length || !themeStore.sider.autoSelectFirstMenu) {
return null;
}
const secondLevelFirstMenu = secondLevelMenus.value[0];
if (!secondLevelFirstMenu) {
return null;
}
function findDeepest(menu: App.Global.Menu): RouteKey {
if (!menu.children?.length) {
return menu.routeKey;
}
return findDeepest(menu.children[0]);
}
return findDeepest(secondLevelFirstMenu);
}
function activeDeepestLevelMenuKey() {
const deepestLevelMenuKey = getDeepestLevelMenuKey();
if (!deepestLevelMenuKey) return;
// select the deepest second level menu
handleSelectSecondLevelMenu(deepestLevelMenuKey);
}
watch(
() => route.name,
() => {
getActiveFirstLevelMenuKey();
// if there are child level menus, get the active second level menu key
if (hasChildLevelMenus.value) {
getActiveSecondLevelMenuKey();
}
},
{ immediate: true }
);
@@ -121,7 +159,10 @@ function useMixMenu() {
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
childLevelMenus,
hasChildLevelMenus,
getDeepestLevelMenuKey,
activeDeepestLevelMenuKey
};
}

View File

@@ -2,6 +2,7 @@
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials';
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
@@ -18,12 +19,28 @@ const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
useMixMenuContext('TopHybridHeaderFirst');
const {
firstLevelMenus,
secondLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
activeDeepestLevelMenuKey
} = useMixMenuContext('TopHybridHeaderFirst');
const { selectedKey } = useMenu();
const expandedKeys = ref<string[]>([]);
/**
* Handle first level menu select
* @param key RouteKey
*/
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);
// if there are second level menus, select the deepest one by default
activeDeepestLevelMenuKey();
}
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
@@ -49,7 +66,7 @@ watch(
:options="firstLevelMenus"
:indent="18"
responsive
@update:value="handleSelectFirstLevelMenu"
@update:value="handleSelectMenu"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
@@ -13,9 +14,25 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
useMixMenuContext('TopHybridSidebarFirst');
const {
firstLevelMenus,
secondLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
activeDeepestLevelMenuKey
} = useMixMenuContext('TopHybridSidebarFirst');
const { selectedKey } = useMenu();
/**
* Handle first level menu select
* @param key RouteKey
*/
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);
// if there are second level menus, select the deepest one by default
activeDeepestLevelMenuKey();
}
</script>
<template>
@@ -37,7 +54,7 @@ const { selectedKey } = useMenu();
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectFirstLevelMenu"
@select="handleSelectMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
/>
</div>

View File

@@ -33,15 +33,15 @@ const {
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
childLevelMenus,
hasChildLevelMenus,
activeDeepestLevelMenuKey
} = useMixMenuContext('VerticalHybridHeaderFirst');
const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
const showDrawer = computed(() => hasChildLevelMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(key: RouteKey) {
handleSelectSecondLevelMenu(key);
@@ -51,12 +51,33 @@ function handleSelectMixMenu(key: RouteKey) {
}
}
/**
* Handle second level menu selection based on autoSelectFirstMenu setting:
* - When disabled: Activate first second-level menu for display only, expand third-level menu if exists
* - When enabled: Navigate to the deepest menu automatically
*/
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);
if (secondLevelMenus.value.length > 0) {
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
if (secondLevelMenus.value.length === 0) return;
const secondFirstMenuKey = secondLevelMenus.value[0].routeKey;
// Case 1: autoSelectFirstMenu disabled - only activate menu for display
if (!themeStore.sider.autoSelectFirstMenu) {
// Check if there are third-level menus
const hasChildren = secondLevelMenus.value.find(menu => menu.key === secondFirstMenuKey)?.children?.length;
// If there are third-level menus, expand them
if (hasChildren) {
handleSelectMixMenu(secondFirstMenuKey);
}
return;
}
// Case 2: autoSelectFirstMenu enabled - navigate to deepest menu
activeDeepestLevelMenuKey();
setDrawerVisible(false);
}
function handleResetActiveMenu() {
@@ -114,7 +135,9 @@ watch(
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
:style="{
width: appStore.mixSiderFixed && hasChildLevelMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px'
}"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"

View File

@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
const visible = defineModel<boolean>('visible');
const { removeTab, clearTabs, clearLeftTabs, clearRightTabs } = useTabStore();
const { removeTab, clearTabs, clearLeftTabs, clearRightTabs, fixTab, unfixTab, isTabRetain, homeTab } = useTabStore();
const { SvgIconVNode } = useSvgIcon();
type DropdownOption = {
@@ -64,6 +64,23 @@ const options = computed(() => {
icon: SvgIconVNode({ icon: 'ant-design:line-outlined', fontSize: 18 })
}
];
if (props.tabId !== homeTab?.id) {
if (isTabRetain(props.tabId)) {
opts.push({
key: 'unpin',
label: $t('dropdown.unpin'),
icon: SvgIconVNode({ icon: 'mdi:pin-off-outline', fontSize: 18 })
});
} else {
opts.push({
key: 'pin',
label: $t('dropdown.pin'),
icon: SvgIconVNode({ icon: 'mdi:pin-outline', fontSize: 18 })
});
}
}
const { excludeKeys, disabledKeys } = props;
const result = opts.filter(opt => !excludeKeys.includes(opt.key));
@@ -98,6 +115,12 @@ const dropdownAction: Record<App.Global.DropdownKey, () => void> = {
},
closeAll() {
clearTabs();
},
pin() {
fixTab(props.tabId);
},
unpin() {
unfixTab(props.tabId);
}
};

View File

@@ -12,6 +12,7 @@ const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layoutMode.value.includes('hybrid'));
const isHybridLayoutMode = computed(() => layoutMode.value.includes('hybrid'));
</script>
<template>
@@ -32,6 +33,12 @@ const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layou
<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>
<SettingItem v-if="isHybridLayoutMode" key="6" :label="$t('theme.layout.sider.autoSelectFirstMenu')">
<template #suffix>
<IconTooltip :desc="$t('theme.layout.sider.autoSelectFirstMenuTip')" />
</template>
<NSwitch v-model:value="themeStore.sider.autoSelectFirstMenu" />
</SettingItem>
</TransitionGroup>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { defu } from 'defu';
import { useThemeStore } from '@/store/modules/theme';
import { themeSettings } from '@/theme/settings';
import { $t } from '@/locales';
defineOptions({
@@ -31,6 +33,8 @@ type ThemePreset = Pick<
desc: string;
i18nkey?: string;
version: string;
/** Optional NaiveUI theme overrides */
naiveui?: App.Theme.NaiveUIThemeOverride;
};
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
@@ -76,7 +80,9 @@ const getPresetDesc = (preset: ThemePreset): string => {
}
};
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
const applyPreset = (preset: ThemePreset): void => {
const mergedPreset = defu(preset, themeSettings);
const { themeScheme, grayscale, colourWeakness, layout, watermark, naiveui, ...rest } = mergedPreset;
themeStore.setThemeScheme(themeScheme);
themeStore.setGrayscale(grayscale);
themeStore.setColourWeakness(colourWeakness);
@@ -96,6 +102,9 @@ const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark
tokens: { ...rest.tokens }
});
// Apply NaiveUI theme overrides if present
themeStore.setNaiveThemeOverrides(naiveui);
window.$message?.success($t('theme.appearance.preset.applySuccess'));
};
</script>

View File

@@ -160,7 +160,10 @@ const local: App.I18n.Schema = {
collapsedWidth: 'Sider Collapsed Width',
mixWidth: 'Mix Sider Width',
mixCollapsedWidth: 'Mix Sider Collapse Width',
mixChildMenuWidth: 'Mix Child Menu Width'
mixChildMenuWidth: 'Mix Child Menu Width',
autoSelectFirstMenu: 'Auto Select First Submenu',
autoSelectFirstMenuTip:
'When a first-level menu is clicked, the first submenu is automatically selected and navigated to the deepest level'
},
footer: {
title: 'Footer Settings',
@@ -336,7 +339,9 @@ const local: App.I18n.Schema = {
closeOther: 'Close Other',
closeLeft: 'Close Left',
closeRight: 'Close Right',
closeAll: 'Close All'
closeAll: 'Close All',
pin: 'Pin Tab',
unpin: 'Unpin Tab'
},
icon: {
themeConfig: 'Theme Configuration',

View File

@@ -157,7 +157,9 @@ const local: App.I18n.Schema = {
collapsedWidth: '侧边栏折叠宽度',
mixWidth: '混合布局侧边栏宽度',
mixCollapsedWidth: '混合布局侧边栏折叠宽度',
mixChildMenuWidth: '混合布局子菜单宽度'
mixChildMenuWidth: '混合布局子菜单宽度',
autoSelectFirstMenu: '自动选择第一个子菜单',
autoSelectFirstMenuTip: '点击一级菜单时,自动选择并导航到第一个子菜单的最深层级'
},
footer: {
title: '底部设置',
@@ -333,7 +335,9 @@ const local: App.I18n.Schema = {
closeOther: '关闭其它',
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeAll: '关闭所有'
closeAll: '关闭所有',
pin: '固定标签',
unpin: '取消固定'
},
icon: {
themeConfig: '主题配置',

View File

@@ -1,18 +1,25 @@
// @unocss-include
import { getRgb } from '@sa/color';
import { getColorPalette, getRgb } from '@sa/color';
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 palette = getColorPalette(themeColor);
const { r, g, b } = getRgb(themeColor);
const primaryColor = `--primary-color: ${r} ${g} ${b}`;
const svgCssVars = Array.from(palette.entries())
.map(([key, value]) => `--logo-color-${key}: ${value}`)
.join(';');
const cssVars = `${primaryColor}; ${svgCssVars}`;
if (darkMode) {
toggleHtmlClass(DARK_CLASS).add();
}
@@ -24,8 +31,6 @@ export function setupLoading() {
'right-0 bottom-0 animate-delay-1500'
];
const logoWithClass = systemLogo.replace('<svg', `<svg class="size-128px text-primary"`);
const dot = loadingClasses
.map(item => {
return `<div class="absolute w-16px h-16px bg-primary rounded-8px animate-pulse ${item}"></div>`;
@@ -33,8 +38,10 @@ export function setupLoading() {
.join('\n');
const loading = `
<div class="fixed-center flex-col bg-layout" style="${primaryColor}">
${logoWithClass}
<div class="fixed-center flex-col bg-layout" style="${cssVars}">
<div class="w-128px h-128px">
${getLogoSvg()}
</div>
<div class="w-56px h-56px my-36px">
<div class="relative h-full animate-spin">
${dot}
@@ -49,3 +56,155 @@ export function setupLoading() {
app.innerHTML = loading;
}
}
function getLogoSvg() {
const logoSvg = `<svg
width="100%"
height="100%"
version="1.1"
viewBox="0 0 1000 1000"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g>
<path
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient_2)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
fill="url(#LinearGradient_3)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
fill="url(#LinearGradient_4)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
fill="url(#LinearGradient_5)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
fill="url(#LinearGradient_6)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
fill="url(#LinearGradient_7)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
</g>
<defs>
<linearGradient
id="LinearGradient"
gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-700)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
<linearGradient
id="LinearGradient_2"
gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-300)" />
<stop offset="1" stop-color="var(--logo-color-500)" />
</linearGradient>
<linearGradient
id="LinearGradient_3"
gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-600)" />
<stop offset="1" stop-color="var(--logo-color-700)" />
</linearGradient>
<linearGradient
id="LinearGradient_4"
gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-400)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
<linearGradient
id="LinearGradient_5"
gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-500)" />
<stop offset="1" stop-color="var(--logo-color-300)" />
</linearGradient>
<linearGradient
id="LinearGradient_6"
gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-300)" />
<stop offset="1" stop-color="var(--logo-color-500)" />
</linearGradient>
<linearGradient
id="LinearGradient_7"
gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-700)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
</defs>
</svg>
`;
return logoSvg;
}

View File

@@ -18,6 +18,7 @@ import {
getTabByRoute,
getTabIdByRoute,
isTabInTabs,
reorderFixedTabs,
updateTabByI18nKey,
updateTabsByI18nKey
} from './shared';
@@ -248,6 +249,48 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
await clearTabs(excludes);
}
/**
* Fix tab
*
* @param tabId
*/
function fixTab(tabId: string) {
const tabIndex = tabs.value.findIndex(t => t.id === tabId);
if (tabIndex === -1) return;
const tab = tabs.value[tabIndex];
const fixedCount = getFixedTabIds(tabs.value).length;
tab.fixedIndex = fixedCount;
if (tabIndex !== fixedCount) {
tabs.value.splice(tabIndex, 1);
tabs.value.splice(fixedCount, 0, tab);
}
reorderFixedTabs(tabs.value);
}
/**
* Unfix tab
*
* @param tabId
*/
function unfixTab(tabId: string) {
const tabIndex = tabs.value.findIndex(t => t.id === tabId);
if (tabIndex === -1) return;
const tab = tabs.value[tabIndex];
tab.fixedIndex = undefined;
const fixedCount = getFixedTabIds(tabs.value).length;
if (tabIndex !== fixedCount) {
tabs.value.splice(tabIndex, 1);
tabs.value.splice(fixedCount, 0, tab);
}
reorderFixedTabs(tabs.value);
}
/**
* Set new label of tab
*
@@ -318,6 +361,7 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
/** All tabs */
tabs: allTabs,
activeTabId,
homeTab,
initHomeTab,
initTabStore,
addTab,
@@ -328,6 +372,8 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
clearTabs,
clearLeftTabs,
clearRightTabs,
fixTab,
unfixTab,
switchRouteByTab,
setTabLabel,
resetTabLabel,

View File

@@ -198,6 +198,18 @@ export function getFixedTabIds(tabs: App.Global.Tab[]) {
return fixedTabs.map(tab => tab.id);
}
/**
* Reorder fixed tabs fixedIndex
*
* @param tabs
*/
export function reorderFixedTabs(tabs: App.Global.Tab[]) {
const fixedTabs = getFixedTabs(tabs);
fixedTabs.forEach((t, i) => {
t.fixedIndex = i;
});
}
/**
* Update tabs label
*

View File

@@ -24,6 +24,9 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
/** Theme settings */
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
/** Optional NaiveUI theme overrides from preset */
const naiveThemeOverrides: Ref<App.Theme.NaiveUIThemeOverride | undefined> = ref(undefined);
/** Watermark time instance with controls */
const { now: watermarkTime, pause: pauseWatermarkTime, resume: resumeWatermarkTime } = useNow({ controls: true });
@@ -53,7 +56,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
});
/** Naive theme */
const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value));
const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value, naiveThemeOverrides.value));
/**
* Settings json
@@ -198,6 +201,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
}
}
/**
* Set NaiveUI theme overrides
*
* @param overrides NaiveUI theme overrides or undefined to clear
*/
function setNaiveThemeOverrides(overrides?: App.Theme.NaiveUIThemeOverride) {
naiveThemeOverrides.value = overrides;
}
/** Only run timer when watermark is visible and time display is enabled */
function updateWatermarkTimer() {
const { watermark } = settings.value;
@@ -284,6 +296,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
updateThemeColors,
setThemeLayout,
setWatermarkEnableUserName,
setWatermarkEnableTime
setWatermarkEnableTime,
setNaiveThemeOverrides
};
});

View File

@@ -236,11 +236,15 @@ function getNaiveThemeColors(colors: App.Theme.ThemeColor, recommended = false)
/**
* Get naive theme
*
* @param settings Theme settings object.
* @param settings.recommendColor Whether to use recommended color palette.
* @param settings.themeRadius Border radius to use in the theme (in px).
* @param colors Theme colors
* @param settings Theme settings object
* @param overrides Optional manual overrides from preset
*/
export function getNaiveTheme(colors: App.Theme.ThemeColor, settings: App.Theme.ThemeSetting) {
export function getNaiveTheme(
colors: App.Theme.ThemeColor,
settings: App.Theme.ThemeSetting,
overrides?: GlobalThemeOverrides
) {
const { primary: colorLoading } = colors;
const theme: GlobalThemeOverrides = {
@@ -256,5 +260,7 @@ export function getNaiveTheme(colors: App.Theme.ThemeColor, settings: App.Theme.
}
};
return theme;
// If there are overrides, merge them with priority
// overrides has higher priority than auto-generated theme
return overrides ? defu(overrides, theme) : theme;
}

View File

@@ -2,10 +2,8 @@
"name": "Azir's Preset",
"desc": "It is a cold and elegant preset that Azir likes",
"i18nkey": "theme.appearance.preset.azir",
"version": "1.0.0",
"version": "1.0.1",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": true,
"themeColor": "#78a878",
"otherColor": {
@@ -14,58 +12,7 @@
"warning": "#d4bb9d",
"error": "#c49a9a"
},
"themeRadius": 6,
"isInfoFollowPrimary": true,
"layout": {
"mode": "vertical-mix",
"scrollMode": "wrapper"
},
"page": {
"animate": true,
"animateMode": "zoom-fade"
},
"header": {
"height": 64,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 48,
"mode": "chrome",
"closeTabByMiddleClick": false
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": true,
"height": 56,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": true,
"timeFormat": "YYYY-MM-DD HH:mm:ss"
},
"tokens": {
"light": {
"colors": {
@@ -87,5 +34,19 @@
"base-text": "rgb(224, 224, 224)"
}
}
},
"naiveui": {
"Alert": {
"borderRadiusMedium": "12px",
"fontWeightStrong": "600",
"paddingMedium": "0 20px"
},
"Card": {
"borderRadius": "16px",
"paddingMedium": "24px"
},
"Input": {
"borderRadius": "10px"
}
}
}

View File

@@ -2,33 +2,12 @@
"name": "Compact Preset",
"desc": "Compact layout preset for small screens",
"i18nkey": "theme.appearance.preset.compact",
"version": "1.0.0",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#646cff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"version": "1.0.1",
"themeRadius": 6,
"isInfoFollowPrimary": true,
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": {
"height": 48,
"breadcrumb": {
"visible": true,
"showIcon": true
"visible": false
},
"multilingual": {
"visible": false
@@ -44,7 +23,6 @@
"mode": "button",
"closeTabByMiddleClick": false
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 180,
@@ -54,38 +32,6 @@
"mixChildMenuWidth": 180
},
"footer": {
"visible": false,
"fixed": false,
"height": 40,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": false,
"timeFormat": "YYYY-MM-DD HH:mm"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
"visible": false
}
}

View File

@@ -2,12 +2,12 @@
"name": "Dark Preset",
"desc": "Dark theme preset for night time usage",
"i18nkey": "theme.appearance.preset.dark",
"version": "1.0.0",
"version": "1.0.1",
"themeScheme": "dark",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#409eff",
"themeColor": "#646cff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
@@ -46,7 +46,7 @@
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": true,
"inverted": false,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,

View File

@@ -48,7 +48,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
collapsedWidth: 64,
mixWidth: 90,
mixCollapsedWidth: 64,
mixChildMenuWidth: 200
mixChildMenuWidth: 200,
autoSelectFirstMenu: false
},
footer: {
visible: true,

View File

@@ -4,6 +4,9 @@ declare namespace App {
namespace Theme {
type ColorPaletteNumber = import('@sa/color').ColorPaletteNumber;
/** NaiveUI theme overrides that can be specified in preset */
type NaiveUIThemeOverride = import('naive-ui').GlobalThemeOverrides;
/** Theme setting */
interface ThemeSetting {
/** Theme scheme */
@@ -93,6 +96,8 @@ declare namespace App {
mixCollapsedWidth: number;
/** Child menu width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or 'top-hybrid-header-first' */
mixChildMenuWidth: number;
/** Whether to auto select the first submenu */
autoSelectFirstMenu: boolean;
};
/** Footer */
footer: {
@@ -279,7 +284,7 @@ declare namespace App {
type FormRule = import('naive-ui').FormItemRule;
/** The global dropdown key */
type DropdownKey = 'closeCurrent' | 'closeOther' | 'closeLeft' | 'closeRight' | 'closeAll';
type DropdownKey = 'closeCurrent' | 'closeOther' | 'closeLeft' | 'closeRight' | 'closeAll' | 'pin' | 'unpin';
}
/**
@@ -426,6 +431,8 @@ declare namespace App {
mixWidth: string;
mixCollapsedWidth: string;
mixChildMenuWidth: string;
autoSelectFirstMenu: string;
autoSelectFirstMenuTip: string;
};
footer: {
title: string;

View File

@@ -56,7 +56,7 @@ const bgColor = computed(() => {
<NCard :bordered="false" class="relative z-4 w-auto rd-12px">
<div class="w-400px lt-sm:w-300px">
<header class="flex-y-center justify-between">
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
<SystemLogo class="size-64px lt-sm:size-48px" />
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
<div class="i-flex-col">
<ThemeSchemaSwitch

View File

@@ -15,7 +15,7 @@ const gap = computed(() => (appStore.isMobile ? 0 : 16));
<template>
<NSpace vertical :size="16">
<NAlert :title="$t('common.warning')" type="warning">
<NAlert :title="$t('common.tip')" type="warning">
{{ $t('page.home.branchDesc') }}
</NAlert>
<HeaderBanner />