فهرست منبع

perf: improve the logic related to login expiration

vince 10 ماه پیش
والد
کامیت
d62a3da009
43فایلهای تغییر یافته به همراه550 افزوده شده و 345 حذف شده
  1. 23 0
      apps/backend-mock/src/modules/mock/mock.controller.ts
  2. 2 0
      apps/backend-mock/src/modules/mock/mock.module.ts
  3. 1 0
      apps/web-antd/src/apis/modules/index.ts
  4. 10 0
      apps/web-antd/src/apis/modules/mock.ts
  5. 29 55
      apps/web-antd/src/forward/request.ts
  6. 5 5
      apps/web-antd/src/layouts/basic.vue
  7. 6 1
      apps/web-antd/src/locales/langs/en-US.json
  8. 5 0
      apps/web-antd/src/locales/langs/zh-CN.json
  9. 3 18
      apps/web-antd/src/router/guard.ts
  10. 42 0
      apps/web-antd/src/router/routes/modules/demos.ts
  11. 25 8
      apps/web-antd/src/store/modules/access.ts
  12. 5 3
      apps/web-antd/src/store/modules/app.ts
  13. 3 0
      apps/web-antd/src/views/demos/features/hide-menu-children/children.vue
  14. 13 0
      apps/web-antd/src/views/demos/features/hide-menu-children/parent.vue
  15. 40 0
      apps/web-antd/src/views/demos/features/login-expired/index.vue
  16. 1 1
      internal/lint-configs/eslint-config/package.json
  17. 1 1
      internal/node-utils/package.json
  18. 2 2
      package.json
  19. 1 0
      packages/@core/forward/preferences/src/config.ts
  20. 9 0
      packages/@core/forward/preferences/src/types.ts
  21. 1 0
      packages/@core/forward/request/package.json
  22. 0 1
      packages/@core/forward/request/src/request-client/index.ts
  23. 10 4
      packages/@core/forward/request/src/request-client/modules/interceptor.ts
  24. 76 11
      packages/@core/forward/request/src/request-client/request-client.ts
  25. 25 2
      packages/@core/forward/request/src/request-client/types.ts
  26. 0 25
      packages/@core/forward/request/src/request-client/util.test.ts
  27. 0 7
      packages/@core/forward/request/src/request-client/util.ts
  28. 12 1
      packages/@core/locales/src/langs/en-US.json
  29. 12 1
      packages/@core/locales/src/langs/zh-CN.json
  30. 11 4
      packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue
  31. 1 0
      packages/@core/ui-kit/shadcn-ui/src/index.ts
  32. 0 1
      packages/business/layouts/package.json
  33. 0 1
      packages/business/layouts/src/widgets/index.ts
  34. 0 1
      packages/business/layouts/src/widgets/login-dialog/index.ts
  35. 0 48
      packages/business/layouts/src/widgets/login-dialog/login-dialog.vue
  36. 8 8
      packages/business/layouts/src/widgets/preferences/preferences-sheet.vue
  37. 1 0
      packages/business/universal-ui/src/authentication/index.ts
  38. 55 0
      packages/business/universal-ui/src/authentication/login-expired-modal.vue
  39. 6 15
      packages/business/universal-ui/src/authentication/login.vue
  40. 15 2
      packages/business/universal-ui/src/authentication/typings.ts
  41. 1 1
      packages/business/universal-ui/src/fallback/fallback.vue
  42. 90 117
      pnpm-lock.yaml
  43. 0 1
      scripts/vsh/package.json

+ 23 - 0
apps/backend-mock/src/modules/mock/mock.controller.ts

@@ -0,0 +1,23 @@
+import type { Response } from 'express';
+
+import { Controller, Get, Query, Res } from '@nestjs/common';
+
+@Controller('mock')
+export class MockController {
+  /**
+   * 用于模拟任意的状态码
+   * @param res
+   */
+  @Get('status')
+  async mockAnyStatus(
+    @Res() res: Response,
+    @Query() { status }: { status: string },
+  ) {
+    res.status(Number.parseInt(status, 10)).send({
+      code: 1,
+      data: null,
+      error: null,
+      message: `code is ${status}`,
+    });
+  }
+}

+ 2 - 0
apps/backend-mock/src/modules/mock/mock.module.ts

@@ -1,8 +1,10 @@
 import { Module } from '@nestjs/common';
 
+import { MockController } from './mock.controller';
 import { MockService } from './mock.service';
 
 @Module({
+  controllers: [MockController],
   exports: [MockService],
   providers: [MockService],
 })

+ 1 - 0
apps/web-antd/src/apis/modules/index.ts

@@ -1,2 +1,3 @@
 export * from './menu';
+export * from './mock';
 export * from './user';

+ 10 - 0
apps/web-antd/src/apis/modules/mock.ts

@@ -0,0 +1,10 @@
+import { requestClient } from '#/forward';
+
+/**
+ * 模拟人意状态码
+ */
+async function getMockStatus(status: string) {
+  return requestClient.get('/mock/status', { params: { status } });
+}
+
+export { getMockStatus };

+ 29 - 55
apps/web-antd/src/forward/request.ts

@@ -1,23 +1,14 @@
 /**
  * 该文件可自行根据业务逻辑进行调整
  */
+import type { HttpResponse } from '@vben-core/request';
 
-import type { AxiosResponse } from '@vben-core/request';
-
-import { RequestClient, isCancelError } from '@vben-core/request';
-import { useCoreAccessStore } from '@vben-core/stores';
+import { preferences } from '@vben-core/preferences';
+import { RequestClient } from '@vben-core/request';
 
 import { message } from 'ant-design-vue';
 
