Browse Source

feat: add some test case

vben 9 months ago
parent
commit
b200ae9997
40 changed files with 1469 additions and 452 deletions
  1. 2 0
      apps/antd-view/package.json
  2. 139 0
      apps/antd-view/src/router/guard.ts
  3. 0 184
      apps/antd-view/src/router/guard/access.ts
  4. 0 55
      apps/antd-view/src/router/guard/index.ts
  5. 2 2
      apps/antd-view/src/router/index.ts
  6. 8 15
      apps/antd-view/src/store/modules/example.test.ts
  7. 1 0
      apps/antd-view/src/store/modules/example.ts
  8. 2 1
      apps/antd-view/src/views/_essential/authentication/login.vue
  9. 2 1
      packages/@vben-core/forward/helpers/package.json
  10. 171 0
      packages/@vben-core/forward/helpers/src/generator-menus.test.ts
  11. 71 0
      packages/@vben-core/forward/helpers/src/generator-menus.ts
  12. 128 0
      packages/@vben-core/forward/helpers/src/generator-routes.test.ts
  13. 40 0
      packages/@vben-core/forward/helpers/src/generator-routes.ts
  14. 2 0
      packages/@vben-core/forward/helpers/src/index.ts
  15. 18 0
      packages/@vben-core/forward/helpers/src/nested-object.test.ts
  16. 4 1
      packages/@vben-core/forward/helpers/tsconfig.json
  17. 0 4
      packages/@vben-core/forward/preferences/src/config.ts
  18. 268 0
      packages/@vben-core/forward/preferences/src/preferences.test.ts
  19. 14 7
      packages/@vben-core/forward/preferences/src/preferences.ts
  20. 0 0
      packages/@vben-core/forward/request/.gitkeep
  21. 76 15
      packages/@vben-core/forward/stores/src/modules/access.test.ts
  22. 309 0
      packages/@vben-core/forward/stores/src/modules/tabs.test.ts
  23. 3 0
      packages/@vben-core/shared/typings/package.json
  24. 1 0
      packages/@vben-core/shared/typings/src/index.ts
  25. 90 0
      packages/@vben-core/shared/typings/src/vue-router.ts
  26. 7 0
      packages/@vben-core/shared/typings/vue-router.d.ts
  27. 1 1
      packages/@vben-core/uikit/shadcn-ui/src/components/input-password/input-password.vue
  28. 1 1
      packages/business/common-ui/src/fallback/fallback.vue
  29. 0 0
      packages/business/common-ui/src/fallback/icons/fallback-icon.vue
  30. 1 2
      packages/hooks/package.json
  31. 1 1
      packages/hooks/src/index.ts
  32. 0 1
      packages/hooks/src/use-request.ts
  33. 7 0
      packages/request/build.config.ts
  34. 46 0
      packages/request/package.json
  35. 1 0
      packages/request/src/index.ts
  36. 11 0
      packages/request/src/use-request.ts
  37. 5 0
      packages/request/tsconfig.json
  38. 3 88
      packages/types/global.d.ts
  39. 30 73
      pnpm-lock.yaml
  40. 4 0
      vben-admin.code-workspace

+ 2 - 0
apps/antd-view/package.json

@@ -22,6 +22,7 @@
     "typecheck": "vue-tsc --noEmit --skipLibCheck"
   },
   "dependencies": {
+    "@vben-core/helpers": "workspace:*",
     "@vben-core/preferences": "workspace:*",
     "@vben-core/stores": "workspace:*",
     "@vben/common-ui": "workspace:*",
@@ -30,6 +31,7 @@
     "@vben/icons": "workspace:*",
     "@vben/layouts": "workspace:*",
     "@vben/locales": "workspace:*",
+    "@vben/request": "workspace:*",
     "@vben/styles": "workspace:*",
     "@vben/types": "workspace:*",
     "@vben/utils": "workspace:*",

+ 139 - 0
apps/antd-view/src/router/guard.ts

@@ -0,0 +1,139 @@
+import { generatorMenus, generatorRoutes } from '@vben-core/helpers';
+import { preferences } from '@vben-core/preferences';
+import { useAccessStore } from '@vben-core/stores';
+import type { RouteLocationNormalized, Router } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { $t } from '@vben/locales';
+import { startProgress, stopProgress } from '@vben/utils';
+import { useTitle } from '@vueuse/core';
+
+import { dynamicRoutes } from '@/router/routes';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function configCommonGuard(router: Router) {
+  // 记录已经加载的页面
+  const loadedPaths = new Set<string>();
+
+  router.beforeEach(async (to) => {
+    // 页面加载进度条
+    if (preferences.transition.progress) {
+      startProgress();
+    }
+    to.meta.loaded = loadedPaths.has(to.path);
+    return true;
+  });
+
+  router.afterEach((to) => {
+    // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+    loadedPaths.add(to.path);
+
+    // 关闭页面加载进度条
+    if (preferences.transition.progress) {
+      stopProgress();
+    }
+
+    // 动态修改标题
+    if (preferences.app.dynamicTitle) {
+      const { title } = to.meta;
+      useTitle(`${$t(title)} - ${preferences.app.name}`);
+    }
+  });
+}
+
+// 不需要权限的页面白名单
+const WHITE_ROUTE_NAMES = new Set<string>([]);
+
+/**
+ * 跳转登录页面
+ * @param to
+ */
+function loginPageMeta(to: RouteLocationNormalized) {
+  return {
+    path: LOGIN_PATH,
+    // 如不需要,直接删除 query
+    query: { redirect: encodeURIComponent(to.fullPath) },
+    // 携带当前跳转的页面,登录后重新跳转该页面
+    replace: true,
+  };
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function configAccessGuard(router: Router) {
+  router.beforeEach(async (to, from) => {
+    const accessStore = useAccessStore();
+    const accessToken = accessStore.getAccessToken;
+
+    // accessToken 检查
+    if (!accessToken) {
+      if (to.path === '/') {
+        return loginPageMeta(to);
+      }
+
+      // 明确声明忽略权限访问权限,则可以访问
+      if (to.meta.ignoreAccess) {
+        return true;
+      }
+
+      // 白名单路由列表检查
+      // TODO: 不是很需要,通过 ignoreAccess 也可以做到,考虑删除
+      if (WHITE_ROUTE_NAMES.has(to.name as string)) {
+        return true;
+      }
+
+      // 没有访问权限,跳转登录页面
+      if (to.fullPath !== LOGIN_PATH) {
+        return loginPageMeta(to);
+      }
+      return to;
+    }
+
+    const accessRoutes = accessStore.getAccessRoutes;
+
+    // 是否已经生成过动态路由
+    if (accessRoutes && accessRoutes.length > 0) {
+      return true;
+    }
+
+    // 生成路由表
+    // 当前登录用户拥有的角色标识列表
+    const userRoles = accessStore.getUserRoles;
+    const routes = await generatorRoutes(dynamicRoutes, userRoles);
+    // 动态添加到router实例内
+    routes.forEach((route) => router.addRoute(route));
+
+    const menus = await generatorMenus(routes, router);
+
+    // 保存菜单信息和路由信息
+    accessStore.setAccessMenus(menus);
+    accessStore.setAccessRoutes(routes);
+    const redirectPath = (from.query.redirect || to.path) as string;
+    const redirect = decodeURIComponent(redirectPath);
+
+    return {
+      path: redirect,
+      replace: true,
+    };
+  });
+}
+
+export { configAccessGuard };
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+  /** 通用 */
+  configCommonGuard(router);
+  /** 权限访问 */
+  configAccessGuard(router);
+}
+
+export { createRouterGuard };

+ 0 - 184
apps/antd-view/src/router/guard/access.ts

@@ -1,184 +0,0 @@
-import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben/types';
-
-import { useAccessStore } from '@vben-core/stores';
-import type { RouteRecordRaw, Router } from 'vue-router';
-
-import { LOGIN_PATH } from '@vben/constants';
-import { filterTree, mapTree, traverseTreeValues } from '@vben/utils';
-
-import { dynamicRoutes } from '@/router/routes';
-
-// 不需要权限的页面白名单
-const WHITE_ROUTE_NAMES = new Set<string>([]);
-
-/**
- * 权限访问守卫配置
- * @param router
- */
-function configAccessGuard(router: Router) {
-  router.beforeEach(async (to, from) => {
-    const accessStore = useAccessStore();
-    const accessToken = accessStore.getAccessToken;
-
-    // accessToken 检查
-    if (!accessToken) {
-      // 明确声明忽略权限访问权限,则可以访问
-      if (to.meta.ignoreAccess) {
-        return true;
-      }
-
-      // 白名单路由列表检查
-      // TODO: 不是很需要,通过 ignoreAccess 也可以做到,考虑删除
-      if (WHITE_ROUTE_NAMES.has(to.name as string)) {
-        return true;
-      }
-
-      // 没有访问权限,跳转登录页面
-      if (to.fullPath !== LOGIN_PATH) {
-        return {
-          path: LOGIN_PATH,
-          // 如不需要,直接删除 query
-          query: { redirect: encodeURIComponent(to.fullPath) },
-          // 携带当前跳转的页面,登录后重新跳转该页面
-          replace: true,
-        };
-      }
-      return to;
-    }
-
-    const accessRoutes = accessStore.getAccessRoutes;
-
-    // 是否已经生成过动态路由
-    if (accessRoutes && accessRoutes.length > 0) {
-      return true;
-    }
-
-    // 生成路由表
-    // 当前登录用户拥有的角色标识列表
-    const userRoles = accessStore.getUserRoles;
-    const routes = await generatorRoutes(userRoles);
-    // 动态添加到router实例内
-    routes.forEach((route) => router.addRoute(route));
-
-    const menus = await generatorMenus(routes, router);
-
-    // 保存菜单信息和路由信息
-    accessStore.setAccessMenus(menus);
-    accessStore.setAccessRoutes(routes);
-    const redirectPath = (from.query.redirect || to.path) as string;
-    const redirect = decodeURIComponent(redirectPath);
-
-    return {
-      path: redirect,
-      replace: true,
-    };
-  });
-}
-
-/**
- * 动态生成路由
- */
-async function generatorRoutes(roles: string[]): Promise<RouteRecordRaw[]> {
-  // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
-  return filterTree(dynamicRoutes, (route) => {
-    return hasVisible(route) && hasAuthority(route, roles);
-  });
-}
-
-/**
- * 根据 routes 生成菜单列表
- * @param routes
- */
-async function generatorMenus(
-  routes: RouteRecordRaw[],
-  router: Router,
-): Promise<MenuRecordRaw[]> {
-  // 获取所有router最终的path及name
-  const finalRoutes = traverseTreeValues(
-    router.getRoutes(),
-    ({ name, path }) => {
-      return {
-        name,
-        path,
-      };
-    },
-  );
-
-  const menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
-    // 路由表的路径写法有多种,这里从router获取到最终的path并赋值
-    const matchRoute = finalRoutes.find(
-      (finalRoute) => finalRoute.name === route.name,
-    );
-
-    // 转换为菜单结构
-    const path = matchRoute?.path ?? route.path;
-    const { meta, name: routeName, redirect, children } = route;
-    const {
-      badge,
-      badgeType,
-      badgeVariants,
-      hideChildrenInMenu = false,
-      icon,
-      orderNo,
-      target,
-      title = '',
-    } = meta || {};
-
-    const name = (title || routeName || '') as string;
-
-    // 隐藏子菜单
-    const resultChildren = hideChildrenInMenu
-      ? []
-      : (children as MenuRecordRaw[]);
-
-    // 将菜单的所有父级和父级菜单记录到菜单项内
-    if (resultChildren && resultChildren.length > 0) {
-      resultChildren.forEach((child) => {
-        child.parents = [...(route.parents || []), path];
-        child.parent = path;
-      });
-    }
-    // 隐藏子菜单
-    const resultPath = hideChildrenInMenu ? redirect : target || path;
-    return {
-      badge,
-      badgeType,
-      badgeVariants,
-      icon,
-      name,
-      orderNo,
-      parent: route.parent,
-      parents: route.parents,
-      path: resultPath,
-      children: resultChildren,
-    };
-  });
-
-  return menus;
-}
-
-/**
- * 判断路由是否有权限访问
- * @param route
- * @param access
- */
-function hasAuthority(route: RouteRecordRaw, access: string[]) {
-  const authority = route.meta?.authority;
-  if (!authority) {
-    return true;
-  }
-  const authSet = new Set(authority);
-  return access.some((value) => {
-    return authSet.has(value);
-  });
-}
-
-/**
- * 判断路由是否需要在菜单中显示
- * @param route
- */
-function hasVisible(route: RouteRecordRaw) {
-  return !route.meta?.hideInMenu;
-}
-
-export { configAccessGuard };

