11 Commits

Author SHA1 Message Date
003c43140b chore(projects): 更换图标 2025-08-16 13:34:25 +08:00
6850347b89 Merge branch 'master' into tauri 2025-08-16 11:56:14 +08:00
4e625111ce chore(projects): release v1.1.3 2025-08-16 11:14:41 +08:00
AN
8524ae7666 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-08-15 17:49:07 +08:00
AN
d6ae85d218 fix(projects): 修复新增用户未查询角色列表问题 2025-08-15 17:45:11 +08:00
fb5a59d81b Merge branch 'master' into tauri
# Conflicts:
#	package.json
#	pnpm-lock.yaml
2025-07-24 17:49:12 +08:00
8147f1652e chore(release): release tauri v1.1.0 2025-07-01 10:53:41 +08:00
d5be9dc08e Merge branch 'master' into tauri 2025-06-05 22:16:58 +08:00
2f9576f53d Merge branch 'master' into tauri 2025-05-21 11:02:20 +08:00
ba7395ac18 Merge branch 'dev' into tauri 2025-05-17 20:17:20 +08:00
d49728a796 feat: 新增 tauri 适配 2025-05-16 17:50:23 +08:00
204 changed files with 26328 additions and 2640 deletions

View File

@ -1,5 +1,44 @@
# 更新日志 # 更新日志
## [v1.1.3](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.2...v1.1.3) (2025-08-16)
###    🐞 Bug 修复
- **hooks**:
- 非安全环境下不使用流式下载 &nbsp;-&nbsp; by @m-xlsea [<samp>(f8983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f8983557)
- 修复oss下载时未转码问题 &nbsp;-&nbsp; by **AN** [<samp>(2d31d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2d31d7dc)
- **project**:
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 &nbsp;-&nbsp; by **wang_rui** [<samp>(b96c4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b96c46ba)
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 Merge pull request !25 from littleghost2016/dev &nbsp;-&nbsp; by **不寻俗** [<samp>(90276)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9027632b)
- **projects**:
- 修复一级菜单隐藏失效问题 &nbsp;-&nbsp; by **AN** [<samp>(8fcc7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8fcc70d7)
- 修复日期搜索条件清除问题 &nbsp;-&nbsp; by **AN** [<samp>(52318)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/52318c10)
- 修复登录过期事件监听未被重置 &nbsp;-&nbsp; by @m-xlsea [<samp>(71037)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/71037439)
- 修复用户新增时角色下拉包含超级管理员问题 &nbsp;-&nbsp; by **AN** [<samp>(a15b6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a15b683b)
- 修复用户导入功能无法更新问题 &nbsp;-&nbsp; by **AN** [<samp>(4e983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e9839bd)
- Fix the icon size in the image preview toolbar &nbsp;-&nbsp; by @m-xlsea [<samp>(4539f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4539fe01)
- 修复新增用户未查询角色列表问题 &nbsp;-&nbsp; by **AN** [<samp>(d6ae8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d6ae85d2)
- **readme**:
- update GitHub stars and forks links for gitee &nbsp;-&nbsp; by @soybeanjs [<samp>(923eb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/923eb98a)
### &nbsp;&nbsp;&nbsp;💅 重构
- **menu**:
- 菜单管理中隐藏的菜单显示灰色 &nbsp;-&nbsp; by **NicholasLD** [<samp>(adca2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/adca2e26)
- 菜单管理中隐藏的菜单显示灰色 Merge pull request !24 from NicholasLD/N/A &nbsp;-&nbsp; by **不寻俗** [<samp>(4eb77)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4eb77eac)
- **projects**:
- 菜单列表新增禁用菜单样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(e5383)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e538355f)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **other**: update the ESLint validation configuration to support more file types. &nbsp;-&nbsp; by **Azir-11** [<samp>(8d7f9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8d7f91dc)
- **readme**: remove DartNode sponsorship badge from README files &nbsp;-&nbsp; by @soybeanjs [<samp>(33ade)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/33ade539)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[![Azir-11](https://github.com/NicholasLD.png?size=48)](https://github.com/NicholasLD)&nbsp;&nbsp;
[wang_rui](mailto:wrr1996@163.com)
## [v1.1.2](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.1...v1.1.2) (2025-07-24) ## [v1.1.2](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.1...v1.1.2) (2025-07-24)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复 ### &nbsp;&nbsp;&nbsp;🐞 Bug 修复

View File

@ -22,7 +22,7 @@
# 📢 重要通知 # 📢 重要通知
1.1.2 版本已经正式发布工作流版本迎来首个版本(请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议: 1.1.3 版本已经正式发布工作流版本请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议:
- 在生产环境使用前进行充分测试 - 在生产环境使用前进行充分测试
- 关注项目更新,及时获取最新版本 - 关注项目更新,及时获取最新版本
- 积极反馈问题,帮助我们快速迭代 - 积极反馈问题,帮助我们快速迭代
@ -40,6 +40,11 @@
<p style="font-weight: bold; font-size: 24px;">后端需要替换代码生成模板与菜单 SQL详细请看 <a href="#代码生成与菜单更新">代码生成与菜单更新</a></p> <p style="font-weight: bold; font-size: 24px;">后端需要替换代码生成模板与菜单 SQL详细请看 <a href="#代码生成与菜单更新">代码生成与菜单更新</a></p>
# 💎 友情链接
- [Snail Job Pro](https://pro.snailjob.opensnail.com/home) - 灵活,可靠和快速的分布式任务重试和分布式任务调度平台
- [AiZuDa - 爱组搭(飞龙工作流企业版)](https://naiveui.aizuda.com) - 像搭积木一样进行低代码甚至零代码快速构建应用
## 📋 项目概述 ## 📋 项目概述
RuoYi-Plus-Soybean 是一个现代化的企业级多租户管理系统,它结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。 RuoYi-Plus-Soybean 是一个现代化的企业级多租户管理系统,它结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。

View File

@ -1,7 +1,7 @@
{ {
"name": "ruoyi-vue-plus", "name": "ruoyi-vue-plus",
"type": "module", "type": "module",
"version": "1.1.2", "version": "1.1.3",
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。", "description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
"author": { "author": {
"name": "xlsea", "name": "xlsea",
@ -41,18 +41,21 @@
"scripts": { "scripts": {
"build": "vite build --mode prod", "build": "vite build --mode prod",
"build:dev": "vite build --mode dev", "build:dev": "vite build --mode dev",
"build:tauri": "pnpm tauri build",
"build:test": "vite build --mode test", "build:test": "vite build --mode test",
"cleanup": "sa cleanup", "cleanup": "sa cleanup",
"commit": "sa git-commit", "commit": "sa git-commit",
"commit:zh": "sa git-commit -l=zh-cn", "commit:zh": "sa git-commit -l=zh-cn",
"dev": "vite --mode dev", "dev": "vite --mode dev",
"dev:prod": "vite --mode prod", "dev:prod": "vite --mode prod",
"dev:tauri": "pnpm tauri dev",
"dev:test": "vite --mode test", "dev:test": "vite --mode test",
"gen-route": "sa gen-route", "gen-route": "sa gen-route",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"prepare": "simple-git-hooks", "prepare": "simple-git-hooks",
"preview": "vite preview", "preview": "vite preview",
"release": "sa release", "release": "sa release",
"tauri-icon": "pnpm tauri icon ./public/logo.png",
"typecheck": "vue-tsc --noEmit --skipLibCheck", "typecheck": "vue-tsc --noEmit --skipLibCheck",
"update-pkg": "sa update-pkg" "update-pkg": "sa update-pkg"
}, },
@ -65,6 +68,7 @@
"@sa/materials": "workspace:*", "@sa/materials": "workspace:*",
"@sa/tinymce": "workspace:*", "@sa/tinymce": "workspace:*",
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"@tauri-apps/api": "2.5.0",
"@types/streamsaver": "^2.0.5", "@types/streamsaver": "^2.0.5",
"@vueuse/core": "13.5.0", "@vueuse/core": "13.5.0",
"clipboard": "2.0.11", "clipboard": "2.0.11",
@ -83,16 +87,17 @@
"vue": "3.5.17", "vue": "3.5.17",
"vue-advanced-cropper": "^2.8.9", "vue-advanced-cropper": "^2.8.9",
"vue-draggable-plus": "0.6.0", "vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.10", "vue-i18n": "11.1.9",
"vue-router": "4.5.1" "vue-router": "4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@elegant-router/vue": "0.3.8", "@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.359", "@iconify/json": "2.2.357",
"@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.1",
"@types/node": "24.0.15", "@tauri-apps/cli": "2.5.0",
"@types/node": "24.0.13",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.3.3", "@unocss/eslint-config": "66.3.3",
"@unocss/preset-icons": "66.3.3", "@unocss/preset-icons": "66.3.3",
@ -112,14 +117,14 @@
"typescript": "5.8.3", "typescript": "5.8.3",
"unplugin-icons": "22.1.0", "unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.8.0", "unplugin-vue-components": "28.8.0",
"vite": "7.0.5", "vite": "7.0.4",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vite-plugin-static-copy": "^3.1.0", "vite-plugin-static-copy": "^3.1.0",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.7", "vite-plugin-vue-devtools": "7.7.7",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.3" "vue-tsc": "3.0.1"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify", "commit-msg": "pnpm sa git-commit-verify",

View File

@ -13,12 +13,11 @@ import type {
ResponseType ResponseType
} from './type'; } from './type';
function createCommonRequest< function createCommonRequest<ResponseData = any>(
ResponseData, axiosConfig?: CreateAxiosDefaults,
ApiData = ResponseData, options?: Partial<RequestOption<ResponseData>>
State extends Record<string, unknown> = Record<string, unknown> ) {
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) { const opts = createDefaultOptions<ResponseData>(options);
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
const axiosConf = createAxiosConfig(axiosConfig); const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf); const instance = axios.create(axiosConf);
@ -81,6 +80,14 @@ function createCommonRequest<
} }
); );
function cancelRequest(requestId: string) {
const abortController = abortControllerMap.get(requestId);
if (abortController) {
abortController.abort();
abortControllerMap.delete(requestId);
}
}
function cancelAllRequest() { function cancelAllRequest() {
abortControllerMap.forEach(abortController => { abortControllerMap.forEach(abortController => {
abortController.abort(); abortController.abort();
@ -91,6 +98,7 @@ function createCommonRequest<
return { return {
instance, instance,
opts, opts,
cancelRequest,
cancelAllRequest cancelAllRequest
}; };
} }
@ -101,27 +109,27 @@ function createCommonRequest<
* @param axiosConfig axios config * @param axiosConfig axios config
* @param options request options * @param options request options
*/ */
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>( export function createRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults, axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData, ApiData, State>> options?: Partial<RequestOption<ResponseData>>
) { ) {
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options); const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const request: RequestInstance<ApiData, State> = async function request< const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
T extends ApiData = ApiData, config: CustomAxiosRequestConfig
R extends ResponseType = 'json' ) {
>(config: CustomAxiosRequestConfig) {
const response: AxiosResponse<ResponseData> = await instance(config); const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json'; const responseType = response.config?.responseType || 'json';
if (responseType === 'json') { if (responseType === 'json') {
return opts.transform(response); return opts.transformBackendResponse(response);
} }
return response.data as MappedType<R, T>; return response.data as MappedType<R, T>;
} as RequestInstance<ApiData, State>; } as RequestInstance<State>;
request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest; request.cancelAllRequest = cancelAllRequest;
request.state = {} as State; request.state = {} as State;
@ -136,14 +144,14 @@ export function createRequest<ResponseData, ApiData, State extends Record<string
* @param axiosConfig axios config * @param axiosConfig axios config
* @param options request options * @param options request options
*/ */
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>( export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults, axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData, ApiData, State>> options?: Partial<RequestOption<ResponseData>>
) { ) {
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options); const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest< const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
T extends ApiData = ApiData, T = any,
R extends ResponseType = 'json' R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) { >(config: CustomAxiosRequestConfig) {
try { try {
@ -152,21 +160,20 @@ export function createFlatRequest<ResponseData, ApiData, State extends Record<st
const responseType = response.config?.responseType || 'json'; const responseType = response.config?.responseType || 'json';
if (responseType === 'json') { if (responseType === 'json') {
const data = await opts.transform(response); const data = opts.transformBackendResponse(response);
return { data, error: null, response }; return { data, error: null, response };
} }
return { data: response.data as MappedType<R, T>, error: null, response }; return { data: response.data as MappedType<R, T>, error: null };
} catch (error) { } catch (error) {
return { data: null, error, response: (error as AxiosError<ResponseData>).response }; return { data: null, error, response: (error as AxiosError<ResponseData>).response };
} }
} as FlatRequestInstance<ResponseData, ApiData, State>; } as FlatRequestInstance<State, ResponseData>;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest; flatRequest.cancelAllRequest = cancelAllRequest;
flatRequest.state = { flatRequest.state = {} as State;
...opts.defaultState
} as State;
return flatRequest; return flatRequest;
} }

View File

@ -4,27 +4,15 @@ import { stringify } from 'qs';
import { isHttpSuccess } from './shared'; import { isHttpSuccess } from './shared';
import type { RequestOption } from './type'; import type { RequestOption } from './type';
export function createDefaultOptions< export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
ResponseData, const opts: RequestOption<ResponseData> = {
ApiData = ResponseData,
State extends Record<string, unknown> = Record<string, unknown>
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
const opts: RequestOption<ResponseData, ApiData, State> = {
defaultState: {} as State,
transform: async response => response.data as unknown as ApiData,
transformBackendResponse: async response => response.data as unknown as ApiData,
onRequest: async config => config, onRequest: async config => config,
isBackendSuccess: _response => true, isBackendSuccess: _response => true,
onBackendFail: async () => {}, onBackendFail: async () => {},
transformBackendResponse: async response => response.data,
onError: async () => {} onError: async () => {}
}; };
if (options?.transform) {
opts.transform = options.transform;
} else {
opts.transform = options?.transformBackendResponse || opts.transform;
}
Object.assign(opts, options); Object.assign(opts, options);
return opts; return opts;

View File

@ -8,30 +8,7 @@ export type ContentType =
| 'application/x-www-form-urlencoded' | 'application/x-www-form-urlencoded'
| 'application/octet-stream'; | 'application/octet-stream';
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>; export interface RequestOption<ResponseData = any> {
export interface RequestOption<
ResponseData,
ApiData = ResponseData,
State extends Record<string, unknown> = Record<string, unknown>
> {
/**
* The default state
*/
defaultState?: State;
/**
* transform the response data to the api data
*
* @param response Axios response
*/
transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
/**
* transform the response data to the api data
*
* @deprecated use `transform` instead, will be removed in the next major version v3
* @param response Axios response
*/
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
/** /**
* The hook before request * The hook before request
* *
@ -58,6 +35,12 @@ export interface RequestOption<
response: AxiosResponse<ResponseData>, response: AxiosResponse<ResponseData>,
instance: AxiosInstance instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>; ) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
* @param response Axios response
*/
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
/** /**
* The hook to handle error * The hook to handle error
* *
@ -85,7 +68,15 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
responseType?: R; responseType?: R;
}; };
export interface RequestInstanceCommon<State extends Record<string, unknown>> { export interface RequestInstanceCommon<T> {
/**
* cancel the request by request id
*
* if the request provide abort controller sign from config, it will not collect in the abort controller map
*
* @param requestId
*/
cancelRequest: (requestId: string) => void;
/** /**
* cancel all request * cancel all request
* *
@ -93,35 +84,32 @@ export interface RequestInstanceCommon<State extends Record<string, unknown>> {
*/ */
cancelAllRequest: () => void; cancelAllRequest: () => void;
/** you can set custom state in the request instance */ /** you can set custom state in the request instance */
state: State; state: T;
} }
/** The request instance */ /** The request instance */
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> { export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>( <T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
config: CustomAxiosRequestConfig<R>
): Promise<MappedType<R, T>>;
} }
export type FlatResponseSuccessData<ResponseData, ApiData> = { export type FlatResponseSuccessData<T = any, ResponseData = any> = {
data: ApiData; data: T;
error: null; error: null;
response: AxiosResponse<ResponseData>; response: AxiosResponse<ResponseData>;
}; };
export type FlatResponseFailData<ResponseData> = { export type FlatResponseFailData<ResponseData = any> = {
data: null; data: null;
error: AxiosError<ResponseData>; error: AxiosError<ResponseData>;
response: AxiosResponse<ResponseData>; response: AxiosResponse<ResponseData>;
}; };
export type FlatResponseData<ResponseData, ApiData> = export type FlatResponseData<T = any, ResponseData = any> =
| FlatResponseSuccessData<ResponseData, ApiData> | FlatResponseSuccessData<T, ResponseData>
| FlatResponseFailData<ResponseData>; | FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>> export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
extends RequestInstanceCommon<State> { <T = any, 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<MappedType<R, T>, ResponseData>>;
} }

View File

@ -3,7 +3,9 @@ import useLoading from './use-loading';
import useCountDown from './use-count-down'; import useCountDown from './use-count-down';
import useContext from './use-context'; import useContext from './use-context';
import useSvgIconRender from './use-svg-icon-render'; import useSvgIconRender from './use-svg-icon-render';
import useTable from './use-table'; import useHookTable from './use-table';
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable }; export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
export type * from './use-table';
export * from './use-signal';
export * from './use-table';

View File

@ -1,4 +1,5 @@
import { inject, provide } from 'vue'; import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
/** /**
* Use context * Use context
@ -11,7 +12,7 @@ import { inject, provide } from 'vue';
* import { ref } from 'vue'; * import { ref } from 'vue';
* import { useContext } from '@sa/hooks'; * import { useContext } from '@sa/hooks';
* *
* export const [provideDemoContext, useDemoContext] = useContext('demo', () => { * export const { setupStore, useStore } = useContext('demo', () => {
* const count = ref(0); * const count = ref(0);
* *
* function increment() { * function increment() {
@ -34,10 +35,10 @@ import { inject, provide } from 'vue';
* <div>A</div> * <div>A</div>
* </template> * </template>
* <script setup lang="ts"> * <script setup lang="ts">
* import { provideDemoContext } from './context'; * import { setupStore } from './context';
* *
* provideDemoContext(); * setupStore();
* // const { increment } = provideDemoContext(); // also can control the store in the parent component * // const { increment } = setupStore(); // also can control the store in the parent component
* </script> * </script>
* ``` // B.vue * ``` // B.vue
* ```vue * ```vue
@ -45,9 +46,9 @@ import { inject, provide } from 'vue';
* <div>B</div> * <div>B</div>
* </template> * </template>
* <script setup lang="ts"> * <script setup lang="ts">
* import { useDemoContext } from './context'; * import { useStore } from './context';
* *
* const { count, increment } = useDemoContext(); * const { count, increment } = useStore();
* </script> * </script>
* ```; * ```;
* *
@ -56,41 +57,40 @@ import { inject, provide } from 'vue';
* @param contextName Context name * @param contextName Context name
* @param fn Context function * @param fn Context function
*/ */
export default function useContext<Arguments extends Array<any>, T>( export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
contextName: string, type Context = ReturnType<T>;
composable: (...args: Arguments) => T
) {
const key = Symbol(contextName);
/** const { useProvide, useInject: useStore } = createContext<Context>(contextName);
* Injects the context value.
*
* @param consumerName - The name of the component that is consuming the context. If provided, the component must be
* used within the context provider.
* @param defaultValue - The default value to return if the context is not provided.
* @returns The context value.
*/
const useInject = <N extends string | null | undefined = undefined>(
consumerName?: N,
defaultValue?: T
): N extends null | undefined ? T | null : T => {
const value = inject(key, defaultValue);
if (consumerName && !value) { function setupStore(...args: Parameters<T>) {
throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``); const context: Context = fn(...args);
} return useProvide(context);
}
// @ts-expect-error - we want to return null if the value is undefined or null return {
return value || null; /** Setup store in the parent component */
setupStore,
/** Use store in the child component */
useStore
};
}
/** Create context */
function createContext<T>(contextName: string) {
const injectKey: InjectionKey<T> = Symbol(contextName);
function useProvide(context: T) {
provide(injectKey, context);
return context;
}
function useInject() {
return inject(injectKey) as T;
}
return {
useProvide,
useInject
}; };
const useProvide = (...args: Arguments) => {
const value = composable(...args);
provide(key, value);
return value;
};
return [useProvide, useInject] as const;
} }

View File

@ -6,31 +6,31 @@ import type {
CreateAxiosDefaults, CreateAxiosDefaults,
CustomAxiosRequestConfig, CustomAxiosRequestConfig,
MappedType, MappedType,
RequestInstanceCommon,
RequestOption, RequestOption,
ResponseType ResponseType
} from '@sa/axios'; } from '@sa/axios';
import useLoading from './use-loading'; import useLoading from './use-loading';
export type HookRequestInstanceResponseSuccessData<ApiData> = { export type HookRequestInstanceResponseSuccessData<T = any> = {
data: Ref<ApiData>; data: Ref<T>;
error: Ref<null>; error: Ref<null>;
}; };
export type HookRequestInstanceResponseFailData<ResponseData> = { export type HookRequestInstanceResponseFailData<ResponseData = any> = {
data: Ref<null>; data: Ref<null>;
error: Ref<AxiosError<ResponseData>>; error: Ref<AxiosError<ResponseData>>;
}; };
export type HookRequestInstanceResponseData<ResponseData, ApiData> = { export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
loading: Ref<boolean>; loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>); } & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>> export interface HookRequestInstance<ResponseData = any> {
extends RequestInstanceCommon<State> { <T = any, R extends ResponseType = 'json'>(
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig config: CustomAxiosRequestConfig
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>; ): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
} }
/** /**
@ -39,26 +39,25 @@ export interface HookRequestInstance<ResponseData, ApiData, State extends Record
* @param axiosConfig * @param axiosConfig
* @param options * @param options
*/ */
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>( export default function createHookRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults, axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData, ApiData, State>> options?: Partial<RequestOption<ResponseData>>
) { ) {
const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options); const request = createFlatRequest<ResponseData>(axiosConfig, options);
const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest< const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
T extends ApiData = ApiData, config: CustomAxiosRequestConfig
R extends ResponseType = 'json' ) {
>(config: CustomAxiosRequestConfig) {
const { loading, startLoading, endLoading } = useLoading(); const { loading, startLoading, endLoading } = useLoading();
const data = ref(null) as Ref<MappedType<R, T>>; const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
const error = ref(null) as Ref<AxiosError<ResponseData> | null>; const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
startLoading(); startLoading();
request(config).then(res => { request(config).then(res => {
if (res.data) { if (res.data) {
data.value = res.data as MappedType<R, T>; data.value = res.data;
} else { } else {
error.value = res.error; error.value = res.error;
} }
@ -71,8 +70,9 @@ export default function createHookRequest<ResponseData, ApiData, State extends R
data, data,
error error
}; };
} as HookRequestInstance<ResponseData, ApiData, State>; } as HookRequestInstance<ResponseData>;
hookRequest.cancelRequest = request.cancelRequest;
hookRequest.cancelAllRequest = request.cancelAllRequest; hookRequest.cancelAllRequest = request.cancelAllRequest;
return hookRequest; return hookRequest;

View File

@ -0,0 +1,144 @@
import { computed, ref, shallowRef, triggerRef } from 'vue';
import type {
ComputedGetter,
DebuggerOptions,
Ref,
ShallowRef,
WritableComputedOptions,
WritableComputedRef
} from 'vue';
type Updater<T> = (value: T) => T;
type Mutator<T> = (value: T) => void;
/**
* Signal is a reactive value that can be set, updated or mutated
*
* @example
* ```ts
* const count = useSignal(0);
*
* // `watchEffect`
* watchEffect(() => {
* console.log(count());
* });
*
* // watch
* watch(count, value => {
* console.log(value);
* });
*
* // useComputed
* const double = useComputed(() => count() * 2);
* const writeableDouble = useComputed({
* get: () => count() * 2,
* set: value => count.set(value / 2)
* });
* ```
*/
export interface Signal<T> {
(): Readonly<T>;
/**
* Set the value of the signal
*
* It recommend use `set` for primitive values
*
* @param value
*/
set(value: T): void;
/**
* Update the value of the signal using an updater function
*
* It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
*
* @param updater
*/
update(updater: Updater<T>): void;
/**
* Mutate the value of the signal using a mutator function
*
* this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
*
* It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
*
* @param mutator
*/
mutate(mutator: Mutator<T>): void;
/**
* Get the reference of the signal
*
* Sometimes it can be useful to make `v-model` work with the signal
*
* ```vue
* <template>
* <input v-model="model.count" />
* </template>;
*
* <script setup lang="ts">
* const state = useSignal({ count: 0 }, { useRef: true });
*
* const model = state.getRef();
* </script>
* ```
*/
getRef(): Readonly<ShallowRef<Readonly<T>>>;
}
export interface ReadonlySignal<T> {
(): Readonly<T>;
}
export interface SignalOptions {
/**
* Whether to use `ref` to store the value
*
* @default false use `sharedRef` to store the value
*/
useRef?: boolean;
}
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
const { useRef } = options || {};
const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
return createSignal(state);
}
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
export function useComputed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
) {
const isGetter = typeof getterOrOptions === 'function';
const computedValue = computed(getterOrOptions as any, debugOptions);
if (isGetter) {
return () => computedValue.value as ReadonlySignal<T>;
}
return createSignal(computedValue);
}
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
const signal = () => state.value;
signal.set = (value: T) => {
state.value = value;
};
signal.update = (updater: Updater<T>) => {
state.value = updater(state.value);
};
signal.mutate = (mutator: Mutator<T>) => {
mutator(state.value);
triggerRef(state);
};
signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
return signal;
}

View File

@ -1,20 +1,12 @@
import { computed, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import type { Ref, VNodeChild } from 'vue'; import type { Ref, VNodeChild } from 'vue';
import { jsonClone } from '@sa/utils';
import useBoolean from './use-boolean'; import useBoolean from './use-boolean';
import useLoading from './use-loading'; import useLoading from './use-loading';
export interface PaginationData<T> { export type MaybePromise<T> = T | Promise<T>;
data: T[];
pageNum: number;
pageSize: number;
total: number;
}
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[]; export type ApiFn = (args: any) => Promise<unknown>;
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
response: ResponseData
) => GetApiData<ApiData, Pagination>;
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild); export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
@ -22,64 +14,74 @@ export type TableColumnCheck = {
key: string; key: string;
title: TableColumnCheckTitle; title: TableColumnCheckTitle;
checked: boolean; checked: boolean;
visible: boolean;
}; };
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> { export type TableDataWithIndex<T> = T & { index: number };
/**
* api function to get table data export type TransformedData<T> = {
*/ data: TableDataWithIndex<T>[];
api: () => Promise<ResponseData>; total?: number;
/** };
* whether to enable pagination
*/ export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
pagination?: Pagination;
/** export type TableConfig<A extends ApiFn, T, C> = {
* transform api response to table data /** api function to get table data */
*/ apiFn: A;
transform: Transform<ResponseData, ApiData, Pagination>; /** api params */
/** apiParams?: Parameters<A>[0];
* columns factory /** transform api response to table data */
*/ transformer: Transformer<T, Awaited<ReturnType<A>>>;
columns: () => Column[]; /** columns factory */
columns: () => C[];
/** /**
* get column checks * get column checks
*
* @param columns
*/ */
getColumnChecks: (columns: Column[]) => TableColumnCheck[]; getColumnChecks: (columns: C[]) => TableColumnCheck[];
/** /**
* get columns * get columns
*
* @param columns
*/ */
getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[]; getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
/** /**
* callback when response fetched * callback when response fetched
*
* @param transformed transformed data
*/ */
onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>; onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
/** /**
* whether to get data immediately * whether to get data immediately
* *
* @default true * @default true
*/ */
immediate?: boolean; immediate?: boolean;
} };
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>( export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
) {
const { loading, startLoading, endLoading } = useLoading(); const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean(); const { bool: empty, setBool: setEmpty } = useBoolean();
const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options; const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const data = ref([]) as Ref<ApiData[]>; const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>; const allColumns = ref(config.columns()) as Ref<C[]>;
const $columns = computed(() => getColumns(columns(), columnChecks.value)); const data: Ref<TableDataWithIndex<T>[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
function reloadColumns() { function reloadColumns() {
allColumns.value = config.columns();
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked])); const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
const defaultChecks = getColumnChecks(columns()); const defaultChecks = getColumnChecks(allColumns.value);
columnChecks.value = defaultChecks.map(col => ({ columnChecks.value = defaultChecks.map(col => ({
...col, ...col,
@ -88,21 +90,48 @@ export default function useTable<ResponseData, ApiData, Column, Pagination exten
} }
async function getData() { async function getData() {
try { startLoading();
startLoading();
const response = await api(); const formattedParams = formatSearchParams(searchParams);
const transformed = transform(response); const response = await apiFn(formattedParams);
data.value = getTableData(transformed, pagination); const transformed = transformer(response as Awaited<ReturnType<A>>);
setEmpty(data.value.length === 0); data.value = transformed.data;
await onFetched?.(transformed); setEmpty(transformed.data.length === 0);
} finally {
endLoading(); await config.onFetched?.(transformed);
}
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formattedParams[key] = value;
}
});
return formattedParams;
}
/**
* update search params
*
* @param params
*/
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, jsonClone(apiParams));
getData();
} }
if (immediate) { if (immediate) {
@ -113,20 +142,12 @@ export default function useTable<ResponseData, ApiData, Column, Pagination exten
loading, loading,
empty, empty,
data, data,
columns: $columns, columns,
columnChecks, columnChecks,
reloadColumns, reloadColumns,
getData getData,
searchParams,
updateSearchParams,
resetSearchParams
}; };
} }
function getTableData<ApiData, Pagination extends boolean>(
data: GetApiData<ApiData, Pagination>,
pagination?: Pagination
) {
if (pagination) {
return (data as PaginationData<ApiData>).data;
}
return data as ApiData[];
}

