refactor(project): re-adjust the overall folder
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# @vben/utils
|
||||
|
||||
用于多个 `app` 公用的工具包,继承了 `@vben-core/toolkit` 的所有能力。业务上有通用的工具函数可以放在这里。
|
||||
用于多个 `app` 公用的工具包,继承了 `@vben-core/toolkit`、`@vben-core/helper`、`@vben-core/request` 的所有能力。业务上有通用的工具函数可以放在这里。
|
||||
|
||||
## 用法
|
||||
|
||||
|
@@ -11,8 +11,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"stub": "pnpm unbuild --stub"
|
||||
"#build": "pnpm unbuild"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -26,7 +25,8 @@
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
"#default": "./dist/index.mjs",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
@@ -38,6 +38,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/toolkit": "workspace:*"
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"vue-router": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
88
packages/utils/src/helpers/find-menu-by-path.test.ts
Normal file
88
packages/utils/src/helpers/find-menu-by-path.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { findMenuByPath, findRootMenuByPath } from './find-menu-by-path';
|
||||
|
||||
// 示例菜单数据
|
||||
const menus: any[] = [
|
||||
{ path: '/', children: [] },
|
||||
{ path: '/about', children: [] },
|
||||
{
|
||||
path: '/contact',
|
||||
children: [
|
||||
{ path: '/contact/email', children: [] },
|
||||
{ path: '/contact/phone', children: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/services',
|
||||
children: [
|
||||
{ path: '/services/design', children: [] },
|
||||
{
|
||||
path: '/services/development',
|
||||
children: [{ path: '/services/development/web', children: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('menu Finder Tests', () => {
|
||||
it('finds a top-level menu', () => {
|
||||
const menu = findMenuByPath(menus, '/about');
|
||||
expect(menu).toBeDefined();
|
||||
expect(menu?.path).toBe('/about');
|
||||
});
|
||||
|
||||
it('finds a nested menu', () => {
|
||||
const menu = findMenuByPath(menus, '/services/development/web');
|
||||
expect(menu).toBeDefined();
|
||||
expect(menu?.path).toBe('/services/development/web');
|
||||
});
|
||||
|
||||
it('returns null for a non-existent path', () => {
|
||||
const menu = findMenuByPath(menus, '/non-existent');
|
||||
expect(menu).toBeNull();
|
||||
});
|
||||
|
||||
it('handles empty menus list', () => {
|
||||
const menu = findMenuByPath([], '/about');
|
||||
expect(menu).toBeNull();
|
||||
});
|
||||
|
||||
it('handles menu items without children', () => {
|
||||
const menu = findMenuByPath(
|
||||
[{ path: '/only', children: undefined }] as any[],
|
||||
'/only',
|
||||
);
|
||||
expect(menu).toBeDefined();
|
||||
expect(menu?.path).toBe('/only');
|
||||
});
|
||||
|
||||
it('finds root menu by path', () => {
|
||||
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
|
||||
menus,
|
||||
'/services/development/web',
|
||||
);
|
||||
|
||||
expect(findMenu).toBeDefined();
|
||||
expect(rootMenu).toBeUndefined();
|
||||
expect(rootMenuPath).toBeUndefined();
|
||||
expect(findMenu?.path).toBe('/services/development/web');
|
||||
});
|
||||
|
||||
it('returns null for undefined or empty path', () => {
|
||||
const menuUndefinedPath = findMenuByPath(menus);
|
||||
const menuEmptyPath = findMenuByPath(menus, '');
|
||||
expect(menuUndefinedPath).toBeNull();
|
||||
expect(menuEmptyPath).toBeNull();
|
||||
});
|
||||
|
||||
it('checks for root menu when path does not exist', () => {
|
||||
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
|
||||
menus,
|
||||
'/non-existent',
|
||||
);
|
||||
expect(findMenu).toBeNull();
|
||||
expect(rootMenu).toBeUndefined();
|
||||
expect(rootMenuPath).toBeUndefined();
|
||||
});
|
||||
});
|
37
packages/utils/src/helpers/find-menu-by-path.ts
Normal file
37
packages/utils/src/helpers/find-menu-by-path.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
function findMenuByPath(
|
||||
list: MenuRecordRaw[],
|
||||
path?: string,
|
||||
): MenuRecordRaw | null {
|
||||
for (const menu of list) {
|
||||
if (menu.path === path) {
|
||||
return menu;
|
||||
}
|
||||
const findMenu = menu.children && findMenuByPath(menu.children, path);
|
||||
if (findMenu) {
|
||||
return findMenu;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根菜单
|
||||
* @param menus
|
||||
* @param path
|
||||
*/
|
||||
function findRootMenuByPath(menus: MenuRecordRaw[], path?: string) {
|
||||
const findMenu = findMenuByPath(menus, path);
|
||||
const rootMenuPath = findMenu?.parents?.[0];
|
||||
const rootMenu = rootMenuPath
|
||||
? menus.find((item) => item.path === rootMenuPath)
|
||||
: undefined;
|
||||
return {
|
||||
findMenu,
|
||||
rootMenu,
|
||||
rootMenuPath,
|
||||
};
|
||||
}
|
||||
|
||||
export { findMenuByPath, findRootMenuByPath };
|
238
packages/utils/src/helpers/generate-menus.test.ts
Normal file
238
packages/utils/src/helpers/generate-menus.test.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { generateMenus } from './generate-menus'; // 替换为您的实际路径
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
type Router,
|
||||
type RouteRecordRaw,
|
||||
} from 'vue-router';
|
||||
|
||||
// Nested route setup to test child inclusion and hideChildrenInMenu functionality
|
||||
|
||||
describe('generateMenus', () => {
|
||||
// 模拟路由数据
|
||||
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' },
|
||||
]),
|
||||
};
|
||||
|
||||
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: '首页',
|
||||
order: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/home',
|
||||
show: true,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'about-icon',
|
||||
name: '关于',
|
||||
order: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/about',
|
||||
show: true,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
const menus = await generateMenus(mockRoutes, mockRouter as any);
|
||||
expect(menus).toEqual(expectedMenus);
|
||||
});
|
||||
|
||||
it('includes additional meta properties in menu items', async () => {
|
||||
const mockRoutesWithMeta = [
|
||||
{
|
||||
meta: { icon: 'user-icon', order: 1, title: 'Profile' },
|
||||
name: 'profile',
|
||||
path: '/profile',
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any);
|
||||
expect(menus).toEqual([
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'user-icon',
|
||||
name: 'Profile',
|
||||
order: 1,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/profile',
|
||||
show: true,
|
||||
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 generateMenus(mockRoutesWithParams, mockRouter as any);
|
||||
expect(menus).toEqual([
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'details-icon',
|
||||
name: 'User Details',
|
||||
order: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/users/:userId',
|
||||
show: true,
|
||||
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 generateMenus(
|
||||
mockRoutesWithRedirect,
|
||||
mockRouter as any,
|
||||
);
|
||||
expect(menus).toEqual([
|
||||
// Assuming your generateMenus function excludes redirect routes from the menu
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: undefined,
|
||||
name: 'redirectedRoute',
|
||||
order: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/old-path',
|
||||
show: true,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'path-icon',
|
||||
name: 'New Path',
|
||||
order: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/new-path',
|
||||
show: true,
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const routes: any = [
|
||||
{
|
||||
meta: { order: 2, title: 'Home' },
|
||||
name: 'home',
|
||||
path: '/',
|
||||
},
|
||||
{
|
||||
meta: { order: 1, title: 'About' },
|
||||
name: 'about',
|
||||
path: '/about',
|
||||
},
|
||||
];
|
||||
|
||||
const router: Router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
it('should generate menu list with correct order', async () => {
|
||||
const menus = await generateMenus(routes, router);
|
||||
const expectedMenus = [
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: undefined,
|
||||
name: 'About',
|
||||
order: 1,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/about',
|
||||
show: true,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: undefined,
|
||||
name: 'Home',
|
||||
order: 2,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/',
|
||||
show: true,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
expect(menus).toEqual(expectedMenus);
|
||||
});
|
||||
|
||||
it('should handle empty routes', async () => {
|
||||
const emptyRoutes: any[] = [];
|
||||
const menus = await generateMenus(emptyRoutes, router);
|
||||
expect(menus).toEqual([]);
|
||||
});
|
||||
});
|
78
packages/utils/src/helpers/generate-menus.ts
Normal file
78
packages/utils/src/helpers/generate-menus.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
||||
import type { Router, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { filterTree, mapTree } from '@vben-core/toolkit';
|
||||
|
||||
/**
|
||||
* 根据 routes 生成菜单列表
|
||||
* @param routes
|
||||
*/
|
||||
async function generateMenus(
|
||||
routes: RouteRecordRaw[],
|
||||
router: Router,
|
||||
): Promise<MenuRecordRaw[]> {
|
||||
// 将路由列表转换为一个以 name 为键的对象映射
|
||||
// 获取所有router最终的path及name
|
||||
const finalRoutesMap: { [key: string]: string } = Object.fromEntries(
|
||||
router.getRoutes().map(({ name, path }) => [name, path]),
|
||||
);
|
||||
|
||||
let 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,
|
||||
link,
|
||||
order,
|
||||
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 : link || path;
|
||||
return {
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
icon,
|
||||
name,
|
||||
order,
|
||||
parent: route.parent,
|
||||
parents: route.parents,
|
||||
path: resultPath as string,
|
||||
show: !route?.meta?.hideInMenu,
|
||||
children: resultChildren || [],
|
||||
};
|
||||
});
|
||||
|
||||
// 对菜单进行排序
|
||||
menus = menus.sort((a, b) => (a.order || 999) - (b.order || 999));
|
||||
|
||||
const finalMenus = filterTree(menus, (menu) => {
|
||||
return !!menu.show;
|
||||
});
|
||||
return finalMenus;
|
||||
}
|
||||
|
||||
export { generateMenus };
|
82
packages/utils/src/helpers/generate-routes-backend.ts
Normal file
82
packages/utils/src/helpers/generate-routes-backend.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { mapTree } from '@vben-core/toolkit';
|
||||
import {
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
RouteRecordStringComponent,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
/**
|
||||
* 动态生成路由 - 后端方式
|
||||
*/
|
||||
async function generateRoutesByBackend(
|
||||
options: GenerateMenuAndRoutesOptions,
|
||||
): Promise<RouteRecordRaw[]> {
|
||||
const { fetchMenuListAsync, layoutMap = {}, pageMap = {} } = options;
|
||||
|
||||
try {
|
||||
const menuRoutes = await fetchMenuListAsync?.();
|
||||
if (!menuRoutes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizePageMap: ComponentRecordType = {};
|
||||
|
||||
for (const [key, value] of Object.entries(pageMap)) {
|
||||
normalizePageMap[normalizeViewPath(key)] = value;
|
||||
}
|
||||
|
||||
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
|
||||
|
||||
return routes;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function convertRoutes(
|
||||
routes: RouteRecordStringComponent[],
|
||||
layoutMap: ComponentRecordType,
|
||||
pageMap: ComponentRecordType,
|
||||
): RouteRecordRaw[] {
|
||||
return mapTree(routes, (node) => {
|
||||
const route = node as unknown as RouteRecordRaw;
|
||||
const { component, name } = node;
|
||||
|
||||
if (!name) {
|
||||
console.error('route name is required', route);
|
||||
}
|
||||
|
||||
// layout转换
|
||||
if (component && layoutMap[component]) {
|
||||
route.component = layoutMap[component];
|
||||
// 页面组件转换
|
||||
} else if (component) {
|
||||
const normalizePath = normalizeViewPath(component);
|
||||
route.component =
|
||||
pageMap[
|
||||
normalizePath.endsWith('.vue')
|
||||
? normalizePath
|
||||
: `${normalizePath}.vue`
|
||||
];
|
||||
}
|
||||
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeViewPath(path: string): string {
|
||||
// 去除相对路径前缀
|
||||
const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, '');
|
||||
|
||||
// 确保路径以 '/' 开头
|
||||
const viewPath = normalizedPath.startsWith('/')
|
||||
? normalizedPath
|
||||
: `/${normalizedPath}`;
|
||||
|
||||
// TODO: 这里耦合了vben-admin的目录结构
|
||||
return viewPath.replace(/^\/views/, '');
|
||||
}
|
||||
export { generateRoutesByBackend };
|
105
packages/utils/src/helpers/generate-routes-frontend.test.ts
Normal file
105
packages/utils/src/helpers/generate-routes-frontend.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
generateRoutesByFrontend,
|
||||
hasAuthority,
|
||||
} from './generate-routes-frontend';
|
||||
|
||||
// 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('generateRoutesByFrontend', () => {
|
||||
it('should handle routes without children', async () => {
|
||||
const generatedRoutes = await generateRoutesByFrontend(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 generateRoutesByFrontend(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 generateRoutesByFrontend(
|
||||
routesWithMissingMeta as RouteRecordRaw[],
|
||||
['admin'],
|
||||
);
|
||||
expect(generatedRoutes).toEqual([
|
||||
{ path: '/path1' },
|
||||
{ meta: {}, path: '/path2' },
|
||||
{ meta: { authority: ['admin'] }, path: '/path3' },
|
||||
]);
|
||||
});
|
||||
});
|
58
packages/utils/src/helpers/generate-routes-frontend.ts
Normal file
58
packages/utils/src/helpers/generate-routes-frontend.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { filterTree, mapTree } from '@vben-core/toolkit';
|
||||
|
||||
/**
|
||||
* 动态生成路由 - 前端方式
|
||||
*/
|
||||
async function generateRoutesByFrontend(
|
||||
routes: RouteRecordRaw[],
|
||||
roles: string[],
|
||||
forbiddenComponent?: RouteRecordRaw['component'],
|
||||
): Promise<RouteRecordRaw[]> {
|
||||
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
||||
const finalRoutes = filterTree(routes, (route) => {
|
||||
return hasAuthority(route, roles);
|
||||
});
|
||||
|
||||
if (!forbiddenComponent) {
|
||||
return finalRoutes;
|
||||
}
|
||||
|
||||
// 如果有禁止访问的页面,将禁止访问的页面替换为403页面
|
||||
return mapTree(finalRoutes, (route) => {
|
||||
if (menuHasVisibleWithForbidden(route)) {
|
||||
route.component = forbiddenComponent;
|
||||
}
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否有权限访问
|
||||
* @param route
|
||||
* @param access
|
||||
*/
|
||||
function hasAuthority(route: RouteRecordRaw, access: string[]) {
|
||||
const authority = route.meta?.authority;
|
||||
if (!authority) {
|
||||
return true;
|
||||
}
|
||||
const canAccess = access.some((value) => authority.includes(value));
|
||||
|
||||
return canAccess || (!canAccess && menuHasVisibleWithForbidden(route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否在菜单中显示,但是访问会被重定向到403
|
||||
* @param route
|
||||
*/
|
||||
function menuHasVisibleWithForbidden(route: RouteRecordRaw) {
|
||||
return (
|
||||
!!route.meta?.authority &&
|
||||
Reflect.has(route.meta || {}, 'menuVisibleWithForbidden') &&
|
||||
!!route.meta?.menuVisibleWithForbidden
|
||||
);
|
||||
}
|
||||
|
||||
export { generateRoutesByFrontend, hasAuthority };
|
5
packages/utils/src/helpers/index.ts
Normal file
5
packages/utils/src/helpers/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './find-menu-by-path';
|
||||
export * from './generate-menus';
|
||||
export * from './generate-routes-backend';
|
||||
export * from './generate-routes-frontend';
|
||||
export * from './merge-route-modules';
|
68
packages/utils/src/helpers/merge-route-modules.test.ts
Normal file
68
packages/utils/src/helpers/merge-route-modules.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import type { RouteModuleType } from './merge-route-modules';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { mergeRouteModules } from './merge-route-modules';
|
||||
|
||||
describe('mergeRouteModules', () => {
|
||||
it('should merge route modules correctly', () => {
|
||||
const routeModules: Record<string, RouteModuleType> = {
|
||||
'./dynamic-routes/about.ts': {
|
||||
default: [
|
||||
{
|
||||
component: () => Promise.resolve({ template: '<div>About</div>' }),
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
},
|
||||
],
|
||||
},
|
||||
'./dynamic-routes/home.ts': {
|
||||
default: [
|
||||
{
|
||||
component: () => Promise.resolve({ template: '<div>Home</div>' }),
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const expectedRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: expect.any(Function),
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
},
|
||||
{
|
||||
component: expect.any(Function),
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedRoutes = mergeRouteModules(routeModules);
|
||||
expect(mergedRoutes).toEqual(expectedRoutes);
|
||||
});
|
||||
|
||||
it('should handle empty modules', () => {
|
||||
const routeModules: Record<string, RouteModuleType> = {};
|
||||
const expectedRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
const mergedRoutes = mergeRouteModules(routeModules);
|
||||
expect(mergedRoutes).toEqual(expectedRoutes);
|
||||
});
|
||||
|
||||
it('should handle modules with no default export', () => {
|
||||
const routeModules: Record<string, RouteModuleType> = {
|
||||
'./dynamic-routes/empty.ts': {
|
||||
default: [],
|
||||
},
|
||||
};
|
||||
const expectedRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
const mergedRoutes = mergeRouteModules(routeModules);
|
||||
expect(mergedRoutes).toEqual(expectedRoutes);
|
||||
});
|
||||
});
|
28
packages/utils/src/helpers/merge-route-modules.ts
Normal file
28
packages/utils/src/helpers/merge-route-modules.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
// 定义模块类型
|
||||
interface RouteModuleType {
|
||||
default: RouteRecordRaw[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并动态路由模块的默认导出
|
||||
* @param routeModules 动态导入的路由模块对象
|
||||
* @returns 合并后的路由配置数组
|
||||
*/
|
||||
function mergeRouteModules(
|
||||
routeModules: Record<string, unknown>,
|
||||
): RouteRecordRaw[] {
|
||||
const mergedRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
for (const routeModule of Object.values(routeModules)) {
|
||||
const moduleRoutes = (routeModule as RouteModuleType)?.default ?? [];
|
||||
mergedRoutes.push(...moduleRoutes);
|
||||
}
|
||||
|
||||
return mergedRoutes;
|
||||
}
|
||||
|
||||
export { mergeRouteModules };
|
||||
|
||||
export type { RouteModuleType };
|
@@ -1 +1,2 @@
|
||||
export * from './helpers';
|
||||
export * from '@vben-core/toolkit';
|
||||
|
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@vben-core/typings/vue-router"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user