feat: add some test case
parent
fc423c3657
commit
b200ae9997
|
@ -22,6 +22,7 @@
|
||||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vben-core/helpers": "workspace:*",
|
||||||
"@vben-core/preferences": "workspace:*",
|
"@vben-core/preferences": "workspace:*",
|
||||||
"@vben-core/stores": "workspace:*",
|
"@vben-core/stores": "workspace:*",
|
||||||
"@vben/common-ui": "workspace:*",
|
"@vben/common-ui": "workspace:*",
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"@vben/icons": "workspace:*",
|
"@vben/icons": "workspace:*",
|
||||||
"@vben/layouts": "workspace:*",
|
"@vben/layouts": "workspace:*",
|
||||||
"@vben/locales": "workspace:*",
|
"@vben/locales": "workspace:*",
|
||||||
|
"@vben/request": "workspace:*",
|
||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
|
|
|
@ -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 };
|
|
@ -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 };
|
|
|
@ -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 };
|
|
|
@ -3,7 +3,7 @@ import type { RouteRecordName, RouteRecordRaw } from 'vue-router';
|
||||||
import { traverseTreeValues } from '@vben/utils';
|
import { traverseTreeValues } from '@vben/utils';
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
|
||||||
import { createRouteGuard } from './guard';
|
import { createRouterGuard } from './guard';
|
||||||
import { staticRoutes } from './routes';
|
import { staticRoutes } from './routes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,6 +54,6 @@ function resetRoutes() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 创建路由守卫
|
// 创建路由守卫
|
||||||
createRouteGuard(router);
|
createRouterGuard(router);
|
||||||
|
|
||||||
export { resetRoutes, router };
|
export { resetRoutes, router };
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import {
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
// beforeEach,
|
|
||||||
describe,
|
|
||||||
// expect,
|
|
||||||
it,
|
|
||||||
} from 'vitest';
|
|
||||||
|
|
||||||
// import { useAccessStore } from '../modules/access';
|
import { useCounterStore } from './example';
|
||||||
|
|
||||||
describe('useCounterStore', () => {
|
describe('useCounterStore', () => {
|
||||||
it('app Name with test', () => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
// let referenceStore = usePreferencesStore();
|
});
|
||||||
|
|
||||||
// beforeEach(() => {
|
it('count test', () => {
|
||||||
// referenceStore = usePreferencesStore();
|
setActivePinia(createPinia());
|
||||||
// });
|
const counterStore = useCounterStore();
|
||||||
|
|
||||||
// expect(referenceStore.appName).toBe('vben-admin');
|
expect(counterStore.count).toBe(0);
|
||||||
// referenceStore.setAppName('vbenAdmin');
|
|
||||||
// expect(referenceStore.getAppName).toBe('vbenAdmin');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,5 +9,6 @@ export const useCounterStore = defineStore('counter', {
|
||||||
getters: {
|
getters: {
|
||||||
double: (state) => state.count * 2,
|
double: (state) => state.count * 2,
|
||||||
},
|
},
|
||||||
|
persist: [],
|
||||||
state: () => ({ count: 0 }),
|
state: () => ({ count: 0 }),
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { useAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
import { getUserInfo, userLogin } from '@/services';
|
import { getUserInfo, userLogin } from '@/services';
|
||||||
import { AuthenticationLogin } from '@vben/common-ui';
|
import { AuthenticationLogin } from '@vben/common-ui';
|
||||||
import { useRequest } from '@vben/hooks';
|
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { useRequest } from '@vben/request';
|
||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
@ -35,6 +35,7 @@ const { loading: userInfoLoading, runAsync: runGetUserInfo } = useRequest(
|
||||||
async function handleLogin(values: LoginAndRegisterParams) {
|
async function handleLogin(values: LoginAndRegisterParams) {
|
||||||
// 异步处理用户登录操作并获取 accessToken
|
// 异步处理用户登录操作并获取 accessToken
|
||||||
// Asynchronously handle the user login operation and obtain the accessToken
|
// Asynchronously handle the user login operation and obtain the accessToken
|
||||||
|
|
||||||
const { accessToken } = await runUserLogin(values);
|
const { accessToken } = await runUserLogin(values);
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
// 如果成功获取到 accessToken
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben-core/toolkit": "workspace:*",
|
"@vben-core/toolkit": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*"
|
"@vben-core/typings": "workspace:*",
|
||||||
|
"vue-router": "^4.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 };
|
|
@ -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' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 };
|
|
@ -1,2 +1,4 @@
|
||||||
export * from './flatten-object';
|
export * from './flatten-object';
|
||||||
|
export * from './generator-menus';
|
||||||
|
export * from './generator-routes';
|
||||||
export * from './nested-object';
|
export * from './nested-object';
|
||||||
|
|
|
@ -94,4 +94,22 @@ describe('nestedObject', () => {
|
||||||
|
|
||||||
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$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"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,6 @@ const defaultPreferences: Preferences = {
|
||||||
split: true,
|
split: true,
|
||||||
styleType: 'rounded',
|
styleType: 'rounded',
|
||||||
},
|
},
|
||||||
|
|
||||||
shortcutKeys: { enable: true },
|
shortcutKeys: { enable: true },
|
||||||
sidebar: {
|
sidebar: {
|
||||||
collapse: false,
|
collapse: false,
|
||||||
|
@ -56,17 +55,14 @@ const defaultPreferences: Preferences = {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
width: 240,
|
width: 240,
|
||||||
},
|
},
|
||||||
|
|
||||||
tabbar: {
|
tabbar: {
|
||||||
enable: true,
|
enable: true,
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
colorPrimary: 'hsl(211 91% 39%)',
|
colorPrimary: 'hsl(211 91% 39%)',
|
||||||
},
|
},
|
||||||
|
|
||||||
transition: {
|
transition: {
|
||||||
enable: true,
|
enable: true,
|
||||||
name: 'fade-slide',
|
name: 'fade-slide',
|
||||||
|
|
|
@ -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)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -85,15 +85,21 @@ class PreferenceManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
|
||||||
|
*/
|
||||||
|
private loadCachedPreferences() {
|
||||||
|
return this.cache?.getItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载偏好设置
|
* 加载偏好设置
|
||||||
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
|
|
||||||
* @returns {Preferences} 加载的偏好设置
|
* @returns {Preferences} 加载的偏好设置
|
||||||
*/
|
*/
|
||||||
private loadPreferences(): Preferences {
|
private loadPreferences(): Preferences | null {
|
||||||
const savedPreferences = this.cache?.getItem(STORAGE_KEY);
|
return this.loadCachedPreferences() || { ...defaultPreferences };
|
||||||
return savedPreferences || { ...defaultPreferences };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听状态和系统偏好设置的变化。
|
* 监听状态和系统偏好设置的变化。
|
||||||
*/
|
*/
|
||||||
|
@ -239,7 +245,7 @@ class PreferenceManager {
|
||||||
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
||||||
|
|
||||||
// 加载并合并当前存储的偏好设置
|
// 加载并合并当前存储的偏好设置
|
||||||
const mergedPreference = merge({}, this.loadPreferences(), overrides);
|
const mergedPreference = merge({}, this.loadCachedPreferences(), overrides);
|
||||||
|
|
||||||
// 更新偏好设置
|
// 更新偏好设置
|
||||||
this.updatePreferences(mergedPreference);
|
this.updatePreferences(mergedPreference);
|
||||||
|
@ -274,9 +280,10 @@ class PreferenceManager {
|
||||||
* @param updates - 要更新的偏好设置
|
* @param updates - 要更新的偏好设置
|
||||||
*/
|
*/
|
||||||
public updatePreferences(updates: DeepPartial<Preferences>) {
|
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.state, mergedState);
|
||||||
|
|
||||||
Object.assign(this.flattenedState, flattenObject(this.state));
|
Object.assign(this.flattenedState, flattenObject(this.state));
|
||||||
|
|
||||||
// 根据更新的键值执行相应的操作
|
// 根据更新的键值执行相应的操作
|
||||||
|
@ -286,4 +293,4 @@ class PreferenceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferencesManager = new PreferenceManager();
|
const preferencesManager = new PreferenceManager();
|
||||||
export { isDarkTheme, preferencesManager };
|
export { PreferenceManager, isDarkTheme, preferencesManager };
|
||||||
|
|
|
@ -1,24 +1,85 @@
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import {
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
// beforeEach,
|
|
||||||
describe,
|
|
||||||
// expect,
|
|
||||||
it,
|
|
||||||
} from 'vitest';
|
|
||||||
|
|
||||||
// import { useAccessStore } from '../modules/access';
|
import { useAccessStore } from './access';
|
||||||
|
|
||||||
describe('useAccessStore', () => {
|
describe('useAccessStore', () => {
|
||||||
it('app Name with test', () => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
// let referenceStore = usePreferencesStore();
|
});
|
||||||
|
|
||||||
// beforeEach(() => {
|
it('updates accessMenus state', () => {
|
||||||
// referenceStore = usePreferencesStore();
|
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');
|
it('updates userInfo and userRoles state', () => {
|
||||||
// referenceStore.setAppName('vbenAdmin');
|
const store = useAccessStore();
|
||||||
// expect(referenceStore.getAppName).toBe('vbenAdmin');
|
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([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -28,6 +28,9 @@
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
"development": "./src/index.ts",
|
"development": "./src/index.ts",
|
||||||
"default": "./dist/index.mjs"
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"./vue-router": {
|
||||||
|
"types": "./vue-router.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|
|
@ -4,3 +4,4 @@ export type * from './flatten';
|
||||||
export type * from './menu-record';
|
export type * from './menu-record';
|
||||||
export type * from './tabs';
|
export type * from './tabs';
|
||||||
export type * from './tools';
|
export type * from './tools';
|
||||||
|
export type * from './vue-router';
|
||||||
|
|
|
@ -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 };
|
|
@ -0,0 +1,7 @@
|
||||||
|
import 'vue-router';
|
||||||
|
|
||||||
|
import type { RouteMeta as IRouteMeta } from '@vben-core/typings';
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta extends IRouteMeta {}
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ const show = ref(false);
|
||||||
</template>
|
</template>
|
||||||
</VbenInput>
|
</VbenInput>
|
||||||
<div
|
<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"
|
@click="show = !show"
|
||||||
>
|
>
|
||||||
<IcOutlineVisibility v-if="show" />
|
<IcOutlineVisibility v-if="show" />
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { $t } from '@vben/locales';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import FeedbackIcon from './fallback-icon.vue';
|
import FeedbackIcon from './icons/fallback-icon.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "3.4.27",
|
"vue": "3.4.27"
|
||||||
"vue-hooks-plus": "^2.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from './use-request';
|
export {};
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as useRequest } from 'vue-hooks-plus/es/useRequest';
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: ['src/index'],
|
||||||
|
});
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './use-request';
|
|
@ -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';
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/web.json",
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -1,92 +1,7 @@
|
||||||
import 'vue-router';
|
import 'vue-router';
|
||||||
|
|
||||||
declare module 'vue-router' {
|
import type { RouteMeta as IRouteMeta } from '@vben-core/typings';
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
declare module 'vue-router' {
|
||||||
* 当前路由在标签页不展现
|
interface RouteMeta extends IRouteMeta {}
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
103
pnpm-lock.yaml
103
pnpm-lock.yaml
|
@ -94,6 +94,9 @@ importers:
|
||||||
|
|
||||||
apps/antd-view:
|
apps/antd-view:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@vben-core/helpers':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/@vben-core/forward/helpers
|
||||||
'@vben-core/preferences':
|
'@vben-core/preferences':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/@vben-core/forward/preferences
|
version: link:../../packages/@vben-core/forward/preferences
|
||||||
|
@ -118,6 +121,9 @@ importers:
|
||||||
'@vben/locales':
|
'@vben/locales':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/locales
|
version: link:../../packages/locales
|
||||||
|
'@vben/request':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/request
|
||||||
'@vben/styles':
|
'@vben/styles':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/styles
|
version: link:../../packages/styles
|
||||||
|
@ -465,6 +471,9 @@ importers:
|
||||||
'@vben-core/typings':
|
'@vben-core/typings':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../shared/typings
|
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:
|
packages/@vben-core/forward/preferences:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -756,9 +765,6 @@ importers:
|
||||||
vue:
|
vue:
|
||||||
specifier: 3.4.27
|
specifier: 3.4.27
|
||||||
version: 3.4.27(typescript@5.4.5)
|
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:
|
packages/icons:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -781,6 +787,12 @@ importers:
|
||||||
specifier: ^9.13.1
|
specifier: ^9.13.1
|
||||||
version: 9.13.1(vue@3.4.27(typescript@5.4.5))
|
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:
|
packages/styles:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vben-core/design':
|
'@vben-core/design':
|
||||||
|
@ -2375,9 +2387,6 @@ packages:
|
||||||
'@types/http-cache-semantics@4.0.4':
|
'@types/http-cache-semantics@4.0.4':
|
||||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||||
|
|
||||||
'@types/js-cookie@3.0.6':
|
|
||||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
|
||||||
|
|
||||||
'@types/jsdom@21.1.7':
|
'@types/jsdom@21.1.7':
|
||||||
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
||||||
|
|
||||||
|
@ -3432,10 +3441,6 @@ packages:
|
||||||
decimal.js@10.4.3:
|
decimal.js@10.4.3:
|
||||||
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
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:
|
decompress-response@6.0.0:
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -3999,10 +4004,6 @@ packages:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
filter-obj@1.1.0:
|
|
||||||
resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
finalhandler@1.1.2:
|
finalhandler@1.1.2:
|
||||||
resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
|
resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
@ -6029,14 +6030,6 @@ packages:
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
hasBin: true
|
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:
|
querystringify@2.2.0:
|
||||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||||
|
|
||||||
|
@ -6275,10 +6268,6 @@ packages:
|
||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
engines: {node: '>=v12.22.7'}
|
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:
|
scroll-into-view-if-needed@2.2.31:
|
||||||
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
|
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
|
||||||
|
|
||||||
|
@ -6456,10 +6445,6 @@ packages:
|
||||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
split-on-first@1.1.0:
|
|
||||||
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
split2@4.2.0:
|
split2@4.2.0:
|
||||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
engines: {node: '>= 10.x'}
|
engines: {node: '>= 10.x'}
|
||||||
|
@ -6493,10 +6478,6 @@ packages:
|
||||||
stream-transform@2.1.3:
|
stream-transform@2.1.3:
|
||||||
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
|
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:
|
string-argv@0.3.2:
|
||||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||||
engines: {node: '>=0.6.19'}
|
engines: {node: '>=0.6.19'}
|
||||||
|
@ -7189,17 +7170,22 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=6.0.0'
|
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:
|
vue-i18n@9.13.1:
|
||||||
resolution: {integrity: sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==}
|
resolution: {integrity: sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: 3.4.27
|
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:
|
vue-router@4.3.2:
|
||||||
resolution: {integrity: sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==}
|
resolution: {integrity: sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -9102,8 +9088,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4': {}
|
'@types/http-cache-semantics@4.0.4': {}
|
||||||
|
|
||||||
'@types/js-cookie@3.0.6': {}
|
|
||||||
|
|
||||||
'@types/jsdom@21.1.7':
|
'@types/jsdom@21.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.13.0
|
'@types/node': 20.13.0
|
||||||
|
@ -10322,8 +10306,6 @@ snapshots:
|
||||||
|
|
||||||
decimal.js@10.4.3: {}
|
decimal.js@10.4.3: {}
|
||||||
|
|
||||||
decode-uri-component@0.2.2: {}
|
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-response: 3.1.0
|
mimic-response: 3.1.0
|
||||||
|
@ -11040,8 +11022,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
|
|
||||||
filter-obj@1.1.0: {}
|
|
||||||
|
|
||||||
finalhandler@1.1.2:
|
finalhandler@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
|
@ -13015,17 +12995,6 @@ snapshots:
|
||||||
pngjs: 5.0.0
|
pngjs: 5.0.0
|
||||||
yargs: 15.4.1
|
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: {}
|
querystringify@2.2.0: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
@ -13278,8 +13247,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
xmlchars: 2.2.0
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
screenfull@5.2.0: {}
|
|
||||||
|
|
||||||
scroll-into-view-if-needed@2.2.31:
|
scroll-into-view-if-needed@2.2.31:
|
||||||
dependencies:
|
dependencies:
|
||||||
compute-scroll-into-view: 1.0.20
|
compute-scroll-into-view: 1.0.20
|
||||||
|
@ -13457,8 +13424,6 @@ snapshots:
|
||||||
|
|
||||||
speakingurl@14.0.1: {}
|
speakingurl@14.0.1: {}
|
||||||
|
|
||||||
split-on-first@1.1.0: {}
|
|
||||||
|
|
||||||
split2@4.2.0: {}
|
split2@4.2.0: {}
|
||||||
|
|
||||||
split@0.3.3:
|
split@0.3.3:
|
||||||
|
@ -13487,8 +13452,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
mixme: 0.5.10
|
mixme: 0.5.10
|
||||||
|
|
||||||
strict-uri-encode@2.0.0: {}
|
|
||||||
|
|
||||||
string-argv@0.3.2: {}
|
string-argv@0.3.2: {}
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
|
@ -14355,17 +14318,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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)):
|
vue-i18n@9.13.1(vue@3.4.27(typescript@5.4.5)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/core-base': 9.13.1
|
'@intlify/core-base': 9.13.1
|
||||||
|
@ -14373,6 +14325,11 @@ snapshots:
|
||||||
'@vue/devtools-api': 6.6.2
|
'@vue/devtools-api': 6.6.2
|
||||||
vue: 3.4.27(typescript@5.4.5)
|
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)):
|
vue-router@4.3.2(vue@3.4.27(typescript@5.4.5)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.2
|
'@vue/devtools-api': 6.6.2
|
||||||
|
|
|
@ -116,6 +116,10 @@
|
||||||
"name": "@vben/locales",
|
"name": "@vben/locales",
|
||||||
"path": "packages/locales",
|
"path": "packages/locales",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "@vben/request",
|
||||||
|
"path": "packages/request",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "@vben/styles",
|
"name": "@vben/styles",
|
||||||
"path": "packages/styles",
|
"path": "packages/styles",
|
||||||
|
|
Loading…
Reference in New Issue