View File

@ -127,6 +127,7 @@ function handleClickMask() {
:class="[ :class="[
style['layout-header'], style['layout-header'],
commonClass, commonClass,
headerClass,
headerLeftGapClass, headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab } { 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]" ]"

View File

@ -6,6 +6,12 @@ interface AdminLayoutHeaderConfig {
* @default true * @default true
*/ */
headerVisible?: boolean; headerVisible?: boolean;
/**
* Header class
*
* @default ''
*/
headerClass?: string;
/** /**
* Header height * Header height
* *

View File

@ -0,0 +1,15 @@
{
"name": "@sa/fetch",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"ofetch": "1.4.1"
}
}

View File

@ -0,0 +1,10 @@
import { ofetch } from 'ofetch';
import type { FetchOptions } from 'ofetch';
export function createRequest(options: FetchOptions) {
const request = ofetch.create(options);
return request;
}
export default createRequest;

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -15,7 +15,7 @@
"devDependencies": { "devDependencies": {
"@soybeanjs/changelog": "0.3.24", "@soybeanjs/changelog": "0.3.24",
"bumpp": "10.2.0", "bumpp": "10.2.0",
"c12": "3.1.0", "c12": "3.0.4",
"cac": "6.7.14", "cac": "6.7.14",
"consola": "3.4.2", "consola": "3.4.2",
"enquirer": "2.4.1", "enquirer": "2.4.1",

View File

@ -32,8 +32,7 @@ export function createStorage<T extends object>(type: StorageType, storagePrefix
storageData = JSON.parse(json); storageData = JSON.parse(json);
} catch {} } catch {}
// storageData may be `false` if it is boolean type if (storageData) {
if (storageData !== null) {
return storageData as T[K]; return storageData as T[K];
} }
} }

334
pnpm-lock.yaml generated
View File

@ -32,6 +32,9 @@ importers:
'@sa/utils': '@sa/utils':
specifier: workspace:* specifier: workspace:*
version: link:packages/utils version: link:packages/utils
'@tauri-apps/api':
specifier: 2.5.0
version: 2.5.0
'@types/streamsaver': '@types/streamsaver':
specifier: ^2.0.5 specifier: ^2.0.5
version: 2.0.5 version: 2.0.5
@ -87,8 +90,8 @@ importers:
specifier: 0.6.0 specifier: 0.6.0
version: 0.6.0(@types/sortablejs@1.15.8) version: 0.6.0(@types/sortablejs@1.15.8)
vue-i18n: vue-i18n:
specifier: 11.1.10 specifier: 11.1.9
version: 11.1.10(vue@3.5.17(typescript@5.8.3)) version: 11.1.9(vue@3.5.17(typescript@5.8.3))
vue-router: vue-router:
specifier: 4.5.1 specifier: 4.5.1
version: 4.5.1(vue@3.5.17(typescript@5.8.3)) version: 4.5.1(vue@3.5.17(typescript@5.8.3))
@ -97,8 +100,8 @@ importers:
specifier: 0.3.8 specifier: 0.3.8
version: 0.3.8 version: 0.3.8
'@iconify/json': '@iconify/json':
specifier: 2.2.359 specifier: 2.2.357
version: 2.2.359 version: 2.2.357
'@sa/scripts': '@sa/scripts':
specifier: workspace:* specifier: workspace:*
version: link:packages/scripts version: link:packages/scripts
@ -108,9 +111,12 @@ importers:
'@soybeanjs/eslint-config': '@soybeanjs/eslint-config':
specifier: 1.7.1 specifier: 1.7.1
version: 1.7.1(@typescript-eslint/utils@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(@unocss/eslint-config@66.3.3(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.35.1(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2))))(eslint@9.31.0(jiti@2.4.2))(svelte-eslint-parser@1.3.0)(typescript@5.8.3)(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2))) version: 1.7.1(@typescript-eslint/utils@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(@unocss/eslint-config@66.3.3(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.35.1(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2))))(eslint@9.31.0(jiti@2.4.2))(svelte-eslint-parser@1.3.0)(typescript@5.8.3)(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2)))
'@tauri-apps/cli':
specifier: 2.5.0
version: 2.5.0
'@types/node': '@types/node':
specifier: 24.0.15 specifier: 24.0.13
version: 24.0.15 version: 24.0.13
'@types/nprogress': '@types/nprogress':
specifier: 0.2.3 specifier: 0.2.3
version: 0.2.3 version: 0.2.3
@ -131,13 +137,13 @@ importers:
version: 66.3.3 version: 66.3.3
'@unocss/vite': '@unocss/vite':
specifier: 66.3.3 specifier: 66.3.3
version: 66.3.3(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)) version: 66.3.3(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: 6.0.0 specifier: 6.0.0
version: 6.0.0(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)) version: 6.0.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))
'@vitejs/plugin-vue-jsx': '@vitejs/plugin-vue-jsx':
specifier: 5.0.1 specifier: 5.0.1
version: 5.0.1(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)) version: 5.0.1(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))
consola: consola:
specifier: 3.4.2 specifier: 3.4.2
version: 3.4.2 version: 3.4.2
@ -169,29 +175,29 @@ importers:
specifier: 28.8.0 specifier: 28.8.0
version: 28.8.0(@babel/parser@7.28.0)(vue@3.5.17(typescript@5.8.3)) version: 28.8.0(@babel/parser@7.28.0)(vue@3.5.17(typescript@5.8.3))
vite: vite:
specifier: 7.0.5 specifier: 7.0.4
version: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
vite-plugin-monaco-editor: vite-plugin-monaco-editor:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0(monaco-editor@0.52.2) version: 1.1.0(monaco-editor@0.52.2)
vite-plugin-progress: vite-plugin-progress:
specifier: 0.0.7 specifier: 0.0.7
version: 0.0.7(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) version: 0.0.7(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))
vite-plugin-static-copy: vite-plugin-static-copy:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.1(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) version: 3.1.1(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))
vite-plugin-svg-icons: vite-plugin-svg-icons:
specifier: 2.0.1 specifier: 2.0.1
version: 2.0.1(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) version: 2.0.1(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))
vite-plugin-vue-devtools: vite-plugin-vue-devtools:
specifier: 7.7.7 specifier: 7.7.7
version: 7.7.7(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)) version: 7.7.7(rollup@4.45.1)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))
vue-eslint-parser: vue-eslint-parser:
specifier: 10.2.0 specifier: 10.2.0
version: 10.2.0(eslint@9.31.0(jiti@2.4.2)) version: 10.2.0(eslint@9.31.0(jiti@2.4.2))
vue-tsc: vue-tsc:
specifier: 3.0.3 specifier: 3.0.1
version: 3.0.3(typescript@5.8.3) version: 3.0.1(typescript@5.8.3)
packages/alova: packages/alova:
dependencies: dependencies:
@ -255,6 +261,12 @@ importers:
specifier: 0.9.1 specifier: 0.9.1
version: 0.9.1 version: 0.9.1
packages/ofetch:
dependencies:
ofetch:
specifier: 1.4.1
version: 1.4.1
packages/scripts: packages/scripts:
devDependencies: devDependencies:
'@soybeanjs/changelog': '@soybeanjs/changelog':
@ -264,8 +276,8 @@ importers:
specifier: 10.2.0 specifier: 10.2.0
version: 10.2.0 version: 10.2.0
c12: c12:
specifier: 3.1.0 specifier: 3.0.4
version: 3.1.0 version: 3.0.4
cac: cac:
specifier: 6.7.14 specifier: 6.7.14
version: 6.7.14 version: 6.7.14
@ -758,8 +770,8 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'} engines: {node: '>=18.18'}
'@iconify/json@2.2.359': '@iconify/json@2.2.357':
resolution: {integrity: sha512-nOIaROD3xeLiFGvJu0YIgeu4Hqbmz6T71b0lsFv1TY6Uu6Lk/5Z8GhDByIE2/zfgxvxfv3f+5A/DkLHmMXYu8Q==} resolution: {integrity: sha512-v8fr/KwcJ0qsoEJ69k1+M928bfzNmmApyJBTIAwwIzHZrVEUneHTEOJRy7OVYKisauBMVVH067I2uFNoPA92iA==}
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@ -772,16 +784,16 @@ packages:
peerDependencies: peerDependencies:
vue: '>=3' vue: '>=3'
'@intlify/core-base@11.1.10': '@intlify/core-base@11.1.9':
resolution: {integrity: sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==} resolution: {integrity: sha512-Lrdi4wp3XnGhWmB/mMD/XtfGUw1Jt+PGpZI/M63X1ZqhTDjNHRVCs/i8vv8U1cwaj1A9fb0bkCQHLSL0SK+pIQ==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
'@intlify/message-compiler@11.1.10': '@intlify/message-compiler@11.1.9':
resolution: {integrity: sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==} resolution: {integrity: sha512-84SNs3Ikjg0rD1bOuchzb3iK1vR2/8nxrkyccIl5DjFTeMzE/Fxv6X+A7RN5ZXjEWelc1p5D4kHA6HEOhlKL5Q==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
'@intlify/shared@11.1.10': '@intlify/shared@11.1.9':
resolution: {integrity: sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==} resolution: {integrity: sha512-H/83xgU1l8ox+qG305p6ucmoy93qyjIPnvxGWRA7YdOoHe1tIiW9IlEu4lTdsOR7cfP1ecrwyflQSqXdXBacXA==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
'@isaacs/balanced-match@4.0.1': '@isaacs/balanced-match@4.0.1':
@ -1117,6 +1129,85 @@ packages:
vue-eslint-parser: vue-eslint-parser:
optional: true optional: true
'@tauri-apps/api@2.5.0':
resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==}
'@tauri-apps/cli-darwin-arm64@2.5.0':
resolution: {integrity: sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.5.0':
resolution: {integrity: sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.5.0':
resolution: {integrity: sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.5.0':
resolution: {integrity: sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.5.0':
resolution: {integrity: sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.5.0':
resolution: {integrity: sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.5.0':
resolution: {integrity: sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.5.0':
resolution: {integrity: sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.5.0':
resolution: {integrity: sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.5.0':
resolution: {integrity: sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==}
engines: {node: '>= 10'}
hasBin: true
'@tinymce/tinymce-vue@6.2.0': '@tinymce/tinymce-vue@6.2.0':
resolution: {integrity: sha512-HiXKB+M3mJnWO6/8kY0HsP255+8zLZw5JMqHKVUvsXvzYyHW+splXXwYDYOkCYqf39R5nBqQaK2l2WL9rz3y5w==} resolution: {integrity: sha512-HiXKB+M3mJnWO6/8kY0HsP255+8zLZw5JMqHKVUvsXvzYyHW+splXXwYDYOkCYqf39R5nBqQaK2l2WL9rz3y5w==}
peerDependencies: peerDependencies:
@ -1154,8 +1245,8 @@ packages:
'@types/node@10.17.60': '@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
'@types/node@24.0.15': '@types/node@24.0.13':
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==} resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==}
'@types/nprogress@0.2.3': '@types/nprogress@0.2.3':
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
@ -1436,14 +1527,14 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0
vue: ^3.2.25 vue: ^3.2.25
'@volar/language-core@2.4.20': '@volar/language-core@2.4.17':
resolution: {integrity: sha512-dRDF1G33xaAIDqR6+mXUIjXYdu9vzSxlMGfMEwBxQsfY/JMUEXSpLTR057oTKlUQ2nIvCmP9k94A8h8z2VrNSA==} resolution: {integrity: sha512-chmRZMbKmcGpKMoO7Reb70uiLrzo0KWC2CkFttKUuKvrE+VYgi+fL9vWMJ07Fv5ulX0V1TAyyacN9q3nc5/ecA==}
'@volar/source-map@2.4.20': '@volar/source-map@2.4.17':
resolution: {integrity: sha512-mVjmFQH8mC+nUaVwmbxoYUy8cww+abaO8dWzqPUjilsavjxH0jCJ3Mp8HFuHsdewZs2c+SP+EO7hCd8Z92whJg==} resolution: {integrity: sha512-QDybtQyO3Ms/NjFqNHTC5tbDN2oK5VH7ZaKrcubtfHBDj63n2pizHC3wlMQ+iT55kQXZUUAbmBX5L1C8CHFeBw==}
'@volar/typescript@2.4.20': '@volar/typescript@2.4.17':
resolution: {integrity: sha512-Oc4DczPwQyXcVbd+5RsNEqX6ia0+w3p+klwdZQ6ZKhFjWoBP9PCPQYlKYRi/tDemWphW93P/Vv13vcE9I9D2GQ==} resolution: {integrity: sha512-3paEFNh4P5DkgNUB2YkTRrfUekN4brAXxd3Ow1syMqdIPtCZHbUy4AW99S5RO/7mzyTWPMdDSo3mqTpB/LPObQ==}
'@vue/babel-helper-vue-transform-on@1.4.0': '@vue/babel-helper-vue-transform-on@1.4.0':
resolution: {integrity: sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==} resolution: {integrity: sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==}
@ -1493,8 +1584,8 @@ packages:
'@vue/devtools-shared@7.7.7': '@vue/devtools-shared@7.7.7':
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
'@vue/language-core@3.0.3': '@vue/language-core@3.0.1':
resolution: {integrity: sha512-I9wY0ULMN9tMSua+2C7g+ez1cIziVMUzIHlDYGSl2rtru3Eh4sXj95vZ+4GBuXwwPnEmYfzSApVbXiVbI8V5Gg==} resolution: {integrity: sha512-sq+/Mc1IqIexWEQ+Q2XPiDb5SxSvY5JPqHnMOl/PlF5BekslzduX8dglSkpC17VeiAQB6dpS+4aiwNLJRduCNw==}
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
peerDependenciesMeta: peerDependenciesMeta:
@ -1707,8 +1798,8 @@ packages:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
c12@3.1.0: c12@3.0.4:
resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} resolution: {integrity: sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==}
peerDependencies: peerDependencies:
magicast: ^0.3.5 magicast: ^0.3.5
peerDependenciesMeta: peerDependenciesMeta:
@ -4019,8 +4110,8 @@ packages:
peerDependencies: peerDependencies:
vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
vite@7.0.5: vite@7.0.4:
resolution: {integrity: sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==} resolution: {integrity: sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -4104,8 +4195,8 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.4.37 vue: ^3.4.37
vue-i18n@11.1.10: vue-i18n@11.1.9:
resolution: {integrity: sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==} resolution: {integrity: sha512-N9ZTsXdRmX38AwS9F6Rh93RtPkvZTkSy/zNv63FTIwZCUbLwwrpqlKz9YQuzFLdlvRdZTnWAUE5jMxr8exdl7g==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
@ -4115,8 +4206,8 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
vue-tsc@3.0.3: vue-tsc@3.0.1:
resolution: {integrity: sha512-uU1OMSzWE8/y0+kDTc0iEIu9v82bmFkGyJpAO/x3wQqBkkHkButKgtygREyOkxL4E/xtcf/ExvgNhhjdzonldw==} resolution: {integrity: sha512-UvMLQD0hAGL1g/NfEQelnSVB4H5gtf/gz2lJKjMMwWNOUmSNyWkejwJagAxEbSjtV5CPPJYslOtoSuqJ63mhdg==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: '>=5.0.0' typescript: '>=5.0.0'
@ -4630,7 +4721,7 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {} '@humanwhocodes/retry@0.4.3': {}
'@iconify/json@2.2.359': '@iconify/json@2.2.357':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
pathe: 1.1.2 pathe: 1.1.2
@ -4655,17 +4746,17 @@ snapshots:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
'@intlify/core-base@11.1.10': '@intlify/core-base@11.1.9':
dependencies: dependencies:
'@intlify/message-compiler': 11.1.10 '@intlify/message-compiler': 11.1.9
'@intlify/shared': 11.1.10 '@intlify/shared': 11.1.9
'@intlify/message-compiler@11.1.10': '@intlify/message-compiler@11.1.9':
dependencies: dependencies:
'@intlify/shared': 11.1.10 '@intlify/shared': 11.1.9
source-map-js: 1.2.1 source-map-js: 1.2.1
'@intlify/shared@11.1.10': {} '@intlify/shared@11.1.9': {}
'@isaacs/balanced-match@4.0.1': {} '@isaacs/balanced-match@4.0.1': {}
@ -4930,6 +5021,55 @@ snapshots:
- eslint-import-resolver-node - eslint-import-resolver-node
- supports-color - supports-color
'@tauri-apps/api@2.5.0': {}
'@tauri-apps/cli-darwin-arm64@2.5.0':
optional: true
'@tauri-apps/cli-darwin-x64@2.5.0':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.5.0':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.5.0':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.5.0':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.5.0':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.5.0':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.5.0':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.5.0':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.5.0':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.5.0':
optional: true
'@tauri-apps/cli@2.5.0':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.5.0
'@tauri-apps/cli-darwin-x64': 2.5.0
'@tauri-apps/cli-linux-arm-gnueabihf': 2.5.0
'@tauri-apps/cli-linux-arm64-gnu': 2.5.0
'@tauri-apps/cli-linux-arm64-musl': 2.5.0
'@tauri-apps/cli-linux-riscv64-gnu': 2.5.0
'@tauri-apps/cli-linux-x64-gnu': 2.5.0
'@tauri-apps/cli-linux-x64-musl': 2.5.0
'@tauri-apps/cli-win32-arm64-msvc': 2.5.0
'@tauri-apps/cli-win32-ia32-msvc': 2.5.0
'@tauri-apps/cli-win32-x64-msvc': 2.5.0
'@tinymce/tinymce-vue@6.2.0(tinymce@7.9.1)(vue@3.5.17(typescript@5.8.3))': '@tinymce/tinymce-vue@6.2.0(tinymce@7.9.1)(vue@3.5.17(typescript@5.8.3))':
dependencies: dependencies:
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
@ -4959,7 +5099,7 @@ snapshots:
'@types/node@10.17.60': {} '@types/node@10.17.60': {}
'@types/node@24.0.15': '@types/node@24.0.13':
dependencies: dependencies:
undici-types: 7.8.0 undici-types: 7.8.0
@ -4973,7 +5113,7 @@ snapshots:
'@types/svgo@2.6.4': '@types/svgo@2.6.4':
dependencies: dependencies:
'@types/node': 24.0.15 '@types/node': 24.0.13
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
@ -5204,7 +5344,7 @@ snapshots:
dependencies: dependencies:
'@unocss/core': 66.3.3 '@unocss/core': 66.3.3
'@unocss/vite@66.3.3(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))': '@unocss/vite@66.3.3(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@unocss/config': 66.3.3 '@unocss/config': 66.3.3
@ -5215,7 +5355,7 @@ snapshots:
pathe: 2.0.3 pathe: 2.0.3
tinyglobby: 0.2.14 tinyglobby: 0.2.14
unplugin-utils: 0.2.4 unplugin-utils: 0.2.4
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
transitivePeerDependencies: transitivePeerDependencies:
- vue - vue
@ -5278,32 +5418,32 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1': '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true optional: true
'@vitejs/plugin-vue-jsx@5.0.1(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))': '@vitejs/plugin-vue-jsx@5.0.1(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))':
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
'@rolldown/pluginutils': 1.0.0-beta.27 '@rolldown/pluginutils': 1.0.0-beta.27
'@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.28.0) '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.28.0)
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitejs/plugin-vue@6.0.0(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))': '@vitejs/plugin-vue@6.0.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.19 '@rolldown/pluginutils': 1.0.0-beta.19
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
'@volar/language-core@2.4.20': '@volar/language-core@2.4.17':
dependencies: dependencies:
'@volar/source-map': 2.4.20 '@volar/source-map': 2.4.17
'@volar/source-map@2.4.20': {} '@volar/source-map@2.4.17': {}
'@volar/typescript@2.4.20': '@volar/typescript@2.4.17':
dependencies: dependencies:
'@volar/language-core': 2.4.20 '@volar/language-core': 2.4.17
path-browserify: 1.0.1 path-browserify: 1.0.1
vscode-uri: 3.1.0 vscode-uri: 3.1.0
@ -5377,14 +5517,14 @@ snapshots:
dependencies: dependencies:
'@vue/devtools-kit': 7.7.7 '@vue/devtools-kit': 7.7.7
'@vue/devtools-core@7.7.7(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))': '@vue/devtools-core@7.7.7(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))':
dependencies: dependencies:
'@vue/devtools-kit': 7.7.7 '@vue/devtools-kit': 7.7.7
'@vue/devtools-shared': 7.7.7 '@vue/devtools-shared': 7.7.7
mitt: 3.0.1 mitt: 3.0.1
nanoid: 5.1.5 nanoid: 5.1.5
pathe: 2.0.3 pathe: 2.0.3
vite-hot-client: 2.1.0(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) vite-hot-client: 2.1.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- vite - vite
@ -5403,16 +5543,16 @@ snapshots:
dependencies: dependencies:
rfdc: 1.4.1 rfdc: 1.4.1
'@vue/language-core@3.0.3(typescript@5.8.3)': '@vue/language-core@3.0.1(typescript@5.8.3)':
dependencies: dependencies:
'@volar/language-core': 2.4.20 '@volar/language-core': 2.4.17
'@vue/compiler-dom': 3.5.17 '@vue/compiler-dom': 3.5.17
'@vue/compiler-vue2': 2.7.16 '@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.17 '@vue/shared': 3.5.17
alien-signals: 2.0.5 alien-signals: 2.0.5
minimatch: 10.0.3
muggle-string: 0.4.1 muggle-string: 0.4.1
path-browserify: 1.0.1 path-browserify: 1.0.1
picomatch: 4.0.3
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
@ -5617,7 +5757,7 @@ snapshots:
dependencies: dependencies:
ansis: 4.1.0 ansis: 4.1.0
args-tokenizer: 0.3.0 args-tokenizer: 0.3.0
c12: 3.1.0 c12: 3.0.4
cac: 6.7.14 cac: 6.7.14
escalade: 3.2.0 escalade: 3.2.0
jsonc-parser: 3.3.1 jsonc-parser: 3.3.1
@ -5633,7 +5773,7 @@ snapshots:
dependencies: dependencies:
run-applescript: 7.0.0 run-applescript: 7.0.0
c12@3.1.0: c12@3.0.4:
dependencies: dependencies:
chokidar: 4.0.3 chokidar: 4.0.3
confbox: 0.2.2 confbox: 0.2.2
@ -8130,11 +8270,11 @@ snapshots:
evtd: 0.2.4 evtd: 0.2.4
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
vite-hot-client@2.1.0(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): vite-hot-client@2.1.0(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)):
dependencies: dependencies:
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
vite-plugin-inspect@0.8.9(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): vite-plugin-inspect@0.8.9(rollup@4.45.1)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)):
dependencies: dependencies:
'@antfu/utils': 0.7.10 '@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.2.0(rollup@4.45.1) '@rollup/pluginutils': 5.2.0(rollup@4.45.1)
@ -8145,7 +8285,7 @@ snapshots:
perfect-debounce: 1.0.0 perfect-debounce: 1.0.0
picocolors: 1.1.1 picocolors: 1.1.1
sirv: 3.0.1 sirv: 3.0.1
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
@ -8154,23 +8294,23 @@ snapshots:
dependencies: dependencies:
monaco-editor: 0.52.2 monaco-editor: 0.52.2
vite-plugin-progress@0.0.7(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): vite-plugin-progress@0.0.7(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)):
dependencies: dependencies:
picocolors: 1.1.1 picocolors: 1.1.1
progress: 2.0.3 progress: 2.0.3
rd: 2.0.1 rd: 2.0.1
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
vite-plugin-static-copy@3.1.1(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): vite-plugin-static-copy@3.1.1(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)):
dependencies: dependencies:
chokidar: 3.6.0 chokidar: 3.6.0
fs-extra: 11.3.0 fs-extra: 11.3.0
p-map: 7.0.3 p-map: 7.0.3
picocolors: 1.1.1 picocolors: 1.1.1
tinyglobby: 0.2.14 tinyglobby: 0.2.14
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
vite-plugin-svg-icons@2.0.1(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): vite-plugin-svg-icons@2.0.1(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)):
dependencies: dependencies:
'@types/svgo': 2.6.4 '@types/svgo': 2.6.4
cors: 2.8.5 cors: 2.8.5
@ -8180,27 +8320,27 @@ snapshots:
pathe: 0.2.0 pathe: 0.2.0
svg-baker: 1.7.0 svg-baker: 1.7.0
svgo: 2.8.0 svgo: 2.8.0
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite-plugin-vue-devtools@7.7.7(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)): vite-plugin-vue-devtools@7.7.7(rollup@4.45.1)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)):
dependencies: dependencies:
'@vue/devtools-core': 7.7.7(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)) '@vue/devtools-core': 7.7.7(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))
'@vue/devtools-kit': 7.7.7 '@vue/devtools-kit': 7.7.7
'@vue/devtools-shared': 7.7.7 '@vue/devtools-shared': 7.7.7
execa: 9.6.0 execa: 9.6.0
sirv: 3.0.1 sirv: 3.0.1
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
vite-plugin-inspect: 0.8.9(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) vite-plugin-inspect: 0.8.9(rollup@4.45.1)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))
vite-plugin-vue-inspector: 5.3.2(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)) vite-plugin-vue-inspector: 5.3.2(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- rollup - rollup
- supports-color - supports-color
- vue - vue
vite-plugin-vue-inspector@5.3.2(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)): vite-plugin-vue-inspector@5.3.2(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)):
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.0) '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.0)
@ -8211,11 +8351,11 @@ snapshots:
'@vue/compiler-dom': 3.5.17 '@vue/compiler-dom': 3.5.17
kolorist: 1.8.0 kolorist: 1.8.0
magic-string: 0.30.17 magic-string: 0.30.17
vite: 7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0) vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0): vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0):
dependencies: dependencies:
esbuild: 0.25.6 esbuild: 0.25.6
fdir: 6.4.6(picomatch@4.0.3) fdir: 6.4.6(picomatch@4.0.3)
@ -8224,7 +8364,7 @@ snapshots:
rollup: 4.45.1 rollup: 4.45.1
tinyglobby: 0.2.14 tinyglobby: 0.2.14
optionalDependencies: optionalDependencies:
'@types/node': 24.0.15 '@types/node': 24.0.13
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.4.2 jiti: 2.4.2
sass: 1.89.2 sass: 1.89.2
@ -8269,10 +8409,10 @@ snapshots:
dependencies: dependencies:
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
vue-i18n@11.1.10(vue@3.5.17(typescript@5.8.3)): vue-i18n@11.1.9(vue@3.5.17(typescript@5.8.3)):
dependencies: dependencies:
'@intlify/core-base': 11.1.10 '@intlify/core-base': 11.1.9
'@intlify/shared': 11.1.10 '@intlify/shared': 11.1.9
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
@ -8281,10 +8421,10 @@ snapshots:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
vue: 3.5.17(typescript@5.8.3) vue: 3.5.17(typescript@5.8.3)
vue-tsc@3.0.3(typescript@5.8.3): vue-tsc@3.0.1(typescript@5.8.3):
dependencies: dependencies:
'@volar/typescript': 2.4.20 '@volar/typescript': 2.4.17
'@vue/language-core': 3.0.3(typescript@5.8.3) '@vue/language-core': 3.0.1(typescript@5.8.3)
typescript: 5.8.3 typescript: 5.8.3
vue@3.5.17(typescript@5.8.3): vue@3.5.17(typescript@5.8.3):

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

3
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

4580
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2", features = [] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,7 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
{
"migrated": {
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

8
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

57
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,57 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"frontendDist": "../dist",
"devUrl": "http://localhost:9527"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "RuoYi-Plus-Soybean",
"targets": "all",
"externalBin": [],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"linux": {
"deb": {
"depends": []
}
}
},
"productName": "RuoYi-Plus-Soybean",
"mainBinaryName": "RuoYi-Plus-Soybean",
"version": "1.0.0",
"identifier": "org.dromara.admin",
"plugins": {},
"app": {
"windows": [
{
"fullscreen": false,
"height": 768,
"resizable": true,
"title": "RuoYi-Plus-Soybean",
"width": 1366,
"useHttpsScheme": true
}
],
"security": {
"csp": null
}
}
}

View File

@ -4,6 +4,7 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
import type { WatermarkProps } from 'naive-ui'; import type { WatermarkProps } from 'naive-ui';
import { useAppStore } from './store/modules/app'; import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme'; import { useThemeStore } from './store/modules/theme';
import { useAuthStore } from './store/modules/auth';
import { naiveDateLocales, naiveLocales } from './locales/naive'; import { naiveDateLocales, naiveLocales } from './locales/naive';
defineOptions({ defineOptions({
@ -12,6 +13,7 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { userInfo } = useAuthStore();
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined)); const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
@ -24,19 +26,24 @@ const naiveDateLocale = computed(() => {
}); });
const watermarkProps = computed<WatermarkProps>(() => { const watermarkProps = computed<WatermarkProps>(() => {
const appTitle = import.meta.env.VITE_APP_TITLE || 'RuoYi-Vue-Plus';
const content =
themeStore.watermark.enableUserName && userInfo.user?.userName
? `${userInfo.user?.nickName}@${appTitle} ${userInfo.user?.userName}`
: appTitle;
return { return {
content: themeStore.watermarkContent, content,
cross: true, cross: true,
fullscreen: true, fullscreen: true,
fontSize: 14, fontSize: 14,
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)',
lineHeight: 14, lineHeight: 14,
width: 384, width: 200,
height: 384, height: 300,
xOffset: 12, xOffset: 12,
yOffset: 60, yOffset: 60,
rotate: -13, rotate: -18,
zIndex: 9999, zIndex: 9999
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)'
}; };
}); });
</script> </script>

View File

@ -22,12 +22,7 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
</NButton> </NButton>
</template> </template>
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable"> <VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
<div <div v-for="item in columns" :key="item.key" class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)">
v-for="item in columns"
:key="item.key"
class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
:class="{ hidden: !item.visible }"
>
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" /> <icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1"> <NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
<template v-if="typeof item.title === 'function'"> <template v-if="typeof item.title === 'function'">

View File

@ -1,42 +0,0 @@
<script lang="ts" setup>
import { computed, useSlots } from 'vue';
import type { PopoverPlacement } from 'naive-ui';
defineOptions({ name: 'IconTooltip' });
interface Props {
icon?: string;
localIcon?: string;
desc?: string;
placement?: PopoverPlacement;
}
const props = withDefaults(defineProps<Props>(), {
icon: 'mdi-help-circle',
localIcon: '',
desc: '',
placement: 'top'
});
const slots = useSlots();
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
throw new Error('icon or localIcon is required when no custom trigger slot is provided');
}
</script>
<template>
<NTooltip :placement="placement">
<template #trigger>
<slot name="trigger">
<div class="cursor-pointer">
<SvgIcon :icon="icon" :local-icon="localIcon" />
</div>
</slot>
</template>
<slot>
<span>{{ desc }}</span>
</slot>
</NTooltip>
</template>

View File

@ -5,9 +5,9 @@ export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__'; export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = { export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
light: 'theme.appearance.themeSchema.light', light: 'theme.themeSchema.light',
dark: 'theme.appearance.themeSchema.dark', dark: 'theme.themeSchema.dark',
auto: 'theme.appearance.themeSchema.auto' auto: 'theme.themeSchema.auto'
}; };
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord); export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
@ -21,61 +21,49 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
}; };
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = { export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
vertical: 'theme.layout.layoutMode.vertical', vertical: 'theme.layoutMode.vertical',
'vertical-mix': 'theme.layout.layoutMode.vertical-mix', 'vertical-mix': 'theme.layoutMode.vertical-mix',
'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first', horizontal: 'theme.layoutMode.horizontal',
horizontal: 'theme.layout.layoutMode.horizontal', 'horizontal-mix': 'theme.layoutMode.horizontal-mix'
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
}; };
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord); export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = { export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
wrapper: 'theme.layout.content.scrollMode.wrapper', wrapper: 'theme.scrollMode.wrapper',
content: 'theme.layout.content.scrollMode.content' content: 'theme.scrollMode.content'
}; };
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord); export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = { export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
chrome: 'theme.layout.tab.mode.chrome', chrome: 'theme.tab.mode.chrome',
button: 'theme.layout.tab.mode.button' button: 'theme.tab.mode.button'
}; };
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord); export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = { export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
'fade-slide': 'theme.layout.content.page.mode.fade-slide', 'fade-slide': 'theme.page.mode.fade-slide',
fade: 'theme.layout.content.page.mode.fade', fade: 'theme.page.mode.fade',
'fade-bottom': 'theme.layout.content.page.mode.fade-bottom', 'fade-bottom': 'theme.page.mode.fade-bottom',
'fade-scale': 'theme.layout.content.page.mode.fade-scale', 'fade-scale': 'theme.page.mode.fade-scale',
'zoom-fade': 'theme.layout.content.page.mode.zoom-fade', 'zoom-fade': 'theme.page.mode.zoom-fade',
'zoom-out': 'theme.layout.content.page.mode.zoom-out', 'zoom-out': 'theme.page.mode.zoom-out',
none: 'theme.layout.content.page.mode.none' none: 'theme.page.mode.none'
}; };
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord); export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = { export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.layout.resetCacheStrategy.close', close: 'theme.resetCacheStrategy.close',
refresh: 'theme.layout.resetCacheStrategy.refresh' refresh: 'theme.resetCacheStrategy.refresh'
}; };
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord); export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
export const DARK_CLASS = 'dark'; export const DARK_CLASS = 'dark';
export const watermarkTimeFormatOptions = [
{ label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
{ label: 'YYYY/MM/DD HH:mm', value: 'YYYY/MM/DD HH:mm' },
{ label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
{ label: 'HH:mm', value: 'HH:mm' },
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
{ label: 'MM-DD HH:mm', value: 'MM-DD HH:mm' }
];
export const themeTableSizeRecord: Record<UnionKey.ThemeTableSize, App.I18n.I18nKey> = { export const themeTableSizeRecord: Record<UnionKey.ThemeTableSize, App.I18n.I18nKey> = {
small: 'theme.table.size.small', small: 'theme.table.size.small',
medium: 'theme.table.size.medium', medium: 'theme.table.size.medium',

View File

@ -1,4 +1,4 @@
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue'; import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
import { useElementSize } from '@vueuse/core'; import { useElementSize } from '@vueuse/core';
import * as echarts from 'echarts/core'; import * as echarts from 'echarts/core';
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts'; import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const darkMode = computed(() => themeStore.darkMode); const darkMode = computed(() => themeStore.darkMode);
const domRef = shallowRef<HTMLElement | null>(null); const domRef = ref<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 }; const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize); const { width, height } = useElementSize(domRef, initialSize);
const chart = shallowRef<echarts.ECharts | null>(null); let chart: echarts.ECharts | null = null;
const chartOptions: T = optionsFactory(); const chartOptions: T = optionsFactory();
const { const {
@ -111,9 +111,18 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
onDestroy onDestroy
} = hooks; } = hooks;
/**
* whether can render chart
*
* when domRef is ready and initialSize is valid
*/
function canRender() {
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
}
/** is chart rendered */ /** is chart rendered */
function isRendered() { function isRendered() {
return Boolean(domRef.value && chart.value); return Boolean(domRef.value && chart);
} }
/** /**
@ -122,59 +131,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
* @param callback callback function * @param callback callback function
*/ */
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) { async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
if (!isRendered()) return;
const updatedOpts = callback(chartOptions, optionsFactory); const updatedOpts = callback(chartOptions, optionsFactory);
Object.assign(chartOptions, updatedOpts); Object.assign(chartOptions, updatedOpts);
await nextTick();
if (!isRendered()) return;
if (isRendered()) { if (isRendered()) {
chart.value?.clear(); chart?.clear();
} }
chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' }); chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
await onUpdated?.(chart.value!); await onUpdated?.(chart!);
} }
function setOptions(options: T) { function setOptions(options: T) {
chart.value?.setOption(options); chart?.setOption(options);
} }
/** render chart */ /** render chart */
async function render() { async function render() {
if (isRendered()) return; if (!isRendered()) {
const chartTheme = darkMode.value ? 'dark' : 'light';
const chartTheme = darkMode.value ? 'dark' : 'light'; await nextTick();
chart.value = echarts.init(domRef.value, chartTheme); chart = echarts.init(domRef.value, chartTheme);
chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' }); chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
await onRender?.(chart.value!); await onRender?.(chart);
}
} }
/** resize chart */ /** resize chart */
function resize() { function resize() {
chart.value?.resize(); chart?.resize();
} }
/** destroy chart */ /** destroy chart */
async function destroy() { async function destroy() {
if (!chart.value) return; if (!chart) return;
await onDestroy?.(chart.value); await onDestroy?.(chart);
chart.value?.dispose(); chart?.dispose();
chart.value = null; chart = null;
} }
/** change chart theme */ /** change chart theme */
async function changeTheme() { async function changeTheme() {
await destroy(); await destroy();
await render(); await render();
await onUpdated?.(chart.value!); await onUpdated?.(chart!);
} }
/** /**
@ -187,29 +196,30 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
initialSize.width = w; initialSize.width = w;
initialSize.height = h; initialSize.height = h;
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
return;
}
// resize chart // resize chart
if (isRendered()) { if (isRendered()) {
resize(); resize();
return;
} }
// render chart // render chart
await render(); await render();
if (chart.value) { if (chart) {
await onUpdated?.(chart.value); await onUpdated?.(chart);
} }
} }
scope.run(() => { scope.run(() => {
watch( watch([width, height], ([newWidth, newHeight]) => {
[width, height], renderChartBySize(newWidth, newHeight);
([newWidth, newHeight]) => { });
renderChartBySize(newWidth, newHeight);
},
{ flush: 'post' }
);
watch(darkMode, () => { watch(darkMode, () => {
changeTheme(); changeTheme();
@ -223,7 +233,6 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
return { return {
domRef, domRef,
chart,
updateOptions, updateOptions,
setOptions setOptions
}; };

View File

@ -1,312 +1,195 @@
import { computed, effectScope, onScopeDispose, reactive, ref, shallowRef, watch } from 'vue'; import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { PaginationProps } from 'naive-ui'; import type { PaginationProps } from 'naive-ui';
import { useBoolean, useTable } from '@sa/hooks';
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
import type { FlatResponseData } from '@sa/axios';
import { jsonClone } from '@sa/utils'; import { jsonClone } from '@sa/utils';
import { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales'; import { $t } from '@/locales';
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit< type TableData = NaiveUI.TableData;
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>, type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
'pagination' | 'getColumnChecks' | 'getColumns' type TableColumn<T> = NaiveUI.TableColumn<T>;
> & {
/**
* get column visible
*
* @param column
*
* @default true
*
* @returns true if the column is visible, false otherwise
*/
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
};
const SELECTION_KEY = '__selection__'; export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
const EXPAND_KEY = '__expand__';
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
const scope = effectScope();
const appStore = useAppStore();
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
...options,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns
});
// calculate the total width of the table this is used for horizontal scrolling
const scrollX = computed(() => {
return result.columns.value.reduce((acc, column) => {
return acc + Number(column.width ?? column.minWidth ?? 120);
}, 0);
});
scope.run(() => {
watch(
() => appStore.locale,
() => {
result.reloadColumns();
}
);
});
onScopeDispose(() => {
scope.stop();
});
return {
...result,
scrollX
};
}
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
/**
* whether to show the total count of the table
*
* @default true
*/
showTotal?: boolean;
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
};
export function useNaivePaginatedTable<ResponseData, ApiData>(
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
) {
const scope = effectScope(); const scope = effectScope();
const appStore = useAppStore(); const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile); const isMobile = computed(() => appStore.isMobile);
const showTotal = computed(() => options.showTotal ?? true); const { apiFn, apiParams, immediate, showTotal = true } = config;
const pagination = reactive({ const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const { rows: records = [], total = 0 } = res.data || {};
const current = searchParams.pageNum as number;
const size = (searchParams.pageSize || 0) as number;
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
const pageSize = size <= 0 ? 10 : size;
const recordsWithIndex = records.map((item, index) => {
return {
...item,
index: (current - 1) * pageSize + index + 1
};
});
return {
data: recordsWithIndex,
pageNum: current,
pageSize,
total
};
},
getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
return filteredColumns;
},
onFetched: async transformed => {
const { total } = transformed;
updatePagination({
page: searchParams.pageNum,
pageSize: searchParams.pageSize,
itemCount: total
});
},
immediate
});
const pagination: PaginationProps = reactive({
page: 1, page: 1,
pageSize: 10, pageSize: 10,
itemCount: 0,
showSizePicker: true, showSizePicker: true,
itemCount: 0,
pageSizes: [10, 15, 20, 25, 30], pageSizes: [10, 15, 20, 25, 30],
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined, onUpdatePage: async (page: number) => {
onUpdatePage(page) {
pagination.page = page; pagination.page = page;
updateSearchParams({
pageNum: page,
pageSize: pagination.pageSize!
});
getData();
}, },
onUpdatePageSize(pageSize) { onUpdatePageSize: async (pageSize: number) => {
pagination.pageSize = pageSize; pagination.pageSize = pageSize;
pagination.page = 1; pagination.page = 1;
updateSearchParams({
pageNum: pagination.page,
pageSize
});
getData();
}, },
...options.paginationProps ...(showTotal
}) as PaginationProps; ? {
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
}
: {})
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly // this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => { const mobilePagination = computed(() => {
const p: PaginationProps = { const p: PaginationProps = {
...pagination, ...pagination,
pageSlot: isMobile.value ? 3 : 9, pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
}; };
return p; return p;
}); });
const paginationParams = computed(() => { function updatePagination(update: Partial<PaginationProps>) {
const { page, pageSize } = pagination; Object.assign(pagination, update);
return {
page,
pageSize
};
});
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
...options,
pagination: true,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns,
onFetched: data => {
pagination.itemCount = data.total;
}
});
async function getDataByPage(page: number = 1) {
if (page !== pagination.page) {
pagination.page = page;
return;
}
await result.getData();
} }
scope.run(() => { /**
watch( * get data by page number
() => appStore.locale, *
() => { * @param pageNum the page number. default is 1
result.reloadColumns(); */
} async function getDataByPage(pageNum: number = 1) {
); updatePagination({
page: pageNum
watch(paginationParams, async newVal => {
await options.onPaginationParamsChange?.(newVal);
await result.getData();
}); });
});
onScopeDispose(() => { updateSearchParams({
scope.stop(); pageNum,
}); pageSize: pagination.pageSize!
});
return {
...result,
getDataByPage,
pagination,
mobilePagination
};
}
export function useTableOperate<TableData>(
data: Ref<TableData[]>,
idKey: keyof TableData,
getData: () => Promise<void>
) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
/** the editing row data */
const editingData = shallowRef<TableData | null>(null);
function handleEdit(id: TableData[keyof TableData]) {
operateType.value = 'edit';
const findItem = data.value.find(item => item[idKey] === id) || null;
editingData.value = jsonClone(findItem);
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = shallowRef<string[]>([]);
/** the hook after the batch delete operation is completed */
async function onBatchDeleted() {
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData(); await getData();
} }
/** the hook after the delete operation is completed */
async function onDeleted() {
window.$message?.success($t('common.deleteSuccess'));
await getData();
}
return {
drawerVisible,
openDrawer,
closeDrawer,
operateType,
handleAdd,
editingData,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
};
}
export function defaultTransform<ApiData>(
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
): PaginationData<ApiData> {
const { data, error } = response;
if (error) {
return {
data: [],
pageNum: 1,
pageSize: 10,
total: 0
};
}
const { rows: records, pageSize: current, pageNum: size, total } = data;
return {
data: records,
pageNum: current,
pageSize: size,
total
};
}
type UseNaiveTreeTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, false> & {
keyField: keyof ApiData;
defaultExpandAll?: boolean;
};
export function useNaiveTreeTable<ResponseData, ApiData>(options: UseNaiveTreeTableOptions<ResponseData, ApiData>) {
const scope = effectScope();
const appStore = useAppStore();
const rows: Ref<ApiData[]> = ref([]);
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
...options,
pagination: false,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns,
onFetched: transformData => {
const data: ApiData[] = [];
const collect = (nodes: any[]) => {
nodes.forEach(node => {
data.push(node);
if (node?.children?.length) {
collect(node.children);
}
});
};
collect(transformData);
rows.value = data;
}
});
const { keyField = 'id', defaultExpandAll = false } = options;
const expandedRowKeys = ref<ApiData[keyof ApiData][]>([]);
const { bool: isCollapse, toggle: toggleCollapse } = useBoolean(defaultExpandAll);
/** expand all nodes */
function expandAll() {
toggleCollapse();
expandedRowKeys.value = rows.value.map(item => item[keyField as keyof ApiData]);
}
/** collapse all nodes */
function collapseAll() {
toggleCollapse();
expandedRowKeys.value = [];
}
scope.run(() => { scope.run(() => {
watch( watch(
() => appStore.locale, () => appStore.locale,
() => { () => {
result.reloadColumns(); reloadColumns();
} }
); );
}); });
@ -316,19 +199,27 @@ export function useNaiveTreeTable<ResponseData, ApiData>(options: UseNaiveTreeTa
}); });
return { return {
...result, loading,
rows, empty,
isCollapse, data,
expandedRowKeys, columns,
expandAll, columnChecks,
collapseAll reloadColumns,
pagination,
mobilePagination,
updatePagination,
getData,
getDataByPage,
searchParams,
updateSearchParams,
resetSearchParams
}; };
} }
export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof ApiData, getData: () => Promise<void>) { export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean(); const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = shallowRef<NaiveUI.TableOperateType>('add'); const operateType = ref<NaiveUI.TableOperateType>('add');
function handleAdd() { function handleAdd() {
operateType.value = 'add'; operateType.value = 'add';
@ -336,18 +227,18 @@ export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof
} }
/** the editing row data */ /** the editing row data */
const editingData = shallowRef<ApiData | null>(null); const editingData: Ref<T | null> = ref(null);
function handleEdit(id: ApiData[keyof ApiData]) { function handleEdit(field: keyof T, id: CommonType.IdType) {
operateType.value = 'edit'; operateType.value = 'edit';
const findItem = data.value.find(item => item[idKey] === id) || null; const findItem = data.value.find(item => item[field] === id) || null;
editingData.value = jsonClone(findItem); editingData.value = jsonClone(findItem);
openDrawer(); openDrawer();
} }
/** the checked row keys of table */ /** the checked row keys of table */
const checkedRowKeys = shallowRef<string[]>([]); const checkedRowKeys = ref<CommonType.IdType[]>([]);
/** the hook after the batch delete operation is completed */ /** the hook after the batch delete operation is completed */
async function onBatchDeleted() { async function onBatchDeleted() {
@ -379,135 +270,6 @@ export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof
}; };
} }
type TreeTableOptions<ApiData> = { function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
/** id field name */
idField?: keyof ApiData;
/** parent id field name */
parentIdField?: keyof ApiData;
/** children field name */
childrenField?: keyof ApiData;
/** filter function */
filterFn?: (node: ApiData) => boolean;
};
export function treeTransform<ApiData>(
response: FlatResponseData<any, ApiData[]>,
options: TreeTableOptions<ApiData>
): ApiData[] {
const { data, error } = response;
if (error || !data.length) {
return [];
}
const { idField = 'id', parentIdField = 'parentId', childrenField = 'children', filterFn = () => true } = options;
// 使用 Map 替代普通对象,提高性能
const childrenMap = new Map<ApiData[keyof ApiData], ApiData[]>();
const nodeMap = new Map<ApiData[keyof ApiData], ApiData>();
const tree: ApiData[] = [];
// 第一遍遍历:构建节点映射
for (const item of data) {
const id = item[idField as keyof ApiData];
const parentId = item[parentIdField as keyof ApiData];
nodeMap.set(id, item);
if (!childrenMap.has(parentId)) {
childrenMap.set(parentId, []);
}
// 应用过滤函数
if (filterFn(item)) {
childrenMap.get(parentId)!.push(item);
}
}
// 第二遍遍历:找出根节点
for (const item of data) {
const parentId = item[parentIdField as keyof ApiData];
if (!nodeMap.has(parentId) && filterFn(item)) {
tree.push(item);
}
}
// 递归构建树形结构
const buildTree = (node: ApiData) => {
const id = node[idField as keyof ApiData];
const children = childrenMap.get(id);
if (children?.length) {
// 使用类型断言确保类型安全
(node as any)[childrenField] = children;
for (const child of children) {
buildTree(child);
}
} else {
// 如果没有子节点,设置为 undefined
(node as any)[childrenField] = undefined;
}
};
// 从根节点开始构建树
for (const root of tree) {
buildTree(root);
}
return tree;
}
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
cols: Column[],
getColumnVisible?: (column: Column) => boolean
) {
const checks: TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true,
visible: getColumnVisible?.(column) ?? true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true,
visible: getColumnVisible?.(column) ?? false
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true,
visible: getColumnVisible?.(column) ?? false
});
}
});
return checks;
}
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
const columnMap = new Map<string, Column>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
return filteredColumns;
}
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key); return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
} }