+ 0 - 55
apps/antd-view/src/router/guard/index.ts

@@ -1,55 +0,0 @@
-import { preferences } from '@vben-core/preferences';
-import type { Router } from 'vue-router';
-
-import { $t } from '@vben/locales';
-import { startProgress, stopProgress } from '@vben/utils';
-import { useTitle } from '@vueuse/core';
-
-import { configAccessGuard } from './access';
-
-/**
- * 通用守卫配置
- * @param router
- */
-function configCommonGuard(router: Router) {
-  // 记录已经加载的页面
-  const loadedPaths = new Set<string>();
-
-  router.beforeEach(async (to) => {
-    // 页面加载进度条
-    if (preferences.transition.progress) {
-      startProgress();
-    }
-    to.meta.loaded = loadedPaths.has(to.path);
-    return true;
-  });
-
-  router.afterEach((to) => {
-    // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
-    loadedPaths.add(to.path);
-
-    // 关闭页面加载进度条
-    if (preferences.transition.progress) {
-      stopProgress();
-    }
-
-    // 动态修改标题
-    if (preferences.app.dynamicTitle) {
-      const { title } = to.meta;
-      useTitle(`${$t(title)} - ${preferences.app.name}`);
-    }
-  });
-}
-
-/**
- * 项目守卫配置
- * @param router
- */
-function createRouteGuard(router: Router) {
-  /** 通用 */
-  configCommonGuard(router);
-  /** 权限访问 */
-  configAccessGuard(router);
-}
-
-export { createRouteGuard };

+ 2 - 2
apps/antd-view/src/router/index.ts

@@ -3,7 +3,7 @@ import type { RouteRecordName, RouteRecordRaw } from 'vue-router';
 import { traverseTreeValues } from '@vben/utils';
 import { createRouter, createWebHashHistory } from 'vue-router';
 
