物业代码生成

This commit is contained in:
2025-06-18 11:03:42 +08:00
commit 1262d4c745
1881 changed files with 249599 additions and 0 deletions

View 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);
});
});

View 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();
});
});

View 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([]);
});
});

View 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' },
]);
});
});

View 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);
});
});

View 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;
}

View 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 };

View 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 };

View 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 };

View 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 };

View 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;
}

View 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';

View 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 };

View 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]>);
}
},
};
}

View 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;
}

View 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);
}
});
}

View 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;
}

View 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[];
}

View 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 },
); // 确保事件只触发一次
}
}

View 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)}`;
}

View 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';