chore: rename @vben-core -> @core

This commit is contained in:
vben
2024-06-08 21:27:43 +08:00
parent 1d6b1f4926
commit dcc1fcd528
388 changed files with 63 additions and 113 deletions

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,46 @@
{
"name": "@vben-core/helpers",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/@vben-core/forward/helpers"
},
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"scripts": {
"build": "pnpm unbuild",
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"sideEffects": false,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"imports": {
"#*": "./src/*"
},
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"vue-router": "^4.3.2"
}
}

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"types": ["@vben-core/typings/vue-router"]
},
"include": ["src"],
"exclude": ["node_modules"]
}