-import { createRouteGuard } from './guard';
+import { createRouterGuard } from './guard';
 import { staticRoutes } from './routes';
 
 /**
@@ -54,6 +54,6 @@ function resetRoutes() {
   });
 }
 // 创建路由守卫
-createRouteGuard(router);
+createRouterGuard(router);
 
 export { resetRoutes, router };

+ 8 - 15
apps/antd-view/src/store/modules/example.test.ts

@@ -1,24 +1,17 @@
 import { createPinia, setActivePinia } from 'pinia';
-import {
-  //  beforeEach,
-  describe,
-  // expect,
-  it,
-} from 'vitest';
+import { beforeEach, describe, expect, it } from 'vitest';
 
-// import { useAccessStore } from '../modules/access';
+import { useCounterStore } from './example';
 
 describe('useCounterStore', () => {
-  it('app Name with test', () => {
+  beforeEach(() => {
     setActivePinia(createPinia());
-    // let referenceStore = usePreferencesStore();
+  });
 
-    // beforeEach(() => {
-    //   referenceStore = usePreferencesStore();
-    // });
+  it('count test', () => {
+    setActivePinia(createPinia());
+    const counterStore = useCounterStore();
 
-    // expect(referenceStore.appName).toBe('vben-admin');
-    // referenceStore.setAppName('vbenAdmin');
-    // expect(referenceStore.getAppName).toBe('vbenAdmin');
+    expect(counterStore.count).toBe(0);
   });
 });

+ 1 - 0
apps/antd-view/src/store/modules/example.ts

@@ -9,5 +9,6 @@ export const useCounterStore = defineStore('counter', {
   getters: {
     double: (state) => state.count * 2,
   },
+  persist: [],
   state: () => ({ count: 0 }),
 });

+ 2 - 1
apps/antd-view/src/views/_essential/authentication/login.vue

@@ -5,8 +5,8 @@ import { useAccessStore } from '@vben-core/stores';
 
 import { getUserInfo, userLogin } from '@/services';
 import { AuthenticationLogin } from '@vben/common-ui';
-import { useRequest } from '@vben/hooks';
 import { $t } from '@vben/locales';
+import { useRequest } from '@vben/request';
 import { notification } from 'ant-design-vue';
 import { computed } from 'vue';
 import { useRouter } from 'vue-router';
@@ -35,6 +35,7 @@ const { loading: userInfoLoading, runAsync: runGetUserInfo } = useRequest(
 async function handleLogin(values: LoginAndRegisterParams) {
   // 异步处理用户登录操作并获取 accessToken
   // Asynchronously handle the user login operation and obtain the accessToken
+
   const { accessToken } = await runUserLogin(values);
 
   // 如果成功获取到 accessToken

+ 2 - 1
packages/@vben-core/forward/helpers/package.json

@@ -40,6 +40,7 @@
   },
   "dependencies": {
     "@vben-core/toolkit": "workspace:*",
-    "@vben-core/typings": "workspace:*"
+    "@vben-core/typings": "workspace:*",
+    "vue-router": "^4.3.2"
   }
 }

+ 171 - 0
packages/@vben-core/forward/helpers/src/generator-menus.test.ts

@@ -0,0 +1,171 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { generatorMenus } from './generator-menus'; // 替换为您的实际路径
+import type { RouteRecordRaw } from 'vue-router';
+
+// 模拟路由数据
+const mockRoutes = [
+  {
+    meta: { icon: 'home-icon', title: '首页' },
+    name: 'home',
+    path: '/home',
+  },
+  {
+    meta: { hideChildrenInMenu: true, icon: 'about-icon', title: '关于' },
+    name: 'about',
+    path: '/about',
+    children: [
+      {
+        path: 'team',
+        name: 'team',
+        meta: { icon: 'team-icon', title: '团队' },
+      },
+    ],
+  },
+] as RouteRecordRaw[];
+
+// 模拟 Vue 路由器实例
+const mockRouter = {
+  getRoutes: vi.fn(() => [
+    { name: 'home', path: '/home' },
+    { name: 'about', path: '/about' },
+    { name: 'team', path: '/about/team' },
+  ]),
+};
+
+// Nested route setup to test child inclusion and hideChildrenInMenu functionality
+
+describe('generatorMenus', () => {
+  it('the correct menu list should be generated according to the route', async () => {
+    const expectedMenus = [
+      {
+        badge: undefined,
+        badgeType: undefined,
+        badgeVariants: undefined,
+        icon: 'home-icon',
+        name: '首页',
+        orderNo: undefined,
+        parent: undefined,
+        parents: undefined,
+        path: '/home',
+        children: [],
+      },
+      {
+        badge: undefined,
+        badgeType: undefined,
+        badgeVariants: undefined,
+        icon: 'about-icon',
+        name: '关于',
+        orderNo: undefined,
+        parent: undefined,
+        parents: undefined,
+        path: '/about',
+        children: [],
+      },
+    ];
+
+    const menus = await generatorMenus(mockRoutes, mockRouter as any);
+    expect(menus).toEqual(expectedMenus);
+  });
+
+  it('includes additional meta properties in menu items', async () => {
+    const mockRoutesWithMeta = [
+      {
+        meta: { icon: 'user-icon', orderNo: 1, title: 'Profile' },
+        name: 'profile',
+        path: '/profile',
+      },
+    ] as RouteRecordRaw[];
+
+    const menus = await generatorMenus(mockRoutesWithMeta, mockRouter as any);
+    expect(menus).toEqual([
+      {
+        badge: undefined,
+        badgeType: undefined,
+        badgeVariants: undefined,
+        icon: 'user-icon',
+        name: 'Profile',
+        orderNo: 1,
+        parent: undefined,
+        parents: undefined,
+        path: '/profile',
+        children: [],
+      },
+    ]);
+  });
+
+  it('handles dynamic route parameters correctly', async () => {
+    const mockRoutesWithParams = [
+      {
+        meta: { icon: 'details-icon', title: 'User Details' },
+        name: 'userDetails',
+        path: '/users/:userId',
+      },
+    ] as RouteRecordRaw[];
+
+    const menus = await generatorMenus(mockRoutesWithParams, mockRouter as any);
+    expect(menus).toEqual([
+      {
+        badge: undefined,
+        badgeType: undefined,
+        badgeVariants: undefined,
+        icon: 'details-icon',
+        name: 'User Details',
+        orderNo: undefined,
+        parent: undefined,
+        parents: undefined,
+        path: '/users/:userId',
+        children: [],
+      },
+    ]);
+  });
+
+  it('processes routes with redirects correctly', async () => {
+    const mockRoutesWithRedirect = [
+      {
+        name: 'redirectedRoute',
+        path: '/old-path',
+        redirect: '/new-path',
+      },
+      {
+        meta: { icon: 'path-icon', title: 'New Path' },
+        name: 'newPath',
+        path: '/new-path',
+      },
+    ] as RouteRecordRaw[];
+
+    const menus = await generatorMenus(
+      mockRoutesWithRedirect,
+      mockRouter as any,
+    );
+    console.log(111, menus);
+
+    expect(menus).toEqual([
+      // Assuming your generatorMenus function excludes redirect routes from the menu
+      {
+        badge: undefined,
+        badgeType: undefined,
+        badgeVariants: undefined,
+        icon: undefined,
+        name: 'redirectedRoute',
+        orderNo: undefined,
+        parent: undefined,
+        parents: undefined,
+        path: '/old-path',
+        children: [],
+      },
+      {
+        badge: undefined,
+        badgeType: undefined,
+        badgeVariants: undefined,
+        icon: 'path-icon',
+        name: 'New Path',
+        orderNo: undefined,
+        parent: undefined,
+        parents: undefined,
+        path: '/new-path',
+        children: [],
+      },
+    ]);
+  });
+});

+ 71 - 0
packages/@vben-core/forward/helpers/src/generator-menus.ts

@@ -0,0 +1,71 @@
+import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
+
+import { mapTree } from '@vben-core/toolkit';
+import type { RouteRecordRaw, Router } from 'vue-router';
+
+/**
+ * 根据 routes 生成菜单列表
+ * @param routes
+ */
+async function generatorMenus(
+  routes: RouteRecordRaw[],
+  router: Router,
+): Promise<MenuRecordRaw[]> {
+  // 将路由列表转换为一个以 name 为键的对象映射
+  // 获取所有router最终的path及name
+  const finalRoutesMap: { [key: string]: string } = Object.fromEntries(
+    router.getRoutes().map(({ name, path }) => [name, path]),
+  );
+
+  const menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
+    // 路由表的路径写法有多种,这里从router获取到最终的path并赋值
+    const path = finalRoutesMap[route.name as string] ?? route.path;
+
+    // 转换为菜单结构
+    // const path = matchRoute?.path ?? route.path;
+    const { meta, name: routeName, redirect, children } = route;
+    const {
+      badge,
+      badgeType,
+      badgeVariants,
+      hideChildrenInMenu = false,
+      icon,
+      orderNo,
+      target,
+      title = '',
+    } = meta || {};
+
+    const name = (title || routeName || '') as string;
+
+    // 隐藏子菜单
+    const resultChildren = hideChildrenInMenu
+      ? []
+      : (children as MenuRecordRaw[]);
+
+    // 将菜单的所有父级和父级菜单记录到菜单项内
+    if (resultChildren && resultChildren.length > 0) {
+      resultChildren.forEach((child) => {
+        child.parents = [...(route.parents || []), path];
+        child.parent = path;
+      });
+    }
+    // 隐藏子菜单
+    const resultPath = hideChildrenInMenu ? redirect || path : target || path;
+    return {
+      badge,
+      badgeType,
+      badgeVariants,
+      icon,
+      name,
+      orderNo,
+      parent: route.parent,
+      parents: route.parents,
+      path: resultPath as string,
+      children: resultChildren || [],
+    };
+  });
+
+  return menus;
+}
+
+export { generatorMenus };

+ 128 - 0
packages/@vben-core/forward/helpers/src/generator-routes.test.ts

