71 changed files with 1028 additions and 504 deletions
@ -0,0 +1,6 @@ |
|||||
|
@port = 5320 |
||||
|
@type = application/json |
||||
|
@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwicm9sZXMiOlsiYWRtaW4iXSwidXNlcm5hbWUiOiJ2YmVuIiwiaWF0IjoxNzE5ODkwMTEwLCJleHAiOjE3MTk5NzY1MTB9.eyAFsQ2Jk_mAQGvrEL1jF9O6YmLZ_PSYj5aokL6fCuU |
||||
|
GET http://localhost:{{port}}/api/menu/getAll HTTP/1.1 |
||||
|
content-type: {{ type }} |
||||
|
Authorization: {{ token }} |
||||
@ -0,0 +1,9 @@ |
|||||
|
class CreateUserDto { |
||||
|
id: number; |
||||
|
password: string; |
||||
|
realName: string; |
||||
|
roles: string[]; |
||||
|
username: string; |
||||
|
} |
||||
|
|
||||
|
export { CreateUserDto }; |
||||
@ -0,0 +1,62 @@ |
|||||
|
import { sleep } from '@/utils'; |
||||
|
import { Controller, Get, HttpCode, HttpStatus, Request } from '@nestjs/common'; |
||||
|
|
||||
|
@Controller('menu') |
||||
|
export class MenuController { |
||||
|
/** |
||||
|
* 获取用户所有菜单 |
||||
|
*/ |
||||
|
@Get('getAll') |
||||
|
@HttpCode(HttpStatus.OK) |
||||
|
async getAll(@Request() req: Request) { |
||||
|
// 模拟请求延迟
|
||||
|
await sleep(1000); |
||||
|
// 请求用户的id
|
||||
|
const userId = req.user.id; |
||||
|
|
||||
|
// TODO: 改为表方式获取
|
||||
|
const dashboardMenus = [ |
||||
|
{ |
||||
|
component: 'BasicLayout', |
||||
|
meta: { |
||||
|
order: -1, |
||||
|
title: 'page.dashboard.title', |
||||
|
}, |
||||
|
name: 'Dashboard', |
||||
|
path: '/', |
||||
|
redirect: '/analytics', |
||||
|
children: [ |
||||
|
{ |
||||
|
name: 'Analytics', |
||||
|
path: '/analytics', |
||||
|
component: '/dashboard/analytics/index', |
||||
|
meta: { |
||||
|
affixTab: true, |
||||
|
title: 'page.dashboard.analytics', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Workspace', |
||||
|
path: '/workspace', |
||||
|
component: '/dashboard/workspace/index', |
||||
|
meta: { |
||||
|
title: 'page.dashboard.workspace', |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
const MOCK_MENUS = [ |
||||
|
{ |
||||
|
menus: [...dashboardMenus], |
||||
|
userId: 0, |
||||
|
}, |
||||
|
{ |
||||
|
menus: [...dashboardMenus], |
||||
|
userId: 1, |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
return MOCK_MENUS.find((item) => item.userId === userId)?.menus ?? []; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { MenuController } from './menu.controller'; |
||||
|
import { MenuService } from './menu.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [MenuController], |
||||
|
providers: [MenuService], |
||||
|
}) |
||||
|
export class MenuModule {} |
||||
@ -0,0 +1,4 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class MenuService {} |
||||
@ -0,0 +1,5 @@ |
|||||
|
function sleep(ms: number) { |
||||
|
return new Promise((resolve) => setTimeout(resolve, ms)); |
||||
|
} |
||||
|
|
||||
|
export { sleep }; |
||||
|
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 5.3 KiB |
@ -1 +1,2 @@ |
|||||
|
export * from './menu'; |
||||
export * from './user'; |
export * from './user'; |
||||
|
|||||
@ -0,0 +1,12 @@ |
|||||
|
import type { RouteRecordStringComponent } from '@vben/types'; |
||||
|
|
||||
|
import { requestClient } from '#/forward'; |
||||
|
|
||||
|
/** |
||||
|
* 获取用户所有菜单 |
||||
|
*/ |
||||
|
async function getAllMenus() { |
||||
|
return requestClient.get<RouteRecordStringComponent[]>('/menu/getAll'); |
||||
|
} |
||||
|
|
||||
|
export { getAllMenus }; |
||||
@ -0,0 +1,40 @@ |
|||||
|
import type { GeneratorMenuAndRoutesOptions } from '@vben/access'; |
||||
|
import type { ComponentRecordType } from '@vben/types'; |
||||
|
|
||||
|
import { generateMenusAndRoutes } from '@vben/access'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
import { preferences } from '@vben-core/preferences'; |
||||
|
|
||||
|
import { message } from 'ant-design-vue'; |
||||
|
|
||||
|
import { getAllMenus } from '#/apis'; |
||||
|
import { BasicLayout, IFrameView } from '#/layouts'; |
||||
|
|
||||
|
const forbiddenPage = () => import('#/views/_essential/fallback/forbidden.vue'); |
||||
|
|
||||
|
async function generateAccess(options: GeneratorMenuAndRoutesOptions) { |
||||
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); |
||||
|
|
||||
|
const layoutMap: ComponentRecordType = { |
||||
|
BasicLayout, |
||||
|
IFrameView, |
||||
|
}; |
||||
|
|
||||
|
return await generateMenusAndRoutes(preferences.app.accessMode, { |
||||
|
...options, |
||||
|
fetchMenuListAsync: async () => { |
||||
|
message.loading({ |
||||
|
content: `${$t('common.loading-menu')}...`, |
||||
|
duration: 1.5, |
||||
|
}); |
||||
|
return await getAllMenus(); |
||||
|
}, |
||||
|
// 可以指定没有权限跳转403页面
|
||||
|
forbiddenComponent: forbiddenPage, |
||||
|
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
|
layoutMap, |
||||
|
pageMap, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export { generateAccess }; |
||||
@ -0,0 +1,9 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Fallback } from '@vben/universal-ui'; |
||||
|
|
||||
|
defineOptions({ name: 'AccessBackendButtonControl' }); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Fallback status="comming-soon" /> |
||||
|
</template> |
||||
@ -0,0 +1,9 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Fallback } from '@vben/universal-ui'; |
||||
|
|
||||
|
defineOptions({ name: 'AccessFrontend' }); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Fallback status="comming-soon" /> |
||||
|
</template> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Fallback } from '@vben/universal-ui'; |
||||
|
|
||||
|
defineOptions({ name: 'AccessFrontendAccessTest1' }); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Fallback |
||||
|
description="当前页面仅 Admin 角色可见" |
||||
|
status="comming-soon" |
||||
|
title="页面访问测试" |
||||
|
/> |
||||
|
</template> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Fallback } from '@vben/universal-ui'; |
||||
|
|
||||
|
defineOptions({ name: 'AccessFrontendAccessTest2' }); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Fallback |
||||
|
description="当前页面仅 User 角色可见" |
||||
|
status="comming-soon" |
||||
|
title="页面访问测试" |
||||
|
/> |
||||
|
</template> |
||||
@ -0,0 +1,9 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Fallback } from '@vben/universal-ui'; |
||||
|
|
||||
|
defineOptions({ name: 'AccessFrontendButtonControl' }); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Fallback status="comming-soon" /> |
||||
|
</template> |
||||
@ -0,0 +1,45 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useAccess } from '@vben/access'; |
||||
|
import { useAccessStore } from '@vben-core/stores'; |
||||
|
|
||||
|
import { Button } from 'ant-design-vue'; |
||||
|
|
||||
|
defineOptions({ name: 'AccessBackend' }); |
||||
|
|
||||
|
const { currentAccessMode } = useAccess(); |
||||
|
const accessStore = useAccessStore(); |
||||
|
|
||||
|
function roleButtonType(role: string) { |
||||
|
return accessStore.getUserRoles.includes(role) ? 'primary' : 'default'; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="p-5"> |
||||
|
<div class="card-box p-5"> |
||||
|
<h1 class="text-xl font-semibold">前端页面访问演示</h1> |
||||
|
<div class="text-foreground/80 mt-2"> |
||||
|
由于刷新的时候会请求用户信息接口,会根据接口重置角色信息,所以刷新后界面会恢复原样。如果不需要,可以注释对应的代码。 |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<template v-if="currentAccessMode === 'frontend'"> |
||||
|
<div class="card-box mt-5 p-5 font-semibold"> |
||||
|
当前权限模式: |
||||
|
<span class="text-primary mx-4">{{ currentAccessMode }}</span> |
||||
|
<Button type="primary">切换权限模式</Button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-box mt-5 p-5 font-semibold"> |
||||
|
当前用户角色: |
||||
|
<span class="text-primary mx-4">{{ accessStore.getUserRoles }}</span> |
||||
|
<Button :type="roleButtonType('admin')"> 切换为 Admin 角色 </Button> |
||||
|
<Button :type="roleButtonType('user')" class="mx-4"> |
||||
|
切换为 User 角色 |
||||
|
</Button> |
||||
|
|
||||
|
<div class="text-foreground/80 mt-2">角色后请查看左侧菜单变化</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
</template> |
||||
@ -1,6 +1,4 @@ |
|||||
export * from './find-menu-by-path'; |
export * from './find-menu-by-path'; |
||||
export * from './flatten-object'; |
export * from './flatten-object'; |
||||
export * from './generator-menus'; |
|
||||
export * from './generator-routes'; |
|
||||
export * from './merge-route-modules'; |
export * from './merge-route-modules'; |
||||
export * from './nested-object'; |
export * from './nested-object'; |
||||
|
|||||
@ -1,2 +1,2 @@ |
|||||
export * from './access'; |
export * from './access'; |
||||
export * from './tabs'; |
export * from './tabbar'; |
||||
|
|||||
@ -1,22 +0,0 @@ |
|||||
import { describe, expect, it } from 'vitest'; |
|
||||
|
|
||||
import { generateUUID } from './hash'; |
|
||||
|
|
||||
describe('generateUUID', () => { |
|
||||
it('should return a string', () => { |
|
||||
const uuid = generateUUID(); |
|
||||
expect(typeof uuid).toBe('string'); |
|
||||
}); |
|
||||
|
|
||||
it('should be length 32', () => { |
|
||||
const uuid = generateUUID(); |
|
||||
expect(uuid.length).toBe(36); |
|
||||
}); |
|
||||
|
|
||||
it('should have the correct format', () => { |
|
||||
const uuid = generateUUID(); |
|
||||
const uuidRegex = |
|
||||
/^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i; |
|
||||
expect(uuidRegex.test(uuid)).toBe(true); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,31 +0,0 @@ |
|||||
/** |
|
||||
* 生成一个UUID(通用唯一标识符)。 |
|
||||
* |
|
||||
* UUID是一种用于软件构建的标识符,其目的是能够生成一个唯一的ID,以便在全局范围内标识信息。 |
|
||||
* 此函数用于生成一个符合version 4的UUID,这种UUID是随机生成的。 |
|
||||
* |
|
||||
* 生成的UUID的格式为:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx |
|
||||
* 其中,x是任意16进制数字,y是一个16进制数字,取值范围为[8, b]。 |
|
||||
* |
|
||||
* @returns {string} 生成的UUID。 |
|
||||
*/ |
|
||||
function generateUUID(): string { |
|
||||
let d = Date.now(); |
|
||||
if ( |
|
||||
typeof performance !== 'undefined' && |
|
||||
typeof performance.now === 'function' |
|
||||
) { |
|
||||
d += performance.now(); // use high-precision timer if available
|
|
||||
} |
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll( |
|
||||
/[xy]/g, |
|
||||
(c) => { |
|
||||
const r = Math.trunc((d + Math.random() * 16) % 16); |
|
||||
d = Math.floor(d / 16); |
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); |
|
||||
}, |
|
||||
); |
|
||||
return uuid; |
|
||||
} |
|
||||
|
|
||||
export { generateUUID }; |
|
||||
@ -0,0 +1 @@ |
|||||
|
export { default } from '@vben/tailwind-config/postcss'; |
||||
@ -0,0 +1,26 @@ |
|||||
|
<!-- |
||||
|
Access control component for fine-grained access control. |
||||
|
--> |
||||
|
<script lang="ts" setup> |
||||
|
interface Props { |
||||
|
/** |
||||
|
* Specified role is visible |
||||
|
* - When the permission mode is 'frontend', the value can be a role value. |
||||
|
* - When the permission mode is 'backend', the value can be a code permission value. |
||||
|
* @default '' |
||||
|
*/ |
||||
|
value?: string[]; |
||||
|
} |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'Authority', |
||||
|
}); |
||||
|
|
||||
|
withDefaults(defineProps<Props>(), { |
||||
|
value: undefined, |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<slot></slot> |
||||
|
</template> |
||||
@ -0,0 +1,87 @@ |
|||||
|
import type { |
||||
|
ComponentRecordType, |
||||
|
RouteRecordStringComponent, |
||||
|
} from '@vben/types'; |
||||
|
import type { RouteRecordRaw } from 'vue-router'; |
||||
|
|
||||
|
import type { GeneratorMenuAndRoutesOptions } from '../types'; |
||||
|
|
||||
|
import { $t } from '@vben/locales'; |
||||
|
import { mapTree } from '@vben-core/toolkit'; |
||||
|
|
||||
|
/** |
||||
|
* 动态生成路由 - 后端方式 |
||||
|
*/ |
||||
|
async function generateRoutesByBackend( |
||||
|
options: GeneratorMenuAndRoutesOptions, |
||||
|
): Promise<RouteRecordRaw[]> { |
||||
|
const { fetchMenuListAsync, layoutMap, pageMap } = options; |
||||
|
|
||||
|
try { |
||||
|
const menuRoutes = await fetchMenuListAsync?.(); |
||||
|
if (!menuRoutes) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
const normalizePageMap: ComponentRecordType = {}; |
||||
|
|
||||
|
for (const [key, value] of Object.entries(pageMap)) { |
||||
|
normalizePageMap[normalizeViewPath(key)] = value; |
||||
|
} |
||||
|
|
||||
|
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap); |
||||
|
return routes; |
||||
|
} catch (error) { |
||||
|
console.error(error); |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function convertRoutes( |
||||
|
routes: RouteRecordStringComponent[], |
||||
|
layoutMap: ComponentRecordType, |
||||
|
pageMap: ComponentRecordType, |
||||
|
): RouteRecordRaw[] { |
||||
|
return mapTree(routes, (node) => { |
||||
|
const route = node as unknown as RouteRecordRaw; |
||||
|
const { component, name } = node; |
||||
|
|
||||
|
if (!name) { |
||||
|
console.error('route name is required', route); |
||||
|
} |
||||
|
|
||||
|
// layout转换
|
||||
|
if (component && layoutMap[component]) { |
||||
|
route.component = layoutMap[component]; |
||||
|
// 页面组件转换
|
||||
|
} else if (component) { |
||||
|
const normalizePath = normalizeViewPath(component); |
||||
|
route.component = |
||||
|
pageMap[ |
||||
|
normalizePath.endsWith('.vue') |
||||
|
? normalizePath |
||||
|
: `${normalizePath}.vue` |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
// 国际化转化
|
||||
|
if (route.meta?.title) { |
||||
|
route.meta.title = $t(route.meta.title); |
||||
|
} |
||||
|
|
||||
|
return route; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function normalizeViewPath(path: string): string { |
||||
|
// 去除相对路径前缀
|
||||
|
const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, ''); |
||||
|
|
||||
|
// 确保路径以 '/' 开头
|
||||
|
const viewPath = normalizedPath.startsWith('/') |
||||
|
? normalizedPath |
||||
|
: `/${normalizedPath}`; |
||||
|
|
||||
|
return viewPath.replace(/^\/views/, ''); |
||||
|
} |
||||
|
export { generateRoutesByBackend }; |
||||
@ -0,0 +1,76 @@ |
|||||
|
import type { accessModeType } from '@vben-core/preferences'; |
||||
|
import type { RouteRecordRaw } from 'vue-router'; |
||||
|
|
||||
|
import type { GeneratorMenuAndRoutesOptions } from '../types'; |
||||
|
|
||||
|
import { generateMenus } from './generate-menus'; |
||||
|
import { generateRoutesByBackend } from './generate-routes-backend'; |
||||
|
import { generateRoutesByFrontend } from './generate-routes-frontend'; |
||||
|
|
||||
|
async function generateMenusAndRoutes( |
||||
|
mode: accessModeType, |
||||
|
options: GeneratorMenuAndRoutesOptions, |
||||
|
) { |
||||
|
const { router } = options; |
||||
|
// 生成路由
|
||||
|
const accessibleRoutes = await generateRoutes(mode, options); |
||||
|
|
||||
|
// 动态添加到router实例内
|
||||
|
accessibleRoutes.forEach((route) => router.addRoute(route)); |
||||
|
|
||||
|
// 生成菜单
|
||||
|
const accessibleMenus = await generateMenus1(mode, accessibleRoutes, options); |
||||
|
|
||||
|
return { accessibleMenus, accessibleRoutes }; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Generate routes |
||||
|
* @param mode |
||||
|
*/ |
||||
|
async function generateRoutes( |
||||
|
mode: accessModeType, |
||||
|
options: GeneratorMenuAndRoutesOptions, |
||||
|
) { |
||||
|
const { forbiddenComponent, roles, routes } = options; |
||||
|
|
||||
|
switch (mode) { |
||||
|
// 允许所有路由访问,不做任何过滤处理
|
||||
|
case 'allow-all': { |
||||
|
return routes; |
||||
|
} |
||||
|
case 'frontend': { |
||||
|
return await generateRoutesByFrontend( |
||||
|
routes, |
||||
|
roles || [], |
||||
|
forbiddenComponent, |
||||
|
); |
||||
|
} |
||||
|
case 'backend': { |
||||
|
return await generateRoutesByBackend(options); |
||||
|
} |
||||
|
default: { |
||||
|
return routes; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function generateMenus1( |
||||
|
mode: accessModeType, |
||||
|
routes: RouteRecordRaw[], |
||||
|
options: GeneratorMenuAndRoutesOptions, |
||||
|
) { |
||||
|
const { router } = options; |
||||
|
switch (mode) { |
||||
|
case 'allow-all': |
||||
|
case 'frontend': |
||||
|
case 'backend': { |
||||
|
return await generateMenus(routes, router); |
||||
|
} |
||||
|
default: { |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { generateMenusAndRoutes }; |
||||
@ -0,0 +1,4 @@ |
|||||
|
export { default as Authority } from './authority.vue'; |
||||
|
export * from './generate-menu-and-routes'; |
||||
|
export type * from './types'; |
||||
|
export * from './use-access'; |
||||
@ -0,0 +1,17 @@ |
|||||
|
import type { |
||||
|
ComponentRecordType, |
||||
|
RouteRecordStringComponent, |
||||
|
} from '@vben/types'; |
||||
|
import type { RouteRecordRaw, Router } from 'vue-router'; |
||||
|
|
||||
|
interface GeneratorMenuAndRoutesOptions { |
||||
|
fetchMenuListAsync?: () => Promise<RouteRecordStringComponent[]>; |
||||
|
forbiddenComponent?: RouteRecordRaw['component']; |
||||
|
layoutMap?: ComponentRecordType; |
||||
|
pageMap?: ComponentRecordType; |
||||
|
roles?: string[]; |
||||
|
router: Router; |
||||
|
routes: RouteRecordRaw[]; |
||||
|
} |
||||
|
|
||||
|
export type { GeneratorMenuAndRoutesOptions }; |
||||
@ -0,0 +1,28 @@ |
|||||
|
import { computed } from 'vue'; |
||||
|
|
||||
|
import { preferences } from '@vben-core/preferences'; |
||||
|
import { useAccessStore } from '@vben-core/stores'; |
||||
|
|
||||
|
function useAccess() { |
||||
|
const accessStore = useAccessStore(); |
||||
|
const currentAccessMode = computed(() => { |
||||
|
return preferences.app.accessMode; |
||||
|
}); |
||||
|
|
||||
|
/** |
||||
|
* 更改账号角色 |
||||
|
* @param roles |
||||
|
*/ |
||||
|
async function changeRoles(roles: string[]): Promise<void> { |
||||
|
if (preferences.app.accessMode !== 'frontend') { |
||||
|
throw new Error( |
||||
|
'The current access mode is not frontend, so the role cannot be changed', |
||||
|
); |
||||
|
} |
||||
|
accessStore.setUserRoles(roles); |
||||
|
} |
||||
|
|
||||
|
return { changeRoles, currentAccessMode }; |
||||
|
} |
||||
|
|
||||
|
export { useAccess }; |
||||
@ -0,0 +1 @@ |
|||||
|
export { default } from '@vben/tailwind-config'; |
||||
@ -1,6 +1,9 @@ |
|||||
{ |
{ |
||||
"$schema": "https://json.schemastore.org/tsconfig", |
"$schema": "https://json.schemastore.org/tsconfig", |
||||
"extends": "@vben/tsconfig/library.json", |
"extends": "@vben/tsconfig/web.json", |
||||
|
"compilerOptions": { |
||||
|
"types": ["@vben/types/global"] |
||||
|
}, |
||||
"include": ["src"], |
"include": ["src"], |
||||
"exclude": ["node_modules"] |
"exclude": ["node_modules"] |
||||
} |
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
import { defineConfig } from '@vben/vite-config'; |
||||
|
|
||||
|
export default defineConfig(); |
||||
@ -1,7 +0,0 @@ |
|||||
import { defineBuildConfig } from 'unbuild'; |
|
||||
|
|
||||
export default defineBuildConfig({ |
|
||||
clean: true, |
|
||||
declaration: true, |
|
||||
entries: ['src/index'], |
|
||||
}); |
|
||||
@ -1 +0,0 @@ |
|||||
export {}; |
|
||||
@ -1,3 +1,4 @@ |
|||||
|
export type * from './router'; |
||||
export type * from './ui'; |
export type * from './ui'; |
||||
export type * from './user'; |
export type * from './user'; |
||||
export type * from '@vben-core/typings'; |
export type * from '@vben-core/typings'; |
||||
|
|||||
@ -0,0 +1,13 @@ |
|||||
|
import type { RouteRecordRaw } from 'vue-router'; |
||||
|
|
||||
|
import type { Component } from 'vue'; |
||||
|
|
||||
|
// 定义递归类型以将 RouteRecordRaw 的 component 属性更改为 string
|
||||
|
type RouteRecordStringComponent<T = string> = { |
||||
|
children?: RouteRecordStringComponent<T>[]; |
||||
|
component: T; |
||||
|
} & Omit<RouteRecordRaw, 'children' | 'component'>; |
||||
|
|
||||
|
type ComponentRecordType = Record<string, () => Promise<Component>>; |
||||
|
|
||||
|
export type { ComponentRecordType, RouteRecordStringComponent }; |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue