物业代码生成
This commit is contained in:
19
packages/utils/src/helpers/__tests__/enum-options.test.ts
Normal file
19
packages/utils/src/helpers/__tests__/enum-options.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { optionsToEnum } from '../enum-options';
|
||||
|
||||
describe('optionsToEnum Test', () => {
|
||||
it('should return an enum object', () => {
|
||||
const genderOptions = [
|
||||
{ label: '男', value: 1, enumName: 'GENDER_MALE' },
|
||||
{ label: '女', value: 2, enumName: 'GENDER_FEMALE' },
|
||||
] as const;
|
||||
|
||||
const enumTest = optionsToEnum(genderOptions);
|
||||
const male = enumTest.GENDER_MALE;
|
||||
const female = enumTest.GENDER_FEMALE;
|
||||
|
||||
expect(male).toBe(1);
|
||||
expect(female).toBe(2);
|
||||
});
|
||||
});
|
@@ -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();
|
||||
});
|
||||
});
|
233
packages/utils/src/helpers/__tests__/generate-menus.test.ts
Normal file
233
packages/utils/src/helpers/__tests__/generate-menus.test.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import type { Router, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { generateMenus } from '../generate-menus';
|
||||
|
||||
// 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 = 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 = 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 = 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 = 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 = 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 = generateMenus(emptyRoutes, router);
|
||||
expect(menus).toEqual([]);
|
||||
});
|
||||
});
|
@@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
});
|
47
packages/utils/src/helpers/enum-options.ts
Normal file
47
packages/utils/src/helpers/enum-options.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @author dap
|
||||
* @description 枚举选项
|
||||
*/
|
||||
|
||||
/**
|
||||
* 定义options类型
|
||||
*/
|
||||
export interface EnumsOption {
|
||||
/**
|
||||
* 枚举名称 建议使用全大写字母_
|
||||
*/
|
||||
enumName: string;
|
||||
/**
|
||||
* option的标签
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* option的值
|
||||
*/
|
||||
value: boolean | number | string;
|
||||
}
|
||||
|
||||
export type EnumResult<T extends readonly EnumsOption[]> = {
|
||||
[key in T[number]['enumName']]: Extract<
|
||||
T[number],
|
||||
{ enumName: key }
|
||||
>['value'];
|
||||
};
|
||||
|
||||
/**
|
||||
* 将options转为枚举
|
||||
* 注意自定义的options需要加上as const作为常量处理
|
||||
* 详见: packages\utils\src\helpers\__tests__\enum-options.test.ts
|
||||
* @param options 枚举选项
|
||||
* @returns 转枚举
|
||||
*/
|
||||
export function optionsToEnum<T extends readonly EnumsOption[]>(
|
||||
options: T,
|
||||
): EnumResult<T> {
|
||||
type K = T[number]['enumName'];
|
||||
const result = {} as EnumResult<T>;
|
||||
options.forEach((item) => {
|
||||
result[item.enumName as K] = item.value;
|
||||
});
|
||||
return result;
|
||||
}
|
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, level = 0) {
|
||||
const findMenu = findMenuByPath(menus, path);
|
||||
const rootMenuPath = findMenu?.parents?.[level];
|
||||
const rootMenu = rootMenuPath
|
||||
? menus.find((item) => item.path === rootMenuPath)
|
||||
: undefined;
|
||||
return {
|
||||
findMenu,
|
||||
rootMenu,
|
||||
rootMenuPath,
|
||||
};
|
||||
}
|
||||
|
||||
export { findMenuByPath, findRootMenuByPath };
|
90
packages/utils/src/helpers/generate-menus.ts
Normal file
90
packages/utils/src/helpers/generate-menus.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Router, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import type {
|
||||
ExRouteRecordRaw,
|
||||
MenuRecordRaw,
|
||||
RouteMeta,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
import { filterTree, mapTree } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 根据 routes 生成菜单列表
|
||||
* @param routes - 路由配置列表
|
||||
* @param router - Vue Router 实例
|
||||
* @returns 生成的菜单列表
|
||||
*/
|
||||
function generateMenus(
|
||||
routes: RouteRecordRaw[],
|
||||
router: Router,
|
||||
): MenuRecordRaw[] {
|
||||
// 将路由列表转换为一个以 name 为键的对象映射
|
||||
const finalRoutesMap: { [key: string]: string } = Object.fromEntries(
|
||||
router.getRoutes().map(({ name, path }) => [name, path]),
|
||||
);
|
||||
|
||||
let menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
|
||||
// 获取最终的路由路径
|
||||
const path = finalRoutesMap[route.name as string] ?? route.path ?? '';
|
||||
|
||||
const {
|
||||
meta = {} as RouteMeta,
|
||||
name: routeName,
|
||||
redirect,
|
||||
children = [],
|
||||
} = route;
|
||||
const {
|
||||
activeIcon,
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
hideChildrenInMenu = false,
|
||||
icon,
|
||||
link,
|
||||
order,
|
||||
title = '',
|
||||
} = meta;
|
||||
|
||||
// 确保菜单名称不为空
|
||||
const name = (title || routeName || '') as string;
|
||||
|
||||
// 处理子菜单
|
||||
const resultChildren = hideChildrenInMenu
|
||||
? []
|
||||
: ((children as MenuRecordRaw[]) ?? []);
|
||||
|
||||
// 设置子菜单的父子关系
|
||||
if (resultChildren.length > 0) {
|
||||
resultChildren.forEach((child) => {
|
||||
child.parents = [...(route.parents ?? []), path];
|
||||
child.parent = path;
|
||||
});
|
||||
}
|
||||
|
||||
// 确定最终路径
|
||||
const resultPath = hideChildrenInMenu ? redirect || path : link || path;
|
||||
|
||||
return {
|
||||
activeIcon,
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
icon,
|
||||
name,
|
||||
order,
|
||||
parent: route.parent,
|
||||
parents: route.parents,
|
||||
path: resultPath,
|
||||
show: !meta.hideInMenu,
|
||||
children: resultChildren,
|
||||
};
|
||||
});
|
||||
|
||||
// 对菜单进行排序,避免order=0时被替换成999的问题
|
||||
menus = menus.sort((a, b) => (a?.order ?? 999) - (b?.order ?? 999));
|
||||
|
||||
// 过滤掉隐藏的菜单项
|
||||
return filterTree(menus, (menu) => !!menu.show);
|
||||
}
|
||||
|
||||
export { generateMenus };
|
89
packages/utils/src/helpers/generate-routes-backend.ts
Normal file
89
packages/utils/src/helpers/generate-routes-backend.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import type {
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
RouteRecordStringComponent,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
import { mapTree } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 动态生成路由 - 后端方式
|
||||
*/
|
||||
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);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const pageKey = normalizePath.endsWith('.vue')
|
||||
? normalizePath
|
||||
: `${normalizePath}.vue`;
|
||||
if (pageMap[pageKey]) {
|
||||
route.component = pageMap[pageKey];
|
||||
} else {
|
||||
// console.error(`route component is invalid: ${pageKey}`, route);
|
||||
// route.component = pageMap['/_core/fallback/not-found.vue'];
|
||||
console.error(`未找到对应组件: /views${component}.vue`);
|
||||
// 默认为404页面
|
||||
route.component = layoutMap.NotFoundComponent;
|
||||
}
|
||||
}
|
||||
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeViewPath(path: string): string {
|
||||
// 去除相对路径前缀
|
||||
const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, '');
|
||||
|
||||
// 确保路径以 '/' 开头
|
||||
const viewPath = normalizedPath.startsWith('/')
|
||||
? normalizedPath
|
||||
: `/${normalizedPath}`;
|
||||
|
||||
// 这里耦合了vben-admin的目录结构
|
||||
return viewPath.replace(/^\/views/, '');
|
||||
}
|
||||
export { generateRoutesByBackend };
|
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/shared/utils';
|
||||
|
||||
/**
|
||||
* 动态生成路由 - 前端方式
|
||||
*/
|
||||
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 };
|
66
packages/utils/src/helpers/get-popup-container.ts
Normal file
66
packages/utils/src/helpers/get-popup-container.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* If the node is holding inside a form, return the form element,
|
||||
* otherwise return the parent node of the given element or
|
||||
* the document body if the element is not provided.
|
||||
*/
|
||||
export function getPopupContainer(node?: HTMLElement): HTMLElement {
|
||||
return (
|
||||
node?.closest('form') ?? (node?.parentNode as HTMLElement) ?? document.body
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* VxeTable专用弹窗层
|
||||
* 解决问题: https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IB1DM3
|
||||
* @param node 触发的元素
|
||||
* @param tableId 表格ID,用于区分不同表格(可选)
|
||||
* @returns 挂载节点
|
||||
*/
|
||||
export function getVxePopupContainer(
|
||||
node?: HTMLElement,
|
||||
tableId?: string,
|
||||
): HTMLElement {
|
||||
if (!node) return document.body;
|
||||
|
||||
// 检查是否在固定列内
|
||||
const isInFixedColumn =
|
||||
node.closest('.vxe-table--fixed-wrapper') ||
|
||||
node.closest('.vxe-table--fixed-left-wrapper') ||
|
||||
node.closest('.vxe-table--fixed-right-wrapper');
|
||||
|
||||
// 如果在固定列内,则挂载到固定列容器
|
||||
if (isInFixedColumn) {
|
||||
// 优先查找表格容器及父级容器
|
||||
const tableContainer =
|
||||
// 查找通用固定列容器
|
||||
node.closest('.vxe-table--fixed-wrapper') ||
|
||||
// 查找固定列容器(左侧固定列)
|
||||
node.closest('.vxe-table--fixed-left-wrapper') ||
|
||||
// 查找固定列容器(右侧固定列)
|
||||
node.closest('.vxe-table--fixed-right-wrapper');
|
||||
|
||||
// 如果指定了tableId,可以查找特定ID的表格
|
||||
if (tableId && tableContainer) {
|
||||
const specificTable = tableContainer.closest(
|
||||
`[data-table-id="${tableId}"]`,
|
||||
);
|
||||
if (specificTable) {
|
||||
return specificTable as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
return tableContainer as HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置行高度需要特殊处理
|
||||
*/
|
||||
const fixedHeightElement = node.closest('td.col--cs-height');
|
||||
if (fixedHeightElement) {
|
||||
// 默认为hidden 显示异常
|
||||
(fixedHeightElement as HTMLTableCellElement).style.overflow = 'visible';
|
||||
}
|
||||
|
||||
// 兜底方案:使用元素的父节点或文档体
|
||||
return (node.parentNode as HTMLElement) || document.body;
|
||||
}
|
14
packages/utils/src/helpers/index.ts
Normal file
14
packages/utils/src/helpers/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export * from './enum-options';
|
||||
export * from './find-menu-by-path';
|
||||
export * from './generate-menus';
|
||||
export * from './generate-routes-backend';
|
||||
export * from './generate-routes-frontend';
|
||||
export * from './get-popup-container';
|
||||
export * from './merge-route-modules';
|
||||
export * from './mitt';
|
||||
export * from './request';
|
||||
export * from './reset-routes';
|
||||
export * from './safe';
|
||||
export * from './tree';
|
||||
export * from './unmount-global-loading';
|
||||
export * from './uuid';
|
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 };
|
135
packages/utils/src/helpers/mitt.ts
Normal file
135
packages/utils/src/helpers/mitt.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* copy to https://github.com/developit/mitt
|
||||
* Expand clear method
|
||||
*/
|
||||
export type EventType = string | symbol;
|
||||
|
||||
// An event handler can take an optional event argument
|
||||
// and should not return a value
|
||||
export type Handler<T = unknown> = (event: T) => void;
|
||||
export type WildcardHandler<T = Record<string, unknown>> = (
|
||||
type: keyof T,
|
||||
event: T[keyof T],
|
||||
) => void;
|
||||
|
||||
// An array of all currently registered event handlers for a type
|
||||
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
|
||||
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<
|
||||
WildcardHandler<T>
|
||||
>;
|
||||
|
||||
// A map of event types and their corresponding event handlers.
|
||||
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
|
||||
'*' | keyof Events,
|
||||
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
|
||||
>;
|
||||
|
||||
export interface Emitter<Events extends Record<EventType, unknown>> {
|
||||
all: EventHandlerMap<Events>;
|
||||
|
||||
clear(): void;
|
||||
emit<Key extends keyof Events>(
|
||||
type: undefined extends Events[Key] ? Key : never,
|
||||
): void;
|
||||
|
||||
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
|
||||
off(type: '*', handler: WildcardHandler<Events>): void;
|
||||
|
||||
off<Key extends keyof Events>(
|
||||
type: Key,
|
||||
handler?: Handler<Events[Key]>,
|
||||
): void;
|
||||
on(type: '*', handler: WildcardHandler<Events>): void;
|
||||
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mitt: Tiny (~200b) functional event emitter / pubsub.
|
||||
* @name mitt
|
||||
* @returns {Mitt} any
|
||||
*/
|
||||
export function mitt<Events extends Record<EventType, unknown>>(
|
||||
all?: EventHandlerMap<Events>,
|
||||
): Emitter<Events> {
|
||||
type GenericEventHandler =
|
||||
| Handler<Events[keyof Events]>
|
||||
| WildcardHandler<Events>;
|
||||
all = all || new Map();
|
||||
|
||||
return {
|
||||
/**
|
||||
* A Map of event names to registered handler functions.
|
||||
*/
|
||||
all,
|
||||
|
||||
/**
|
||||
* Clear all
|
||||
*/
|
||||
clear() {
|
||||
this.all.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke all handlers for the given type.
|
||||
* If present, `'*'` handlers are invoked after type-matched handlers.
|
||||
*
|
||||
* Note: Manually firing '*' handlers is not supported.
|
||||
*
|
||||
* @param {string|symbol} type The event type to invoke
|
||||
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
|
||||
* @memberOf mitt
|
||||
*/
|
||||
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
|
||||
let handlers = all?.get(type);
|
||||
if (handlers) {
|
||||
[...(handlers as EventHandlerList<Events[keyof Events]>)].forEach(
|
||||
(handler) => {
|
||||
handler(evt as Events[Key]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
handlers = all?.get('*');
|
||||
if (handlers) {
|
||||
[...(handlers as WildCardEventHandlerList<Events>)].forEach(
|
||||
(handler) => {
|
||||
handler(type, evt as Events[Key]);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an event handler for the given type.
|
||||
* If `handler` is omitted, all handlers of the given type are removed.
|
||||
* @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
|
||||
* @param {Function} [handler] Handler function to remove
|
||||
* @memberOf mitt
|
||||
*/
|
||||
off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
|
||||
const handlers: Array<GenericEventHandler> | undefined = all?.get(type);
|
||||
if (handlers) {
|
||||
if (handler) {
|
||||
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
|
||||
} else {
|
||||
all?.set(type, []);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Register an event handler for the given type.
|
||||
* @param {string|symbol} type Type of event to listen for, or `'*'` for all events
|
||||
* @param {Function} handler Function to call in response to given event
|
||||
* @memberOf mitt
|
||||
*/
|
||||
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
|
||||
const handlers: Array<GenericEventHandler> | undefined = all?.get(type);
|
||||
if (handlers) {
|
||||
handlers.push(handler);
|
||||
} else {
|
||||
all?.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
24
packages/utils/src/helpers/request.ts
Normal file
24
packages/utils/src/helpers/request.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 一些发送请求 需要用到的工具
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add the object as a parameter to the URL
|
||||
* @param baseUrl url
|
||||
* @param obj
|
||||
* @returns {string}
|
||||
* eg:
|
||||
* let obj = {a: '3', b: '4'}
|
||||
* setObjToUrlParams('www.baidu.com', obj)
|
||||
* ==>www.baidu.com?a=3&b=4
|
||||
*/
|
||||
export function setObjToUrlParams(baseUrl: string, obj: any): string {
|
||||
let parameters = '';
|
||||
for (const key in obj) {
|
||||
parameters += `${key}=${encodeURIComponent(obj[key])}&`;
|
||||
}
|
||||
parameters = parameters.replace(/&$/, '');
|
||||
return /\?$/.test(baseUrl)
|
||||
? baseUrl + parameters
|
||||
: baseUrl.replace(/\/?$/, '?') + parameters;
|
||||
}
|
31
packages/utils/src/helpers/reset-routes.ts
Normal file
31
packages/utils/src/helpers/reset-routes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Router, RouteRecordName, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { traverseTreeValues } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* @zh_CN 重置所有路由,如有指定白名单除外
|
||||
*/
|
||||
export function resetStaticRoutes(router: Router, routes: RouteRecordRaw[]) {
|
||||
// 获取静态路由所有节点包含子节点的 name,并排除不存在 name 字段的路由
|
||||
const staticRouteNames = traverseTreeValues<
|
||||
RouteRecordRaw,
|
||||
RouteRecordName | undefined
|
||||
>(routes, (route) => {
|
||||
// 这些路由需要指定 name,防止在路由重置时,不能删除没有指定 name 的路由
|
||||
if (!route.name) {
|
||||
console.warn(
|
||||
`The route with the path ${route.path} needs to have the field name specified.`,
|
||||
);
|
||||
}
|
||||
return route.name;
|
||||
});
|
||||
|
||||
const { getRoutes, hasRoute, removeRoute } = router;
|
||||
const allRoutes = getRoutes();
|
||||
allRoutes.forEach(({ name }) => {
|
||||
// 存在于路由表且非白名单才需要删除
|
||||
if (name && !staticRouteNames.includes(name) && hasRoute(name)) {
|
||||
removeRoute(name);
|
||||
}
|
||||
});
|
||||
}
|
10
packages/utils/src/helpers/safe.ts
Normal file
10
packages/utils/src/helpers/safe.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 跟后台逻辑一致
|
||||
* Number.isSafeInteger形参只能为Number类型 其他的直接返回false
|
||||
* @param str 数字
|
||||
* @returns 安全数内返回number类型 否则返回原字符串
|
||||
*/
|
||||
export function safeParseNumber(str: string): number | string {
|
||||
const num = Number(str);
|
||||
return Number.isSafeInteger(num) ? num : str;
|
||||
}
|
400
packages/utils/src/helpers/tree.ts
Normal file
400
packages/utils/src/helpers/tree.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
interface TreeHelperConfig {
|
||||
children: string;
|
||||
id: string;
|
||||
pid: string;
|
||||
}
|
||||
|
||||
type Fn = (node: any, parentNode?: any) => any;
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: TreeHelperConfig = {
|
||||
id: 'id',
|
||||
pid: 'parentId',
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
// 获取配置。 Object.assign 从一个或多个源对象复制到目标对象
|
||||
const getConfig = (config: Partial<TreeHelperConfig>) =>
|
||||
Object.assign({}, DEFAULT_CONFIG, config);
|
||||
|
||||
// tree from list
|
||||
// 列表中的树
|
||||
export function listToTree<T = any>(
|
||||
list: any[],
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T[] {
|
||||
const conf = getConfig(config) as TreeHelperConfig;
|
||||
const nodeMap = new Map();
|
||||
const result: T[] = [];
|
||||
const { id, pid, children } = conf;
|
||||
|
||||
for (const node of list) {
|
||||
node[children] = node[children] || [];
|
||||
nodeMap.set(node[id], node);
|
||||
}
|
||||
for (const node of list) {
|
||||
const parent = nodeMap.get(node[pid]);
|
||||
(parent ? parent[children] : result).push(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function treeToList<T = any>(
|
||||
tree: any,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T {
|
||||
config = getConfig(config);
|
||||
const { children } = config;
|
||||
const result: any = [...tree];
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (!result[i][children!]) continue;
|
||||
result.splice(i + 1, 0, ...result[i][children!]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findNode<T = any>(
|
||||
tree: any,
|
||||
func: Fn,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): null | T {
|
||||
config = getConfig(config);
|
||||
const { children } = config;
|
||||
const list = [...tree];
|
||||
for (const node of list) {
|
||||
if (func(node)) return node;
|
||||
node[children!] && list.push(...node[children!]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findNodeAll<T = any>(
|
||||
tree: any,
|
||||
func: Fn,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T[] {
|
||||
config = getConfig(config);
|
||||
const { children } = config;
|
||||
const list = [...tree];
|
||||
const result: T[] = [];
|
||||
for (const node of list) {
|
||||
func(node) && result.push(node);
|
||||
node[children!] && list.push(...node[children!]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findPath<T = any>(
|
||||
tree: any,
|
||||
func: Fn,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): null | T | T[] {
|
||||
config = getConfig(config);
|
||||
const path: T[] = [];
|
||||
const list = [...tree];
|
||||
const visitedSet = new Set();
|
||||
const { children } = config;
|
||||
while (list.length > 0) {
|
||||
const node = list[0];
|
||||
if (visitedSet.has(node)) {
|
||||
path.pop();
|
||||
list.shift();
|
||||
} else {
|
||||
visitedSet.add(node);
|
||||
node[children!] && list.unshift(...node[children!]);
|
||||
path.push(node);
|
||||
if (func(node)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findPathAll(
|
||||
tree: any,
|
||||
func: Fn,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
) {
|
||||
config = getConfig(config);
|
||||
const path: any[] = [];
|
||||
const list = [...tree];
|
||||
const result: any[] = [];
|
||||
const { children } = config;
|
||||
const visitedSet = new Set();
|
||||
while (list.length > 0) {
|
||||
const node = list[0];
|
||||
if (visitedSet.has(node)) {
|
||||
path.pop();
|
||||
list.shift();
|
||||
} else {
|
||||
visitedSet.add(node);
|
||||
node[children!] && list.unshift(...node[children!]);
|
||||
path.push(node);
|
||||
func(node) && result.push([...path]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function filter<T = any>(
|
||||
tree: T[],
|
||||
func: (n: T) => boolean,
|
||||
// Partial 将 T 中的所有属性设为可选
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T[] {
|
||||
// 获取配置
|
||||
config = getConfig(config);
|
||||
const children = config.children as string;
|
||||
|
||||
function listFilter(list: T[]) {
|
||||
return list
|
||||
.map((node: any) => ({ ...node }))
|
||||
.filter((node) => {
|
||||
// 递归调用 对含有children项 进行再次调用自身函数 listFilter
|
||||
node[children] = node[children] && listFilter(node[children]);
|
||||
// 执行传入的回调 func 进行过滤
|
||||
return func(node) || (node[children] && node[children].length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
return listFilter(tree);
|
||||
}
|
||||
|
||||
export function forEach<T = any>(
|
||||
tree: T[],
|
||||
func: (n: T) => any,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): void {
|
||||
config = getConfig(config);
|
||||
const list: any[] = [...tree];
|
||||
const { children } = config;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
// func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿
|
||||
if (func(list[i])) {
|
||||
return;
|
||||
}
|
||||
children &&
|
||||
list[i][children] &&
|
||||
list.splice(i + 1, 0, ...list[i][children]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Extract tree specified structure
|
||||
* @description: 提取树指定结构
|
||||
*/
|
||||
export function treeMap<T = any>(
|
||||
treeData: T[],
|
||||
opt: { children?: string; conversion: Fn },
|
||||
): T[] {
|
||||
return treeData.map((item) => treeMapEach(item, opt));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Extract tree specified structure
|
||||
* @description: 提取树指定结构
|
||||
*/
|
||||
export function treeMapEach(
|
||||
data: any,
|
||||
{ conversion, children = 'children' }: { children?: string; conversion: Fn },
|
||||
) {
|
||||
const haveChildren =
|
||||
Array.isArray(data[children]) && data[children].length > 0;
|
||||
const conversionData = conversion(data) || {};
|
||||
return haveChildren
|
||||
? {
|
||||
...conversionData,
|
||||
[children]: data[children].map((i: number) =>
|
||||
treeMapEach(i, {
|
||||
children,
|
||||
conversion,
|
||||
}),
|
||||
),
|
||||
}
|
||||
: {
|
||||
...conversionData,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历树结构
|
||||
* @param treeDatas 树
|
||||
* @param callBack 回调
|
||||
* @param parentNode 父节点
|
||||
*/
|
||||
export function eachTree(treeDatas: any[], callBack: Fn, parentNode = {}) {
|
||||
treeDatas.forEach((element) => {
|
||||
const newNode = callBack(element, parentNode) || element;
|
||||
if (element.children) {
|
||||
eachTree(element.children, callBack, newNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果节点的children为空, 则删除children属性
|
||||
export function removeEmptyChildren(data: any[], childrenField = 'children') {
|
||||
data.forEach((item) => {
|
||||
if (!item[childrenField]) {
|
||||
return;
|
||||
}
|
||||
if (item[childrenField].length > 0) {
|
||||
removeEmptyChildren(item[childrenField]);
|
||||
} else {
|
||||
Reflect.deleteProperty(item, childrenField);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line jsdoc/require-returns-check
|
||||
/**
|
||||
*
|
||||
* 添加全名 如 祖先节点-父节点-子节点
|
||||
* @param treeData 已经是tree数据
|
||||
* @param labelName 标签的字段名称
|
||||
* @param splitStr 分隔符
|
||||
* @returns void 无返回值 会修改原始数据
|
||||
*/
|
||||
export function addFullName(
|
||||
treeData: any[],
|
||||
labelName = 'label',
|
||||
splitStr = '-',
|
||||
) {
|
||||
function addFullNameProperty(node: any, parentNames: any[] = []) {
|
||||
const fullNameParts = [...parentNames, node[labelName]];
|
||||
node.fullName = fullNameParts.join(splitStr);
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach((childNode: any) => {
|
||||
addFullNameProperty(childNode, fullNameParts);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
treeData.forEach((item: any) => {
|
||||
addFullNameProperty(item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* https://blog.csdn.net/Web_J/article/details/129281329
|
||||
* 给出节点nodeId 找到所有父节点ID
|
||||
* @param treeList 树形结构list
|
||||
* @param nodeId 要寻找的节点ID
|
||||
* @param config config
|
||||
* @returns 父节点ID数组
|
||||
*/
|
||||
export function findParentsIds(
|
||||
treeList: any[],
|
||||
nodeId: number,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
) {
|
||||
const conf = getConfig(config) as TreeHelperConfig;
|
||||
const { id, children } = conf;
|
||||
|
||||
// 用于存储所有父节点ID的数组
|
||||
const parentIds: number[] = [];
|
||||
|
||||
function traverse(node: any, nodeId: number) {
|
||||
if (node[id] === nodeId) {
|
||||
return true;
|
||||
}
|
||||
if (node[children]) {
|
||||
// 如果当前节点有子节点,则继续遍历子节点
|
||||
for (const childNode of node[children]) {
|
||||
if (traverse(childNode, nodeId)) {
|
||||
// 如果在子节点中找到了子节点的父节点,则将当前节点的ID添加到父节点ID数组中,并返回true表示已经找到了子节点
|
||||
parentIds.push(node[id]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const node of treeList) {
|
||||
if (traverse(node, nodeId)) {
|
||||
// 如果在当前节点的子树中找到了子节点的父节点,则直接退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parentIds.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 给出节点数组 找到所有父节点ID
|
||||
* @param treeList 树形结构list
|
||||
* @param nodeIds 要寻找的节点ID list
|
||||
* @param config config
|
||||
* @returns 父节点ID数组
|
||||
*/
|
||||
export function findGroupParentIds(
|
||||
treeList: any[],
|
||||
nodeIds: number[],
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
) {
|
||||
// 用于存储所有父节点ID的Set 主要为了去重
|
||||
const parentIds = new Set<number>();
|
||||
|
||||
nodeIds.forEach((nodeId) => {
|
||||
findParentsIds(treeList, nodeId, config).forEach((parentId) => {
|
||||
parentIds.add(parentId);
|
||||
});
|
||||
});
|
||||
|
||||
return [...parentIds].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到所有ID 返回数组
|
||||
* @param treeList list
|
||||
* @param config
|
||||
* @returns ID数组
|
||||
*/
|
||||
export function findAllIds(
|
||||
treeList: any[],
|
||||
config: Partial<TreeHelperConfig> = DEFAULT_CONFIG,
|
||||
) {
|
||||
const conf = getConfig(config) as TreeHelperConfig;
|
||||
const { id, children } = conf;
|
||||
const ids: number[] = [];
|
||||
|
||||
treeList.forEach((item) => {
|
||||
if (item[children]) {
|
||||
const tempIds = findAllIds(item[children], config);
|
||||
ids.push(...tempIds);
|
||||
}
|
||||
ids.push(item[id]);
|
||||
});
|
||||
|
||||
return [...ids].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 这里抄的filterByLevel函数
|
||||
* @description 主要用于获取指定层级的节点数组
|
||||
*/
|
||||
export function findIdsByLevel(
|
||||
level = 1,
|
||||
list?: any[],
|
||||
config: Partial<TreeHelperConfig> = DEFAULT_CONFIG,
|
||||
currentLevel = 1,
|
||||
) {
|
||||
if (!level) {
|
||||
return [];
|
||||
}
|
||||
const res: (number | string)[] = [];
|
||||
const data = list || [];
|
||||
for (const item of data) {
|
||||
const { id: keyField, children: childrenField } = config;
|
||||
const key = keyField ? item[keyField] : '';
|
||||
const children = childrenField ? item[childrenField] : [];
|
||||
res.push(key);
|
||||
if (children && children.length > 0 && currentLevel < level) {
|
||||
currentLevel += 1;
|
||||
res.push(...findIdsByLevel(level, children, config, currentLevel));
|
||||
}
|
||||
}
|
||||
return res as number[] | string[];
|
||||
}
|
31
packages/utils/src/helpers/unmount-global-loading.ts
Normal file
31
packages/utils/src/helpers/unmount-global-loading.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 移除并销毁loading
|
||||
* 放在这里是而不是放在 index.html 的app标签内,是因为这样比较不会生硬,渲染过快可能会有闪烁
|
||||
* 通过先添加css动画隐藏,在动画结束后在移除loading节点来改善体验
|
||||
* 不好的地方是会增加一些代码量
|
||||
* 自定义loading可以见:https://doc.vben.pro/guide/in-depth/loading.html
|
||||
*/
|
||||
export function unmountGlobalLoading() {
|
||||
// 查找全局 loading 元素
|
||||
const loadingElement = document.querySelector('#__app-loading__');
|
||||
|
||||
if (loadingElement) {
|
||||
// 添加隐藏类,触发过渡动画
|
||||
loadingElement.classList.add('hidden');
|
||||
|
||||
// 查找所有需要移除的注入 loading 元素
|
||||
const injectLoadingElements = document.querySelectorAll(
|
||||
'[data-app-loading^="inject"]',
|
||||
);
|
||||
|
||||
// 当过渡动画结束时,移除 loading 元素和所有注入的 loading 元素
|
||||
loadingElement.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
loadingElement.remove(); // 移除 loading 元素
|
||||
injectLoadingElements.forEach((el) => el.remove()); // 移除所有注入的 loading 元素
|
||||
},
|
||||
{ once: true },
|
||||
); // 确保事件只触发一次
|
||||
}
|
||||
}
|
42
packages/utils/src/helpers/uuid.ts
Normal file
42
packages/utils/src/helpers/uuid.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
const hexList: string[] = [];
|
||||
for (let i = 0; i <= 15; i++) {
|
||||
hexList[i] = i.toString(16);
|
||||
}
|
||||
|
||||
export function buildUUID(): string {
|
||||
let uuid = '';
|
||||
for (let i = 1; i <= 36; i++) {
|
||||
switch (i) {
|
||||
case 9:
|
||||
case 14:
|
||||
case 19:
|
||||
case 24: {
|
||||
uuid += '-';
|
||||
|
||||
break;
|
||||
}
|
||||
case 15: {
|
||||
uuid += 4;
|
||||
|
||||
break;
|
||||
}
|
||||
case 20: {
|
||||
uuid += hexList[(Math.random() * 4) | 8];
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
uuid += hexList[Math.trunc(Math.random() * 16)];
|
||||
}
|
||||
}
|
||||
}
|
||||
return uuid.replaceAll('-', '');
|
||||
}
|
||||
|
||||
let unique = 0;
|
||||
export function buildShortUUID(prefix = ''): string {
|
||||
const time = Date.now();
|
||||
const random = Math.floor(Math.random() * 1_000_000_000);
|
||||
unique++;
|
||||
return `${prefix}_${random}${unique}${String(time)}`;
|
||||
}
|
5
packages/utils/src/index.ts
Normal file
5
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './helpers';
|
||||
export * from '@vben-core/shared/cache';
|
||||
export * from '@vben-core/shared/color';
|
||||
export * from '@vben-core/shared/utils';
|
||||
export { fileTypeFromBlob } from 'file-type';
|
Reference in New Issue
Block a user