@@ -0,0 +1,128 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { describe, expect, it } from 'vitest';
+
+import { generatorRoutes, hasAuthority, hasVisible } from './generator-routes';
+
+// Mock 路由数据
+const mockRoutes = [
+  {
+    meta: {
+      authority: ['admin', 'user'],
+      hideInMenu: false,
+    },
+    path: '/dashboard',
+    children: [
+      {
+        path: '/dashboard/overview',
+        meta: { authority: ['admin'], hideInMenu: false },
+      },
+      {
+        path: '/dashboard/stats',
+        meta: { authority: ['user'], hideInMenu: true },
+      },
+    ],
+  },
+  {
+    meta: { authority: ['admin'], hideInMenu: false },
+    path: '/settings',
+  },
+  {
+    meta: { hideInMenu: false },
+    path: '/profile',
+  },
+] as RouteRecordRaw[];
+
+describe('hasAuthority', () => {
+  it('should return true if there is no authority defined', () => {
+    expect(hasAuthority(mockRoutes[2], ['admin'])).toBe(true);
+  });
+
+  it('should return true if the user has the required authority', () => {
+    expect(hasAuthority(mockRoutes[0], ['admin'])).toBe(true);
+  });
+
+  it('should return false if the user does not have the required authority', () => {
+    expect(hasAuthority(mockRoutes[1], ['user'])).toBe(false);
+  });
+});
+
+describe('hasVisible', () => {
+  it('should return true if hideInMenu is not set or false', () => {
+    expect(hasVisible(mockRoutes[0])).toBe(true);
+    expect(hasVisible(mockRoutes[2])).toBe(true);
+  });
+
+  it('should return false if hideInMenu is true', () => {
+    expect(hasVisible(mockRoutes[0].children?.[1])).toBe(false);
+  });
+});
+
+describe('generatorRoutes', () => {
+  it('should filter routes based on authority and visibility', async () => {
+    const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
+    // The user should have access to /dashboard/stats, but it should be filtered out because it's not visible
+    expect(generatedRoutes).toEqual([
+      {
+        meta: { authority: ['admin', 'user'], hideInMenu: false },
+        path: '/dashboard',
+        children: [],
+      },
+      // Note: We expect /settings to be filtered out because the user does not have 'admin' authority
+      {
+        meta: { hideInMenu: false },
+        path: '/profile',
+      },
+    ]);
+  });
+
+  it('should handle routes without children', async () => {
+    const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
+    expect(generatedRoutes).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          path: '/profile', // This route has no children and should be included
+        }),
+      ]),
+    );
+  });
+
+  it('should handle empty roles array', async () => {
+    const generatedRoutes = await generatorRoutes(mockRoutes, []);
+    expect(generatedRoutes).toEqual(
+      expect.arrayContaining([
+        // Only routes without authority should be included
+        expect.objectContaining({
+          path: '/profile',
+        }),
+      ]),
+    );
+    expect(generatedRoutes).not.toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          path: '/dashboard',
+        }),
+        expect.objectContaining({
+          path: '/settings',
+        }),
+      ]),
+    );
+  });
+
+  it('should handle missing meta fields', async () => {
+    const routesWithMissingMeta = [
+      { path: '/path1' }, // No meta
+      { meta: {}, path: '/path2' }, // Empty meta
+      { meta: { authority: ['admin'] }, path: '/path3' }, // Only authority
+    ];
+    const generatedRoutes = await generatorRoutes(
+      routesWithMissingMeta as RouteRecordRaw[],
+      ['admin'],
+    );
+    expect(generatedRoutes).toEqual([
+      { path: '/path1' },
+      { meta: {}, path: '/path2' },
+      { meta: { authority: ['admin'] }, path: '/path3' },
+    ]);
+  });
+});

+ 40 - 0
packages/@vben-core/forward/helpers/src/generator-routes.ts

@@ -0,0 +1,40 @@
+import { filterTree } from '@vben-core/toolkit';
+import type { RouteRecordRaw } from 'vue-router';
+/**
+ * 动态生成路由
+ */
+async function generatorRoutes(
+  routes: RouteRecordRaw[],
+  roles: string[],
+): Promise<RouteRecordRaw[]> {
+  // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
+  return filterTree(routes, (route) => {
+    return hasVisible(route) && hasAuthority(route, roles);
+  });
+}
+
+/**
+ * 判断路由是否有权限访问
+ * @param route
+ * @param access
+ */
+function hasAuthority(route: RouteRecordRaw, access: string[]) {
+  const authority = route.meta?.authority;
+
+  if (!authority) {
+    return true;
+  }
+  return access.some((value) => {
+    return authority.includes(value);
+  });
+}
+
+/**
+ * 判断路由是否需要在菜单中显示
+ * @param route
+ */
+function hasVisible(route?: RouteRecordRaw) {
+  return !route?.meta?.hideInMenu;
+}
+
+export { generatorRoutes, hasAuthority, hasVisible };

+ 2 - 0
packages/@vben-core/forward/helpers/src/index.ts

@@ -1,2 +1,4 @@
 export * from './flatten-object';
+export * from './generator-menus';
+export * from './generator-routes';
 export * from './nested-object';

+ 18 - 0
packages/@vben-core/forward/helpers/src/nested-object.test.ts

@@ -94,4 +94,22 @@ describe('nestedObject', () => {
 
     expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
   });
+
+  it('should correctly nest an object based on the specified level', () => {
+    const obj = {
+      oneFiveSix: 'Value156',
+      oneTwoFour: 'Value124',
+      oneTwoThree: 'Value123',
+    };
+
+    const nested = nestedObject(obj, 2);
+
+    expect(nested).toEqual({
+      one: {
+        fiveSix: 'Value156',
+        twoFour: 'Value124',
+        twoThree: 'Value123',
+      },
+    });
+  });
 });

+ 4 - 1
packages/@vben-core/forward/helpers/tsconfig.json

@@ -1,5 +1,8 @@
 {
   "$schema": "https://json.schemastore.org/tsconfig",
-  "extends": "@vben/tsconfig/library.json",
+  "extends": "@vben/tsconfig/web.json",
+  "compilerOptions": {
+    "types": ["@vben-core/typings/vue-router"]
+  },
   "include": ["src"]
 }

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

@@ -45,7 +45,6 @@ const defaultPreferences: Preferences = {
     split: true,
     styleType: 'rounded',
   },
