From b200ae9997b8c323e25fe8409d89e85e24c97a57 Mon Sep 17 00:00:00 2001 From: vben Date: Sun, 2 Jun 2024 15:04:37 +0800 Subject: [PATCH] feat: add some test case --- apps/antd-view/package.json | 2 + apps/antd-view/src/router/guard.ts | 139 ++++++++ apps/antd-view/src/router/guard/access.ts | 184 ----------- apps/antd-view/src/router/guard/index.ts | 55 ---- apps/antd-view/src/router/index.ts | 4 +- .../src/store/modules/example.test.ts | 23 +- apps/antd-view/src/store/modules/example.ts | 1 + .../views/_essential/authentication/login.vue | 3 +- .../@vben-core/forward/helpers/package.json | 3 +- .../helpers/src/generator-menus.test.ts | 171 ++++++++++ .../forward/helpers/src/generator-menus.ts | 71 ++++ .../helpers/src/generator-routes.test.ts | 128 ++++++++ .../forward/helpers/src/generator-routes.ts | 40 +++ .../@vben-core/forward/helpers/src/index.ts | 2 + .../forward/helpers/src/nested-object.test.ts | 18 + .../@vben-core/forward/helpers/tsconfig.json | 5 +- .../forward/preferences/src/config.ts | 4 - .../preferences/src/preferences.test.ts | 268 +++++++++++++++ .../forward/preferences/src/preferences.ts | 21 +- packages/@vben-core/forward/request/.gitkeep | 0 .../forward/stores/src/modules/access.test.ts | 91 +++++- .../forward/stores/src/modules/tabs.test.ts | 309 ++++++++++++++++++ .../@vben-core/shared/typings/package.json | 3 + .../@vben-core/shared/typings/src/index.ts | 1 + .../shared/typings/src/vue-router.ts | 90 +++++ .../@vben-core/shared/typings/vue-router.d.ts | 7 + .../input-password/input-password.vue | 2 +- .../common-ui/src/fallback/fallback.vue | 2 +- .../fallback/{ => icons}/fallback-icon.vue | 0 packages/hooks/package.json | 3 +- packages/hooks/src/index.ts | 2 +- packages/hooks/src/use-request.ts | 1 - packages/request/build.config.ts | 7 + packages/request/package.json | 46 +++ packages/request/src/index.ts | 1 + packages/request/src/use-request.ts | 11 + packages/request/tsconfig.json | 5 + packages/types/global.d.ts | 91 +----- pnpm-lock.yaml | 103 ++---- vben-admin.code-workspace | 4 + 40 files changed, 1469 insertions(+), 452 deletions(-) create mode 100644 apps/antd-view/src/router/guard.ts delete mode 100644 apps/antd-view/src/router/guard/access.ts delete mode 100644 apps/antd-view/src/router/guard/index.ts create mode 100644 packages/@vben-core/forward/helpers/src/generator-menus.test.ts create mode 100644 packages/@vben-core/forward/helpers/src/generator-menus.ts create mode 100644 packages/@vben-core/forward/helpers/src/generator-routes.test.ts create mode 100644 packages/@vben-core/forward/helpers/src/generator-routes.ts create mode 100644 packages/@vben-core/forward/preferences/src/preferences.test.ts delete mode 100644 packages/@vben-core/forward/request/.gitkeep create mode 100644 packages/@vben-core/forward/stores/src/modules/tabs.test.ts create mode 100644 packages/@vben-core/shared/typings/src/vue-router.ts create mode 100644 packages/@vben-core/shared/typings/vue-router.d.ts rename packages/business/common-ui/src/fallback/{ => icons}/fallback-icon.vue (100%) delete mode 100644 packages/hooks/src/use-request.ts create mode 100644 packages/request/build.config.ts create mode 100644 packages/request/package.json create mode 100644 packages/request/src/index.ts create mode 100644 packages/request/src/use-request.ts create mode 100644 packages/request/tsconfig.json diff --git a/apps/antd-view/package.json b/apps/antd-view/package.json index 5479fc2f..7b376999 100644 --- a/apps/antd-view/package.json +++ b/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:*", diff --git a/apps/antd-view/src/router/guard.ts b/apps/antd-view/src/router/guard.ts new file mode 100644 index 00000000..aa80320e --- /dev/null +++ b/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(); + + 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([]); + +/** + * 跳转登录页面 + * @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 }; diff --git a/apps/antd-view/src/router/guard/access.ts b/apps/antd-view/src/router/guard/access.ts deleted file mode 100644 index dbdf6571..00000000 --- a/apps/antd-view/src/router/guard/access.ts +++ /dev/null @@ -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([]); - -/** - * 权限访问守卫配置 - * @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 { - // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限 - return filterTree(dynamicRoutes, (route) => { - return hasVisible(route) && hasAuthority(route, roles); - }); -} - -/** - * 根据 routes 生成菜单列表 - * @param routes - */ -async function generatorMenus( - routes: RouteRecordRaw[], - router: Router, -): Promise { - // 获取所有router最终的path及name - const finalRoutes = traverseTreeValues( - router.getRoutes(), - ({ name, path }) => { - return { - name, - path, - }; - }, - ); - - const menus = mapTree(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 }; diff --git a/apps/antd-view/src/router/guard/index.ts b/apps/antd-view/src/router/guard/index.ts deleted file mode 100644 index 9dd074fc..00000000 --- a/apps/antd-view/src/router/guard/index.ts +++ /dev/null @@ -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(); - - 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 }; diff --git a/apps/antd-view/src/router/index.ts b/apps/antd-view/src/router/index.ts index aa6df538..eed2a80a 100644 --- a/apps/antd-view/src/router/index.ts +++ b/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 }; diff --git a/apps/antd-view/src/store/modules/example.test.ts b/apps/antd-view/src/store/modules/example.test.ts index 830c6f9d..c8dfe6d9 100644 --- a/apps/antd-view/src/store/modules/example.test.ts +++ b/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); }); }); diff --git a/apps/antd-view/src/store/modules/example.ts b/apps/antd-view/src/store/modules/example.ts index 6e714435..ceb1e81b 100644 --- a/apps/antd-view/src/store/modules/example.ts +++ b/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 }), }); diff --git a/apps/antd-view/src/views/_essential/authentication/login.vue b/apps/antd-view/src/views/_essential/authentication/login.vue index 5b7e7479..18ef3f8f 100644 --- a/apps/antd-view/src/views/_essential/authentication/login.vue +++ b/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 diff --git a/packages/@vben-core/forward/helpers/package.json b/packages/@vben-core/forward/helpers/package.json index 3ab594a6..ce597313 100644 --- a/packages/@vben-core/forward/helpers/package.json +++ b/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" } } diff --git a/packages/@vben-core/forward/helpers/src/generator-menus.test.ts b/packages/@vben-core/forward/helpers/src/generator-menus.test.ts new file mode 100644 index 00000000..0357631e --- /dev/null +++ b/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: [], + }, + ]); + }); +}); diff --git a/packages/@vben-core/forward/helpers/src/generator-menus.ts b/packages/@vben-core/forward/helpers/src/generator-menus.ts new file mode 100644 index 00000000..c5127a36 --- /dev/null +++ b/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 { + // 将路由列表转换为一个以 name 为键的对象映射 + // 获取所有router最终的path及name + const finalRoutesMap: { [key: string]: string } = Object.fromEntries( + router.getRoutes().map(({ name, path }) => [name, path]), + ); + + const menus = mapTree(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 }; diff --git a/packages/@vben-core/forward/helpers/src/generator-routes.test.ts b/packages/@vben-core/forward/helpers/src/generator-routes.test.ts new file mode 100644 index 00000000..51bac936 --- /dev/null +++ b/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' }, + ]); + }); +}); diff --git a/packages/@vben-core/forward/helpers/src/generator-routes.ts b/packages/@vben-core/forward/helpers/src/generator-routes.ts new file mode 100644 index 00000000..c1e2674b --- /dev/null +++ b/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 { + // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限 + 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 }; diff --git a/packages/@vben-core/forward/helpers/src/index.ts b/packages/@vben-core/forward/helpers/src/index.ts index 6bc95c7f..06b99225 100644 --- a/packages/@vben-core/forward/helpers/src/index.ts +++ b/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'; diff --git a/packages/@vben-core/forward/helpers/src/nested-object.test.ts b/packages/@vben-core/forward/helpers/src/nested-object.test.ts index a0036fbc..85acb391 100644 --- a/packages/@vben-core/forward/helpers/src/nested-object.test.ts +++ b/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', + }, + }); + }); }); diff --git a/packages/@vben-core/forward/helpers/tsconfig.json b/packages/@vben-core/forward/helpers/tsconfig.json index 03b23c68..426bc92a 100644 --- a/packages/@vben-core/forward/helpers/tsconfig.json +++ b/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"] } diff --git a/packages/@vben-core/forward/preferences/src/config.ts b/packages/@vben-core/forward/preferences/src/config.ts index da936866..f721fd85 100644 --- a/packages/@vben-core/forward/preferences/src/config.ts +++ b/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', diff --git a/packages/@vben-core/forward/preferences/src/preferences.test.ts b/packages/@vben-core/forward/preferences/src/preferences.test.ts new file mode 100644 index 00000000..2c8c2a4a --- /dev/null +++ b/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)', + ); + }); +}); diff --git a/packages/@vben-core/forward/preferences/src/preferences.ts b/packages/@vben-core/forward/preferences/src/preferences.ts index b571c6f0..4d3067ba 100644 --- a/packages/@vben-core/forward/preferences/src/preferences.ts +++ b/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) { - 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 }; diff --git a/packages/@vben-core/forward/request/.gitkeep b/packages/@vben-core/forward/request/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/@vben-core/forward/stores/src/modules/access.test.ts b/packages/@vben-core/forward/stores/src/modules/access.test.ts index ad0ebb1c..a7c3d50a 100644 --- a/packages/@vben-core/forward/stores/src/modules/access.test.ts +++ b/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(); + }); - // beforeEach(() => { - // 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' }, + ]); + }); - // expect(referenceStore.appName).toBe('vben-admin'); - // referenceStore.setAppName('vbenAdmin'); - // expect(referenceStore.getAppName).toBe('vbenAdmin'); + 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); + + 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([]); + }); + + // 测试设置空的访问路由列表 + it('handles empty accessRoutes correctly', () => { + const store = useAccessStore(); + store.setAccessRoutes([]); + expect(store.accessRoutes).toEqual([]); }); }); diff --git a/packages/@vben-core/forward/stores/src/modules/tabs.test.ts b/packages/@vben-core/forward/stores/src/modules/tabs.test.ts new file mode 100644 index 00000000..255c1115 --- /dev/null +++ b/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); + }); +}); diff --git a/packages/@vben-core/shared/typings/package.json b/packages/@vben-core/shared/typings/package.json index 6659fe83..c1dd159a 100644 --- a/packages/@vben-core/shared/typings/package.json +++ b/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": { diff --git a/packages/@vben-core/shared/typings/src/index.ts b/packages/@vben-core/shared/typings/src/index.ts index db31fdba..25beb352 100644 --- a/packages/@vben-core/shared/typings/src/index.ts +++ b/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'; diff --git a/packages/@vben-core/shared/typings/src/vue-router.ts b/packages/@vben-core/shared/typings/src/vue-router.ts new file mode 100644 index 00000000..c0e12590 --- /dev/null +++ b/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 }; diff --git a/packages/@vben-core/shared/typings/vue-router.d.ts b/packages/@vben-core/shared/typings/vue-router.d.ts new file mode 100644 index 00000000..abe9f926 --- /dev/null +++ b/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 {} +} diff --git a/packages/@vben-core/uikit/shadcn-ui/src/components/input-password/input-password.vue b/packages/@vben-core/uikit/shadcn-ui/src/components/input-password/input-password.vue index 9773755c..87e4f591 100644 --- a/packages/@vben-core/uikit/shadcn-ui/src/components/input-password/input-password.vue +++ b/packages/@vben-core/uikit/shadcn-ui/src/components/input-password/input-password.vue @@ -44,7 +44,7 @@ const show = ref(false);
diff --git a/packages/business/common-ui/src/fallback/fallback.vue b/packages/business/common-ui/src/fallback/fallback.vue index 65315280..49bcc642 100644 --- a/packages/business/common-ui/src/fallback/fallback.vue +++ b/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 { /** diff --git a/packages/business/common-ui/src/fallback/fallback-icon.vue b/packages/business/common-ui/src/fallback/icons/fallback-icon.vue similarity index 100% rename from packages/business/common-ui/src/fallback/fallback-icon.vue rename to packages/business/common-ui/src/fallback/icons/fallback-icon.vue diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 63763850..075795bf 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -41,7 +41,6 @@ } }, "dependencies": { - "vue": "3.4.27", - "vue-hooks-plus": "^2.1.0" + "vue": "3.4.27" } } diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 30a58dec..cb0ff5c3 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1 +1 @@ -export * from './use-request'; +export {}; diff --git a/packages/hooks/src/use-request.ts b/packages/hooks/src/use-request.ts deleted file mode 100644 index 497c7c8d..00000000 --- a/packages/hooks/src/use-request.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useRequest } from 'vue-hooks-plus/es/useRequest'; diff --git a/packages/request/build.config.ts b/packages/request/build.config.ts new file mode 100644 index 00000000..97e572c5 --- /dev/null +++ b/packages/request/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/packages/request/package.json b/packages/request/package.json new file mode 100644 index 00000000..f792319e --- /dev/null +++ b/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" + } +} diff --git a/packages/request/src/index.ts b/packages/request/src/index.ts new file mode 100644 index 00000000..30a58dec --- /dev/null +++ b/packages/request/src/index.ts @@ -0,0 +1 @@ +export * from './use-request'; diff --git a/packages/request/src/use-request.ts b/packages/request/src/use-request.ts new file mode 100644 index 00000000..5b1c4a81 --- /dev/null +++ b/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'; diff --git a/packages/request/tsconfig.json b/packages/request/tsconfig.json new file mode 100644 index 00000000..b7594e8b --- /dev/null +++ b/packages/request/tsconfig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"] +} diff --git a/packages/types/global.d.ts b/packages/types/global.d.ts index 119d5f0f..abe9f926 100644 --- a/packages/types/global.d.ts +++ b/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 {} } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25a9a8ae..95d8d827 100644 --- a/pnpm-lock.yaml +++ b/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 diff --git a/vben-admin.code-workspace b/vben-admin.code-workspace index 1634a281..d08b18dc 100644 --- a/vben-admin.code-workspace +++ b/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",