-interface HttpResponse<T = any> {
-  /**
-   * 0 表示成功 其他表示失败
-   * 0 means success, others means fail
-   */
-  code: number;
-  data: T;
-  message: string;
-}
+import { useAccessStore } from '#/store';
 
 /**
  * 创建请求实例
@@ -29,57 +20,40 @@ function createRequestClient() {
     // 为每个请求携带 Authorization
     makeAuthorization: () => {
       return {
-        handler: () => {
-          // 这里不能用 useAccessStore,因为 useAccessStore 会导致循环引用
-          const accessStore = useCoreAccessStore();
+        // 默认
+        key: 'Authorization',
+        tokenHandler: () => {
+          const accessStore = useAccessStore();
           return {
             refreshToken: `Bearer ${accessStore.refreshToken}`,
             token: `Bearer ${accessStore.accessToken}`,
           };
         },
-        // 默认
-        key: 'Authorization',
+        unAuthorizedHandler: async () => {
+          const accessStore = useAccessStore();
+          accessStore.setAccessToken(null);
+
+          if (preferences.app.loginExpiredMode === 'modal') {
+            accessStore.openLoginExpiredModal = true;
+          } else {
+            // 退出登录
+            await accessStore.logout();
+          }
+        },
       };
     },
+    makeErrorMessage: (msg) => message.error(msg),
   });
-  setupRequestInterceptors(client);
-  return client;
-}
-
-function setupRequestInterceptors(client: RequestClient) {
-  client.addResponseInterceptor(
-    (response: AxiosResponse<HttpResponse>) => {
-      const { data: responseData, status } = response;
-
-      const { code, data, message: msg } = responseData;
-
-      if (status >= 200 && status < 400 && code === 0) {
-        return data;
-      } else {
-        message.error(msg);
-        throw new Error(msg);
-      }
-    },
-    (error: any) => {
-      if (isCancelError(error)) {
-        return Promise.reject(error);
-      }
+  client.addResponseInterceptor<HttpResponse>((response) => {
+    const { data: responseData, status } = response;
 
-      const err: string = error?.toString?.() ?? '';
-      let errMsg = '';
-      if (err?.includes('Network Error')) {
-        errMsg = '网络错误。';
-      } else if (error?.message?.includes?.('timeout')) {
-        errMsg = '请求超时。';
-      } else {
-        const data = error?.response?.data;
-        errMsg = (data?.message || data?.error?.message) ?? '';
-      }
-
-      message.error(errMsg);
-      return Promise.reject(error);
-    },
-  );
+    const { code, data, message: msg } = responseData;
+    if (status >= 200 && status < 400 && code === 0) {
+      return data;
+    }
+    throw new Error(msg);
+  });
+  return client;
 }
 
 const requestClient = createRequestClient();

+ 5 - 5
apps/web-antd/src/layouts/basic.vue

@@ -6,11 +6,11 @@ import { LOGIN_PATH } from '@vben/constants';
 import { IcRoundCreditScore, MdiDriveDocument, MdiGithub } from '@vben/icons';
 import {
   BasicLayout,
-  LoginDialog,
   Notification,
   NotificationItem,
   UserDropdown,
 } from '@vben/layouts';
+import { AuthenticationLoginExpiredModal } from '@vben/universal-ui';
 import { openWindow } from '@vben/utils';
 import { preferences } from '@vben-core/preferences';
 
@@ -85,7 +85,7 @@ const menus = computed(() => [
 
 const appStore = useAppStore();
 const accessStore = useAccessStore();
-const { showLoginDialog, userInfo } = toRefs(accessStore);
+const { openLoginExpiredModal, userInfo } = toRefs(accessStore);
 const router = useRouter();
 
 async function handleLogout() {
@@ -124,11 +124,11 @@ function handleMakeAll() {
       />
     </template>
     <template #dialog>
-      <LoginDialog
-        :open="showLoginDialog"
+      <AuthenticationLoginExpiredModal
+        v-model:open="openLoginExpiredModal"
         password-placeholder="123456"
         username-placeholder="vben"
-        @login="accessStore.authLogin"
+        @submit="accessStore.authLogin"
       />
     </template>
   </BasicLayout>

+ 6 - 1
apps/web-antd/src/locales/langs/en-US.json

@@ -28,7 +28,12 @@
         "embedded": "Embedded",
         "externalLink": "External Link"
       },
-      "fallback": { "title": "Fallback Page" }
+      "fallback": { "title": "Fallback Page" },
+      "features": {
+        "title": "Features",
+        "hideChildrenInMenu": "Hide Menu Children",
+        "loginExpired": "Login Expired"
+      }
     }
   }
 }

+ 5 - 0
apps/web-antd/src/locales/langs/zh-CN.json

@@ -30,6 +30,11 @@
       },
       "fallback": {
         "title": "缺省页"
+      },
+      "features": {
+        "title": "功能",
+        "hideChildrenInMenu": "隐藏菜单子项",
+        "loginExpired": "登录过期"
       }
     }
   }

+ 3 - 18
apps/web-antd/src/router/guard.ts

@@ -93,24 +93,9 @@ function setupAccessGuard(router: Router) {
     // 生成路由表
     // 当前登录用户拥有的角色标识列表
     let userRoles: string[] = [];
-    try {
-      const userInfo =
-        accessStore.userInfo || (await accessStore.fetchUserInfo());
-      userRoles = userInfo.roles ?? [];
-    } catch (error: any) {
-      if (error.status === 409) {
-        accessStore.setShowLoginDialog(true);
-      } else if (error.status === 401) {
-        accessStore.reset();
-        return {
-          path: LOGIN_PATH,
-          // 如不需要,直接删除 query
-          query: { redirect: encodeURIComponent(to.fullPath) },
-          // 携带当前跳转的页面,登录后重新跳转该页面
-          replace: true,
-        };
-      }
-    }
+    const userInfo =
+      accessStore.userInfo || (await accessStore.fetchUserInfo());
+    userRoles = userInfo.roles ?? [];
 
     // 生成菜单和路由
     const { accessibleMenus, accessibleRoutes } = await generateAccess({

+ 42 - 0
apps/web-antd/src/router/routes/modules/demos.ts

@@ -125,6 +125,48 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      {
+        meta: {
+          icon: 'mdi:feature-highlight',
+          title: $t('page.demos.features.title'),
+        },
+        name: 'Features',
+        path: '/features',
+        redirect: '/features/hide-menu-children',
+        children: [
+          {
+            name: 'HideChildrenInMenuParent',
+            path: 'hide-children-in-menu',
+            component: () =>
+              import('#/views/demos/features/hide-menu-children/parent.vue'),
+            meta: {
+              hideChildrenInMenu: true,
+              icon: 'ic:round-menu',
+              title: 'page.demos.features.hideChildrenInMenu',
+            },
+            children: [
+              {
+                name: 'HideChildrenInMenuChildren',
+                path: 'hide-children-in-menu',
+                component: () =>
+                  import(
+                    '#/views/demos/features/hide-menu-children/children.vue'
+                  ),
+              },
+            ],
+          },
+          {
+            name: 'LoginExpired',
+            path: 'login-expired',
+            component: () =>
+              import('#/views/demos/features/login-expired/index.vue'),
+            meta: {
+              icon: 'mdi:encryption-expiration',
+              title: $t('page.demos.features.loginExpired'),
+            },
+          },
+        ],
+      },
       {
         meta: {
           icon: 'mdi:lightbulb-error-outline',

+ 25 - 8
apps/web-antd/src/store/modules/access.ts

@@ -5,7 +5,7 @@ import type { RouteRecordRaw } from 'vue-router';
 import { computed, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
-import { DEFAULT_HOME_PATH } from '@vben/constants';
+import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 import { useCoreAccessStore } from '@vben-core/stores';
 
 import { defineStore } from 'pinia';
@@ -17,12 +17,10 @@ export const useAccessStore = defineStore('access', () => {
   const router = useRouter();
   const loading = ref(false);
 
-  const showLoginDialog = ref(false);
-  function setShowLoginDialog(value: boolean) {
-    showLoginDialog.value = value;
-  }
+  const openLoginExpiredModal = ref(false);
 
   const accessToken = computed(() => coreStoreAccess.accessToken);
+  const refreshToken = computed(() => coreStoreAccess.refreshToken);
   const userRoles = computed(() => coreStoreAccess.userRoles);
   const userInfo = computed(() => coreStoreAccess.userInfo);
   const accessRoutes = computed(() => coreStoreAccess.accessRoutes);
@@ -31,6 +29,10 @@ export const useAccessStore = defineStore('access', () => {
     coreStoreAccess.setAccessMenus(menus);
   }
 
+  function setAccessToken(token: null | string) {
+    coreStoreAccess.setAccessToken(token);
+  }
+
   function setAccessRoutes(routes: RouteRecordRaw[]) {
     coreStoreAccess.setAccessRoutes(routes);
   }
@@ -70,7 +72,7 @@ export const useAccessStore = defineStore('access', () => {
         coreStoreAccess.setUserInfo(userInfo);
         coreStoreAccess.setAccessCodes(accessCodes);
 
-        showLoginDialog.value = false;
+        openLoginExpiredModal.value = false;
         onSuccess
           ? await onSuccess?.()
           : await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
@@ -85,6 +87,19 @@ export const useAccessStore = defineStore('access', () => {
     };
   }
 
+  async function logout() {
+    coreStoreAccess.$reset();
+    openLoginExpiredModal.value = false;
+
+    // 回登陆页带上当前路由地址
+    await router.replace({
+      path: LOGIN_PATH,
+      query: {
+        redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+      },
+    });
+  }
+
   async function fetchUserInfo() {
     let userInfo: UserInfo | null = null;
     userInfo = await getUserInfo();
@@ -102,11 +117,13 @@ export const useAccessStore = defineStore('access', () => {
     authLogin,
     fetchUserInfo,
     loading,
+    logout,
+    openLoginExpiredModal,
+    refreshToken,
     reset,
     setAccessMenus,
     setAccessRoutes,
-    setShowLoginDialog,
-    showLoginDialog,
+    setAccessToken,
     userInfo,
     userRoles,
   };

+ 5 - 3
apps/web-antd/src/store/modules/app.ts

@@ -1,16 +1,18 @@
-import { useCoreAccessStore, useCoreTabbarStore } from '@vben-core/stores';
+import { useCoreTabbarStore } from '@vben-core/stores';
 
 import { defineStore } from 'pinia';
 
+import { useAccessStore } from './access';
+
 export const useAppStore = defineStore('app', () => {
-  const coreStoreAccess = useCoreAccessStore();
+  const accessStore = useAccessStore();
   const coreTabbarStore = useCoreTabbarStore();
 
   /**
    * 重置所有状态
    */
   async function resetAppState() {
-    coreStoreAccess.$reset();
+    accessStore.$reset();
     coreTabbarStore.$reset();
   }
 