-
   shortcutKeys: { enable: true },
   sidebar: {
     collapse: false,
@@ -56,17 +55,14 @@ const defaultPreferences: Preferences = {
     hidden: false,
     width: 240,
   },
-
   tabbar: {
     enable: true,
     keepAlive: true,
     showIcon: true,
   },
-
   theme: {
     colorPrimary: 'hsl(211 91% 39%)',
   },
-
   transition: {
     enable: true,
     name: 'fade-slide',

+ 268 - 0
packages/@vben-core/forward/preferences/src/preferences.test.ts

@@ -0,0 +1,268 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { defaultPreferences } from './config';
+import { PreferenceManager, isDarkTheme } from './preferences';
+
+describe('preferences', () => {
+  let preferenceManager: PreferenceManager;
+  vi.mock('@vben-core/cache', () => {
+    return {
+      StorageManager: vi.fn().mockImplementation(() => {
+        return {
+          getItem: vi.fn(),
+          removeItem: vi.fn(),
+          setItem: vi.fn(),
+        };
+      }),
+    };
+  });
+
+  // 模拟 window.matchMedia 方法
+  vi.stubGlobal(
+    'matchMedia',
+    vi.fn().mockImplementation((query) => ({
+      addEventListener: vi.fn(),
+      addListener: vi.fn(), // Deprecated
+      dispatchEvent: vi.fn(),
+      matches: query === '(prefers-color-scheme: dark)',
+      media: query,
+      onchange: null,
+      removeEventListener: vi.fn(),
+      removeListener: vi.fn(), // Deprecated
+    })),
+  );
+  beforeEach(() => {
+    preferenceManager = new PreferenceManager();
+  });
+
+  it('initPreferences should initialize preferences with overrides and namespace', async () => {
+    const overrides = { theme: { colorPrimary: 'hsl(211 91% 39%)' } };
+    const namespace = 'testNamespace';
+
+    await preferenceManager.initPreferences({ namespace, overrides });
+
+    expect(preferenceManager.getPreferences().theme.colorPrimary).toBe(
+      overrides.theme.colorPrimary,
+    );
+  });
+
+  it('loads default preferences if no saved preferences found', () => {
+    const preferences = preferenceManager.getPreferences();
+    expect(preferences).toEqual(defaultPreferences);
+  });
+
+  it('initializes preferences with overrides', async () => {
+    const overrides: any = {
+      app: {
+        locale: 'en-US',
+        themeMode: 'light',
+      },
+    };
+    await preferenceManager.initPreferences({
+      namespace: 'testNamespace',
+      overrides,
+    });
+
+    // 等待防抖动操作完成
+    // await new Promise((resolve) => setTimeout(resolve, 300)); // 等待100毫秒
+
+    const expected = {
+      ...defaultPreferences,
+      app: {
+        ...defaultPreferences.app,
+        ...overrides.app,
+      },
+    };
+
+    expect(preferenceManager.getPreferences()).toEqual(expected);
+  });
+
+  it('updates theme mode correctly', () => {
+    preferenceManager.updatePreferences({
+      app: { themeMode: 'light' },
+    });
+
+    expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
+  });
+
+  it('updates color modes correctly', () => {
+    preferenceManager.updatePreferences({
+      app: { colorGrayMode: true, colorWeakMode: true },
+    });
+
+    expect(preferenceManager.getPreferences().app.colorGrayMode).toBe(true);
+    expect(preferenceManager.getPreferences().app.colorWeakMode).toBe(true);
+  });
+
+  it('resets preferences to default', () => {
+    // 先更新一些偏好设置
+    preferenceManager.updatePreferences({
+      app: { themeMode: 'light' },
+    });
+
+    // 然后重置偏好设置
+    preferenceManager.resetPreferences();
+
+    expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
+  });
+
+  it('updates isMobile correctly', () => {
+    // 模拟移动端状态
+    vi.stubGlobal(
+      'matchMedia',
+      vi.fn().mockImplementation((query) => ({
+        addEventListener: vi.fn(),
+        addListener: vi.fn(),
+        dispatchEvent: vi.fn(),
+        matches: query === '(max-width: 768px)',
+        media: query,
+        onchange: null,
+        removeEventListener: vi.fn(),
+        removeListener: vi.fn(),
+      })),
+    );
+
+    preferenceManager.updatePreferences({
+      app: { isMobile: true },
+    });
+
+    expect(preferenceManager.getPreferences().app.isMobile).toBe(true);
+  });
+
+  it('updates the locale preference correctly', () => {
+    preferenceManager.updatePreferences({
+      app: { locale: 'en-US' },
+    });
+
+    expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
+  });
+
+  it('updates the sidebar width correctly', () => {
+    preferenceManager.updatePreferences({
+      sidebar: { width: 200 },
+    });
+
+    expect(preferenceManager.getPreferences().sidebar.width).toBe(200);
+  });
+  it('updates the sidebar collapse state correctly', () => {
+    preferenceManager.updatePreferences({
+      sidebar: { collapse: true },
+    });
+
+    expect(preferenceManager.getPreferences().sidebar.collapse).toBe(true);
+  });
+  it('updates the navigation style type correctly', () => {
+    preferenceManager.updatePreferences({
+      navigation: { styleType: 'flat' },
+    } as any);
+
+    expect(preferenceManager.getPreferences().navigation.styleType).toBe(
+      'flat',
+    );
+  });
+
+  it('resets preferences to default correctly', () => {
+    // 先更新一些偏好设置
+    preferenceManager.updatePreferences({
+      app: { locale: 'en-US', themeMode: 'light' },
+      sidebar: { collapse: true, width: 200 },
+    });
+
+    // 然后重置偏好设置
+    preferenceManager.resetPreferences();
+
+    expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
+  });
+
+  it('does not update undefined preferences', () => {
+    const originalPreferences = preferenceManager.getPreferences();
+
+    preferenceManager.updatePreferences({
+      app: { nonexistentField: 'value' },
+    } as any);
+
+    expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
+  });
+
+  it('reverts to default when a preference field is deleted', () => {
+    preferenceManager.updatePreferences({
+      app: { locale: 'en-US' },
+    });
+
+    preferenceManager.updatePreferences({
+      app: { locale: undefined },
+    });
+
+    expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
+  });
+
+  it('ignores updates with invalid preference value types', () => {
+    const originalPreferences = preferenceManager.getPreferences();
+
+    preferenceManager.updatePreferences({
+      app: { isMobile: 'true' as unknown as boolean }, // 错误类型
+    });
+
+    expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
+  });
+
+  it('merges nested preference objects correctly', () => {
+    preferenceManager.updatePreferences({
+      app: { name: 'New App Name' },
+    });
+
+    const expected = {
+      ...defaultPreferences,
+      app: {
+        ...defaultPreferences.app,
+        name: 'New App Name',
+      },
+    };
+
+    expect(preferenceManager.getPreferences()).toEqual(expected);
+  });
+
+  it('applies updates immediately after initialization', async () => {
+    const overrides: any = {
+      app: {
+        locale: 'en-US',
+      },
+    };
+
+    await preferenceManager.initPreferences(overrides);
+
+    preferenceManager.updatePreferences({
+      app: { themeMode: 'light' },
+    });
+
+    expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
+  });
+});
+
+describe('isDarkTheme', () => {
+  it('should return true for dark theme', () => {
+    expect(isDarkTheme('dark')).toBe(true);
+  });
+
+  it('should return false for light theme', () => {
+    expect(isDarkTheme('light')).toBe(false);
+  });
+
+  it('should return system preference for auto theme', () => {
+    vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
+      addEventListener: vi.fn(),
+      addListener: vi.fn(), // Deprecated
+      dispatchEvent: vi.fn(),
+      matches: query === '(prefers-color-scheme: dark)',
+      media: query,
+      onchange: null,
+      removeEventListener: vi.fn(),
+      removeListener: vi.fn(), // Deprecated
+    }));
+
+    expect(isDarkTheme('auto')).toBe(true);
+    expect(window.matchMedia).toHaveBeenCalledWith(
+      '(prefers-color-scheme: dark)',
+    );
+  });
+});

+ 14 - 7
packages/@vben-core/forward/preferences/src/preferences.ts

@@ -85,15 +85,21 @@ class PreferenceManager {
     }
   }
 
+  /**
+   *  从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
+   */
+  private loadCachedPreferences() {
+    return this.cache?.getItem(STORAGE_KEY);
+  }
+
   /**
    * 加载偏好设置
-   * 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
    * @returns {Preferences} 加载的偏好设置
    */
