chore: rename @vben-core -> @core
This commit is contained in:
132
packages/@core/forward/helpers/src/flatten-object.test.ts
Normal file
132
packages/@core/forward/helpers/src/flatten-object.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { flattenObject } from './flatten-object';
|
||||
|
||||
describe('flattenObject', () => {
|
||||
it('should flatten a nested object correctly', () => {
|
||||
const nestedObject = {
|
||||
language: 'en',
|
||||
notifications: {
|
||||
email: true,
|
||||
push: {
|
||||
sound: true,
|
||||
vibration: false,
|
||||
},
|
||||
},
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
language: 'en',
|
||||
notificationsEmail: true,
|
||||
notificationsPushSound: true,
|
||||
notificationsPushVibration: false,
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle empty objects', () => {
|
||||
const nestedObject = {};
|
||||
const expected = {};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle objects with primitive values', () => {
|
||||
const nestedObject = {
|
||||
active: true,
|
||||
age: 30,
|
||||
name: 'Alice',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
active: true,
|
||||
age: 30,
|
||||
name: 'Alice',
|
||||
};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle objects with null values', () => {
|
||||
const nestedObject = {
|
||||
user: {
|
||||
age: null,
|
||||
name: null,
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
userAge: null,
|
||||
userName: null,
|
||||
};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle nested empty objects', () => {
|
||||
const nestedObject = {
|
||||
a: {},
|
||||
b: { c: {} },
|
||||
};
|
||||
|
||||
const expected = {};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle arrays within objects', () => {
|
||||
const nestedObject = {
|
||||
hobbies: ['reading', 'gaming'],
|
||||
name: 'Alice',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
hobbies: ['reading', 'gaming'],
|
||||
name: 'Alice',
|
||||
};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
it('should flatten objects with nested arrays correctly', () => {
|
||||
const nestedObject = {
|
||||
person: {
|
||||
hobbies: ['reading', 'gaming'],
|
||||
name: 'Alice',
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
personHobbies: ['reading', 'gaming'],
|
||||
personName: 'Alice',
|
||||
};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle objects with undefined values', () => {
|
||||
const nestedObject = {
|
||||
user: {
|
||||
age: undefined,
|
||||
name: 'Alice',
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
userAge: undefined,
|
||||
userName: 'Alice',
|
||||
};
|
||||
|
||||
const result = flattenObject(nestedObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
82
packages/@core/forward/helpers/src/flatten-object.ts
Normal file
82
packages/@core/forward/helpers/src/flatten-object.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Flatten } from '@vben-core/typings';
|
||||
|
||||
import { capitalizeFirstLetter } from '@vben-core/toolkit';
|
||||
|
||||
/**
|
||||
* 将嵌套对象扁平化
|
||||
* @param obj - 需要扁平化的对象
|
||||
* @param parentKey - 父键名,用于递归时拼接键名
|
||||
* @param result - 存储结果的对象
|
||||
* @returns 扁平化后的对象
|
||||
*
|
||||
* 示例:
|
||||
* const nestedObj = {
|
||||
* user: {
|
||||
* name: 'Alice',
|
||||
* address: {
|
||||
* city: 'Wonderland',
|
||||
* zip: '12345'
|
||||
* }
|
||||
* },
|
||||
* items: [
|
||||
* { id: 1, name: 'Item 1' },
|
||||
* { id: 2, name: 'Item 2' }
|
||||
* ],
|
||||
* active: true
|
||||
* };
|
||||
* const flatObj = flattenObject(nestedObj);
|
||||
* console.log(flatObj);
|
||||
* 输出:
|
||||
* {
|
||||
* userName: 'Alice',
|
||||
* userAddressCity: 'Wonderland',
|
||||
* userAddressZip: '12345',
|
||||
* items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ],
|
||||
* active: true
|
||||
* }
|
||||
*/
|
||||
function flattenObject<T extends Record<string, any>>(
|
||||
obj: T,
|
||||
parentKey: string = '',
|
||||
result: Record<string, any> = {},
|
||||
): Flatten<T> {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const newKey = parentKey
|
||||
? `${parentKey}${capitalizeFirstLetter(key)}`
|
||||
: key;
|
||||
const value = obj[key];
|
||||
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
flattenObject(value, newKey, result);
|
||||
} else {
|
||||
result[newKey] = value;
|
||||
}
|
||||
});
|
||||
return result as Flatten<T>;
|
||||
}
|
||||
|
||||
export { flattenObject };
|
||||
|
||||
// 定义递归类型,用于推断扁平化后的对象类型
|
||||
// 限制递归深度的辅助类型
|
||||
// type FlattenDepth<
|
||||
// T,
|
||||
// Depth extends number,
|
||||
// CurrentDepth extends number[] = [],
|
||||
// > = {
|
||||
// [K in keyof T as CurrentDepth['length'] extends Depth
|
||||
// ? K
|
||||
// : T[K] extends object
|
||||
// ? `${CurrentDepth['length'] extends 0 ? Uncapitalize<K & string> : Capitalize<K & string>}${keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]> extends string ? Capitalize<keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>> : ''}`
|
||||
// : `${CurrentDepth['length'] extends 0 ? Uncapitalize<K & string> : Capitalize<K & string>}`]: CurrentDepth['length'] extends Depth
|
||||
// ? T[K]
|
||||
// : T[K] extends object
|
||||
// ? FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>[keyof FlattenDepth<
|
||||
// T[K],
|
||||
// Depth,
|
||||
// [...CurrentDepth, 1]
|
||||
// >]
|
||||
// : T[K];
|
||||
// };
|
||||
|
||||
// type Flatten<T, Depth extends number = 4> = FlattenDepth<T, Depth>;
|
230
packages/@core/forward/helpers/src/generator-menus.test.ts
Normal file
230
packages/@core/forward/helpers/src/generator-menus.test.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { generatorMenus } from './generator-menus'; // 替换为您的实际路径
|
||||
import {
|
||||
type RouteRecordRaw,
|
||||
type Router,
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
} from 'vue-router';
|
||||
|
||||
// Nested route setup to test child inclusion and hideChildrenInMenu functionality
|
||||
|
||||
describe('generatorMenus', () => {
|
||||
// 模拟路由数据
|
||||
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',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'about-icon',
|
||||
name: '关于',
|
||||
order: 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', order: 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',
|
||||
order: 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',
|
||||
order: 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,
|
||||
);
|
||||
expect(menus).toEqual([
|
||||
// Assuming your generatorMenus 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',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'path-icon',
|
||||
name: 'New Path',
|
||||
order: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/new-path',
|
||||
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 generatorMenus(routes, router);
|
||||
const expectedMenus = [
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: undefined,
|
||||
name: 'About',
|
||||
order: 1,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/about',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: undefined,
|
||||
name: 'Home',
|
||||
order: 2,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/',
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
expect(menus).toEqual(expectedMenus);
|
||||
});
|
||||
|
||||
it('should handle empty routes', async () => {
|
||||
const emptyRoutes: any[] = [];
|
||||
const menus = await generatorMenus(emptyRoutes, router);
|
||||
expect(menus).toEqual([]);
|
||||
});
|
||||
});
|
73
packages/@core/forward/helpers/src/generator-menus.ts
Normal file
73
packages/@core/forward/helpers/src/generator-menus.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
||||
import type { RouteRecordRaw, Router } from 'vue-router';
|
||||
|
||||
import { mapTree } from '@vben-core/toolkit';
|
||||
|
||||
/**
|
||||
* 根据 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]),
|
||||
);
|
||||
|
||||
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,
|
||||
children: resultChildren || [],
|
||||
};
|
||||
});
|
||||
|
||||
// 对菜单进行排序
|
||||
menus = menus.sort((a, b) => (a.order || 999) - (b.order || 999));
|
||||
return menus;
|
||||
}
|
||||
|
||||
export { generatorMenus };
|
128
packages/@core/forward/helpers/src/generator-routes.test.ts
Normal file
128
packages/@core/forward/helpers/src/generator-routes.test.ts
Normal file
@@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
63
packages/@core/forward/helpers/src/generator-routes.ts
Normal file
63
packages/@core/forward/helpers/src/generator-routes.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { filterTree, mapTree } from '@vben-core/toolkit';
|
||||
/**
|
||||
* 动态生成路由
|
||||
*/
|
||||
async function generatorRoutes(
|
||||
routes: RouteRecordRaw[],
|
||||
roles: string[],
|
||||
forbiddenPage?: RouteRecordRaw['component'],
|
||||
): Promise<RouteRecordRaw[]> {
|
||||
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
||||
const finalRoutes = filterTree(routes, (route) => {
|
||||
return hasVisible(route) && hasAuthority(route, roles);
|
||||
});
|
||||
|
||||
if (!forbiddenPage) {
|
||||
return finalRoutes;
|
||||
}
|
||||
|
||||
// 如果有禁止访问的页面,将禁止访问的页面替换为403页面
|
||||
return mapTree(finalRoutes, (route) => {
|
||||
if (menuHasVisibleWithForbidden(route)) {
|
||||
route.component = forbiddenPage;
|
||||
}
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否有权限访问
|
||||
* @param route
|
||||
* @param access
|
||||
*/
|
||||
function hasAuthority(route: RouteRecordRaw, access: string[]) {
|
||||
const authority = route.meta?.authority;
|
||||
|
||||
if (!authority) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
access.some((value) => authority.includes(value)) ||
|
||||
menuHasVisibleWithForbidden(route)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否需要在菜单中显示
|
||||
* @param route
|
||||
*/
|
||||
function hasVisible(route?: RouteRecordRaw) {
|
||||
return !route?.meta?.hideInMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否在菜单中显示,但是访问会被重定向到403
|
||||
* @param route
|
||||
*/
|
||||
function menuHasVisibleWithForbidden(route: RouteRecordRaw) {
|
||||
return !!route.meta?.menuVisibleWithForbidden;
|
||||
}
|
||||
|
||||
export { generatorRoutes, hasAuthority, hasVisible };
|
5
packages/@core/forward/helpers/src/index.ts
Normal file
5
packages/@core/forward/helpers/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './flatten-object';
|
||||
export * from './generator-menus';
|
||||
export * from './generator-routes';
|
||||
export * from './merge-route-modules';
|
||||
export * from './nested-object';
|
@@ -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/@core/forward/helpers/src/merge-route-modules.ts
Normal file
28
packages/@core/forward/helpers/src/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 };
|
115
packages/@core/forward/helpers/src/nested-object.test.ts
Normal file
115
packages/@core/forward/helpers/src/nested-object.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { nestedObject } from './nested-object';
|
||||
|
||||
describe('nestedObject', () => {
|
||||
it('should convert flat object to nested object with level 1', () => {
|
||||
const flatObject = {
|
||||
anotherKeyExample: 2,
|
||||
commonAppName: 1,
|
||||
someOtherKey: 3,
|
||||
};
|
||||
|
||||
const expectedNestedObject = {
|
||||
anotherKeyExample: 2,
|
||||
commonAppName: 1,
|
||||
someOtherKey: 3,
|
||||
};
|
||||
|
||||
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||
});
|
||||
|
||||
it('should convert flat object to nested object with level 2', () => {
|
||||
const flatObject = {
|
||||
appAnotherKeyExample: 2,
|
||||
appCommonName: 1,
|
||||
appSomeOtherKey: 3,
|
||||
};
|
||||
|
||||
const expectedNestedObject = {
|
||||
app: {
|
||||
anotherKeyExample: 2,
|
||||
commonName: 1,
|
||||
someOtherKey: 3,
|
||||
},
|
||||
};
|
||||
|
||||
expect(nestedObject(flatObject, 2)).toEqual(expectedNestedObject);
|
||||
});
|
||||
|
||||
it('should convert flat object to nested object with level 3', () => {
|
||||
const flatObject = {
|
||||
appAnotherKeyExampleValue: 2,
|
||||
appCommonNameKey: 1,
|
||||
appSomeOtherKeyItem: 3,
|
||||
};
|
||||
|
||||
const expectedNestedObject = {
|
||||
app: {
|
||||
another: {
|
||||
keyExampleValue: 2,
|
||||
},
|
||||
common: {
|
||||
nameKey: 1,
|
||||
},
|
||||
some: {
|
||||
otherKeyItem: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(nestedObject(flatObject, 3)).toEqual(expectedNestedObject);
|
||||
});
|
||||
|
||||
it('should handle empty object', () => {
|
||||
const flatObject = {};
|
||||
|
||||
const expectedNestedObject = {};
|
||||
|
||||
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||
});
|
||||
|
||||
it('should handle single key object', () => {
|
||||
const flatObject = {
|
||||
singleKey: 1,
|
||||
};
|
||||
|
||||
const expectedNestedObject = {
|
||||
singleKey: 1,
|
||||
};
|
||||
|
||||
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||
});
|
||||
|
||||
it('should handle complex keys', () => {
|
||||
const flatObject = {
|
||||
anotherComplexKeyWithParts: 2,
|
||||
complexKeyWithMultipleParts: 1,
|
||||
};
|
||||
|
||||
const expectedNestedObject = {
|
||||
anotherComplexKeyWithParts: 2,
|
||||
complexKeyWithMultipleParts: 1,
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
70
packages/@core/forward/helpers/src/nested-object.ts
Normal file
70
packages/@core/forward/helpers/src/nested-object.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { toLowerCaseFirstLetter } from '@vben-core/toolkit';
|
||||
|
||||
/**
|
||||
* 将扁平对象转换为嵌套对象。
|
||||
*
|
||||
* @template T - 输入对象值的类型
|
||||
* @param {Record<string, T>} obj - 要转换的扁平对象
|
||||
* @param {number} level - 嵌套的层级
|
||||
* @returns {T} 嵌套对象
|
||||
*
|
||||
* @example
|
||||
* 将扁平对象转换为嵌套对象,嵌套层级为 1
|
||||
* const flatObject = {
|
||||
* 'commonAppName': 1,
|
||||
* 'anotherKeyExample': 2,
|
||||
* 'someOtherKey': 3
|
||||
* };
|
||||
* const nestedObject = nestedObject(flatObject, 1);
|
||||
* console.log(nestedObject);
|
||||
* 输出:
|
||||
* {
|
||||
* commonAppName: 1,
|
||||
* anotherKeyExample: 2,
|
||||
* someOtherKey: 3
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* 将扁平对象转换为嵌套对象,嵌套层级为 2
|
||||
* const flatObject = {
|
||||
* 'appCommonName': 1,
|
||||
* 'appAnotherKeyExample': 2,
|
||||
* 'appSomeOtherKey': 3
|
||||
* };
|
||||
* const nestedObject = nestedObject(flatObject, 2);
|
||||
* console.log(nestedObject);
|
||||
* 输出:
|
||||
* {
|
||||
* app: {
|
||||
* commonName: 1,
|
||||
* anotherKeyExample: 2,
|
||||
* someOtherKey: 3
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
function nestedObject<T>(obj: Record<string, T>, level: number): T {
|
||||
const result: any = {};
|
||||
|
||||
for (const key in obj) {
|
||||
const keys = key.split(/(?=[A-Z])/);
|
||||
// 将驼峰式分割为数组;
|
||||
let current = result;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const lowerKey = keys[i].toLowerCase();
|
||||
if (i === level - 1) {
|
||||
const remainingKeys = keys.slice(i).join(''); // 保留后续部分作为键的一部分
|
||||
current[toLowerCaseFirstLetter(remainingKeys)] = obj[key];
|
||||
break;
|
||||
} else {
|
||||
current[lowerKey] = current[lowerKey] || {};
|
||||
current = current[lowerKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
export { nestedObject };
|
Reference in New Issue
Block a user