+ 3 - 0
apps/web-antd/src/views/demos/features/hide-menu-children/children.vue

@@ -0,0 +1,3 @@
+<template>
+  <div>children</div>
+</template>

+ 13 - 0
apps/web-antd/src/views/demos/features/hide-menu-children/parent.vue

@@ -0,0 +1,13 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/universal-ui';
+
+defineOptions({ name: 'HideMenuChildren' });
+</script>
+
+<template>
+  <Fallback
+    description="当前菜单子菜单不可见"
+    status="comming-soon"
+    title="隐藏子菜单"
+  />
+</template>

+ 40 - 0
apps/web-antd/src/views/demos/features/login-expired/index.vue

@@ -0,0 +1,40 @@
+<script lang="ts" setup>
+import type { LoginExpiredModeType } from '@vben-core/preferences';
+
+import { preferences, updatePreferences } from '@vben-core/preferences';
+
+import { Button } from 'ant-design-vue';
+
+import { getMockStatus } from '#/apis';
+
+defineOptions({ name: 'LoginExpired' });
+
+async function handleClick(type: LoginExpiredModeType) {
+  const loginExpiredMode = preferences.app.loginExpiredMode;
+
+  updatePreferences({ app: { loginExpiredMode: type } });
+  await getMockStatus('401');
+  updatePreferences({ app: { loginExpiredMode } });
+}
+</script>
+
+<template>
+  <div class="p-5">
+    <div class="card-box p-5">
+      <h1 class="text-xl font-semibold">登录过期演示</h1>
+      <div class="text-foreground/80 mt-2">
+        401状态码转到登录页,登录成功后跳转回原页面。
+      </div>
+    </div>
+
+    <div class="card-box mt-5 p-5 font-semibold">
+      <div class="mb-3 text-lg">跳转登录页面方式</div>
+      <Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
+    </div>
+
+    <div class="card-box mt-5 p-5 font-semibold">
+      <div class="mb-3 text-lg">登录弹窗方式</div>
+      <Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
+    </div>
+  </div>
+</template>

+ 1 - 1
internal/lint-configs/eslint-config/package.json

@@ -39,7 +39,7 @@
     "eslint-config-prettier": "^9.1.0",
     "eslint-plugin-eslint-comments": "^3.2.0",
     "eslint-plugin-i": "^2.29.1",
-    "eslint-plugin-jsdoc": "^48.6.0",
+    "eslint-plugin-jsdoc": "^48.7.0",
     "eslint-plugin-jsonc": "^2.16.0",
     "eslint-plugin-n": "^17.9.0",
     "eslint-plugin-no-only-tests": "^3.1.0",

+ 1 - 1
internal/node-utils/package.json

@@ -36,7 +36,7 @@
     "nanoid": "^5.0.7",
     "pkg-types": "^1.1.3",
     "prettier": "^3.3.2",
-    "rimraf": "^6.0.0",
+    "rimraf": "^6.0.1",
     "zx": "^7.2.3"
   }
 }

+ 2 - 2
package.json

@@ -68,12 +68,12 @@
     "husky": "^9.0.11",
     "is-ci": "^3.0.1",
     "jsdom": "^24.1.0",
-    "rimraf": "^6.0.0",
+    "rimraf": "^6.0.1",
     "turbo": "^2.0.6",
     "typescript": "^5.5.3",
     "unbuild": "^2.0.0",
     "vite": "^5.3.3",