View File

@ -0,0 +1,237 @@
import { effectScope, onScopeDispose, ref, watch } from 'vue';
import type { Ref } from 'vue';
import { jsonClone } from '@sa/utils';
import { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { handleTree } from '@/utils/common';
import { $t } from '@/locales';
type TableData = NaiveUI.TableData;
type GetTableData<A extends NaiveUI.TreeTableApiFn> = NaiveUI.GetTreeTableData<A>;
type TableColumn<T> = NaiveUI.TableColumn<T>;
export function useTreeTable<A extends NaiveUI.TreeTableApiFn>(
config: NaiveUI.NaiveTreeTableConfig<A> & CommonType.TreeConfig & { defaultExpandAll?: boolean }
) {
const scope = effectScope();
const appStore = useAppStore();
const {
apiFn,
apiParams,
immediate,
idField,
parentIdField = 'parentId',
childrenField = 'children',
defaultExpandAll = false
} = config;
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
const expandedRowKeys = ref<CommonType.IdType[]>([]);
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const records = res.data || [];
if (!records.length) return { data: [] };
const treeData = handleTree(records, {
idField,
parentIdField,
childrenField
});
// if defaultExpandAll is true, expand all nodes
expandedRowKeys.value = defaultExpandAll
? records.map(item => item[idField])
: records.filter(item => item[parentIdField] === 0).map(item => item[idField]) || [];
return { data: treeData };
},
getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title as string,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
return filteredColumns;
},
immediate
});
/** 收集所有节点的key */
function collectAllNodeKeys(treeNodes: any[]): CommonType.IdType[] {
const keys: CommonType.IdType[] = [];
const collect = (nodes: any[]) => {
nodes.forEach(node => {
keys.push(node[idField]);
if (node[childrenField]?.length) {
collect(node[childrenField]);
}
});
};
collect(treeNodes);
return keys;
}
const { bool: isCollapse, toggle: toggleCollapse } = useBoolean(defaultExpandAll);
/** expand all nodes */
function expandAll() {
toggleCollapse();
expandedRowKeys.value = collectAllNodeKeys(data.value);
}
/** collapse all nodes */
function collapseAll() {
toggleCollapse();
expandedRowKeys.value = [];
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
reloadColumns();
}
);
});
onScopeDispose(() => {
scope.stop();
});
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams,
expandedRowKeys,
isCollapse,
expandAll,
collapseAll
};
}
export function useTreeTableOperate<T extends TableData = TableData>(_: Ref<T[]>, getData: () => Promise<void>) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
/** the editing row data */
const editingData: Ref<T | null> = ref(null);
function handleEdit(row: T) {
operateType.value = 'edit';
editingData.value = jsonClone(row);
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = ref<CommonType.IdType[]>([]);
function clearCheckedRowKeys() {
checkedRowKeys.value = [];
}
/** the hook after the batch delete operation is completed */
async function onBatchDeleted() {
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
}
/** the hook after the delete operation is completed */
async function onDeleted() {
window.$message?.success($t('common.deleteSuccess'));
await getData();
}
return {
drawerVisible,
openDrawer,
closeDrawer,
operateType,
handleAdd,
editingData,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted,
clearCheckedRowKeys
};
}
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}

