Merge branch 'main' into example

This commit is contained in:
Soybean
2025-12-04 14:10:30 +08:00
36 changed files with 1812 additions and 1422 deletions

View File

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

View File

@@ -1,6 +1,50 @@
# Changelog # Changelog
## [v2.0.1](https://github.com/soybeanjs/soybean-admin/compare/v2.0.0...v2.0.1) (2025-12-04)
###    🚀 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) ## [v2.0.0](https://github.com/soybeanjs/soybean-admin/compare/v1.3.15...v2.0.0) (2025-11-02)
### &nbsp;&nbsp;&nbsp;🚨 Breaking Changes ### &nbsp;&nbsp;&nbsp;🚨 Breaking Changes

View File

@@ -139,7 +139,7 @@ Refer to the [Code Synchronization](https://docs.soybeanjs.cn/guide/sync) docume
## Ecosystem ## Ecosystem
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): SoybeanAdmin based version of React. - [skyroc-admin](https://github.com/Ohh-889/skyroc-admin): SoybeanAdmin's React version implementation.
- [electron-mock-admin](https://github.com/lixin59/electron-mock-api): A Mock Api management system that helps front-end developers quickly implement interface mocks. - [electron-mock-admin](https://github.com/lixin59/electron-mock-api): A Mock Api management system that helps front-end developers quickly implement interface mocks.
- [T-Shell](https://github.com/TheBlindM/T-Shell): A terminal emulator and SSH client with configurable command prompts. - [T-Shell](https://github.com/TheBlindM/T-Shell): A terminal emulator and SSH client with configurable command prompts.
- [pea](https://github.com/haitang1894/pea) : Adopting SpringBoot3.2 + JDK21, MyBatis-Plus, SpringSecurity security framework, etc., suitable for the simple permission system developed by [soybean-admin](https://gitee.com/honghuangdc/soybean-admin). - [pea](https://github.com/haitang1894/pea) : Adopting SpringBoot3.2 + JDK21, MyBatis-Plus, SpringSecurity security framework, etc., suitable for the simple permission system developed by [soybean-admin](https://gitee.com/honghuangdc/soybean-admin).
@@ -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. - [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. - [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 ## How to Contribute
@@ -194,7 +196,7 @@ Here are the most active contributors from the past year. Thank you all for your
<div> <div>
<p>QQ Group</p> <p>QQ Group</p>
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-4.jpg" style="width:200px" /> <img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-5.jpg" style="width:200px" />
</div> </div>
<!-- <div> <!-- <div>
<p>WeChat Group</p> <p>WeChat Group</p>

View File

@@ -165,7 +165,7 @@ pnpm build
## 周边生态 ## 周边生态
- [react-soybean-admin](https://github.com/mufeng889/react-soybean-admin): 基于SoybeanAdmin的React版本. - [skyroc-admin](https://github.com/Ohh-889/skyroc-admin): SoybeanAdmin的React版本实现.
- [electron-mock-admin](https://github.com/lixin59/electron-mock-api): 一个 Mock Api 管理系统,帮助前端开发伙伴快速实现接口的 mock。 - [electron-mock-admin](https://github.com/lixin59/electron-mock-api): 一个 Mock Api 管理系统,帮助前端开发伙伴快速实现接口的 mock。
- [T-Shell](https://github.com/TheBlindM/T-Shell): 是一个可配置命令提示的终端模拟器和 SSH 客户端。 - [T-Shell](https://github.com/TheBlindM/T-Shell): 是一个可配置命令提示的终端模拟器和 SSH 客户端。
- [pea](https://github.com/haitang1894/pea) : 采用SpringBoot3.2 + JDK21、MyBatis-Plus、SpringSecurity安全框架等适配 [soybean-admin](https://gitee.com/honghuangdc/soybean-admin) 开发的简单权限系统。 - [pea](https://github.com/haitang1894/pea) : 采用SpringBoot3.2 + JDK21、MyBatis-Plus、SpringSecurity安全框架等适配 [soybean-admin](https://gitee.com/honghuangdc/soybean-admin) 开发的简单权限系统。
@@ -177,6 +177,8 @@ pnpm build
- [ba](https://github.com/xiatianYa/Ba-Server): 基于goFrame框架开发的后端服务对接soybean-admin,适配动态路由,接口鉴权限。 - [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分支,适配动态路由,接口鉴权限。 - [soybean-admin-go](https://github.com/WgoW/soybean-admin-go):基于gin+gorm框架开发的go语言后端服务对接soybean-admin的example分支,适配动态路由,接口鉴权限。
更多周边生态请翻阅 [周边生态](https://docs.soybeanjs.cn/zh/awesome) 文档。
## 如何贡献 ## 如何贡献
@@ -222,7 +224,7 @@ pnpm build
<div> <div>
<p>QQ交流群</p> <p>QQ交流群</p>
<img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-4.jpg" style="width:200px" /> <img src="https://soybeanjs-1300612522.cos.ap-guangzhou.myqcloud.com/uPic/qq-soybean-admin-5.jpg" style="width:200px" />
</div> </div>
<!-- <div> <!-- <div>
<p>微信群</p> <p>微信群</p>

View File

@@ -1,7 +1,7 @@
{ {
"name": "soybean-admin", "name": "soybean-admin",
"type": "module", "type": "module",
"version": "2.0.0", "version": "2.0.1",
"description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。", "description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
"author": { "author": {
"name": "Soybean", "name": "Soybean",
@@ -48,7 +48,7 @@
}, },
"dependencies": { "dependencies": {
"@antv/data-set": "0.11.8", "@antv/data-set": "0.11.8",
"@antv/g2": "5.4.2", "@antv/g2": "5.4.6",
"@antv/g6": "5.0.50", "@antv/g6": "5.0.50",
"@better-scroll/core": "2.5.1", "@better-scroll/core": "2.5.1",
"@iconify/vue": "5.0.0", "@iconify/vue": "5.0.0",
@@ -58,34 +58,34 @@
"@sa/hooks": "workspace:*", "@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*", "@sa/materials": "workspace:*",
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"@visactor/vchart": "2.0.7", "@visactor/vchart": "2.0.10",
"@visactor/vchart-theme": "1.12.2", "@visactor/vchart-theme": "1.12.2",
"@visactor/vtable-editors": "1.22.2", "@visactor/vtable-editors": "1.22.6",
"@visactor/vtable-gantt": "1.22.2", "@visactor/vtable-gantt": "1.22.6",
"@visactor/vue-vtable": "1.22.2", "@visactor/vue-vtable": "1.22.6",
"@vueuse/components": "14.0.0", "@vueuse/components": "14.1.0",
"@vueuse/core": "14.0.0", "@vueuse/core": "14.1.0",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"dayjs": "1.11.19", "dayjs": "1.11.19",
"defu": "6.1.4", "defu": "6.1.4",
"dhtmlx-gantt": "9.0.15", "dhtmlx-gantt": "9.1.0",
"dompurify": "3.3.0", "dompurify": "3.3.0",
"echarts": "6.0.0", "echarts": "6.0.0",
"jsbarcode": "3.12.1", "jsbarcode": "3.12.1",
"json5": "2.2.3", "json5": "2.2.3",
"naive-ui": "2.43.1", "naive-ui": "2.43.2",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"pinia": "3.0.3", "pinia": "3.0.4",
"pinyin-pro": "3.27.0", "pinyin-pro": "3.27.0",
"print-js": "1.6.0", "print-js": "1.6.0",
"pro-naive-ui": "3.1.3", "pro-naive-ui": "3.1.4",
"swiper": "12.0.3", "swiper": "12.0.3",
"tailwind-merge": "3.3.1", "tailwind-merge": "3.4.0",
"typeit": "8.8.7", "typeit": "8.8.7",
"vditor": "3.11.2", "vditor": "3.11.2",
"vue": "3.5.22", "vue": "3.5.25",
"vue-draggable-plus": "0.6.0", "vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.12", "vue-i18n": "11.2.2",
"vue-pdf-embed": "2.1.3", "vue-pdf-embed": "2.1.3",
"vue-router": "4.6.3", "vue-router": "4.6.3",
"wangeditor": "4.7.15", "wangeditor": "4.7.15",
@@ -95,39 +95,39 @@
"devDependencies": { "devDependencies": {
"@amap/amap-jsapi-types": "0.0.15", "@amap/amap-jsapi-types": "0.0.15",
"@elegant-router/vue": "0.3.8", "@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.402", "@iconify/json": "2.2.414",
"@sa/scripts": "workspace:*", "@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*", "@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1", "@soybeanjs/eslint-config": "1.7.4",
"@types/bmapgl": "0.0.7", "@types/bmapgl": "0.0.7",
"@types/node": "24.9.2", "@types/node": "24.10.1",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.5.4", "@unocss/eslint-config": "66.5.10",
"@unocss/preset-icons": "66.5.4", "@unocss/preset-icons": "66.5.10",
"@unocss/preset-uno": "66.5.4", "@unocss/preset-uno": "66.5.10",
"@unocss/transformer-directives": "66.5.4", "@unocss/transformer-directives": "66.5.10",
"@unocss/transformer-variant-group": "66.5.4", "@unocss/transformer-variant-group": "66.5.10",
"@unocss/vite": "66.5.4", "@unocss/vite": "66.5.10",
"@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-vue": "6.0.2",
"@vitejs/plugin-vue-jsx": "5.1.1", "@vitejs/plugin-vue-jsx": "5.1.2",
"consola": "3.4.2", "consola": "3.4.2",
"eslint": "9.39.0", "eslint": "9.39.1",
"eslint-plugin-vue": "10.5.1", "eslint-plugin-vue": "10.6.2",
"kolorist": "1.8.0", "kolorist": "1.8.0",
"lint-staged": "16.2.6", "lint-staged": "16.2.7",
"pro-naive-ui-resolver": "1.0.2", "pro-naive-ui-resolver": "1.0.2",
"sass": "1.93.3", "sass": "1.94.2",
"simple-git-hooks": "2.13.1", "simple-git-hooks": "2.13.1",
"tsx": "4.20.6", "tsx": "4.21.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"unplugin-icons": "22.5.0", "unplugin-icons": "22.5.0",
"unplugin-vue-components": "30.0.0", "unplugin-vue-components": "30.0.0",
"vite": "7.1.12", "vite": "7.2.6",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "8.0.3", "vite-plugin-vue-devtools": "8.0.5",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.2" "vue-tsc": "3.1.5"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify", "commit-msg": "pnpm sa git-commit-verify",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/alova", "name": "@sa/alova",
"version": "2.0.0", "version": "2.0.1",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./fetch": "./src/fetch.ts", "./fetch": "./src/fetch.ts",
@@ -15,6 +15,6 @@
"dependencies": { "dependencies": {
"@alova/mock": "2.0.17", "@alova/mock": "2.0.17",
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"alova": "3.3.4" "alova": "3.4.0"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sa/axios", "name": "@sa/axios",
"version": "2.0.0", "version": "2.0.1",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts"
}, },
@@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"axios": "1.13.1", "axios": "1.13.2",
"axios-retry": "4.5.0", "axios-retry": "4.5.0",
"qs": "6.14.0" "qs": "6.14.0"
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2633
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app'; import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
@@ -13,9 +14,25 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = const {
useMixMenuContext('TopHybridSidebarFirst'); firstLevelMenus,
secondLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
activeDeepestLevelMenuKey
} = useMixMenuContext('TopHybridSidebarFirst');
const { selectedKey } = useMenu(); 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> </script>
<template> <template>
@@ -37,7 +54,7 @@ const { selectedKey } = useMenu();
:sider-collapse="appStore.siderCollapse" :sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode" :dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor" :theme-color="themeStore.themeColor"
@select="handleSelectFirstLevelMenu" @select="handleSelectMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse" @toggle-sider-collapse="appStore.toggleSiderCollapse"
/> />
</div> </div>

View File

@@ -33,15 +33,15 @@ const {
isActiveSecondLevelMenuHasChildren, isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu, handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey, getActiveSecondLevelMenuKey,
childLevelMenus childLevelMenus,
hasChildLevelMenus,
activeDeepestLevelMenuKey
} = useMixMenuContext('VerticalHybridHeaderFirst'); } = useMixMenuContext('VerticalHybridHeaderFirst');
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted); const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0); const showDrawer = computed(() => hasChildLevelMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(key: RouteKey) { function handleSelectMixMenu(key: RouteKey) {
handleSelectSecondLevelMenu(key); 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) { function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key); handleSelectFirstLevelMenu(key);
if (secondLevelMenus.value.length > 0) { if (secondLevelMenus.value.length === 0) return;
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
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() { function handleResetActiveMenu() {
@@ -114,7 +135,9 @@ watch(
</FirstLevelMenu> </FirstLevelMenu>
<div <div
class="relative h-full transition-width-300" 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 <DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300" 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 visible = defineModel<boolean>('visible');
const { removeTab, clearTabs, clearLeftTabs, clearRightTabs } = useTabStore(); const { removeTab, clearTabs, clearLeftTabs, clearRightTabs, fixTab, unfixTab, isTabRetain } = useTabStore();
const { SvgIconVNode } = useSvgIcon(); const { SvgIconVNode } = useSvgIcon();
type DropdownOption = { type DropdownOption = {
@@ -64,6 +64,23 @@ const options = computed(() => {
icon: SvgIconVNode({ icon: 'ant-design:line-outlined', fontSize: 18 }) icon: SvgIconVNode({ icon: 'ant-design:line-outlined', fontSize: 18 })
} }
]; ];
if (props.tabId !== '/home') {
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 { excludeKeys, disabledKeys } = props;
const result = opts.filter(opt => !excludeKeys.includes(opt.key)); const result = opts.filter(opt => !excludeKeys.includes(opt.key));
@@ -98,6 +115,12 @@ const dropdownAction: Record<App.Global.DropdownKey, () => void> = {
}, },
closeAll() { closeAll() {
clearTabs(); 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 layoutMode = computed(() => themeStore.layout.mode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layoutMode.value.includes('hybrid')); const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layoutMode.value.includes('hybrid'));
const isHybridLayoutMode = computed(() => layoutMode.value.includes('hybrid'));
</script> </script>
<template> <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')"> <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" /> <NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
</SettingItem> </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> </TransitionGroup>
</template> </template>

View File

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

View File

@@ -160,7 +160,10 @@ const local: App.I18n.Schema = {
collapsedWidth: 'Sider Collapsed Width', collapsedWidth: 'Sider Collapsed Width',
mixWidth: 'Mix Sider Width', mixWidth: 'Mix Sider Width',
mixCollapsedWidth: 'Mix Sider Collapse 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: { footer: {
title: 'Footer Settings', title: 'Footer Settings',
@@ -669,7 +672,9 @@ const local: App.I18n.Schema = {
closeOther: 'Close Other', closeOther: 'Close Other',
closeLeft: 'Close Left', closeLeft: 'Close Left',
closeRight: 'Close Right', closeRight: 'Close Right',
closeAll: 'Close All' closeAll: 'Close All',
pin: 'Pin Tab',
unpin: 'Unpin Tab'
}, },
icon: { icon: {
themeConfig: 'Theme Configuration', themeConfig: 'Theme Configuration',

View File

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

View File

@@ -18,6 +18,7 @@ import {
getTabByRoute, getTabByRoute,
getTabIdByRoute, getTabIdByRoute,
isTabInTabs, isTabInTabs,
reorderFixedTabs,
updateTabByI18nKey, updateTabByI18nKey,
updateTabsByI18nKey updateTabsByI18nKey
} from './shared'; } from './shared';
@@ -248,6 +249,48 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
await clearTabs(excludes); 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 * Set new label of tab
* *
@@ -328,6 +371,8 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
clearTabs, clearTabs,
clearLeftTabs, clearLeftTabs,
clearRightTabs, clearRightTabs,
fixTab,
unfixTab,
switchRouteByTab, switchRouteByTab,
setTabLabel, setTabLabel,
resetTabLabel, resetTabLabel,

View File

@@ -198,6 +198,18 @@ export function getFixedTabIds(tabs: App.Global.Tab[]) {
return fixedTabs.map(tab => tab.id); 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 * Update tabs label
* *

View File

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

View File

@@ -236,11 +236,15 @@ function getNaiveThemeColors(colors: App.Theme.ThemeColor, recommended = false)
/** /**
* Get naive theme * Get naive theme
* *
* @param settings Theme settings object. * @param colors Theme colors
* @param settings.recommendColor Whether to use recommended color palette. * @param settings Theme settings object
* @param settings.themeRadius Border radius to use in the theme (in px). * @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 { primary: colorLoading } = colors;
const theme: GlobalThemeOverrides = { 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", "name": "Azir's Preset",
"desc": "It is a cold and elegant preset that Azir likes", "desc": "It is a cold and elegant preset that Azir likes",
"i18nkey": "theme.appearance.preset.azir", "i18nkey": "theme.appearance.preset.azir",
"version": "1.0.0", "version": "1.0.1",
"themeScheme": "light", "themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": true, "recommendColor": true,
"themeColor": "#78a878", "themeColor": "#78a878",
"otherColor": { "otherColor": {
@@ -14,57 +12,7 @@
"warning": "#d4bb9d", "warning": "#d4bb9d",
"error": "#c49a9a" "error": "#c49a9a"
}, },
"themeRadius": 6,
"isInfoFollowPrimary": true, "isInfoFollowPrimary": true,
"layout": {
"mode": "vertical-mix",
"scrollMode": "wrapper"
},
"page": {
"animate": true,
"animateMode": "zoom-fade"
},
"header": {
"height": 64,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 48,
"mode": "chrome"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": true,
"height": 56,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": true,
"timeFormat": "YYYY-MM-DD HH:mm:ss"
},
"tokens": { "tokens": {
"light": { "light": {
"colors": { "colors": {
@@ -86,5 +34,19 @@
"base-text": "rgb(224, 224, 224)" "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", "name": "Compact Preset",
"desc": "Compact layout preset for small screens", "desc": "Compact layout preset for small screens",
"i18nkey": "theme.appearance.preset.compact", "i18nkey": "theme.appearance.preset.compact",
"version": "1.0.0", "version": "1.0.1",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#646cff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"themeRadius": 6, "themeRadius": 6,
"isInfoFollowPrimary": true,
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": { "header": {
"height": 48, "height": 48,
"breadcrumb": { "breadcrumb": {
"visible": true, "visible": false
"showIcon": true
}, },
"multilingual": { "multilingual": {
"visible": false "visible": false
@@ -41,9 +20,9 @@
"visible": true, "visible": true,
"cache": true, "cache": true,
"height": 36, "height": 36,
"mode": "button" "mode": "button",
"closeTabByMiddleClick": false
}, },
"fixedHeaderAndTab": true,
"sider": { "sider": {
"inverted": false, "inverted": false,
"width": 180, "width": 180,
@@ -53,38 +32,6 @@
"mixChildMenuWidth": 180 "mixChildMenuWidth": 180
}, },
"footer": { "footer": {
"visible": false, "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)"
}
}
} }
} }

View File

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

View File

@@ -41,7 +41,8 @@
"visible": true, "visible": true,
"cache": true, "cache": true,
"height": 44, "height": 44,
"mode": "chrome" "mode": "chrome",
"closeTabByMiddleClick": false
}, },
"fixedHeaderAndTab": true, "fixedHeaderAndTab": true,
"sider": { "sider": {

View File

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

View File

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