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)
### &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>
# 💎 友情链接
- [Snail Job Pro](https://pro.snailjob.opensnail.com/home) - 灵活,可靠和快速的分布式任务重试和分布式任务调度平台
- [AiZuDa - 爱组搭(飞龙工作流企业版)](https://naiveui.aizuda.com) - 像搭积木一样进行低代码甚至零代码快速构建应用
## 📋 项目概述
RuoYi-Plus-Soybean 是一个现代化的企业级多租户管理系统,它结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。

View File

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

View File

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

View File

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

View File

@ -8,30 +8,7 @@ export type ContentType =
| 'application/x-www-form-urlencoded'
| 'application/octet-stream';
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
export interface RequestOption<
ResponseData,
ApiData = ResponseData,
State extends Record<string, unknown> = Record<string, unknown>
> {
/**
* The default state
*/
defaultState?: State;
/**
* transform the response data to the api data
*
* @param response Axios response
*/
transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
/**
* transform the response data to the api data
*
* @deprecated use `transform` instead, will be removed in the next major version v3
* @param response Axios response
*/
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
export interface RequestOption<ResponseData = any> {
/**
* The hook before request
*
@ -58,6 +35,12 @@ export interface RequestOption<
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => 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
*
@ -85,7 +68,15 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
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
*
@ -93,35 +84,32 @@ export interface RequestInstanceCommon<State extends Record<string, unknown>> {
*/
cancelAllRequest: () => void;
/** you can set custom state in the request instance */
state: State;
state: T;
}
/** The request instance */
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<MappedType<R, T>>;
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
}
export type FlatResponseSuccessData<ResponseData, ApiData> = {
data: ApiData;
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
data: T;
error: null;
response: AxiosResponse<ResponseData>;
};
export type FlatResponseFailData<ResponseData> = {
export type FlatResponseFailData<ResponseData = any> = {
data: null;
error: AxiosError<ResponseData>;
response: AxiosResponse<ResponseData>;
};
export type FlatResponseData<ResponseData, ApiData> =
| FlatResponseSuccessData<ResponseData, ApiData>
export type FlatResponseData<T = any, ResponseData = any> =
| FlatResponseSuccessData<T, ResponseData>
| FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
extends RequestInstanceCommon<State> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(
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 useContext from './use-context';
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 type * from './use-table';
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
export * from './use-signal';
export * from './use-table';

View File

@ -1,4 +1,5 @@
import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
/**
* Use context
@ -11,7 +12,7 @@ import { inject, provide } from 'vue';
* import { ref } from 'vue';
* import { useContext } from '@sa/hooks';
*
* export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
* export const { setupStore, useStore } = useContext('demo', () => {
* const count = ref(0);
*
* function increment() {
@ -34,10 +35,10 @@ import { inject, provide } from 'vue';
* <div>A</div>
* </template>
* <script setup lang="ts">
* import { provideDemoContext } from './context';
* import { setupStore } from './context';
*
* provideDemoContext();
* // const { increment } = provideDemoContext(); // also can control the store in the parent component
* setupStore();
* // const { increment } = setupStore(); // also can control the store in the parent component
* </script>
* ``` // B.vue
* ```vue
@ -45,9 +46,9 @@ import { inject, provide } from 'vue';
* <div>B</div>
* </template>
* <script setup lang="ts">
* import { useDemoContext } from './context';
* import { useStore } from './context';
*
* const { count, increment } = useDemoContext();
* const { count, increment } = useStore();
* </script>
* ```;
*
@ -56,41 +57,40 @@ import { inject, provide } from 'vue';
* @param contextName Context name
* @param fn Context function
*/
export default function useContext<Arguments extends Array<any>, T>(
contextName: string,
composable: (...args: Arguments) => T
) {
const key = Symbol(contextName);
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
type Context = ReturnType<T>;
/**
* 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);
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
if (consumerName && !value) {
throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
}
function setupStore(...args: Parameters<T>) {
const context: Context = fn(...args);
return useProvide(context);
}
// @ts-expect-error - we want to return null if the value is undefined or null
return value || null;
return {
/** 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,
CustomAxiosRequestConfig,
MappedType,
RequestInstanceCommon,
RequestOption,
ResponseType
} from '@sa/axios';
import useLoading from './use-loading';
export type HookRequestInstanceResponseSuccessData<ApiData> = {
data: Ref<ApiData>;
export type HookRequestInstanceResponseSuccessData<T = any> = {
data: Ref<T>;
error: Ref<null>;
};
export type HookRequestInstanceResponseFailData<ResponseData> = {
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
data: Ref<null>;
error: Ref<AxiosError<ResponseData>>;
};
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
extends RequestInstanceCommon<State> {
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
export interface HookRequestInstance<ResponseData = any> {
<T = any, R extends ResponseType = 'json'>(
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 options
*/
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
export default function createHookRequest<ResponseData = any>(
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<
T extends ApiData = ApiData,
R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) {
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const { loading, startLoading, endLoading } = useLoading();
const data = ref(null) as Ref<MappedType<R, T>>;
const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
startLoading();
request(config).then(res => {
if (res.data) {
data.value = res.data as MappedType<R, T>;
data.value = res.data;
} else {
error.value = res.error;
}
@ -71,8 +70,9 @@ export default function createHookRequest<ResponseData, ApiData, State extends R
data,
error
};
} as HookRequestInstance<ResponseData, ApiData, State>;
} as HookRequestInstance<ResponseData>;
hookRequest.cancelRequest = request.cancelRequest;
hookRequest.cancelAllRequest = request.cancelAllRequest;
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 { jsonClone } from '@sa/utils';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
export interface PaginationData<T> {
data: T[];
pageNum: number;
pageSize: number;
total: number;
}
export type MaybePromise<T> = T | Promise<T>;
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
response: ResponseData
) => GetApiData<ApiData, Pagination>;
export type ApiFn = (args: any) => Promise<unknown>;
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
@ -22,64 +14,74 @@ export type TableColumnCheck = {
key: string;
title: TableColumnCheckTitle;
checked: boolean;
visible: boolean;
};
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
/**
* api function to get table data
*/
api: () => Promise<ResponseData>;
/**
* whether to enable pagination
*/
pagination?: Pagination;
/**
* transform api response to table data
*/
transform: Transform<ResponseData, ApiData, Pagination>;
/**
* columns factory
*/
columns: () => Column[];
export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = {
data: TableDataWithIndex<T>[];
total?: number;
};
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */
apiFn: A;
/** api params */
apiParams?: Parameters<A>[0];
/** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>;
/** columns factory */
columns: () => C[];
/**
* get column checks
*
* @param columns
*/
getColumnChecks: (columns: Column[]) => TableColumnCheck[];
getColumnChecks: (columns: C[]) => TableColumnCheck[];
/**
* get columns
*
* @param columns
*/
getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
/**
* 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
*
* @default true
*/
immediate?: boolean;
}
};
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
) {
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
const { loading, startLoading, endLoading } = useLoading();
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() {
allColumns.value = config.columns();
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 => ({
...col,
@ -88,21 +90,48 @@ export default function useTable<ResponseData, ApiData, Column, Pagination exten
}
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);
} finally {
endLoading();
}
setEmpty(transformed.data.length === 0);
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) {
@ -113,20 +142,12 @@ export default function useTable<ResponseData, ApiData, Column, Pagination exten
loading,
empty,
data,
columns: $columns,
columns,
columnChecks,
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="[
style['layout-header'],
commonClass,
headerClass,
headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]"

View File

@ -6,6 +6,12 @@ interface AdminLayoutHeaderConfig {
* @default true
*/
headerVisible?: boolean;
/**
* Header class
*
* @default ''
*/
headerClass?: string;
/**
* 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": {
"@soybeanjs/changelog": "0.3.24",
"bumpp": "10.2.0",
"c12": "3.1.0",
"c12": "3.0.4",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",

View File

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

334
pnpm-lock.yaml generated
View File

@ -32,6 +32,9 @@ importers:
'@sa/utils':
specifier: workspace:*
version: link:packages/utils
'@tauri-apps/api':
specifier: 2.5.0
version: 2.5.0
'@types/streamsaver':
specifier: ^2.0.5
version: 2.0.5
@ -87,8 +90,8 @@ importers:
specifier: 0.6.0
version: 0.6.0(@types/sortablejs@1.15.8)
vue-i18n:
specifier: 11.1.10
version: 11.1.10(vue@3.5.17(typescript@5.8.3))
specifier: 11.1.9
version: 11.1.9(vue@3.5.17(typescript@5.8.3))
vue-router:
specifier: 4.5.1
version: 4.5.1(vue@3.5.17(typescript@5.8.3))
@ -97,8 +100,8 @@ importers:
specifier: 0.3.8
version: 0.3.8
'@iconify/json':
specifier: 2.2.359
version: 2.2.359
specifier: 2.2.357
version: 2.2.357
'@sa/scripts':
specifier: workspace:*
version: link:packages/scripts
@ -108,9 +111,12 @@ importers:
'@soybeanjs/eslint-config':
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)))
'@tauri-apps/cli':
specifier: 2.5.0
version: 2.5.0
'@types/node':
specifier: 24.0.15
version: 24.0.15
specifier: 24.0.13
version: 24.0.13
'@types/nprogress':
specifier: 0.2.3
version: 0.2.3
@ -131,13 +137,13 @@ importers:
version: 66.3.3
'@unocss/vite':
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':
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':
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:
specifier: 3.4.2
version: 3.4.2
@ -169,29 +175,29 @@ importers:
specifier: 28.8.0
version: 28.8.0(@babel/parser@7.28.0)(vue@3.5.17(typescript@5.8.3))
vite:
specifier: 7.0.5
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)
specifier: 7.0.4
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:
specifier: ^1.1.0
version: 1.1.0(monaco-editor@0.52.2)
vite-plugin-progress:
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:
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:
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:
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:
specifier: 10.2.0
version: 10.2.0(eslint@9.31.0(jiti@2.4.2))
vue-tsc:
specifier: 3.0.3
version: 3.0.3(typescript@5.8.3)
specifier: 3.0.1
version: 3.0.1(typescript@5.8.3)
packages/alova:
dependencies:
@ -255,6 +261,12 @@ importers:
specifier: 0.9.1
version: 0.9.1
packages/ofetch:
dependencies:
ofetch:
specifier: 1.4.1
version: 1.4.1
packages/scripts:
devDependencies:
'@soybeanjs/changelog':
@ -264,8 +276,8 @@ importers:
specifier: 10.2.0
version: 10.2.0
c12:
specifier: 3.1.0
version: 3.1.0
specifier: 3.0.4
version: 3.0.4
cac:
specifier: 6.7.14
version: 6.7.14
@ -758,8 +770,8 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@iconify/json@2.2.359':
resolution: {integrity: sha512-nOIaROD3xeLiFGvJu0YIgeu4Hqbmz6T71b0lsFv1TY6Uu6Lk/5Z8GhDByIE2/zfgxvxfv3f+5A/DkLHmMXYu8Q==}
'@iconify/json@2.2.357':
resolution: {integrity: sha512-v8fr/KwcJ0qsoEJ69k1+M928bfzNmmApyJBTIAwwIzHZrVEUneHTEOJRy7OVYKisauBMVVH067I2uFNoPA92iA==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@ -772,16 +784,16 @@ packages:
peerDependencies:
vue: '>=3'
'@intlify/core-base@11.1.10':
resolution: {integrity: sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==}
'@intlify/core-base@11.1.9':
resolution: {integrity: sha512-Lrdi4wp3XnGhWmB/mMD/XtfGUw1Jt+PGpZI/M63X1ZqhTDjNHRVCs/i8vv8U1cwaj1A9fb0bkCQHLSL0SK+pIQ==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.1.10':
resolution: {integrity: sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==}
'@intlify/message-compiler@11.1.9':
resolution: {integrity: sha512-84SNs3Ikjg0rD1bOuchzb3iK1vR2/8nxrkyccIl5DjFTeMzE/Fxv6X+A7RN5ZXjEWelc1p5D4kHA6HEOhlKL5Q==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.10':
resolution: {integrity: sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==}
'@intlify/shared@11.1.9':
resolution: {integrity: sha512-H/83xgU1l8ox+qG305p6ucmoy93qyjIPnvxGWRA7YdOoHe1tIiW9IlEu4lTdsOR7cfP1ecrwyflQSqXdXBacXA==}
engines: {node: '>= 16'}
'@isaacs/balanced-match@4.0.1':
@ -1117,6 +1129,85 @@ packages:
vue-eslint-parser:
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':
resolution: {integrity: sha512-HiXKB+M3mJnWO6/8kY0HsP255+8zLZw5JMqHKVUvsXvzYyHW+splXXwYDYOkCYqf39R5nBqQaK2l2WL9rz3y5w==}
peerDependencies:
@ -1154,8 +1245,8 @@ packages:
'@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
'@types/node@24.0.15':
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==}
'@types/node@24.0.13':
resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==}
'@types/nprogress@0.2.3':
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
@ -1436,14 +1527,14 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
vue: ^3.2.25
'@volar/language-core@2.4.20':
resolution: {integrity: sha512-dRDF1G33xaAIDqR6+mXUIjXYdu9vzSxlMGfMEwBxQsfY/JMUEXSpLTR057oTKlUQ2nIvCmP9k94A8h8z2VrNSA==}
'@volar/language-core@2.4.17':
resolution: {integrity: sha512-chmRZMbKmcGpKMoO7Reb70uiLrzo0KWC2CkFttKUuKvrE+VYgi+fL9vWMJ07Fv5ulX0V1TAyyacN9q3nc5/ecA==}
'@volar/source-map@2.4.20':
resolution: {integrity: sha512-mVjmFQH8mC+nUaVwmbxoYUy8cww+abaO8dWzqPUjilsavjxH0jCJ3Mp8HFuHsdewZs2c+SP+EO7hCd8Z92whJg==}
'@volar/source-map@2.4.17':
resolution: {integrity: sha512-QDybtQyO3Ms/NjFqNHTC5tbDN2oK5VH7ZaKrcubtfHBDj63n2pizHC3wlMQ+iT55kQXZUUAbmBX5L1C8CHFeBw==}
'@volar/typescript@2.4.20':
resolution: {integrity: sha512-Oc4DczPwQyXcVbd+5RsNEqX6ia0+w3p+klwdZQ6ZKhFjWoBP9PCPQYlKYRi/tDemWphW93P/Vv13vcE9I9D2GQ==}
'@volar/typescript@2.4.17':
resolution: {integrity: sha512-3paEFNh4P5DkgNUB2YkTRrfUekN4brAXxd3Ow1syMqdIPtCZHbUy4AW99S5RO/7mzyTWPMdDSo3mqTpB/LPObQ==}
'@vue/babel-helper-vue-transform-on@1.4.0':
resolution: {integrity: sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==}
@ -1493,8 +1584,8 @@ packages:
'@vue/devtools-shared@7.7.7':
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
'@vue/language-core@3.0.3':
resolution: {integrity: sha512-I9wY0ULMN9tMSua+2C7g+ez1cIziVMUzIHlDYGSl2rtru3Eh4sXj95vZ+4GBuXwwPnEmYfzSApVbXiVbI8V5Gg==}
'@vue/language-core@3.0.1':
resolution: {integrity: sha512-sq+/Mc1IqIexWEQ+Q2XPiDb5SxSvY5JPqHnMOl/PlF5BekslzduX8dglSkpC17VeiAQB6dpS+4aiwNLJRduCNw==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@ -1707,8 +1798,8 @@ packages:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
c12@3.1.0:
resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==}
c12@3.0.4:
resolution: {integrity: sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==}
peerDependencies:
magicast: ^0.3.5
peerDependenciesMeta:
@ -4019,8 +4110,8 @@ packages:
peerDependencies:
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:
resolution: {integrity: sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==}
vite@7.0.4:
resolution: {integrity: sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@ -4104,8 +4195,8 @@ packages:
peerDependencies:
vue: ^3.4.37
vue-i18n@11.1.10:
resolution: {integrity: sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==}
vue-i18n@11.1.9:
resolution: {integrity: sha512-N9ZTsXdRmX38AwS9F6Rh93RtPkvZTkSy/zNv63FTIwZCUbLwwrpqlKz9YQuzFLdlvRdZTnWAUE5jMxr8exdl7g==}
engines: {node: '>= 16'}
peerDependencies:
vue: ^3.0.0
@ -4115,8 +4206,8 @@ packages:
peerDependencies:
vue: ^3.2.0
vue-tsc@3.0.3:
resolution: {integrity: sha512-uU1OMSzWE8/y0+kDTc0iEIu9v82bmFkGyJpAO/x3wQqBkkHkButKgtygREyOkxL4E/xtcf/ExvgNhhjdzonldw==}
vue-tsc@3.0.1:
resolution: {integrity: sha512-UvMLQD0hAGL1g/NfEQelnSVB4H5gtf/gz2lJKjMMwWNOUmSNyWkejwJagAxEbSjtV5CPPJYslOtoSuqJ63mhdg==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
@ -4630,7 +4721,7 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@iconify/json@2.2.359':
'@iconify/json@2.2.357':
dependencies:
'@iconify/types': 2.0.0
pathe: 1.1.2
@ -4655,17 +4746,17 @@ snapshots:
'@iconify/types': 2.0.0
vue: 3.5.17(typescript@5.8.3)
'@intlify/core-base@11.1.10':
'@intlify/core-base@11.1.9':
dependencies:
'@intlify/message-compiler': 11.1.10
'@intlify/shared': 11.1.10
'@intlify/message-compiler': 11.1.9
'@intlify/shared': 11.1.9
'@intlify/message-compiler@11.1.10':
'@intlify/message-compiler@11.1.9':
dependencies:
'@intlify/shared': 11.1.10
'@intlify/shared': 11.1.9
source-map-js: 1.2.1
'@intlify/shared@11.1.10': {}
'@intlify/shared@11.1.9': {}
'@isaacs/balanced-match@4.0.1': {}
@ -4930,6 +5021,55 @@ snapshots:
- eslint-import-resolver-node
- 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))':
dependencies:
vue: 3.5.17(typescript@5.8.3)
@ -4959,7 +5099,7 @@ snapshots:
'@types/node@10.17.60': {}
'@types/node@24.0.15':
'@types/node@24.0.13':
dependencies:
undici-types: 7.8.0
@ -4973,7 +5113,7 @@ snapshots:
'@types/svgo@2.6.4':
dependencies:
'@types/node': 24.0.15
'@types/node': 24.0.13
'@types/web-bluetooth@0.0.21': {}
@ -5204,7 +5344,7 @@ snapshots:
dependencies:
'@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:
'@ampproject/remapping': 2.3.0
'@unocss/config': 66.3.3
@ -5215,7 +5355,7 @@ snapshots:
pathe: 2.0.3
tinyglobby: 0.2.14
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:
- vue
@ -5278,32 +5418,32 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
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:
'@babel/core': 7.28.0
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
'@rolldown/pluginutils': 1.0.0-beta.27
'@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)
transitivePeerDependencies:
- 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:
'@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)
'@volar/language-core@2.4.20':
'@volar/language-core@2.4.17':
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:
'@volar/language-core': 2.4.20
'@volar/language-core': 2.4.17
path-browserify: 1.0.1
vscode-uri: 3.1.0
@ -5377,14 +5517,14 @@ snapshots:
dependencies:
'@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:
'@vue/devtools-kit': 7.7.7
'@vue/devtools-shared': 7.7.7
mitt: 3.0.1
nanoid: 5.1.5
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)
transitivePeerDependencies:
- vite
@ -5403,16 +5543,16 @@ snapshots:
dependencies:
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:
'@volar/language-core': 2.4.20
'@volar/language-core': 2.4.17
'@vue/compiler-dom': 3.5.17
'@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.17
alien-signals: 2.0.5
minimatch: 10.0.3
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.3
optionalDependencies:
typescript: 5.8.3
@ -5617,7 +5757,7 @@ snapshots:
dependencies:
ansis: 4.1.0
args-tokenizer: 0.3.0
c12: 3.1.0
c12: 3.0.4
cac: 6.7.14
escalade: 3.2.0
jsonc-parser: 3.3.1
@ -5633,7 +5773,7 @@ snapshots:
dependencies:
run-applescript: 7.0.0
c12@3.1.0:
c12@3.0.4:
dependencies:
chokidar: 4.0.3
confbox: 0.2.2
@ -8130,11 +8270,11 @@ snapshots:
evtd: 0.2.4
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:
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:
'@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.2.0(rollup@4.45.1)
@ -8145,7 +8285,7 @@ snapshots:
perfect-debounce: 1.0.0
picocolors: 1.1.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:
- rollup
- supports-color
@ -8154,23 +8294,23 @@ snapshots:
dependencies:
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:
picocolors: 1.1.1
progress: 2.0.3
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:
chokidar: 3.6.0
fs-extra: 11.3.0
p-map: 7.0.3
picocolors: 1.1.1
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:
'@types/svgo': 2.6.4
cors: 2.8.5
@ -8180,27 +8320,27 @@ snapshots:
pathe: 0.2.0
svg-baker: 1.7.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:
- 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:
'@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-shared': 7.7.7
execa: 9.6.0
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-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-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: 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.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.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(tsx@4.20.3)(yaml@2.8.0))
transitivePeerDependencies:
- '@nuxt/kit'
- rollup
- supports-color
- 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:
'@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
kolorist: 1.8.0
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:
- 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:
esbuild: 0.25.6
fdir: 6.4.6(picomatch@4.0.3)
@ -8224,7 +8364,7 @@ snapshots:
rollup: 4.45.1
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 24.0.15
'@types/node': 24.0.13
fsevents: 2.3.3
jiti: 2.4.2
sass: 1.89.2
@ -8269,10 +8409,10 @@ snapshots:
dependencies:
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:
'@intlify/core-base': 11.1.10
'@intlify/shared': 11.1.10
'@intlify/core-base': 11.1.9
'@intlify/shared': 11.1.9
'@vue/devtools-api': 6.6.4
vue: 3.5.17(typescript@5.8.3)
@ -8281,10 +8421,10 @@ snapshots:
'@vue/devtools-api': 6.6.4
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:
'@volar/typescript': 2.4.20
'@vue/language-core': 3.0.3(typescript@5.8.3)
'@volar/typescript': 2.4.17
'@vue/language-core': 3.0.1(typescript@5.8.3)
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 { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme';
import { useAuthStore } from './store/modules/auth';
import { naiveDateLocales, naiveLocales } from './locales/naive';
defineOptions({
@ -12,6 +13,7 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { userInfo } = useAuthStore();
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
@ -24,19 +26,24 @@ const naiveDateLocale = computed(() => {
});
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 {
content: themeStore.watermarkContent,
content,
cross: true,
fullscreen: true,
fontSize: 14,
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)',
lineHeight: 14,
width: 384,
height: 384,
width: 200,
height: 300,
xOffset: 12,
yOffset: 60,
rotate: -13,
zIndex: 9999,
fontColor: themeStore.darkMode ? 'rgba(200, 200, 200, 0.03)' : 'rgba(200, 200, 200, 0.2)'
rotate: -18,
zIndex: 9999
};
});
</script>

View File

@ -22,12 +22,7 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
</NButton>
</template>
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
<div
v-for="item in columns"
:key="item.key"
class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
:class="{ hidden: !item.visible }"
>
<div v-for="item in columns" :key="item.key" class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)">
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
<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 themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
light: 'theme.appearance.themeSchema.light',
dark: 'theme.appearance.themeSchema.dark',
auto: 'theme.appearance.themeSchema.auto'
light: 'theme.themeSchema.light',
dark: 'theme.themeSchema.dark',
auto: 'theme.themeSchema.auto'
};
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> = {
vertical: 'theme.layout.layoutMode.vertical',
'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
horizontal: 'theme.layout.layoutMode.horizontal',
'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
vertical: 'theme.layoutMode.vertical',
'vertical-mix': 'theme.layoutMode.vertical-mix',
horizontal: 'theme.layoutMode.horizontal',
'horizontal-mix': 'theme.layoutMode.horizontal-mix'
};
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
wrapper: 'theme.layout.content.scrollMode.wrapper',
content: 'theme.layout.content.scrollMode.content'
wrapper: 'theme.scrollMode.wrapper',
content: 'theme.scrollMode.content'
};
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
chrome: 'theme.layout.tab.mode.chrome',
button: 'theme.layout.tab.mode.button'
chrome: 'theme.tab.mode.chrome',
button: 'theme.tab.mode.button'
};
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
'fade-slide': 'theme.layout.content.page.mode.fade-slide',
fade: 'theme.layout.content.page.mode.fade',
'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
'fade-scale': 'theme.layout.content.page.mode.fade-scale',
'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
'zoom-out': 'theme.layout.content.page.mode.zoom-out',
none: 'theme.layout.content.page.mode.none'
'fade-slide': 'theme.page.mode.fade-slide',
fade: 'theme.page.mode.fade',
'fade-bottom': 'theme.page.mode.fade-bottom',
'fade-scale': 'theme.page.mode.fade-scale',
'zoom-fade': 'theme.page.mode.zoom-fade',
'zoom-out': 'theme.page.mode.zoom-out',
none: 'theme.page.mode.none'
};
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.layout.resetCacheStrategy.close',
refresh: 'theme.layout.resetCacheStrategy.refresh'
close: 'theme.resetCacheStrategy.close',
refresh: 'theme.resetCacheStrategy.refresh'
};
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
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> = {
small: 'theme.table.size.small',
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 * as echarts from 'echarts/core';
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 darkMode = computed(() => themeStore.darkMode);
const domRef = shallowRef<HTMLElement | null>(null);
const domRef = ref<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
const chart = shallowRef<echarts.ECharts | null>(null);
let chart: echarts.ECharts | null = null;
const chartOptions: T = optionsFactory();
const {
@ -111,9 +111,18 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
onDestroy
} = 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 */
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
*/
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
if (!isRendered()) return;
const updatedOpts = callback(chartOptions, optionsFactory);
Object.assign(chartOptions, updatedOpts);
await nextTick();
if (!isRendered()) return;
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) {
chart.value?.setOption(options);
chart?.setOption(options);
}
/** render chart */
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 */
function resize() {
chart.value?.resize();
chart?.resize();
}
/** destroy chart */
async function destroy() {
if (!chart.value) return;
if (!chart) return;
await onDestroy?.(chart.value);
chart.value?.dispose();
chart.value = null;
await onDestroy?.(chart);
chart?.dispose();
chart = null;
}
/** change chart theme */
async function changeTheme() {
await destroy();
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.height = h;
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
return;
}
// resize chart
if (isRendered()) {
resize();
return;
}
// render chart
await render();
if (chart.value) {
await onUpdated?.(chart.value);
if (chart) {
await onUpdated?.(chart);
}
}
scope.run(() => {
watch(
[width, height],
([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
},
{ flush: 'post' }
);
watch([width, height], ([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
});
watch(darkMode, () => {
changeTheme();
@ -223,7 +233,6 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
return {
domRef,
chart,
updateOptions,
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 { 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 { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
'pagination' | 'getColumnChecks' | 'getColumns'
> & {
/**
* get column visible
*
* @param column
*
* @default true
*
* @returns true if the column is visible, false otherwise
*/
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
};
type TableData = NaiveUI.TableData;
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
type TableColumn<T> = NaiveUI.TableColumn<T>;
const SELECTION_KEY = '__selection__';
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>
) {
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
const scope = effectScope();
const appStore = useAppStore();
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,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
itemCount: 0,
pageSizes: [10, 15, 20, 25, 30],
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
onUpdatePage(page) {
onUpdatePage: async (page: number) => {
pagination.page = page;
updateSearchParams({
pageNum: page,
pageSize: pagination.pageSize!
});
getData();
},
onUpdatePageSize(pageSize) {
onUpdatePageSize: async (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
updateSearchParams({
pageNum: pagination.page,
pageSize
});
getData();
},
...options.paginationProps
}) as PaginationProps;
...(showTotal
? {
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
}
: {})
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
};
return p;
});
const paginationParams = computed(() => {
const { page, pageSize } = pagination;
return {
page,
pageSize
};
});
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
...options,
pagination: true,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns,
onFetched: data => {
pagination.itemCount = data.total;
}
});
async function getDataByPage(page: number = 1) {
if (page !== pagination.page) {
pagination.page = page;
return;
}
await result.getData();
function updatePagination(update: Partial<PaginationProps>) {
Object.assign(pagination, update);
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
result.reloadColumns();
}
);
watch(paginationParams, async newVal => {
await options.onPaginationParamsChange?.(newVal);
await result.getData();
/**
* get data by page number
*
* @param pageNum the page number. default is 1
*/
async function getDataByPage(pageNum: number = 1) {
updatePagination({
page: pageNum
});
});
onScopeDispose(() => {
scope.stop();
});
return {
...result,
getDataByPage,
pagination,
mobilePagination
};
}
export function useTableOperate<TableData>(
data: Ref<TableData[]>,
idKey: keyof TableData,
getData: () => Promise<void>
) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const 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 = [];
updateSearchParams({
pageNum,
pageSize: pagination.pageSize!
});
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(() => {
watch(
() => appStore.locale,
() => {
result.reloadColumns();
reloadColumns();
}
);
});
@ -316,19 +199,27 @@ export function useNaiveTreeTable<ResponseData, ApiData>(options: UseNaiveTreeTa
});
return {
...result,
rows,
isCollapse,
expandedRowKeys,
expandAll,
collapseAll
loading,
empty,
data,
columns,
columnChecks,
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 operateType = shallowRef<NaiveUI.TableOperateType>('add');
const operateType = ref<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
@ -336,18 +227,18 @@ export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof
}
/** 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';
const findItem = data.value.find(item => item[idKey] === id) || null;
const findItem = data.value.find(item => item[field] === id) || null;
editingData.value = jsonClone(findItem);
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = shallowRef<string[]>([]);
const checkedRowKeys = ref<CommonType.IdType[]>([]);
/** the hook after the batch delete operation is completed */
async function onBatchDeleted() {
@ -379,135 +270,6 @@ export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof
};
}
type TreeTableOptions<ApiData> = {
/** 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> {
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
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 GlobalFooter from '../modules/global-footer/index.vue';
import ThemeDrawer from '../modules/theme-drawer/index.vue';
import { provideMixMenuContext } from '../modules/global-menu/context';
import { setupMixMenuContext } from '../context';
defineOptions({
name: 'BaseLayout'
@ -20,7 +20,7 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
@ -31,7 +31,7 @@ const layoutMode = computed(() => {
});
const headerProps = computed(() => {
const { mode } = themeStore.layout;
const { mode, reverseHorizontalMix } = themeStore.layout;
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: {
@ -44,25 +44,15 @@ const headerProps = computed(() => {
showMenu: false,
showMenuToggler: false
},
'vertical-hybrid-header-first': {
showLogo: !isActiveFirstLevelMenuHasChildren.value,
showMenu: true,
showMenuToggler: false
},
horizontal: {
showLogo: true,
showMenu: true,
showMenuToggler: false
},
'top-hybrid-sidebar-first': {
'horizontal-mix': {
showLogo: true,
showMenu: true,
showMenuToggler: false
},
'top-hybrid-header-first': {
showLogo: true,
showMenu: true,
showMenuToggler: isActiveFirstLevelMenuHasChildren.value
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
}
};
@ -73,56 +63,44 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isVerticalHybridHeaderFirst = computed(() => themeStore.layout.mode === 'vertical-hybrid-header-first');
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
const siderWidth = computed(() => getSiderWidth());
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
function getSiderAndCollapsedWidth(isCollapsed: boolean) {
const {
mixChildMenuWidth,
collapsedWidth,
width: themeWidth,
mixCollapsedWidth,
mixWidth: themeMixWidth
} = themeStore.sider;
function getSiderWidth() {
const { reverseHorizontalMix } = themeStore.layout;
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
const width = isCollapsed ? collapsedWidth : themeWidth;
const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
if (isTopHybridHeaderFirst.value) {
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
}
if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) {
return 0;
}
const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
let finalWidth = isMixMode ? mixWidth : width;
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
finalWidth += mixChildMenuWidth;
w += mixChildMenuWidth;
}
if (isVerticalHybridHeaderFirst.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
finalWidth += mixChildMenuWidth;
}
return finalWidth;
}
function getSiderWidth() {
return getSiderAndCollapsedWidth(false);
return w;
}
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(() => {

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 { SimpleScrollbar } from '@sa/materials';
import { transformColorWithOpacity } from '@sa/color';
import type { RouteKey } from '@elegant-router/types';
defineOptions({
name: 'FirstLevelMenu'
@ -21,7 +20,7 @@ interface Props {
const props = defineProps<Props>();
interface Emits {
(e: 'select', menuKey: RouteKey): boolean;
(e: 'select', menu: App.Global.Menu): boolean;
(e: 'toggleSiderCollapse'): void;
}
@ -48,8 +47,8 @@ const selectedBgColor = computed(() => {
return darkMode ? dark : light;
});
function handleClickMixMenu(menuKey: RouteKey) {
emit('select', menuKey);
function handleClickMixMenu(menu: App.Global.Menu) {
emit('select', menu);
}
function toggleSiderCollapse() {
@ -89,7 +88,7 @@ function toggleSiderCollapse() {
:icon="menu.icon"
:active="menu.key === activeMenuKey"
:is-mini="siderCollapse"
@click="handleClickMixMenu(menu.routeKey)"
@click="handleClickMixMenu(menu)"
/>
</SimpleScrollbar>
<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 VerticalMenu from './modules/vertical-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 TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
defineOptions({
name: 'GlobalMenu'
@ -21,10 +20,8 @@ const activeMenu = computed(() => {
const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
vertical: VerticalMenu,
'vertical-mix': VerticalMixMenu,
'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
horizontal: HorizontalMenu,
'top-hybrid-sidebar-first': TopHybridSidebarFirst,
'top-hybrid-header-first': TopHybridHeaderFirst
'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
};
return menuMap[themeStore.layout.mode];

View File

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

View File

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

View File

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

View File

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

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