View File

@ -12,7 +12,7 @@ import GlobalTab from '../modules/global-tab/index.vue';
import GlobalContent from '../modules/global-content/index.vue'; import GlobalContent from '../modules/global-content/index.vue';
import GlobalFooter from '../modules/global-footer/index.vue'; import GlobalFooter from '../modules/global-footer/index.vue';
import ThemeDrawer from '../modules/theme-drawer/index.vue'; import ThemeDrawer from '../modules/theme-drawer/index.vue';
import { provideMixMenuContext } from '../modules/global-menu/context'; import { setupMixMenuContext } from '../context';
defineOptions({ defineOptions({
name: 'BaseLayout' name: 'BaseLayout'
@ -20,7 +20,7 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext(); const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue')); const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
@ -31,7 +31,7 @@ const layoutMode = computed(() => {
}); });
const headerProps = computed(() => { const headerProps = computed(() => {
const { mode } = themeStore.layout; const { mode, reverseHorizontalMix } = themeStore.layout;
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = { const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: { vertical: {
@ -44,25 +44,15 @@ const headerProps = computed(() => {
showMenu: false, showMenu: false,
showMenuToggler: false showMenuToggler: false
}, },
'vertical-hybrid-header-first': {
showLogo: !isActiveFirstLevelMenuHasChildren.value,
showMenu: true,
showMenuToggler: false
},
horizontal: { horizontal: {
showLogo: true, showLogo: true,
showMenu: true, showMenu: true,
showMenuToggler: false showMenuToggler: false
}, },
'top-hybrid-sidebar-first': { 'horizontal-mix': {
showLogo: true, showLogo: true,
showMenu: true, showMenu: true,
showMenuToggler: false showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
},
'top-hybrid-header-first': {
showLogo: true,
showMenu: true,
showMenuToggler: isActiveFirstLevelMenuHasChildren.value
} }
}; };
@ -73,56 +63,44 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix'); const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isVerticalHybridHeaderFirst = computed(() => themeStore.layout.mode === 'vertical-hybrid-header-first'); const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
const siderWidth = computed(() => getSiderWidth()); const siderWidth = computed(() => getSiderWidth());
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth()); const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
function getSiderAndCollapsedWidth(isCollapsed: boolean) { function getSiderWidth() {
const { const { reverseHorizontalMix } = themeStore.layout;
mixChildMenuWidth, const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
collapsedWidth,
width: themeWidth,
mixCollapsedWidth,
mixWidth: themeMixWidth
} = themeStore.sider;
const width = isCollapsed ? collapsedWidth : themeWidth; if (isHorizontalMix.value && reverseHorizontalMix) {
const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
if (isTopHybridHeaderFirst.value) {
return isActiveFirstLevelMenuHasChildren.value ? width : 0; return isActiveFirstLevelMenuHasChildren.value ? width : 0;
} }
if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) { let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
return 0;
}
const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
let finalWidth = isMixMode ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) { if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
finalWidth += mixChildMenuWidth; w += mixChildMenuWidth;
} }
if (isVerticalHybridHeaderFirst.value && appStore.mixSiderFixed && childLevelMenus.value.length) { return w;
finalWidth += mixChildMenuWidth;
}
return finalWidth;
}
function getSiderWidth() {
return getSiderAndCollapsedWidth(false);
} }
function getSiderCollapsedWidth() { function getSiderCollapsedWidth() {
return getSiderAndCollapsedWidth(true); const { reverseHorizontalMix } = themeStore.layout;
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
}
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
w += mixChildMenuWidth;
}
return w;
} }
onMounted(() => { onMounted(() => {

View File

@ -0,0 +1,83 @@
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useContext } from '@sa/hooks';
import { useRouteStore } from '@/store/modules/route';
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
function useMixMenu() {
const route = useRoute();
const routeStore = useRouteStore();
const { selectedKey } = useMenu();
const activeFirstLevelMenuKey = ref('');
function setActiveFirstLevelMenuKey(key: string) {
activeFirstLevelMenuKey.value = key;
}
function getActiveFirstLevelMenuKey() {
const [firstLevelRouteName] = selectedKey.value.split('_');
setActiveFirstLevelMenuKey(firstLevelRouteName);
}
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const childLevelMenus = computed<App.Global.Menu[]>(
() => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const isActiveFirstLevelMenuHasChildren = computed(() => {
if (!activeFirstLevelMenuKey.value) {
return false;
}
const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
return Boolean(findItem?.children?.length);
});
watch(
() => route.name,
() => {
getActiveFirstLevelMenuKey();
},
{ immediate: true }
);
return {
allMenus,
firstLevelMenus,
childLevelMenus,
isActiveFirstLevelMenuHasChildren,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey
};
}
export function useMenu() {
const route = useRoute();
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
return {
selectedKey
};
}

View File

@ -3,7 +3,6 @@ import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core'; import { createReusableTemplate } from '@vueuse/core';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { transformColorWithOpacity } from '@sa/color'; import { transformColorWithOpacity } from '@sa/color';
import type { RouteKey } from '@elegant-router/types';
defineOptions({ defineOptions({
name: 'FirstLevelMenu' name: 'FirstLevelMenu'
@ -21,7 +20,7 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
interface Emits { interface Emits {
(e: 'select', menuKey: RouteKey): boolean; (e: 'select', menu: App.Global.Menu): boolean;
(e: 'toggleSiderCollapse'): void; (e: 'toggleSiderCollapse'): void;
} }
@ -48,8 +47,8 @@ const selectedBgColor = computed(() => {
return darkMode ? dark : light; return darkMode ? dark : light;
}); });
function handleClickMixMenu(menuKey: RouteKey) { function handleClickMixMenu(menu: App.Global.Menu) {
emit('select', menuKey); emit('select', menu);
} }
function toggleSiderCollapse() { function toggleSiderCollapse() {
@ -89,7 +88,7 @@ function toggleSiderCollapse() {
:icon="menu.icon" :icon="menu.icon"
:active="menu.key === activeMenuKey" :active="menu.key === activeMenuKey"
:is-mini="siderCollapse" :is-mini="siderCollapse"
@click="handleClickMixMenu(menu.routeKey)" @click="handleClickMixMenu(menu)"
/> />
</SimpleScrollbar> </SimpleScrollbar>
<MenuToggler <MenuToggler

View File

@ -1,143 +0,0 @@
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useContext } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu', useMixMenu);
function useMixMenu() {
const route = useRoute();
const routeStore = useRouteStore();
const { selectedKey } = useMenu();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
const firstLevelMenus = computed<App.Global.Menu[]>(() =>
routeStore.menus.map(menu => {
const { children: _, ...rest } = menu;
return rest;
})
);
const activeFirstLevelMenuKey = ref('');
function setActiveFirstLevelMenuKey(key: string) {
activeFirstLevelMenuKey.value = key;
}
function getActiveFirstLevelMenuKey() {
const [firstLevelRouteName] = selectedKey.value.split('_');
setActiveFirstLevelMenuKey(firstLevelRouteName);
}
const isActiveFirstLevelMenuHasChildren = computed(() => {
if (!activeFirstLevelMenuKey.value) {
return false;
}
const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
return Boolean(findItem?.children?.length);
});
function handleSelectFirstLevelMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const secondLevelMenus = computed<App.Global.Menu[]>(
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
);
const activeSecondLevelMenuKey = ref('');
function setActiveSecondLevelMenuKey(key: string) {
activeSecondLevelMenuKey.value = key;
}
function getActiveSecondLevelMenuKey() {
const keys = selectedKey.value.split('_');
if (keys.length < 2) {
setActiveSecondLevelMenuKey('');
return;
}
const [firstLevelRouteName, level2SuffixName] = keys;
const secondLevelRouteName = `${firstLevelRouteName}_${level2SuffixName}`;
setActiveSecondLevelMenuKey(secondLevelRouteName);
}
const isActiveSecondLevelMenuHasChildren = computed(() => {
if (!activeSecondLevelMenuKey.value) {
return false;
}
const findItem = secondLevelMenus.value.find(item => item.key === activeSecondLevelMenuKey.value);
return Boolean(findItem?.children?.length);
});
function handleSelectSecondLevelMenu(key: RouteKey) {
setActiveSecondLevelMenuKey(key);
if (!isActiveSecondLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const childLevelMenus = computed<App.Global.Menu[]>(
() => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
);
watch(
() => route.name,
() => {
getActiveFirstLevelMenuKey();
},
{ immediate: true }
);
return {
firstLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
isActiveFirstLevelMenuHasChildren,
handleSelectFirstLevelMenu,
getActiveFirstLevelMenuKey,
secondLevelMenus,
activeSecondLevelMenuKey,
setActiveSecondLevelMenuKey,
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
};
}
export function useMenu() {
const route = useRoute();
const selectedKey = computed(() => {
const { hideInMenu, activeMenu } = route.meta;
const name = route.name as string;
const routeName = (hideInMenu ? activeMenu : name) || name;
return routeName;
});
return {
selectedKey
};
}

View File

@ -5,10 +5,9 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import VerticalMenu from './modules/vertical-menu.vue'; import VerticalMenu from './modules/vertical-menu.vue';
import VerticalMixMenu from './modules/vertical-mix-menu.vue'; import VerticalMixMenu from './modules/vertical-mix-menu.vue';
import VerticalHybridHeaderFirst from './modules/vertical-hybrid-header-first.vue';
import HorizontalMenu from './modules/horizontal-menu.vue'; import HorizontalMenu from './modules/horizontal-menu.vue';
import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue'; import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue'; import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
defineOptions({ defineOptions({
name: 'GlobalMenu' name: 'GlobalMenu'
@ -21,10 +20,8 @@ const activeMenu = computed(() => {
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = { const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
vertical: VerticalMenu, vertical: VerticalMenu,
'vertical-mix': VerticalMixMenu, 'vertical-mix': VerticalMixMenu,
'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
horizontal: HorizontalMenu, horizontal: HorizontalMenu,
'top-hybrid-sidebar-first': TopHybridSidebarFirst, 'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
'top-hybrid-header-first': TopHybridHeaderFirst
}; };
return menuMap[themeStore.layout.mode]; return menuMap[themeStore.layout.mode];

View File

@ -2,7 +2,7 @@
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app'; import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
import { useRouteStore } from '@/store/modules/route'; import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
import { useMenu } from '../context'; import { useMenu } from '../../../context';
defineOptions({ defineOptions({
name: 'HorizontalMenu' name: 'HorizontalMenu'

View File

@ -4,18 +4,25 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
import FirstLevelMenu from '../components/first-level-menu.vue'; import FirstLevelMenu from '../components/first-level-menu.vue';
import { useMenu, useMixMenuContext } from '../context'; import { useMenu, useMixMenuContext } from '../../../context';
defineOptions({ defineOptions({
name: 'TopHybridSidebarFirst' name: 'HorizontalMixMenu'
}); });
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
useMixMenuContext('TopHybridSidebarFirst');
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
}
</script> </script>
<template> <template>
@ -23,24 +30,22 @@ const { selectedKey } = useMenu();
<NMenu <NMenu
mode="horizontal" mode="horizontal"
:value="selectedKey" :value="selectedKey"
:options="secondLevelMenus" :options="childLevelMenus"
:indent="18" :indent="18"
responsive responsive
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"
/> />
</Teleport> </Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full pt-2"> <FirstLevelMenu
<FirstLevelMenu :menus="allMenus"
:menus="firstLevelMenus" :active-menu-key="activeFirstLevelMenuKey"
:active-menu-key="activeFirstLevelMenuKey" :sider-collapse="appStore.siderCollapse"
:sider-collapse="appStore.siderCollapse" :dark-mode="themeStore.darkMode"
:dark-mode="themeStore.darkMode" :theme-color="themeStore.themeColor"
:theme-color="themeStore.themeColor" @select="handleSelectMixMenu"
@select="handleSelectFirstLevelMenu" @toggle-sider-collapse="appStore.toggleSiderCollapse"
@toggle-sider-collapse="appStore.toggleSiderCollapse" />
/>
</div>
</Teleport> </Teleport>
</template> </template>

View File

@ -1,16 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app'; import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route'; import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../context'; import { useMenu, useMixMenuContext } from '../../../context';
defineOptions({ defineOptions({
name: 'TopHybridHeaderFirst' name: 'ReversedHorizontalMixMenu'
}); });
const route = useRoute(); const route = useRoute();
@ -18,10 +19,23 @@ 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,
childLevelMenus,
activeFirstLevelMenuKey,
setActiveFirstLevelMenuKey,
isActiveFirstLevelMenuHasChildren
} = useMixMenuContext();
const { selectedKey } = useMenu(); const { selectedKey } = useMenu();
function handleSelectMixMenu(key: RouteKey) {
setActiveFirstLevelMenuKey(key);
if (!isActiveFirstLevelMenuHasChildren.value) {
routerPushByKeyWithMetaQuery(key);
}
}
const expandedKeys = ref<string[]>([]); const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() { function updateExpandedKeys() {
@ -49,7 +63,7 @@ watch(
:options="firstLevelMenus" :options="firstLevelMenus"
:indent="18" :indent="18"
responsive responsive
@update:value="handleSelectFirstLevelMenu" @update:value="handleSelectMixMenu"
/> />
</Teleport> </Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
@ -61,7 +75,7 @@ watch(
:collapsed="appStore.siderCollapse" :collapsed="appStore.siderCollapse"
:collapsed-width="themeStore.sider.collapsedWidth" :collapsed-width="themeStore.sider.collapsedWidth"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:options="secondLevelMenus" :options="childLevelMenus"
:indent="18" :indent="18"
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"
/> />

View File

@ -1,149 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../context';
import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue';
defineOptions({
name: 'VerticalHybridHeaderFirst'
});
const route = useRoute();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const {
firstLevelMenus,
activeFirstLevelMenuKey,
handleSelectFirstLevelMenu,
getActiveFirstLevelMenuKey,
secondLevelMenus,
activeSecondLevelMenuKey,
isActiveSecondLevelMenuHasChildren,
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
} = useMixMenuContext('VerticalHybridHeaderFirst');
const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMixMenu(key: RouteKey) {
handleSelectSecondLevelMenu(key);
if (isActiveSecondLevelMenuHasChildren.value) {
setDrawerVisible(true);
}
}
function handleSelectMenu(key: RouteKey) {
handleSelectFirstLevelMenu(key);
if (secondLevelMenus.value.length > 0) {
handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
}
}
function handleResetActiveMenu() {
setDrawerVisible(false);
if (!appStore.mixSiderFixed) {
getActiveFirstLevelMenuKey();
getActiveSecondLevelMenuKey();
}
}
const expandedKeys = ref<string[]>([]);
function updateExpandedKeys() {
if (appStore.siderCollapse || !selectedKey.value) {
expandedKeys.value = [];
return;
}
expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
}
watch(
() => route.name,
() => {
updateExpandedKeys();
},
{ immediate: true }
);
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<NMenu
mode="horizontal"
:value="activeFirstLevelMenuKey"
:options="firstLevelMenus"
:indent="18"
responsive
@update:value="handleSelectMenu"
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu
:menus="secondLevelMenus"
:active-menu-key="activeSecondLevelMenuKey"
:inverted="inverted"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
>
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
</FirstLevelMenu>
<div
class="relative h-full transition-width-300"
:style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<DarkModeContainer
class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
:inverted="inverted"
:style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
>
<header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
<h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
<PinToggler
:pin="appStore.mixSiderFixed"
:class="{ 'text-white:88 !hover:text-white': inverted }"
@click="appStore.toggleMixSiderFixed"
/>
</header>
<SimpleScrollbar>
<NMenu
v-model:expanded-keys="expandedKeys"
mode="vertical"
:value="selectedKey"
:options="childLevelMenus"
:inverted="inverted"
:indent="18"
@update:value="routerPushByKeyWithMetaQuery"
/>
</SimpleScrollbar>
</DarkModeContainer>
</div>
</div>
</Teleport>
</template>
<style scoped></style>

View File

@ -7,7 +7,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route'; import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
import { useMenu } from '../context'; import { useMenu } from '../../../context';
defineOptions({ defineOptions({
name: 'VerticalMenu' name: 'VerticalMenu'

View File

@ -3,14 +3,13 @@ import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { SimpleScrollbar } from '@sa/materials'; import { SimpleScrollbar } from '@sa/materials';
import { useBoolean } from '@sa/hooks'; import { useBoolean } from '@sa/hooks';
import type { RouteKey } from '@elegant-router/types';
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app'; import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route'; import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { useMenu, useMixMenuContext } from '../context'; import { useMenu, useMixMenuContext } from '../../../context';
import FirstLevelMenu from '../components/first-level-menu.vue'; import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue'; import GlobalLogo from '../../global-logo/index.vue';
@ -25,26 +24,28 @@ const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush(); const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean(); const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
const { const {
firstLevelMenus, allMenus,
secondLevelMenus, childLevelMenus,
activeFirstLevelMenuKey, activeFirstLevelMenuKey,
isActiveFirstLevelMenuHasChildren, setActiveFirstLevelMenuKey,
getActiveFirstLevelMenuKey, getActiveFirstLevelMenuKey
handleSelectFirstLevelMenu //
} = useMixMenuContext('VerticalMixMenu'); } = useMixMenuContext();
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(() => secondLevelMenus.value.length > 0); const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed)); const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
function handleSelectMenu(key: RouteKey) { function handleSelectMixMenu(menu: App.Global.Menu) {
handleSelectFirstLevelMenu(key); setActiveFirstLevelMenuKey(menu.key);
if (isActiveFirstLevelMenuHasChildren.value) { if (menu.children?.length) {
setDrawerVisible(true); setDrawerVisible(true);
} else {
routerPushByKeyWithMetaQuery(menu.routeKey);
} }
} }
@ -79,13 +80,13 @@ watch(
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`"> <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full flex" @mouseleave="handleResetActiveMenu"> <div class="h-full flex" @mouseleave="handleResetActiveMenu">
<FirstLevelMenu <FirstLevelMenu
:menus="firstLevelMenus" :menus="allMenus"
:active-menu-key="activeFirstLevelMenuKey" :active-menu-key="activeFirstLevelMenuKey"
:inverted="inverted" :inverted="inverted"
:sider-collapse="appStore.siderCollapse" :sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode" :dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor" :theme-color="themeStore.themeColor"
@select="handleSelectMenu" @select="handleSelectMixMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse" @toggle-sider-collapse="appStore.toggleSiderCollapse"
> >
<GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" /> <GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
@ -112,7 +113,7 @@ watch(
v-model:expanded-keys="expandedKeys" v-model:expanded-keys="expandedKeys"
mode="vertical" mode="vertical"
:value="selectedKey" :value="selectedKey"
:options="secondLevelMenus" :options="childLevelMenus"
:inverted="inverted" :inverted="inverted"
:indent="18" :indent="18"
@update:value="routerPushByKeyWithMetaQuery" @update:value="routerPushByKeyWithMetaQuery"

Some files were not shown because too many files have changed in this diff Show More