-  private loadPreferences(): Preferences {
-    const savedPreferences = this.cache?.getItem(STORAGE_KEY);
-    return savedPreferences || { ...defaultPreferences };
+  private loadPreferences(): Preferences | null {
+    return this.loadCachedPreferences() || { ...defaultPreferences };
   }
+
   /**
    * 监听状态和系统偏好设置的变化。
    */
@@ -239,7 +245,7 @@ class PreferenceManager {
     this.initialPreferences = merge({}, overrides, defaultPreferences);
 
     // 加载并合并当前存储的偏好设置
-    const mergedPreference = merge({}, this.loadPreferences(), overrides);
+    const mergedPreference = merge({}, this.loadCachedPreferences(), overrides);
 
     // 更新偏好设置
     this.updatePreferences(mergedPreference);
@@ -274,9 +280,10 @@ class PreferenceManager {
    * @param updates - 要更新的偏好设置
    */
   public updatePreferences(updates: DeepPartial<Preferences>) {
-    const mergedState = merge(updates, markRaw(this.state));
+    const mergedState = merge({}, updates, markRaw(this.state));
 
     Object.assign(this.state, mergedState);
+
     Object.assign(this.flattenedState, flattenObject(this.state));
 
     // 根据更新的键值执行相应的操作
@@ -286,4 +293,4 @@ class PreferenceManager {
 }
 
 const preferencesManager = new PreferenceManager();
-export { isDarkTheme, preferencesManager };
+export { PreferenceManager, isDarkTheme, preferencesManager };

+ 0 - 0
packages/@vben-core/forward/request/.gitkeep


+ 76 - 15
packages/@vben-core/forward/stores/src/modules/access.test.ts

@@ -1,24 +1,85 @@
 import { createPinia, setActivePinia } from 'pinia';
-import {
-  //  beforeEach,
-  describe,
-  // expect,
-  it,
-} from 'vitest';
+import { beforeEach, describe, expect, it } from 'vitest';
 
-// import { useAccessStore } from '../modules/access';
+import { useAccessStore } from './access';
 
 describe('useAccessStore', () => {
-  it('app Name with test', () => {
+  beforeEach(() => {
     setActivePinia(createPinia());
-    // let referenceStore = usePreferencesStore();
+  });
+
+  it('updates accessMenus state', () => {
+    const store = useAccessStore();
+    expect(store.accessMenus).toEqual([]);
+    store.setAccessMenus([{ name: 'Dashboard', path: '/dashboard' }]);
+    expect(store.accessMenus).toEqual([
+      { name: 'Dashboard', path: '/dashboard' },
+    ]);
+  });
+
+  it('updates userInfo and userRoles state', () => {
+    const store = useAccessStore();
+    expect(store.userInfo).toBeNull();
+    expect(store.userRoles).toEqual([]);
+
+    const userInfo: any = { name: 'John Doe', roles: [{ value: 'admin' }] };
+    store.setUserInfo(userInfo);
 
-    // beforeEach(() => {
-    //   referenceStore = usePreferencesStore();
-    // });
+    expect(store.userInfo).toEqual(userInfo);
+    expect(store.userRoles).toEqual(['admin']);
+  });
+
+  it('returns correct userInfo', () => {
+    const store = useAccessStore();
+    const userInfo: any = { name: 'Jane Doe', roles: [{ value: 'user' }] };
+    store.setUserInfo(userInfo);
+    expect(store.getUserInfo).toEqual(userInfo);
+  });
+
+  it('updates accessToken state correctly', () => {
+    const store = useAccessStore();
+    expect(store.accessToken).toBeNull(); // 初始状态
+    store.setAccessToken('abc123');
+    expect(store.accessToken).toBe('abc123');
+  });
+
+  // 测试重置用户信息时的行为
+  it('clears userInfo and userRoles when setting null userInfo', () => {
+    const store = useAccessStore();
+    store.setUserInfo({
+      roles: [{ roleName: 'User', value: 'user' }],
+    } as any);
+    expect(store.userInfo).not.toBeNull();
+    expect(store.userRoles.length).toBeGreaterThan(0);
+
+    store.setUserInfo(null as any); // 重置用户信息
+    expect(store.userInfo).toBeNull();
+    expect(store.userRoles).toEqual([]);
+  });
+
+  it('returns the correct accessToken', () => {
+    const store = useAccessStore();
+    store.setAccessToken('xyz789');
+    expect(store.getAccessToken).toBe('xyz789');
+  });
+
+  // 测试在没有用户角色时返回空数组
+  it('returns an empty array for userRoles if not set', () => {
+    const store = useAccessStore();
+    expect(store.getUserRoles).toEqual([]);
+  });
+
+  // 测试设置空的访问菜单列表
+  it('handles empty accessMenus correctly', () => {
+    const store = useAccessStore();
+    store.setAccessMenus([]);
+    expect(store.accessMenus).toEqual([]);
+  });
 
-    // expect(referenceStore.appName).toBe('vben-admin');
-    // referenceStore.setAppName('vbenAdmin');
-    // expect(referenceStore.getAppName).toBe('vbenAdmin');
+  // 测试设置空的访问路由列表
+  it('handles empty accessRoutes correctly', () => {
+    const store = useAccessStore();
+    store.setAccessRoutes([]);
+    expect(store.accessRoutes).toEqual([]);
   });
 });

+ 309 - 0
packages/@vben-core/forward/stores/src/modules/tabs.test.ts

@@ -0,0 +1,309 @@
+import { createPinia, setActivePinia } from 'pinia';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { createRouter, createWebHistory } from 'vue-router';
+
+import { useTabsStore } from './tabs';
+
+describe('useAccessStore', () => {
+  const router = createRouter({
+    history: createWebHistory(),
+    routes: [],
+  });
+  router.push = vi.fn();
+  router.replace = vi.fn();
+  beforeEach(() => {
+    setActivePinia(createPinia());
+    vi.clearAllMocks();
+  });
+
+  it('adds a new tab', () => {
+    const store = useTabsStore();
+    const tab: any = {
+      fullPath: '/home',
+      meta: {},
+      name: 'Home',
+      path: '/home',
+    };
+    store.addTab(tab);
+    expect(store.tabs.length).toBe(1);
+    expect(store.tabs[0]).toEqual(tab);
+  });
+
+  it('adds a new tab if it does not exist', () => {
+    const store = useTabsStore();
+    const newTab: any = {
+      fullPath: '/new',
+      meta: {},
+      name: 'New',
+      path: '/new',
+    };
+    store.addTab(newTab);
+    expect(store.tabs).toContainEqual(newTab);
+  });
+
+  it('updates an existing tab instead of adding a new one', () => {
+    const store = useTabsStore();
+    const initialTab: any = {
+      fullPath: '/existing',
+      meta: {},
+      name: 'Existing',
+      path: '/existing',
+      query: {},
+    };
+    store.tabs.push(initialTab);
+    const updatedTab = { ...initialTab, query: { id: '1' } };
+    store.addTab(updatedTab);
+    expect(store.tabs.length).toBe(1);
+    expect(store.tabs[0].query).toEqual({ id: '1' });
+  });
+
+  it('closes all tabs', async () => {
+    const store = useTabsStore();
+    store.tabs = [
+      { fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
+    ] as any;
+    router.replace = vi.fn(); // 使用 vitest 的 mock 函数
+
+    await store.closeAllTabs(router);
+
+    expect(store.tabs.length).toBe(0); // 假设没有固定的标签页
+    // expect(router.replace).toHaveBeenCalled();
+  });
+
+  it('returns all tabs including affix tabs', () => {
+    const store = useTabsStore();
+    store.tabs = [
+      { fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
+    ] as any;
+    store.affixTabs = [
+      { meta: { hideInTab: false }, path: '/dashboard' },
+    ] as any;
+
+    const result = store.getTabs;
+    expect(result.length).toBe(2);
+    expect(result.find((tab) => tab.path === '/dashboard')).toBeDefined();
+  });
+
+  it('closes a non-affix tab', () => {
+    const store = useTabsStore();
+    const tab: any = {
+      fullPath: '/closable',
+      meta: {},
+      name: 'Closable',
+      path: '/closable',
+    };
+    store.tabs.push(tab);
+    store._close(tab);
+    expect(store.tabs.length).toBe(0);
+  });
+
+  it('does not close an affix tab', () => {
+    const store = useTabsStore();
+    const affixTab: any = {
+      fullPath: '/affix',
+      meta: { affixTab: true },
+      name: 'Affix',
+      path: '/affix',
+    };
+    store.tabs.push(affixTab);
+    store._close(affixTab);
+    expect(store.tabs.length).toBe(1); // Affix tab should not be closed
+  });
+
+  it('returns all cache tabs', () => {
+    const store = useTabsStore();
+    store.cacheTabs.add('Home');
+    store.cacheTabs.add('About');
+    expect(store.getCacheTabs).toEqual(['Home', 'About']);
+  });
+
+  it('returns all tabs, including affix tabs', () => {
+    const store = useTabsStore();
+    const normalTab: any = {
+      fullPath: '/normal',
+      meta: {},
+      name: 'Normal',
+      path: '/normal',
+    };
+    const affixTab: any = {
+      fullPath: '/affix',
+      meta: { affixTab: true },
+      name: 'Affix',
+      path: '/affix',
+    };
+    store.tabs.push(normalTab);
+    store.affixTabs.push(affixTab);
+    expect(store.getTabs).toContainEqual(normalTab);
+    // expect(store.getTabs).toContainEqual(affixTab);
+  });
+
+  it('navigates to a specific tab', async () => {
+    const store = useTabsStore();
+    const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' };
+
+    await store._goToTab(tab, router);
+
+    expect(router.replace).toHaveBeenCalledWith({
+      params: {},
+      path: '/dashboard',
+      query: {},
+    });
+  });
+
+  it('closes multiple tabs by paths', async () => {
+    const store = useTabsStore();
+    store.addTab({
+      fullPath: '/home',
+      meta: {},
+      name: 'Home',
+      path: '/home',
+    } as any);
+    store.addTab({
+      fullPath: '/about',
+      meta: {},
+      name: 'About',
+      path: '/about',
+    } as any);
+    store.addTab({
+      fullPath: '/contact',
+      meta: {},
+      name: 'Contact',
+      path: '/contact',
+    } as any);
+
+    await store._bulkCloseByPaths(['/home', '/contact']);
+
+    expect(store.tabs).toHaveLength(1);
+    expect(store.tabs[0].name).toBe('About');
+  });
+
+  it('closes all tabs to the left of the specified tab', async () => {
+    const store = useTabsStore();
+    store.addTab({
+      fullPath: '/home',
+      meta: {},
+      name: 'Home',
+      path: '/home',
+    } as any);
+    store.addTab({
+      fullPath: '/about',
+      meta: {},
+      name: 'About',
+      path: '/about',
+    } as any);
+    const targetTab: any = {
+      fullPath: '/contact',
+      meta: {},
+      name: 'Contact',
+      path: '/contact',
+    };
+    store.addTab(targetTab);
+
+    await store.closeLeftTabs(targetTab);
+
+    expect(store.tabs).toHaveLength(1);
+    expect(store.tabs[0].name).toBe('Contact');
+  });
+
+  it('closes all tabs except the specified tab', async () => {
+    const store = useTabsStore();
+    store.addTab({
+      fullPath: '/home',
+      meta: {},
+      name: 'Home',
+      path: '/home',
+    } as any);
+    const targetTab: any = {
+      fullPath: '/about',
+      meta: {},
+      name: 'About',
+      path: '/about',
+    };
+    store.addTab(targetTab);
+    store.addTab({
+      fullPath: '/contact',
+      meta: {},
+      name: 'Contact',
+      path: '/contact',
+    } as any);
+
+    await store.closeOtherTabs(targetTab);
+
+    expect(store.tabs).toHaveLength(1);
+    expect(store.tabs[0].name).toBe('About');
+  });
+
+  it('closes all tabs to the right of the specified tab', async () => {
+    const store = useTabsStore();
+    const targetTab: any = {
+      fullPath: '/home',
+      meta: {},
+      name: 'Home',
+      path: '/home',
+    };
+    store.addTab(targetTab);
+    store.addTab({
+      fullPath: '/about',
+      meta: {},
+      name: 'About',
+      path: '/about',
+    } as any);
+    store.addTab({
+      fullPath: '/contact',
+      meta: {},
+      name: 'Contact',
+      path: '/contact',
+    } as any);
+
+    await store.closeRightTabs(targetTab);
+
+    expect(store.tabs).toHaveLength(1);
+    expect(store.tabs[0].name).toBe('Home');
+  });
+
+  it('closes the tab with the specified key', async () => {
+    const store = useTabsStore();
+    const keyToClose = '/about';
+    store.addTab({
+      fullPath: '/home',
+      meta: {},
+      name: 'Home',
+      path: '/home',
+    } as any);
+    store.addTab({
+      fullPath: keyToClose,
+      meta: {},
+      name: 'About',
+      path: '/about',
+    } as any);
+    store.addTab({
+      fullPath: '/contact',
+      meta: {},
+      name: 'Contact',
+      path: '/contact',
+    } as any);
+
+    await store.closeTabByKey(keyToClose, router);
+
+    expect(store.tabs).toHaveLength(2);
+    expect(
+      store.tabs.find((tab) => tab.fullPath === keyToClose),
+    ).toBeUndefined();
+  });
+
+  it('refreshes the current tab', async () => {
+    const store = useTabsStore();
+    const currentTab: any = {
+      fullPath: '/dashboard',
+      meta: { name: 'Dashboard' },
+      name: 'Dashboard',
+      path: '/dashboard',
+    };
+    router.currentRoute.value = currentTab;
+
+    await store.refreshTab(router);
+
+    expect(store.excludeCacheTabs.has('Dashboard')).toBe(false);
+    expect(store.renderRouteView).toBe(true);
+  });
+});

+ 3 - 0
packages/@vben-core/shared/typings/package.json

@@ -28,6 +28,9 @@
       "types": "./src/index.ts",
       "development": "./src/index.ts",
       "default": "./dist/index.mjs"
+    },
+    "./vue-router": {
+      "types": "./vue-router.d.ts"
     }
   },
   "publishConfig": {

+ 1 - 0
packages/@vben-core/shared/typings/src/index.ts

@@ -4,3 +4,4 @@ export type * from './flatten';
 export type * from './menu-record';
 export type * from './tabs';
 export type * from './tools';
+export type * from './vue-router';

+ 90 - 0
packages/@vben-core/shared/typings/src/vue-router.ts

@@ -0,0 +1,90 @@
+interface RouteMeta {
+  /**
+   * 是否固定标签页
+   * @default false
+   */
+  affixTab?: boolean;
+  /**
+   * 需要特定的角色标识才可以访问
+   * @default []
+   */
+  authority?: string[];
+  /**
+   * 徽标
+   */
+  badge?: string;
+  /**
+   * 徽标类型
+   */
+  badgeType?: 'dot' | 'normal';
+  /**
+   * 徽标颜色
+   */
+  badgeVariants?:
+    | 'default'
+    | 'destructive'
+    | 'primary'
+    | 'success'
+    | 'warning'
+    | string;
+  /**
+   * 当前路由的子级在菜单中不展现
+   * @default false
+   */
+  hideChildrenInMenu?: boolean;
+  /**
+   * 当前路由在面包屑中不展现
+   * @default false
+   */
+  hideInBreadcrumb?: boolean;
+  /**
+   * 当前路由在菜单中不展现
+   * @default false
+   */
+  hideInMenu?: boolean;
+
+  /**
+   * 当前路由在标签页不展现
+   * @default false
+   */
+  hideInTab?: boolean;
+  /**
+   * 路由跳转地址
+   */
+  href?: string;
+  /**
+   * 图标(菜单/tab)
+   */
+  icon?: string;
+  /**
+   * iframe 地址
+   */
+  iframeSrc?: string;
+  /**
+   * 忽略权限,直接可以访问
+   * @default false
+   */
+  ignoreAccess?: boolean;
+  /**
+   * 开启KeepAlive缓存
+   */
+  keepAlive?: boolean;
+  /**
+   * 路由是否已经加载过
+   */
+  loaded?: boolean;
+  /**
+   * 用于路由->菜单排序
+   */
+  orderNo?: number;
+  /**
+   * 外链-跳转路径
+   */
+  target?: string;
+  /**
+   * 标题名称
+   */
+  title: string;
+}
+
+export type { RouteMeta };

+ 7 - 0
packages/@vben-core/shared/typings/vue-router.d.ts

@@ -0,0 +1,7 @@
+import 'vue-router';
+
+import type { RouteMeta as IRouteMeta } from '@vben-core/typings';
+
+declare module 'vue-router' {
+  interface RouteMeta extends IRouteMeta {}
+}

+ 1 - 1
packages/@vben-core/uikit/shadcn-ui/src/components/input-password/input-password.vue

@@ -44,7 +44,7 @@ const show = ref(false);
       </template>
     </VbenInput>
     <div
-      class="hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 top-[10px] flex cursor-pointer pr-3 text-lg leading-5"
+      class="hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 top-3 flex cursor-pointer pr-3 text-lg leading-5"
       @click="show = !show"
     >
       <IcOutlineVisibility v-if="show" />

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

@@ -6,7 +6,7 @@ import { $t } from '@vben/locales';
 import { computed } from 'vue';
 import { useRouter } from 'vue-router';
 
-import FeedbackIcon from './fallback-icon.vue';
+import FeedbackIcon from './icons/fallback-icon.vue';
 
 interface Props {
   /**

+ 0 - 0
packages/business/common-ui/src/fallback/fallback-icon.vue → packages/business/common-ui/src/fallback/icons/fallback-icon.vue


+ 1 - 2
packages/hooks/package.json

@@ -41,7 +41,6 @@
     }
   },
   "dependencies": {
-    "vue": "3.4.27",
-    "vue-hooks-plus": "^2.1.0"
+    "vue": "3.4.27"
   }
 }

+ 1 - 1
packages/hooks/src/index.ts

@@ -1 +1 @@
-export * from './use-request';
+export {};

+ 0 - 1
packages/hooks/src/use-request.ts

@@ -1 +0,0 @@
-export { default as useRequest } from 'vue-hooks-plus/es/useRequest';

+ 7 - 0
packages/request/build.config.ts

@@ -0,0 +1,7 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+  clean: true,
+  declaration: true,
+  entries: ['src/index'],
+});

+ 46 - 0
packages/request/package.json

@@ -0,0 +1,46 @@
+{
+  "name": "@vben/request",
+  "version": "1.0.0",
+  "type": "module",
+  "license": "MIT",
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "packages/request"
+  },
+  "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+  "scripts": {
+    "build": "pnpm unbuild",
+    "stub": "pnpm unbuild --stub"
+  },
+  "files": [
+    "dist"
+  ],
+  "sideEffects": [
+    "**/*.css"
+  ],
+  "main": "./dist/index.mjs",
+  "module": "./dist/index.mjs",
+  "imports": {
+    "#*": "./src/*"
+  },
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "development": "./src/index.ts",
+      "default": "./dist/index.mjs"
+    }
+  },
+  "publishConfig": {
+    "exports": {
+      ".": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/index.mjs"
+      }
+    }
+  },
+  "dependencies": {
+    "vue-request": "^2.0.4"
+  }
+}

+ 1 - 0
packages/request/src/index.ts

@@ -0,0 +1 @@
+export * from './use-request';

+ 11 - 0
packages/request/src/use-request.ts

@@ -0,0 +1,11 @@
+// import { setGlobalOptions, } from 'vue-request';
+
+// setGlobalOptions({
+//   manual: true,
+//   // ...
+// });
+
+/**
+ * @see https://www.attojs.com/guide/documentation/globalOptions.html
+ */
+export * from 'vue-request';

+ 5 - 0
packages/request/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/tsconfig/web.json",
+  "include": ["src"]
+}

+ 3 - 88
packages/types/global.d.ts

@@ -1,92 +1,7 @@
 import 'vue-router';
 
-declare module 'vue-router' {
-  interface RouteMeta {
-    /**
-     * 是否固定标签页
-     * @default false
-     */
-    affixTab?: boolean;
-    /**
-     * 需要特定的角色标识才可以访问
-     * @default []
-     */
-    authority?: string[];
-    /**
-     * 徽标
-     */
-    badge?: string;
-    /**
-     * 徽标类型
-     */
-    badgeType?: 'dot' | 'normal';
-    /**
-     * 徽标颜色
-     */
-    badgeVariants?:
-      | 'default'
-      | 'destructive'
-      | 'primary'
-      | 'success'
-      | 'warning'
-      | string;
-    /**
-     * 当前路由的子级在菜单中不展现
-     * @default false
-     */
-    hideChildrenInMenu?: boolean;
-    /**
-     * 当前路由在面包屑中不展现
-     * @default false
-     */
-    hideInBreadcrumb?: boolean;
-    /**
-     * 当前路由在菜单中不展现
-     * @default false
-     */
-    hideInMenu?: boolean;
+import type { RouteMeta as IRouteMeta } from '@vben-core/typings';
 
-    /**
-     * 当前路由在标签页不展现
-     * @default false
-     */
-    hideInTab?: boolean;
-    /**
-     * 路由跳转地址
-     */
-    href?: string;
-    /**
-     * 图标(菜单/tab)
-     */
-    icon?: string;
-    /**
-     * iframe 地址
-     */
-    iframeSrc?: string;
-    /**
-     * 忽略权限,直接可以访问
-     * @default false
-     */
-    ignoreAccess?: boolean;
-    /**
-     * 开启KeepAlive缓存
-     */
-    keepAlive?: boolean;
-    /**
-     * 路由是否已经加载过
-     */
-    loaded?: boolean;
-    /**
-     * 用于路由->菜单排序
-     */
-    orderNo?: number;
-    /**
-     * 外链-跳转路径
-     */
-    target?: string;
-    /**
-     * 标题名称
-     */
-    title: string;
-  }
+declare module 'vue-router' {
+  interface RouteMeta extends IRouteMeta {}
 }

+ 30 - 73
pnpm-lock.yaml

@@ -94,6 +94,9 @@ importers:
 
   apps/antd-view:
     dependencies:
+      '@vben-core/helpers':
+        specifier: workspace:*
+        version: link:../../packages/@vben-core/forward/helpers
       '@vben-core/preferences':
         specifier: workspace:*
         version: link:../../packages/@vben-core/forward/preferences
@@ -118,6 +121,9 @@ importers:
       '@vben/locales':
         specifier: workspace:*
         version: link:../../packages/locales
+      '@vben/request':
+        specifier: workspace:*
+        version: link:../../packages/request
       '@vben/styles':
         specifier: workspace:*
         version: link:../../packages/styles
@@ -465,6 +471,9 @@ importers:
       '@vben-core/typings':
         specifier: workspace:*
         version: link:../../shared/typings
+      vue-router:
+        specifier: ^4.3.2
+        version: 4.3.2(vue@3.4.27(typescript@5.4.5))
 
   packages/@vben-core/forward/preferences:
     dependencies:
@@ -756,9 +765,6 @@ importers:
       vue:
         specifier: 3.4.27
         version: 3.4.27(typescript@5.4.5)
-      vue-hooks-plus:
-        specifier: ^2.1.0
-        version: 2.1.0(vue@3.4.27(typescript@5.4.5))
 
   packages/icons:
     dependencies:
@@ -781,6 +787,12 @@ importers:
         specifier: ^9.13.1
         version: 9.13.1(vue@3.4.27(typescript@5.4.5))
 
+  packages/request:
+    dependencies:
+      vue-request:
+        specifier: ^2.0.4
+        version: 2.0.4(vue@3.4.27(typescript@5.4.5))
+
   packages/styles:
     dependencies:
       '@vben-core/design':
@@ -2375,9 +2387,6 @@ packages:
   '@types/http-cache-semantics@4.0.4':
     resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
 
-  '@types/js-cookie@3.0.6':
-    resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
-
   '@types/jsdom@21.1.7':
     resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
 
@@ -3432,10 +3441,6 @@ packages:
   decimal.js@10.4.3:
     resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
 
-  decode-uri-component@0.2.2:
-    resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
-    engines: {node: '>=0.10'}
-
   decompress-response@6.0.0:
     resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
     engines: {node: '>=10'}
@@ -3999,10 +4004,6 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
 
-  filter-obj@1.1.0:
-    resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
-    engines: {node: '>=0.10.0'}
-
   finalhandler@1.1.2:
     resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
     engines: {node: '>= 0.8'}
@@ -6029,14 +6030,6 @@ packages:
     engines: {node: '>=10.13.0'}
     hasBin: true
 
-  qs@6.12.1:
-    resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==}
-    engines: {node: '>=0.6'}
-
-  query-string@7.1.3:
-    resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
-    engines: {node: '>=6'}
-
   querystringify@2.2.0:
     resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
 
@@ -6275,10 +6268,6 @@ packages:
     resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
     engines: {node: '>=v12.22.7'}
 
-  screenfull@5.2.0:
-    resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
-    engines: {node: '>=0.10.0'}
-
   scroll-into-view-if-needed@2.2.31:
     resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
 
@@ -6456,10 +6445,6 @@ packages:
     resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
     engines: {node: '>=0.10.0'}
 
-  split-on-first@1.1.0:
-    resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
-    engines: {node: '>=6'}
-
   split2@4.2.0:
     resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
     engines: {node: '>= 10.x'}
@@ -6493,10 +6478,6 @@ packages:
   stream-transform@2.1.3:
     resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
 
-  strict-uri-encode@2.0.0:
-    resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
-    engines: {node: '>=4'}
-
   string-argv@0.3.2:
     resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
     engines: {node: '>=0.6.19'}
@@ -7189,17 +7170,22 @@ packages:
     peerDependencies:
       eslint: '>=6.0.0'
 
-  vue-hooks-plus@2.1.0:
-    resolution: {integrity: sha512-UkwmyoFX8WlfHgkqgDJ1jTLvVohtspRR8JFIZYCAgG01nqYVxoTiHZbEhOdIMH1Ba0CxP/xL26knT1+a2w5JpQ==}
-    peerDependencies:
-      vue: 3.4.27
-
   vue-i18n@9.13.1:
     resolution: {integrity: sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==}
     engines: {node: '>= 16'}
     peerDependencies:
       vue: 3.4.27
 
+  vue-request@2.0.4:
+    resolution: {integrity: sha512-+Tu5rDy6ItF9UdD21Mmbjiq5Pq6NZSN9juH72hNQTMn1whHh4KZPTKWVLK2YS4nzbuEnPs+82G91AA2Fgd93mg==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: 3.4.27
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+
   vue-router@4.3.2:
     resolution: {integrity: sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==}
     peerDependencies:
@@ -9102,8 +9088,6 @@ snapshots:
 
   '@types/http-cache-semantics@4.0.4': {}
 
-  '@types/js-cookie@3.0.6': {}
-
   '@types/jsdom@21.1.7':
     dependencies:
       '@types/node': 20.13.0
@@ -10322,8 +10306,6 @@ snapshots:
 
   decimal.js@10.4.3: {}
 
-  decode-uri-component@0.2.2: {}
-
   decompress-response@6.0.0:
     dependencies:
       mimic-response: 3.1.0
@@ -11040,8 +11022,6 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
-  filter-obj@1.1.0: {}
-
   finalhandler@1.1.2:
     dependencies:
       debug: 2.6.9
@@ -13015,17 +12995,6 @@ snapshots:
       pngjs: 5.0.0
       yargs: 15.4.1
 
-  qs@6.12.1:
-    dependencies:
-      side-channel: 1.0.6
-
-  query-string@7.1.3:
-    dependencies:
-      decode-uri-component: 0.2.2
-      filter-obj: 1.1.0
-      split-on-first: 1.1.0
-      strict-uri-encode: 2.0.0
-
   querystringify@2.2.0: {}
 
   queue-microtask@1.2.3: {}
@@ -13278,8 +13247,6 @@ snapshots:
     dependencies:
       xmlchars: 2.2.0
 
-  screenfull@5.2.0: {}
-
   scroll-into-view-if-needed@2.2.31:
     dependencies:
       compute-scroll-into-view: 1.0.20
@@ -13457,8 +13424,6 @@ snapshots:
 
   speakingurl@14.0.1: {}
 
-  split-on-first@1.1.0: {}
-
   split2@4.2.0: {}
 
   split@0.3.3:
@@ -13487,8 +13452,6 @@ snapshots:
     dependencies:
       mixme: 0.5.10
 
-  strict-uri-encode@2.0.0: {}
-
   string-argv@0.3.2: {}
 
   string-width@4.2.3:
@@ -14355,17 +14318,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  vue-hooks-plus@2.1.0(vue@3.4.27(typescript@5.4.5)):
-    dependencies:
-      '@types/js-cookie': 3.0.6
-      '@vue/devtools-api': 6.6.2
-      js-cookie: 3.0.5
-      lodash: 4.17.21
-      qs: 6.12.1
-      query-string: 7.1.3
-      screenfull: 5.2.0
-      vue: 3.4.27(typescript@5.4.5)
-
   vue-i18n@9.13.1(vue@3.4.27(typescript@5.4.5)):
     dependencies:
       '@intlify/core-base': 9.13.1
@@ -14373,6 +14325,11 @@ snapshots:
       '@vue/devtools-api': 6.6.2
       vue: 3.4.27(typescript@5.4.5)
 
+  vue-request@2.0.4(vue@3.4.27(typescript@5.4.5)):
+    dependencies:
+      vue: 3.4.27(typescript@5.4.5)
+      vue-demi: 0.14.8(vue@3.4.27(typescript@5.4.5))
+
   vue-router@4.3.2(vue@3.4.27(typescript@5.4.5)):
     dependencies:
       '@vue/devtools-api': 6.6.2

+ 4 - 0
vben-admin.code-workspace

@@ -116,6 +116,10 @@
       "name": "@vben/locales",
       "path": "packages/locales",
     },
+    {
+      "name": "@vben/request",
+      "path": "packages/request",
+    },
     {
       "name": "@vben/styles",
       "path": "packages/styles",