-    "vitest": "^2.0.1",
+    "vitest": "^2.0.2",
     "vue-tsc": "^2.0.26"
   },
   "engines": {

+ 1 - 0
packages/@core/forward/preferences/src/config.ts

@@ -16,6 +16,7 @@ const defaultPreferences: Preferences = {
     isMobile: false,
     layout: 'sidebar-nav',
     locale: 'zh-CN',
+    loginExpiredMode: 'page',
     name: 'Vben Admin Pro',
   },
   breadcrumb: {

+ 9 - 0
packages/@core/forward/preferences/src/types.ts

@@ -7,6 +7,12 @@ import type {
   ThemeModeType,
 } from '@vben-core/typings';
 
+/**
+ * 登录过期模式
+ * 'modal' 弹窗模式 | 'page' 页面模式
+ */
+type LoginExpiredModeType = 'modal' | 'page';
+
 type BreadcrumbStyleType = 'background' | 'normal';
 
 type AccessModeType = 'allow-all' | 'backend' | 'frontend';
@@ -44,6 +50,8 @@ interface AppPreferences {
   layout: LayoutType;
   /** 支持的语言 */
   locale: SupportedLanguagesType;
+  /** 登录过期模式 */
+  loginExpiredMode: LoginExpiredModeType;
   /** 应用名 */
   name: string;
 }
@@ -236,6 +244,7 @@ export type {
   HeaderPreferences,
   LayoutHeaderModeType,
   LayoutType,
+  LoginExpiredModeType,
   LogoPreferences,
   NavigationPreferences,
   NavigationStyleType,

+ 1 - 0
packages/@core/forward/request/package.json

@@ -38,6 +38,7 @@
     }
   },
   "dependencies": {
+    "@vben-core/locales": "workspace:*",
     "@vben-core/toolkit": "workspace:*",
     "axios": "^1.7.2",
     "vue-request": "^2.0.4"

+ 0 - 1
packages/@core/forward/request/src/request-client/index.ts

@@ -1,3 +1,2 @@
 export * from './request-client';
 export type * from './types';
-export * from './util';

+ 10 - 4
packages/@core/forward/request/src/request-client/modules/interceptor.ts

@@ -17,16 +17,22 @@ class InterceptorManager {
     ) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>,
     rejected?: (error: any) => any,
   ) {
-    this.axiosInstance.interceptors.request.use(fulfilled, rejected);
+    this.axiosInstance.interceptors.request.use(
+      fulfilled,
+      rejected || ((res) => res),
+    );
   }
 
-  addResponseInterceptor(
+  addResponseInterceptor<T = any>(
     fulfilled: (
-      response: AxiosResponse,
+      response: AxiosResponse<T>,
     ) => AxiosResponse | Promise<AxiosResponse>,
     rejected?: (error: any) => any,
   ) {
-    this.axiosInstance.interceptors.response.use(fulfilled, rejected);
+    this.axiosInstance.interceptors.response.use(
+      fulfilled,
+      rejected || ((res) => res),
+    );
   }
 }
 

+ 76 - 11
packages/@core/forward/request/src/request-client/request-client.ts

@@ -8,6 +8,7 @@ import type {
 
 import type { MakeAuthorizationFn, RequestClientOptions } from './types';
 
+import { $t } from '@vben-core/locales';
 import { merge } from '@vben-core/toolkit';
 
 import axios from 'axios';
@@ -19,6 +20,7 @@ import { FileUploader } from './modules/uploader';
 class RequestClient {
   private instance: AxiosInstance;
   private makeAuthorization: MakeAuthorizationFn | undefined;
+  private options: RequestClientOptions;
   public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
   public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
   public download: FileDownloader['download'];
@@ -39,6 +41,7 @@ class RequestClient {
       timeout: 10_000,
     };
     const { makeAuthorization, ...axiosConfig } = options;
+    this.options = options;
     const requestConfig = merge(axiosConfig, defaultConfig);
 
     this.instance = axios.create(requestConfig);
@@ -77,24 +80,86 @@ class RequestClient {
     });
   }
 
-  private errorHandler(error: any) {
-    return Promise.reject(error);
+  private setupAuthorizationInterceptor() {
+    this.addRequestInterceptor(
+      (config: InternalAxiosRequestConfig) => {
+        const authorization = this.makeAuthorization?.(config);
+        if (authorization) {
+          const { token } = authorization.tokenHandler?.() ?? {};
+          config.headers[authorization.key || 'Authorization'] = token;
+        }
+        return config;
+      },
+      (error: any) => Promise.reject(error),
+    );
   }
 
-  private setupAuthorizationInterceptor() {
-    this.addRequestInterceptor((config: InternalAxiosRequestConfig) => {
-      const authorization = this.makeAuthorization?.(config);
-      if (authorization) {
-        const { token } = authorization.handler?.() ?? {};
-        config.headers[authorization.key || 'Authorization'] = token;
-      }
-      return config;
-    }, this.errorHandler);
+  private setupDefaultResponseInterceptor() {
+    this.addResponseInterceptor(
+      (response: AxiosResponse) => {
+        return response;
+      },
+      (error: any) => {
+        if (axios.isCancel(error)) {
+          return Promise.reject(error);
+        }
+
+        const err: string = error?.toString?.() ?? '';
+        let errMsg = '';
+        if (err?.includes('Network Error')) {
+          errMsg = $t('fallback.http.networkError');
+        } else if (error?.message?.includes?.('timeout')) {
+          errMsg = $t('fallback.http.requestTimeout');
+        }
+        const { makeAuthorization, makeErrorMessage } = this.options;
+        if (errMsg) {
+          makeErrorMessage?.(errMsg);
+          return Promise.reject(error);
+        }
+
+        let errorMessage = error?.response?.data?.error?.message ?? '';
+        const status = error?.response?.status;
+
+        switch (status) {
+          case 400: {
+            errorMessage = $t('fallback.http.badRequest');
+            break;
+          }
+
+          case 401: {
+            errorMessage = $t('fallback.http.unauthorized');
+            makeAuthorization?.().unAuthorizedHandler?.();
+            break;
+          }
+          case 403: {
+            errorMessage = $t('fallback.http.forbidden');
+            break;
+          }
+          // 404请求不存在
+          case 404: {
+            errorMessage = $t('fallback.http.notFound');
+            break;
+          }
+          case 408: {
+            errorMessage = $t('fallback.http.requestTimeout');
+
+            break;
+          }
+          default: {
+            errorMessage = $t('fallback.http.internalServerError');
+          }
+        }
+
+        makeErrorMessage?.(errorMessage);
+        return Promise.reject(error);
+      },
+    );
   }
 
   private setupInterceptors() {
     // 默认拦截器
     this.setupAuthorizationInterceptor();
+    this.setupDefaultResponseInterceptor();
   }
 
   /**

+ 25 - 2
packages/@core/forward/request/src/request-client/types.ts

@@ -7,18 +7,41 @@ type RequestContentType =
   | 'multipart/form-data;charset=utf-8';
 
 interface MakeAuthorization {
-  handler: () => { refreshToken: string; token: string } | null;
   key?: string;
+  tokenHandler: () => { refreshToken: string; token: string } | null;
+  unAuthorizedHandler?: () => Promise<void>;
 }
 
 type MakeAuthorizationFn = (
   config?: InternalAxiosRequestConfig,
 ) => MakeAuthorization;
 
+type ErrorMessageFn = (message: string) => void;
+
 interface RequestClientOptions extends CreateAxiosDefaults {
   /**
    * 用于生成Authorization
    */
   makeAuthorization?: MakeAuthorizationFn;
+  /**
+   * 用于生成错误消息
+   */
+  makeErrorMessage?: ErrorMessageFn;
+}
+
+interface HttpResponse<T = any> {
+  /**
+   * 0 表示成功 其他表示失败
+   * 0 means success, others means fail
+   */
+  code: number;
+  data: T;
+  message: string;
 }
-export type { MakeAuthorizationFn, RequestClientOptions, RequestContentType };
+
+export type {
+  HttpResponse,
+  MakeAuthorizationFn,
+  RequestClientOptions,
+  RequestContentType,
+};

+ 0 - 25
packages/@core/forward/request/src/request-client/util.test.ts

@@ -1,25 +0,0 @@
-import axios from 'axios';
-import { describe, expect, it } from 'vitest';
-
-import { isCancelError } from './util';
-
-describe('isCancelError', () => {
-  const source = axios.CancelToken.source();
-  source.cancel('Operation canceled by the user.');
-
-  it('should detect cancellation', () => {
-    const error = new axios.Cancel('Operation canceled by the user.');
-
-    const result = isCancelError(error);
-
-    expect(result).toBe(true);
-  });
-
-  it('should not detect cancellation on regular errors', () => {
-    const error = new Error('Regular error');
-
-    const result = isCancelError(error);
-
-    expect(result).toBe(false);
-  });
-});

+ 0 - 7
packages/@core/forward/request/src/request-client/util.ts

@@ -1,7 +0,0 @@
-import axios from 'axios';
-
-function isCancelError(error: any) {
-  return axios.isCancel(error);
-}
-
-export { isCancelError };

+ 12 - 1
packages/@core/locales/src/langs/en-US.json

@@ -39,7 +39,16 @@
     "offline": "Offline Page",
     "offlineError": "Oops! Network Error",
     "offlineErrorDesc": "Sorry, can't connect to the internet. Check your connection.",
-    "coming-soon": "Coming Soon"
+    "comingSoon": "Coming Soon",
+    "http": {
+      "requestTimeout": "The request timed out. Please try again later.",
+      "networkError": "A network error occurred. Please check your internet connection and try again.",
+      "badRequest": "Bad Request. Please check your input and try again.",
+      "unauthorized": "Unauthorized. Please log in to continue.",
+      "forbidden": "Forbidden. You do not have permission to access this resource.",
+      "notFound": "Not Found. The requested resource could not be found.",
+      "internalServerError": "Internal Server Error. Something went wrong on our end. Please try again later."
+    }
   },
   "widgets": {
     "document": "Document",
@@ -104,6 +113,8 @@
     "sendCode": "Get Security code",
     "sendText": "Resend in {0}s",
     "thirdPartyLogin": "Or continue with",
+    "loginAgainTitle": "Please Log In Again",
+    "loginAgainSubTitle": "Your login session has expired. Please log in again to continue.",
     "layout": {
       "center": "Align Center",
       "alignLeft": "Align Left",

+ 12 - 1
packages/@core/locales/src/langs/zh-CN.json

@@ -39,7 +39,16 @@
     "offline": "离线页面",
     "offlineError": "哎呀!网络错误",
     "offlineErrorDesc": "抱歉,无法连接到互联网,请检查您的网络连接并重试。",
-    "coming-soon": "即将推出"
+    "comingSoon": "即将推出",
+    "http": {
+      "requestTimeout": "请求超时,请稍后再试。",
+      "networkError": "网络异常,请检查您的网络连接后重试。",
+      "badRequest": "请求错误。请检查您的输入并重试。",
+      "unauthorized": "未授权。请登录以继续。",
+      "forbidden": "禁止访问, 您没有权限访问此资源。",
+      "notFound": "未找到, 请求的资源不存在。",
+      "internalServerError": "内部服务器错误,请稍后再试。"
+    }
   },
   "widgets": {
     "document": "文档",
@@ -104,6 +113,8 @@
     "sendCode": "获取验证码",
     "sendText": "{0}秒后重新获取",
     "thirdPartyLogin": "其他登录方式",
+    "loginAgainTitle": "请重新登录",
+    "loginAgainSubTitle": "您的登录状态已过期,请重新登录以继续。",
     "layout": {
       "center": "居中",
       "alignLeft": "居左",

+ 11 - 4
packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue

@@ -14,13 +14,19 @@ import {
   useForwardPropsEmits,
 } from 'radix-vue';
 
-const props = defineProps<
-  { class?: HTMLAttributes['class'] } & DialogContentProps
->();
+const props = withDefaults(
+  defineProps<
+    {
+      class?: HTMLAttributes['class'];
+      showClose?: boolean;
+    } & DialogContentProps
+  >(),
+  { showClose: true },
+);
 const emits = defineEmits<{ close: [] } & DialogContentEmits>();
 
 const delegatedProps = computed(() => {
-  const { class: _, ...delegated } = props;
+  const { class: _, showClose: __, ...delegated } = props;
 
   return delegated;
 });
@@ -46,6 +52,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
       <slot></slot>
 
       <DialogClose
+        v-if="showClose"
         class="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
         @click="() => emits('close')"
       >

+ 1 - 0
packages/@core/ui-kit/shadcn-ui/src/index.ts

@@ -2,6 +2,7 @@ import './styles/index.css';
 
 export * from './components';
 export {
+  VisuallyHidden,
   useEmitAsProps,
   useForwardExpose,
   useForwardProps,

+ 0 - 1
packages/business/layouts/package.json

@@ -48,7 +48,6 @@
     "@vben-core/stores": "workspace:*",
     "@vben-core/tabs-ui": "workspace:*",
     "@vben-core/toolkit": "workspace:*",
-    "@vben/universal-ui": "workspace:*",
     "@vueuse/core": "^10.11.0",
     "vue": "^3.4.31",
     "vue-router": "^4.4.0"

+ 0 - 1
packages/business/layouts/src/widgets/index.ts

@@ -4,7 +4,6 @@ export { default as CozeAssistant } from './coze-assistant.vue';
 export * from './global-search';
 export { default as LanguageToggle } from './language-toggle.vue';
 export { default as AuthenticationLayoutToggle } from './layout-toggle.vue';
-export * from './login-dialog';
 export * from './notification';
 export * from './preferences';
 export * from './theme-toggle';

+ 0 - 1
packages/business/layouts/src/widgets/login-dialog/index.ts

@@ -1 +0,0 @@
-export { default as LoginDialog } from './login-dialog.vue';

+ 0 - 48
packages/business/layouts/src/widgets/login-dialog/login-dialog.vue

@@ -1,48 +0,0 @@
-<script setup lang="ts">
-import { computed } from 'vue';
-
-import {
-  AuthenticationLogin,
-  AuthenticationProps,
-  LoginAndRegisterParams,
-} from '@vben/universal-ui';
-import { Dialog, DialogContent } from '@vben-core/shadcn-ui';
-
-interface Props extends AuthenticationProps {
-  open: boolean;
-}
-
-defineOptions({
-  name: 'LoginDialog',
-});
-
-const props = withDefaults(defineProps<Props>(), {
-  open: false,
-});
-
-const emit = defineEmits<{
-  login: [LoginAndRegisterParams];
-}>();
-
-const loginProps = computed(() => {
-  const { open: _, ...rest } = props;
-  return rest;
-});
-</script>
-
-<template>
-  <div>
-    <Dialog :open="open" class="flex items-center justify-center">
-      <DialogContent
-        class="top-[50%] w-full translate-y-[-50%] border-none p-0 shadow-xl sm:w-[600px] sm:rounded-2xl"
-      >
-        <div class="p-4">
-          <AuthenticationLogin
-            v-bind="loginProps"
-            @submit="(e) => emit('login', e)"
-          />
-        </div>
-      </DialogContent>
-    </Dialog>
-  </div>
-</template>

+ 8 - 8
packages/business/layouts/src/widgets/preferences/preferences-sheet.vue

@@ -399,21 +399,21 @@ async function handleReset() {
           :disabled="!diffPreference"
           class="mx-4 w-full"
           size="sm"
-          variant="outline"
-          @click="handleClearCache"
+          variant="default"
+          @click="handleCopy"
         >
-          <IcRoundRestartAlt class="mr-2 size-4" />
-          {{ $t('preferences.clearAndLogout') }}
+          <IcRoundFolderCopy class="mr-2 size-3" />
+          {{ $t('preferences.copyPreferences') }}
         </VbenButton>
         <VbenButton
           :disabled="!diffPreference"
           class="mr-4 w-full"
           size="sm"
-          variant="default"
-          @click="handleCopy"
+          variant="ghost"
+          @click="handleClearCache"
         >
-          <IcRoundFolderCopy class="mr-2 size-3" />
-          {{ $t('preferences.copyPreferences') }}
+          <IcRoundRestartAlt class="mr-2 size-4" />
+          {{ $t('preferences.clearAndLogout') }}
         </VbenButton>
       </template>
     </VbenSheet>

+ 1 - 0
packages/business/universal-ui/src/authentication/index.ts

@@ -1,6 +1,7 @@
 export { default as AuthenticationCodeLogin } from './code-login.vue';
 export { default as AuthenticationForgetPassword } from './forget-password.vue';
 export { default as AuthenticationLogin } from './login.vue';
+export { default as AuthenticationLoginExpiredModal } from './login-expired-modal.vue';
 export { default as AuthenticationQrCodeLogin } from './qrcode-login.vue';
 export { default as AuthenticationRegister } from './register.vue';
 export type {

+ 55 - 0
packages/business/universal-ui/src/authentication/login-expired-modal.vue

@@ -0,0 +1,55 @@
+<script setup lang="ts">
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogTitle,
+  VisuallyHidden,
+  useForwardPropsEmits,
+} from '@vben-core/shadcn-ui';
+
+import AuthenticationLogin from './login.vue';
+import { AuthenticationProps, LoginAndRegisterParams } from './typings';
+
+interface Props extends AuthenticationProps {}
+
+defineOptions({
+  name: 'LoginExpiredModal',
+});
+
+const props = withDefaults(defineProps<Props>(), {});
+
+const emit = defineEmits<{
+  submit: [LoginAndRegisterParams];
+}>();
+
+const open = defineModel<boolean>('open');
+
+const forwarded = useForwardPropsEmits(props, emit);
+</script>
+
+<template>
+  <div>
+    <Dialog v-model:open="open">
+      <DialogContent
+        :show-close="false"
+        class="top-1/2 h-full w-full translate-y-[-50%] border-none p-4 py-12 text-center shadow-xl sm:w-[600px] sm:rounded-2xl md:h-[unset] md:px-14 md:pt-12"
+        @escape-key-down="(e) => e.preventDefault()"
+        @interact-outside="(e) => e.preventDefault()"
+      >
+        <VisuallyHidden>
+          <DialogTitle />
+          <DialogDescription />
+        </VisuallyHidden>
+        <AuthenticationLogin
+          v-bind="forwarded"
+          :show-forget-password="false"
+          :show-register="false"
+          :show-remember-me="false"
+          :sub-title="$t('authentication.loginAgainSubTitle')"
+          :title="$t('authentication.loginAgainTitle')"
+        />
+      </DialogContent>
+    </Dialog>
+  </div>
+</template>

+ 6 - 15
packages/business/universal-ui/src/authentication/login.vue

@@ -31,7 +31,10 @@ withDefaults(defineProps<Props>(), {
   showForgetPassword: true,
   showQrcodeLogin: true,
   showRegister: true,
+  showRememberMe: true,
   showThirdPartyLogin: true,
+  subTitle: '',
+  title: '',
   usernamePlaceholder: '',
 });
 
@@ -89,10 +92,10 @@ function handleGo(path: string) {
 <template>
   <div @keypress.enter.prevent="handleSubmit">
     <Title>
-      {{ $t('authentication.welcomeBack') }} 👋🏻
+      {{ title || `${$t('authentication.welcomeBack')} 👋🏻` }}
       <template #desc>
         <span class="text-muted-foreground">
-          {{ $t('authentication.loginSubtitle') }}
+          {{ subTitle || $t('authentication.loginSubtitle') }}
         </span>
       </template>
     </Title>
@@ -120,7 +123,7 @@ function handleGo(path: string) {
     />
 
     <div class="mb-6 mt-4 flex justify-between">
-      <div class="flex-center flex">
+      <div v-if="showRememberMe" class="flex-center">
         <VbenCheckbox v-model:checked="formState.rememberMe" name="rememberMe">
           {{ $t('authentication.rememberMe') }}
         </VbenCheckbox>
@@ -133,10 +136,6 @@ function handleGo(path: string) {
       >
         {{ $t('authentication.forgetPassword') }}
       </span>
-
-      <!-- <VbenButton variant="ghost" @click="handleGo('/auth/forget-password')">
-        忘记密码?
-      </VbenButton> -->
     </div>
     <VbenButton :loading="loading" class="w-full" @click="handleSubmit">
       {{ $t('common.login') }}
@@ -159,14 +158,6 @@ function handleGo(path: string) {
       >
         {{ $t('authentication.qrcodeLogin') }}
       </VbenButton>
-      <!-- <VbenButton
-        :loading="loading"
-        variant="outline"
-        class="w-1/3"
-        @click="handleGo('/auth/register')"
-      >
-        创建账号
-      </VbenButton> -->
     </div>
 
     <!-- 第三方登录 -->

+ 15 - 2
packages/business/universal-ui/src/authentication/typings.ts

@@ -3,7 +3,6 @@ interface AuthenticationProps {
    * @zh_CN 验证码登录路径
    */
   codeLoginPath?: string;
-
   /**
    * @zh_CN 忘记密码路径
    */
@@ -33,7 +32,6 @@ interface AuthenticationProps {
    * @zh_CN 是否显示验证码登录
    */
   showCodeLogin?: boolean;
-
   /**
    * @zh_CN 是否显示忘记密码
    */
@@ -49,11 +47,26 @@ interface AuthenticationProps {
    */
   showRegister?: boolean;
 
+  /**
+   * @zh_CN 是否显示记住账号
+   */
+  showRememberMe?: boolean;
+
   /**
    * @zh_CN 是否显示第三方登录
    */
   showThirdPartyLogin?: boolean;
 
+  /**
+   * @zh_CN 登录框子标题
+   */
+  subTitle?: string;
+
+  /**
+   * @zh_CN 登录框标题
+   */
+  title?: string;
+
   /**
    * @zh_CN 用户名占位符
    */

+ 1 - 1
packages/business/universal-ui/src/fallback/fallback.vue

@@ -52,7 +52,7 @@ const titleText = computed(() => {
       return $t('fallback.offlineError');
     }
     case 'comming-soon': {
-      return $t('fallback.coming-soon');
+      return $t('fallback.comingSoon');
     }
     default: {
       return '';

+ 90 - 117
pnpm-lock.yaml

@@ -75,8 +75,8 @@ importers:
         specifier: ^24.1.0
         version: 24.1.0
       rimraf:
-        specifier: ^6.0.0
-        version: 6.0.0
+        specifier: ^6.0.1
+        version: 6.0.1
       turbo:
         specifier: ^2.0.6
         version: 2.0.6
@@ -90,8 +90,8 @@ importers:
         specifier: ^5.3.3
         version: 5.3.3(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2)
       vitest:
-        specifier: ^2.0.1
-        version: 2.0.1(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2)
+        specifier: ^2.0.2
+        version: 2.0.2(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2)
       vue-tsc:
         specifier: ^2.0.26
         version: 2.0.26(typescript@5.5.3)
@@ -293,8 +293,8 @@ importers:
         specifier: ^2.29.1
         version: 2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)
       eslint-plugin-jsdoc:
-        specifier: ^48.6.0
-        version: 48.6.0(eslint@8.57.0)
+        specifier: ^48.7.0
+        version: 48.7.0(eslint@8.57.0)
       eslint-plugin-jsonc:
         specifier: ^2.16.0
         version: 2.16.0(eslint@8.57.0)
@@ -321,7 +321,7 @@ importers:
         version: 4.0.0(@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)
       eslint-plugin-vitest:
         specifier: ^0.5.4
-        version: 0.5.4(@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)(vitest@2.0.1(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2))
+        version: 0.5.4(@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)(vitest@2.0.2(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2))
       eslint-plugin-vue:
         specifier: ^9.27.0
         version: 9.27.0(eslint@8.57.0)
@@ -423,8 +423,8 @@ importers:
         specifier: ^3.3.2
         version: 3.3.2
       rimraf:
-        specifier: ^6.0.0
-        version: 6.0.0
+        specifier: ^6.0.1
+        version: 6.0.1
       zx:
         specifier: ^7.2.3
         version: 7.2.3
@@ -589,6 +589,9 @@ importers:
 
   packages/@core/forward/request:
     dependencies:
+      '@vben-core/locales':
+        specifier: workspace:*
+        version: link:../../locales
       '@vben-core/toolkit':
         specifier: workspace:*
         version: link:../../shared/toolkit
@@ -878,9 +881,6 @@ importers:
       '@vben-core/toolkit':
         specifier: workspace:*
         version: link:../../@core/shared/toolkit
-      '@vben/universal-ui':
-        specifier: workspace:*
-        version: link:../universal-ui
       '@vueuse/core':
         specifier: ^10.11.0
         version: 10.11.0(vue@3.4.31(typescript@5.5.3))
@@ -2438,8 +2438,8 @@ packages:
     resolution: {integrity: sha512-I238eDtOolvCuvtxrnqtlBaw0BwdQuYqK7eA6XIonicMdOOOb75mqdIzkGDUbS04+1Di007rgm9snFRNeVrOog==}
     engines: {node: '>=16'}
 
-  '@es-joy/jsdoccomment@0.45.0':
-    resolution: {integrity: sha512-U8T5eXLkP78Sr12rR91494GhlEgp8jqs7OaUHbdUffADxU1JQmKjZm5uSyAEGv2oolDMJ+wce7yylfnnwOevtA==}
+  '@es-joy/jsdoccomment@0.46.0':
+    resolution: {integrity: sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==}
     engines: {node: '>=16'}
 
   '@esbuild/aix-ppc64@0.19.12':
@@ -2985,10 +2985,6 @@ packages:
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
 
-  '@jest/schemas@29.6.3':
-    resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   '@jridgewell/gen-mapping@0.3.5':
     resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
     engines: {node: '>=6.0.0'}
@@ -3399,9 +3395,6 @@ packages:
   '@simonwep/pickr@1.8.2':
     resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
 
-  '@sinclair/typebox@0.27.8':
-    resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
-
   '@sindresorhus/is@5.6.0':
     resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
     engines: {node: '>=14.16'}
@@ -3694,20 +3687,23 @@ packages:
       vite: ^5.0.0
       vue: ^3.4.31
 
-  '@vitest/expect@2.0.1':
-    resolution: {integrity: sha512-yw70WL3ZwzbI2O3MOXYP2Shf4vqVkS3q5FckLJ6lhT9VMMtDyWdofD53COZcoeuHwsBymdOZp99r5bOr5g+oeA==}
+  '@vitest/expect@2.0.2':
+    resolution: {integrity: sha512-nKAvxBYqcDugYZ4nJvnm5OR8eDJdgWjk4XM9owQKUjzW70q0icGV2HVnQOyYsp906xJaBDUXw0+9EHw2T8e0mQ==}
+
+  '@vitest/pretty-format@2.0.2':
+    resolution: {integrity: sha512-SBCyOXfGVvddRd9r2PwoVR0fonQjh9BMIcBMlSzbcNwFfGr6ZhOhvBzurjvi2F4ryut2HcqiFhNeDVGwru8tLg==}
 
-  '@vitest/runner@2.0.1':
-    resolution: {integrity: sha512-XfcSXOGGxgR2dQ466ZYqf0ZtDLLDx9mZeQcKjQDLQ9y6Cmk2Wl7wxMuhiYK4Fo1VxCtLcFEGW2XpcfMuiD1Maw==}
+  '@vitest/runner@2.0.2':
+    resolution: {integrity: sha512-OCh437Vi8Wdbif1e0OvQcbfM3sW4s2lpmOjAE7qfLrpzJX2M7J1IQlNvEcb/fu6kaIB9n9n35wS0G2Q3en5kHg==}
 
-  '@vitest/snapshot@2.0.1':
-    resolution: {integrity: sha512-rst79a4Q+J5vrvHRapdfK4BdqpMH0eF58jVY1vYeBo/1be+nkyenGI5SCSohmjf6MkCkI20/yo5oG+0R8qrAnA==}
+  '@vitest/snapshot@2.0.2':
+    resolution: {integrity: sha512-Yc2ewhhZhx+0f9cSUdfzPRcsM6PhIb+S43wxE7OG0kTxqgqzo8tHkXFuFlndXeDMp09G3sY/X5OAo/RfYydf1g==}
 
-  '@vitest/spy@2.0.1':
-    resolution: {integrity: sha512-NLkdxbSefAtJN56GtCNcB4GiHFb5i9q1uh4V229lrlTZt2fnwsTyjLuWIli1xwK2fQspJJmHXHyWx0Of3KTXWA==}
+  '@vitest/spy@2.0.2':
+    resolution: {integrity: sha512-MgwJ4AZtCgqyp2d7WcQVE8aNG5vQ9zu9qMPYQHjsld/QVsrvg78beNrXdO4HYkP0lDahCO3P4F27aagIag+SGQ==}
 
-  '@vitest/utils@2.0.1':
-    resolution: {integrity: sha512-STH+2fHZxlveh1mpU4tKzNgRk7RZJyr6kFGJYCI5vocdfqfPsQrgVC6k7dBWHfin5QNB4TLvRS0Ckly3Dt1uWw==}
+  '@vitest/utils@2.0.2':
+    resolution: {integrity: sha512-pxCY1v7kmOCWYWjzc0zfjGTA3Wmn8PKnlPvSrsA643P1NHl1fOyXj2Q9SaNlrlFE+ivCsxM80Ov3AR82RmHCWQ==}
 
   '@volar/language-core@1.11.1':
     resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==}
@@ -4012,10 +4008,6 @@ packages:
     resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
     engines: {node: '>=8'}
 
-  ansi-styles@5.2.0:
-    resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
-    engines: {node: '>=10'}
-
   ansi-styles@6.2.1:
     resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
     engines: {node: '>=12'}
@@ -4922,10 +4914,6 @@ packages:
   didyoumean@1.2.2:
     resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
 
-  diff-sequences@29.6.3:
-    resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   diff@4.0.2:
     resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
     engines: {node: '>=0.3.1'}
@@ -5029,8 +5017,8 @@ packages:
     engines: {node: '>=0.10.0'}
     hasBin: true
 
-  electron-to-chromium@1.4.823:
-    resolution: {integrity: sha512-4h+oPeAiGQOHFyUJOqpoEcPj/xxlicxBzOErVeYVMMmAiXUXsGpsFd0QXBMaUUbnD8hhSfLf9uw+MlsoIA7j5w==}
+  electron-to-chromium@1.4.824:
+    resolution: {integrity: sha512-GTQnZOP1v0wCuoWzKOxL8rurg9T13QRYISkoICGaZzskBf9laC3V8g9BHTpJv+j9vBRcKOulbGXwMzuzNdVrAA==}
 
   emoji-regex@10.3.0:
     resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
@@ -5213,8 +5201,8 @@ packages:
     peerDependencies:
       eslint: ^7.2.0 || ^8
 
-  eslint-plugin-jsdoc@48.6.0:
-    resolution: {integrity: sha512-UsOdFYWeyYaiGW1OzJaKvRpb88JPF0HGpDkmMDvhfWbTGu3B4TYKhGH3cPGiRjMDxKPA3fJ/+tL823argNxOkA==}
+  eslint-plugin-jsdoc@48.7.0:
+    resolution: {integrity: sha512-5oiVf7Y+ZxGYQTlLq81X72n+S+hjvS/u0upAdbpPEeaIZILK3MKN8lm/6QqKioBjm/qZ0B5XpMQUtc2fUkqXAg==}
     engines: {node: '>=18'}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
@@ -6209,9 +6197,8 @@ packages:
     resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
     engines: {node: '>=6'}
 
-  jackspeak@3.4.2:
-    resolution: {integrity: sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q==}
-    engines: {node: 14 >=14.21 || 16 >=16.20 || >=18}
+  jackspeak@3.4.3:
+    resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
 
   jackspeak@4.0.1:
     resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==}
@@ -6902,8 +6889,8 @@ packages:
   nth-check@2.1.1:
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
 
-  nwsapi@2.2.10:
-    resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==}
+  nwsapi@2.2.12:
+    resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==}
 
   object-assign@4.1.1:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
@@ -7697,10 +7684,6 @@ packages:
     resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
     engines: {node: ^14.13.1 || >=16.0.0}
 
-  pretty-format@29.7.0:
-    resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   process-nextick-args@2.0.1:
     resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
 
@@ -7795,9 +7778,6 @@ packages:
     resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
     hasBin: true
 
-  react-is@18.3.1:
-    resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
-
   read-cache@1.0.0:
     resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
 
@@ -7961,8 +7941,8 @@ packages:
     deprecated: Rimraf versions prior to v4 are no longer supported
     hasBin: true
 
-  rimraf@6.0.0:
-    resolution: {integrity: sha512-u+yqhM92LW+89cxUQK0SRyvXYQmyuKHx0jkx4W7KfwLGLqJnQM5031Uv1trE4gB9XEXBM/s6MxKlfW95IidqaA==}
+  rimraf@6.0.1:
+    resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
     engines: {node: 20 || >=22}
     hasBin: true
 
@@ -8592,6 +8572,10 @@ packages:
     resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==}
     engines: {node: ^18.0.0 || >=20.0.0}
 
+  tinyrainbow@1.2.0:
+    resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+    engines: {node: '>=14.0.0'}
+
   tinyspy@3.0.0:
     resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==}
     engines: {node: '>=14.0.0'}
@@ -8946,8 +8930,8 @@ packages:
     peerDependencies:
       vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0
 
-  vite-node@2.0.1:
-    resolution: {integrity: sha512-nVd6kyhPAql0s+xIVJzuF+RSRH8ZimNrm6U8ZvTA4MXv8CHI17TFaQwRaFiK75YX6XeFqZD4IoAaAfi9OR1XvQ==}
+  vite-node@2.0.2:
+    resolution: {integrity: sha512-w4vkSz1Wo+NIQg8pjlEn0jQbcM/0D+xVaYjhw3cvarTanLLBh54oNiRbsT8PNK5GfuST0IlVXjsNRoNlqvY/fw==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
 
@@ -9049,15 +9033,15 @@ packages:
       postcss:
         optional: true
 
-  vitest@2.0.1:
-    resolution: {integrity: sha512-PBPvNXRJiywtI9NmbnEqHIhcXlk8mB0aKf6REQIaYGY4JtWF1Pg8Am+N0vAuxdg/wUSlxPSVJr8QdjwcVxc2Hg==}
+  vitest@2.0.2:
+    resolution: {integrity: sha512-WlpZ9neRIjNBIOQwBYfBSr0+of5ZCbxT2TVGKW4Lv0c8+srCFIiRdsP7U009t8mMn821HQ4XKgkx5dVWpyoyLw==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
       '@edge-runtime/vm': '*'
       '@types/node': ^18.0.0 || >=20.0.0
-      '@vitest/browser': 2.0.1
-      '@vitest/ui': 2.0.1
+      '@vitest/browser': 2.0.2
+      '@vitest/ui': 2.0.2
       happy-dom: '*'
       jsdom: '*'
     peerDependenciesMeta:
@@ -11250,10 +11234,8 @@ snapshots:
       esquery: 1.6.0
       jsdoc-type-pratt-parser: 4.0.0
 
-  '@es-joy/jsdoccomment@0.45.0':
+  '@es-joy/jsdoccomment@0.46.0':
     dependencies:
-      '@types/eslint': 8.56.10
-      '@types/estree': 1.0.5
       comment-parser: 1.4.1
       esquery: 1.6.0
       jsdoc-type-pratt-parser: 4.0.0
@@ -11626,10 +11608,6 @@ snapshots:
       wrap-ansi: 8.1.0
       wrap-ansi-cjs: wrap-ansi@7.0.0
 
-  '@jest/schemas@29.6.3':
-    dependencies:
-      '@sinclair/typebox': 0.27.8
-
   '@jridgewell/gen-mapping@0.3.5':
     dependencies:
       '@jridgewell/set-array': 1.2.1
@@ -12122,8 +12100,6 @@ snapshots:
       core-js: 3.37.1
       nanopop: 2.4.2
 
-  '@sinclair/typebox@0.27.8': {}
-
   '@sindresorhus/is@5.6.0': {}
 
   '@sindresorhus/merge-streams@2.3.0': {}
@@ -12445,33 +12421,38 @@ snapshots:
       vite: 5.3.3(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2)
       vue: 3.4.31(typescript@5.5.3)
 
-  '@vitest/expect@2.0.1':
+  '@vitest/expect@2.0.2':
     dependencies:
-      '@vitest/spy': 2.0.1
-      '@vitest/utils': 2.0.1
+      '@vitest/spy': 2.0.2
+      '@vitest/utils': 2.0.2
       chai: 5.1.1
+      tinyrainbow: 1.2.0
 
-  '@vitest/runner@2.0.1':
+  '@vitest/pretty-format@2.0.2':
     dependencies:
-      '@vitest/utils': 2.0.1
+      tinyrainbow: 1.2.0
+
+  '@vitest/runner@2.0.2':
+    dependencies:
+      '@vitest/utils': 2.0.2
       pathe: 1.1.2
 
-  '@vitest/snapshot@2.0.1':
+  '@vitest/snapshot@2.0.2':
     dependencies:
+      '@vitest/pretty-format': 2.0.2
       magic-string: 0.30.10
       pathe: 1.1.2
-      pretty-format: 29.7.0
 
-  '@vitest/spy@2.0.1':
+  '@vitest/spy@2.0.2':
     dependencies:
       tinyspy: 3.0.0
 
-  '@vitest/utils@2.0.1':
+  '@vitest/utils@2.0.2':
     dependencies:
-      diff-sequences: 29.6.3
+      '@vitest/pretty-format': 2.0.2
       estree-walker: 3.0.3
       loupe: 3.1.1
-      pretty-format: 29.7.0
+      tinyrainbow: 1.2.0
 
   '@volar/language-core@1.11.1':
     dependencies:
@@ -12877,8 +12858,6 @@ snapshots:
     dependencies:
       color-convert: 2.0.1
 
-  ansi-styles@5.2.0: {}
-
   ansi-styles@6.2.1: {}
 
   ant-design-vue@4.2.3(vue@3.4.31(typescript@5.5.3)):
@@ -13101,7 +13080,7 @@ snapshots:
   browserslist@4.23.2:
     dependencies:
       caniuse-lite: 1.0.30001641
-      electron-to-chromium: 1.4.823
+      electron-to-chromium: 1.4.824
       node-releases: 2.0.14
       update-browserslist-db: 1.1.0(browserslist@4.23.2)
 
@@ -13903,8 +13882,6 @@ snapshots:
 
   didyoumean@1.2.2: {}
 
-  diff-sequences@29.6.3: {}
-
   diff@4.0.2: {}
 
   dijkstrajs@1.0.3: {}
@@ -14006,7 +13983,7 @@ snapshots:
     dependencies:
       jake: 10.9.1
 
-  electron-to-chromium@1.4.823: {}
+  electron-to-chromium@1.4.824: {}
 
   emoji-regex@10.3.0: {}
 
@@ -14287,9 +14264,9 @@ snapshots:
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-jsdoc@48.6.0(eslint@8.57.0):
+  eslint-plugin-jsdoc@48.7.0(eslint@8.57.0):
     dependencies:
-      '@es-joy/jsdoccomment': 0.45.0
+      '@es-joy/jsdoccomment': 0.46.0
       are-docs-informative: 0.0.2
       comment-parser: 1.4.1
       debug: 4.3.5(supports-color@5.5.0)
@@ -14395,13 +14372,13 @@ snapshots:
     optionalDependencies:
       '@typescript-eslint/eslint-plugin': 7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)
 
-  eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)(vitest@2.0.1(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2)):
+  eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)(vitest@2.0.2(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2)):
     dependencies:
       '@typescript-eslint/utils': 7.16.0(eslint@8.57.0)(typescript@5.5.3)
       eslint: 8.57.0
     optionalDependencies:
       '@typescript-eslint/eslint-plugin': 7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)
-      vitest: 2.0.1(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2)
+      vitest: 2.0.2(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2)
     transitivePeerDependencies:
       - supports-color
       - typescript
@@ -14872,7 +14849,7 @@ snapshots:
   glob@10.4.2:
     dependencies:
       foreground-child: 3.2.1
-      jackspeak: 3.4.2
+      jackspeak: 3.4.3
       minimatch: 9.0.5
       minipass: 7.1.2
       package-json-from-dist: 1.0.0
@@ -14881,7 +14858,7 @@ snapshots:
   glob@10.4.5:
     dependencies:
       foreground-child: 3.2.1
-      jackspeak: 3.4.2
+      jackspeak: 3.4.3
       minimatch: 9.0.5
       minipass: 7.1.2
       package-json-from-dist: 1.0.0
@@ -15423,7 +15400,7 @@ snapshots:
 
   iterare@1.2.1: {}
 
-  jackspeak@3.4.2:
+  jackspeak@3.4.3:
     dependencies:
       '@isaacs/cliui': 8.0.2
     optionalDependencies:
@@ -15497,7 +15474,7 @@ snapshots:
       http-proxy-agent: 7.0.2
       https-proxy-agent: 7.0.5
       is-potential-custom-element-name: 1.0.1
-      nwsapi: 2.2.10
+      nwsapi: 2.2.12
       parse5: 7.1.2
       rrweb-cssom: 0.7.1
       saxes: 6.0.0
@@ -16106,7 +16083,7 @@ snapshots:
     dependencies:
       boolbase: 1.0.0
 
-  nwsapi@2.2.10: {}
+  nwsapi@2.2.12: {}
 
   object-assign@4.1.1: {}
 
@@ -16850,12 +16827,6 @@ snapshots:
 
   pretty-bytes@6.1.1: {}
 
-  pretty-format@29.7.0:
-    dependencies:
-      '@jest/schemas': 29.6.3
-      ansi-styles: 5.2.0
-      react-is: 18.3.1
-
   process-nextick-args@2.0.1: {}
 
   promise-inflight@1.0.1: {}
@@ -16952,8 +16923,6 @@ snapshots:
       minimist: 1.2.8
       strip-json-comments: 2.0.1
 
-  react-is@18.3.1: {}
-
   read-cache@1.0.0:
     dependencies:
       pify: 2.3.0
@@ -17121,9 +17090,10 @@ snapshots:
     dependencies:
       glob: 7.2.3
 
-  rimraf@6.0.0:
+  rimraf@6.0.1:
     dependencies:
       glob: 11.0.0
+      package-json-from-dist: 1.0.0
 
   rollup-plugin-dts@6.1.1(rollup@3.29.4)(typescript@5.5.3):
     dependencies:
@@ -17853,6 +17823,8 @@ snapshots:
 
   tinypool@1.0.0: {}
 
+  tinyrainbow@1.2.0: {}
+
   tinyspy@3.0.0: {}
 
   tmp@0.0.33:
@@ -18210,12 +18182,12 @@ snapshots:
     dependencies:
       vite: 5.3.3(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2)
 
-  vite-node@2.0.1(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2):
+  vite-node@2.0.2(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2):
     dependencies:
       cac: 6.7.14
       debug: 4.3.5(supports-color@5.5.0)
       pathe: 1.1.2
-      picocolors: 1.0.1
+      tinyrainbow: 1.2.0
       vite: 5.3.3(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2)
     transitivePeerDependencies:
       - '@types/node'
@@ -18392,25 +18364,26 @@ snapshots:
       - typescript
       - universal-cookie
 
-  vitest@2.0.1(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2):
+  vitest@2.0.2(@types/node@20.14.10)(jsdom@24.1.0)(sass@1.77.7)(terser@5.31.2):
     dependencies:
       '@ampproject/remapping': 2.3.0
-      '@vitest/expect': 2.0.1
-      '@vitest/runner': 2.0.1
-      '@vitest/snapshot': 2.0.1
-      '@vitest/spy': 2.0.1
-      '@vitest/utils': 2.0.1
+      '@vitest/expect': 2.0.2
+      '@vitest/pretty-format': 2.0.2
+      '@vitest/runner': 2.0.2
+      '@vitest/snapshot': 2.0.2
+      '@vitest/spy': 2.0.2
+      '@vitest/utils': 2.0.2
       chai: 5.1.1
       debug: 4.3.5(supports-color@5.5.0)
       execa: 8.0.1
       magic-string: 0.30.10
       pathe: 1.1.2
-      picocolors: 1.0.1
       std-env: 3.7.0
       tinybench: 2.8.0
       tinypool: 1.0.0
+      tinyrainbow: 1.2.0
       vite: 5.3.3(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2)
-      vite-node: 2.0.1(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2)
+      vite-node: 2.0.2(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.2)
       why-is-node-running: 2.3.0
     optionalDependencies:
       '@types/node': 20.14.10

+ 0 - 1
scripts/vsh/package.json

@@ -5,7 +5,6 @@
   "license": "MIT",
   "type": "module",
   "scripts": {
-    "#build": "pnpm unbuild",
     "stub": "pnpm unbuild --stub"
   },
   "files": [