858 lines
26 KiB
TypeScript
858 lines
26 KiB
TypeScript
/**
|
||
* @description 动态路由生成器工具类
|
||
* @author yslg
|
||
* @since 2025-12-12
|
||
*
|
||
* 说明:此文件提供路由生成的通用方法,各个 web 服务可以使用这些方法生成自己的路由
|
||
*/
|
||
|
||
import type { RouteRecordRaw } from 'vue-router'
|
||
import type { TbSysViewDTO } from '@/types'
|
||
|
||
// 为了代码可读性,创建类型别名
|
||
type SysMenu = TbSysViewDTO
|
||
|
||
// 视图类型常量(对应后端的 type 字段)
|
||
const ViewType = {
|
||
MENU: 1, // 菜单
|
||
PAGE: 2, // 页面
|
||
BUTTON: 3 // 按钮
|
||
} as const
|
||
|
||
/**
|
||
* 路由生成器配置
|
||
*/
|
||
export interface RouteGeneratorConfig {
|
||
/**
|
||
* 布局组件映射表
|
||
* key: 布局名称,value: 组件加载函数
|
||
*/
|
||
layoutMap: Record<string, () => Promise<any>>
|
||
|
||
/**
|
||
* 视图组件加载器
|
||
* 用于动态加载视图组件
|
||
*/
|
||
viewLoader: (componentPath: string) => Promise<any> | null
|
||
|
||
/**
|
||
* 静态路由列表(可选)
|
||
* 用于将静态路由转换为菜单项
|
||
*/
|
||
staticRoutes?: RouteRecordRaw[]
|
||
|
||
/**
|
||
* 404 组件路径(可选)
|
||
*/
|
||
notFoundComponent?: () => Promise<any>
|
||
}
|
||
|
||
/**
|
||
* 根据菜单生成路由配置
|
||
* @param menus 用户菜单列表
|
||
* @param config 路由生成器配置
|
||
* @returns Vue Router路由配置数组
|
||
*/
|
||
export function generateRoutes(
|
||
menus: SysMenu[],
|
||
config: RouteGeneratorConfig
|
||
): RouteRecordRaw[] {
|
||
if (!menus || menus.length === 0) {
|
||
return []
|
||
}
|
||
|
||
const routes: RouteRecordRaw[] = []
|
||
const pageRoutes: RouteRecordRaw[] = []
|
||
|
||
// 构建菜单树
|
||
const menuTree = buildMenuTree(menus, config.staticRoutes)
|
||
|
||
// 生成路由
|
||
menuTree.forEach(menu => {
|
||
const route = generateRouteFromMenu(menu, config, true)
|
||
|
||
if (route) {
|
||
routes.push(route)
|
||
|
||
// 递归提取所有 PAGE 类型的子菜单
|
||
extractPageChildren(route, pageRoutes, config)
|
||
}
|
||
})
|
||
|
||
// 将 PAGE 类型的路由添加到路由列表
|
||
routes.push(...pageRoutes)
|
||
|
||
return routes
|
||
}
|
||
|
||
/**
|
||
* 递归提取路由中的 PAGE 类型子菜单
|
||
*/
|
||
function extractPageChildren(
|
||
route: any,
|
||
pageRoutes: RouteRecordRaw[],
|
||
config: RouteGeneratorConfig
|
||
) {
|
||
// 检查当前路由是否有 PAGE 类型的子菜单
|
||
if (route.meta?.pageChildren && Array.isArray(route.meta.pageChildren)) {
|
||
route.meta.pageChildren.forEach((pageMenu: SysMenu) => {
|
||
const pageRoute = generateRouteFromMenu(pageMenu, config, true)
|
||
if (pageRoute) {
|
||
pageRoutes.push(pageRoute)
|
||
} else {
|
||
console.error(`[路由生成] 生成独立PAGE路由失败: ${pageMenu.name}`)
|
||
}
|
||
})
|
||
// 清理临时数据
|
||
delete route.meta.pageChildren
|
||
}
|
||
|
||
// 递归检查子路由
|
||
if (route.children && Array.isArray(route.children)) {
|
||
route.children.forEach((childRoute: any) => {
|
||
extractPageChildren(childRoute, pageRoutes, config)
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据单个菜单生成路由
|
||
* @param menu 菜单对象
|
||
* @param config 路由生成器配置
|
||
* @param isTopLevel 是否是顶层菜单
|
||
* @returns 路由配置
|
||
*/
|
||
function generateRouteFromMenu(
|
||
menu: SysMenu,
|
||
config: RouteGeneratorConfig,
|
||
isTopLevel = true
|
||
): RouteRecordRaw | null {
|
||
// 跳过按钮类型
|
||
if (menu.type === ViewType.BUTTON) {
|
||
return null
|
||
}
|
||
|
||
// 跳过静态路由(已经在 router 中定义,不需要再次添加)
|
||
if (menu.component === '__STATIC_ROUTE__') {
|
||
return null
|
||
}
|
||
|
||
const route: any = {
|
||
path: menu.url || `/${menu.viewId}`,
|
||
name: menu.viewId,
|
||
meta: {
|
||
title: menu.name,
|
||
icon: menu.icon,
|
||
menuId: menu.viewId,
|
||
parentId: menu.parentId,
|
||
orderNum: menu.orderNum,
|
||
type: menu.type,
|
||
hideInMenu: false,
|
||
requiresAuth: true,
|
||
}
|
||
}
|
||
|
||
// 检查是否指定了布局(只有顶层菜单才使用布局)
|
||
const layout = isTopLevel ? (menu as any).layout : null
|
||
const hasChildren = menu.children && menu.children.length > 0
|
||
|
||
// 检查 component 是否是布局组件
|
||
const isComponentLayout = menu.component && (
|
||
config.layoutMap[menu.component] ||
|
||
(typeof menu.component === 'string' && menu.component.includes('Layout'))
|
||
)
|
||
|
||
// 确定路由组件
|
||
if (layout && config.layoutMap[layout]) {
|
||
// 如果指定了布局,使用指定的布局
|
||
route.component = config.layoutMap[layout]
|
||
} else if (isComponentLayout && hasChildren && isTopLevel && menu.component) {
|
||
// 如果 component 是布局组件且有子菜单,使用该布局组件作为父路由组件
|
||
route.component = config.layoutMap[menu.component]
|
||
} else if (hasChildren && isTopLevel) {
|
||
// 如果有子菜单但没有指定布局,根据菜单类型选择默认布局
|
||
if (menu.type === ViewType.MENU && !menu.parentId) {
|
||
route.component = config.layoutMap['SidebarLayout']
|
||
} else {
|
||
route.component = config.layoutMap['BasicLayout']
|
||
}
|
||
} else {
|
||
// 没有子菜单,也没有指定布局,使用具体的页面组件
|
||
if (menu.component) {
|
||
const component = config.viewLoader(menu.component)
|
||
if (component) {
|
||
route.component = component
|
||
} else {
|
||
// 组件加载失败,使用 404
|
||
route.component = config.notFoundComponent || (() => import('vue').then(({ h }) => ({
|
||
default: {
|
||
render() { return h('div', '404') }
|
||
}
|
||
})))
|
||
}
|
||
} else {
|
||
// 使用路由占位组件
|
||
route.component = () => import('vue').then(({ h, resolveComponent }) => ({
|
||
default: {
|
||
render() {
|
||
const RouterView = resolveComponent('RouterView')
|
||
return h(RouterView)
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
|
||
// 处理子路由
|
||
if (layout && config.layoutMap[layout] && menu.component && isTopLevel) {
|
||
// 如果指定了布局,将页面组件作为子路由
|
||
const component = config.viewLoader(menu.component)
|
||
route.children = [{
|
||
path: '',
|
||
name: `${menu.viewId}_page`,
|
||
component: component || route.component,
|
||
meta: route.meta
|
||
}]
|
||
|
||
// 如果还有其他子菜单,继续添加
|
||
if (hasChildren) {
|
||
const pageChildren: SysMenu[] = []
|
||
const normalChildren: SysMenu[] = []
|
||
|
||
menu.children!.forEach((child: SysMenu) => {
|
||
if (child.type === ViewType.PAGE) {
|
||
pageChildren.push(child)
|
||
} else {
|
||
normalChildren.push(child)
|
||
}
|
||
})
|
||
|
||
// 添加普通子菜单
|
||
normalChildren.forEach(child => {
|
||
const childRoute = generateRouteFromMenu(child, config, false)
|
||
if (childRoute) {
|
||
route.children!.push(childRoute)
|
||
}
|
||
})
|
||
|
||
// PAGE 类型的菜单保存到 meta
|
||
if (pageChildren.length > 0) {
|
||
route.meta.pageChildren = pageChildren
|
||
}
|
||
}
|
||
} else if (hasChildren) {
|
||
// 处理有子菜单的情况
|
||
route.children = []
|
||
|
||
// 分离 PAGE 类型的子菜单和普通子菜单
|
||
const pageChildren: SysMenu[] = []
|
||
const normalChildren: SysMenu[] = []
|
||
|
||
menu.children!.forEach((child: SysMenu) => {
|
||
if (child.type === ViewType.PAGE) {
|
||
pageChildren.push(child)
|
||
} else {
|
||
normalChildren.push(child)
|
||
}
|
||
})
|
||
|
||
// 如果当前菜单有组件且有普通子菜单,创建默认子路由
|
||
if (menu.component && !isComponentLayout && normalChildren.length > 0) {
|
||
const component = config.viewLoader(menu.component)
|
||
route.children!.push({
|
||
path: '',
|
||
name: `${menu.viewId}_page`,
|
||
component: component || route.component,
|
||
meta: {
|
||
...route.meta,
|
||
}
|
||
})
|
||
}
|
||
|
||
// 只将普通子菜单加入 children
|
||
normalChildren.forEach(child => {
|
||
const childRoute = generateRouteFromMenu(child, config, false)
|
||
if (childRoute) {
|
||
route.children!.push(childRoute)
|
||
}
|
||
})
|
||
|
||
// PAGE 类型的菜单保存到 meta
|
||
if (pageChildren.length > 0) {
|
||
route.meta.pageChildren = pageChildren
|
||
}
|
||
|
||
// 自动重定向到第一个有URL的子菜单
|
||
if (!route.redirect && route.children.length > 0) {
|
||
const firstChildWithUrl = findFirstMenuWithUrl(normalChildren)
|
||
if (firstChildWithUrl?.url) {
|
||
route.redirect = firstChildWithUrl.url
|
||
}
|
||
}
|
||
}
|
||
|
||
return route
|
||
}
|
||
|
||
/**
|
||
* 查找第一个有URL的菜单
|
||
*/
|
||
function findFirstMenuWithUrl(menus: SysMenu[]): SysMenu | null {
|
||
for (const menu of menus) {
|
||
if (menu.type !== ViewType.BUTTON) {
|
||
if (menu.url) {
|
||
return menu
|
||
}
|
||
if (menu.children && menu.children.length > 0) {
|
||
const found = findFirstMenuWithUrl(menu.children)
|
||
if (found) return found
|
||
}
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 将静态路由转换为菜单项
|
||
*/
|
||
function convertRoutesToMenus(routes: RouteRecordRaw[]): SysMenu[] {
|
||
const menus: SysMenu[] = []
|
||
|
||
routes.forEach(route => {
|
||
if (route.children && route.children.length > 0) {
|
||
route.children.forEach(child => {
|
||
if (child.meta?.menuType !== undefined) {
|
||
const menu: SysMenu = {
|
||
viewId: child.name as string || child.path.replace(/\//g, '-'),
|
||
parentId: '0',
|
||
name: child.meta.title as string || child.name as string,
|
||
url: route.path,
|
||
type: child.meta.menuType as number,
|
||
orderNum: (child.meta.orderNum as number) || -1,
|
||
component: '__STATIC_ROUTE__',
|
||
}
|
||
menus.push(menu)
|
||
}
|
||
})
|
||
} else if (route.meta?.menuType !== undefined) {
|
||
const menu: SysMenu = {
|
||
viewId: route.name as string || route.path.replace(/\//g, '-'),
|
||
parentId: '0',
|
||
name: route.meta.title as string || route.name as string,
|
||
url: route.path,
|
||
type: route.meta.menuType as number,
|
||
orderNum: (route.meta.orderNum as number) || -1,
|
||
component: '__STATIC_ROUTE__',
|
||
}
|
||
menus.push(menu)
|
||
}
|
||
})
|
||
|
||
return menus
|
||
}
|
||
|
||
/**
|
||
* 构建菜单树结构
|
||
* @param menus 菜单列表
|
||
* @param staticRoutes 静态路由列表
|
||
* @returns 菜单树
|
||
*/
|
||
export function buildMenuTree(
|
||
menus: SysMenu[],
|
||
staticRoutes?: RouteRecordRaw[]
|
||
): SysMenu[] {
|
||
// 将静态路由转换为菜单项
|
||
const staticMenus = staticRoutes ? convertRoutesToMenus(staticRoutes) : []
|
||
|
||
// 合并动态菜单和静态菜单
|
||
const allMenus = [...staticMenus, ...menus]
|
||
|
||
if (allMenus.length === 0) {
|
||
return []
|
||
}
|
||
|
||
const menuMap = new Map<string, SysMenu>()
|
||
const rootMenus: SysMenu[] = []
|
||
const maxDepth = allMenus.length
|
||
|
||
// 创建菜单映射
|
||
allMenus.forEach(menu => {
|
||
if (menu.viewId) {
|
||
menuMap.set(menu.viewId, { ...menu, children: [] })
|
||
}
|
||
})
|
||
|
||
// 循环构建树结构
|
||
for (let depth = 0; depth < maxDepth; depth++) {
|
||
let hasChanges = false
|
||
|
||
allMenus.forEach(menu => {
|
||
if (!menu.viewId) return
|
||
|
||
const menuNode = menuMap.get(menu.viewId)
|
||
if (!menuNode) return
|
||
|
||
if (isNodeInTree(menuNode, rootMenus)) {
|
||
return
|
||
}
|
||
|
||
if (!menu.parentId || menu.parentId === '0' || menu.parentId === '') {
|
||
if (!isNodeInTree(menuNode, rootMenus)) {
|
||
rootMenus.push(menuNode)
|
||
hasChanges = true
|
||
}
|
||
} else {
|
||
const parent = menuMap.get(menu.parentId)
|
||
if (parent && isNodeInTree(parent, rootMenus)) {
|
||
if (!parent.children) {
|
||
parent.children = []
|
||
}
|
||
if (!parent.children.includes(menuNode)) {
|
||
parent.children.push(menuNode)
|
||
hasChanges = true
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
if (!hasChanges) {
|
||
break
|
||
}
|
||
}
|
||
|
||
// 排序
|
||
const sortMenus = (menus: SysMenu[]): SysMenu[] => {
|
||
return menus
|
||
.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0))
|
||
.map(menu => ({
|
||
...menu,
|
||
children: menu.children ? sortMenus(menu.children) : []
|
||
}))
|
||
}
|
||
|
||
return sortMenus(rootMenus)
|
||
}
|
||
|
||
/**
|
||
* 检查节点是否已经在树中
|
||
*/
|
||
function isNodeInTree(node: SysMenu, tree: SysMenu[]): boolean {
|
||
for (const treeNode of tree) {
|
||
if (treeNode.viewId === node.viewId) {
|
||
return true
|
||
}
|
||
if (treeNode.children && isNodeInTree(node, treeNode.children)) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 根据权限过滤菜单
|
||
*/
|
||
export function filterMenusByPermissions(
|
||
menus: SysMenu[],
|
||
permissions: string[]
|
||
): SysMenu[] {
|
||
if (!menus || menus.length === 0) {
|
||
return []
|
||
}
|
||
|
||
return menus
|
||
.filter(() => true) // 暂时返回true,后续可根据实际需求过滤
|
||
.map(menu => {
|
||
if (menu.children && menu.children.length > 0) {
|
||
return {
|
||
...menu,
|
||
children: filterMenusByPermissions(menu.children, permissions)
|
||
}
|
||
}
|
||
return menu
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 查找路由路径对应的菜单
|
||
*/
|
||
export function findMenuByPath(menus: SysMenu[], path: string): SysMenu | null {
|
||
for (const menu of menus) {
|
||
if (menu.url === path) {
|
||
return menu
|
||
}
|
||
|
||
if (menu.children && menu.children.length > 0) {
|
||
const found = findMenuByPath(menu.children, path)
|
||
if (found) {
|
||
return found
|
||
}
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 获取菜单路径数组(面包屑导航用)
|
||
*/
|
||
export function getMenuPath(menus: SysMenu[], targetMenuId: string): SysMenu[] {
|
||
const path: SysMenu[] = []
|
||
|
||
function findPath(menuList: SysMenu[]): boolean {
|
||
for (const menu of menuList) {
|
||
path.push(menu)
|
||
|
||
if (menu.viewId === targetMenuId) {
|
||
return true
|
||
}
|
||
|
||
if (menu.children && menu.children.length > 0) {
|
||
if (findPath(menu.children)) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
path.pop()
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
findPath(menus)
|
||
return path
|
||
}
|
||
|
||
/**
|
||
* 获取第一个可访问的菜单URL(用于登录后跳转)
|
||
*/
|
||
export function getFirstAccessibleMenuUrl(menus: SysMenu[]): string | null {
|
||
if (!menus || menus.length === 0) {
|
||
return null
|
||
}
|
||
|
||
const firstMenu = findFirstMenuWithUrl(menus)
|
||
return firstMenu?.url || '/home'
|
||
}
|
||
|
||
/**
|
||
* 从 LocalStorage 加载用户视图数据
|
||
* @param storageKey localStorage 的 key,默认为 'loginDomain'
|
||
* @param viewsPath 视图数据在对象中的路径,默认为 'userViews'
|
||
* @returns 视图列表,如果不存在返回 null
|
||
*/
|
||
export function loadViewsFromStorage(
|
||
storageKey: string = 'loginDomain',
|
||
viewsPath: string = 'userViews'
|
||
): TbSysViewDTO[] | null {
|
||
try {
|
||
const dataStr = localStorage.getItem(storageKey)
|
||
|
||
if (!dataStr) {
|
||
console.log(`[路由工具] LocalStorage 中没有 ${storageKey}`)
|
||
return null
|
||
}
|
||
|
||
const data = JSON.parse(dataStr)
|
||
|
||
// 支持嵌套路径,如 'user.views'
|
||
const paths = viewsPath.split('.')
|
||
let views = data
|
||
|
||
for (const path of paths) {
|
||
views = views?.[path]
|
||
}
|
||
|
||
if (views && Array.isArray(views) && views.length > 0) {
|
||
console.log(`[路由工具] 从 LocalStorage 加载视图,数量: ${views.length}`)
|
||
return views
|
||
}
|
||
|
||
console.log(`[路由工具] ${storageKey} 中没有 ${viewsPath} 或数据为空`)
|
||
return null
|
||
} catch (error) {
|
||
console.error('[路由工具] 从 LocalStorage 加载视图失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成简化的路由配置(用于直接添加到 router)
|
||
* 相比 generateRoutes,这个方法生成的路由更适合动态添加到现有路由树
|
||
*
|
||
* @param views 视图列表
|
||
* @param config 路由生成器配置
|
||
* @param options 额外选项
|
||
* @returns 路由配置数组
|
||
*/
|
||
export interface GenerateSimpleRoutesOptions {
|
||
/**
|
||
* 是否作为根路由的子路由(路径去掉前导 /)
|
||
*/
|
||
asRootChildren?: boolean
|
||
|
||
/**
|
||
* iframe 类型视图的占位组件
|
||
*/
|
||
iframePlaceholder?: () => Promise<any>
|
||
|
||
/**
|
||
* 是否启用详细日志
|
||
*/
|
||
verbose?: boolean
|
||
}
|
||
|
||
export function generateSimpleRoutes(
|
||
views: TbSysViewDTO[],
|
||
config: RouteGeneratorConfig,
|
||
options: GenerateSimpleRoutesOptions = {}
|
||
): RouteRecordRaw[] {
|
||
const {
|
||
asRootChildren = false,
|
||
iframePlaceholder,
|
||
verbose = false
|
||
} = options
|
||
|
||
if (!views || views.length === 0) {
|
||
if (verbose) console.warn('[路由生成] 视图列表为空')
|
||
return []
|
||
}
|
||
|
||
if (verbose) {
|
||
console.log('[路由生成] 开始生成路由,视图数量:', views.length)
|
||
}
|
||
|
||
// 构建视图树
|
||
const viewTree = buildMenuTree(views)
|
||
|
||
if (verbose) {
|
||
console.log('[路由生成] 构建视图树,根节点数量:', viewTree.length)
|
||
}
|
||
|
||
const routes: RouteRecordRaw[] = []
|
||
|
||
// 遍历根节点,生成路由
|
||
viewTree.forEach(view => {
|
||
const route = generateSimpleRoute(view, config, {
|
||
asRootChild: asRootChildren,
|
||
iframePlaceholder,
|
||
verbose
|
||
})
|
||
|
||
if (route) {
|
||
routes.push(route)
|
||
if (verbose) {
|
||
console.log('[路由生成] 已生成路由:', {
|
||
path: route.path,
|
||
name: route.name,
|
||
hasComponent: !!route.component,
|
||
childrenCount: route.children?.length || 0
|
||
})
|
||
}
|
||
} else if (verbose) {
|
||
console.warn('[路由生成] 跳过无效视图:', view.name)
|
||
}
|
||
})
|
||
|
||
return routes
|
||
}
|
||
|
||
/**
|
||
* 从单个视图生成简化路由
|
||
*/
|
||
function generateSimpleRoute(
|
||
view: TbSysViewDTO,
|
||
config: RouteGeneratorConfig,
|
||
options: {
|
||
asRootChild?: boolean
|
||
iframePlaceholder?: () => Promise<any>
|
||
verbose?: boolean
|
||
} = {}
|
||
): RouteRecordRaw | null {
|
||
const { asRootChild = false, iframePlaceholder, verbose = false } = options
|
||
|
||
// 验证必要字段
|
||
if (!view.viewId) {
|
||
if (verbose) console.error('[路由生成] 视图缺少 viewId:', view)
|
||
return null
|
||
}
|
||
|
||
// 判断是否是 iframe 类型
|
||
const isIframe = (view as any).viewType === 'iframe' || !!(view as any).iframeUrl
|
||
|
||
// 处理路径和组件
|
||
let routePath = view.url || `/${view.viewId}`
|
||
let component: any
|
||
|
||
if (isIframe) {
|
||
// iframe 类型:使用占位组件(用于显示iframe内容)
|
||
// 路由路径使用 url 字段(应该设置为不冲突的路径,如 /app/workcase)
|
||
component = iframePlaceholder || (() => import('vue').then(({ h }) => ({
|
||
default: {
|
||
render() { return h('div', { class: 'iframe-placeholder' }, 'Loading...') }
|
||
}
|
||
})))
|
||
} else if (view.component) {
|
||
// route 类型:加载实际组件
|
||
component = config.viewLoader(view.component)
|
||
if (!component) {
|
||
if (verbose) console.warn('[路由生成] 组件加载失败:', view.component, '使用占位组件')
|
||
// 使用占位组件,避免路由无效
|
||
const errorMsg = `组件加载失败: ${view.component}`
|
||
component = () => import('vue').then(({ h }) => ({
|
||
default: {
|
||
render() {
|
||
return h('div', {
|
||
style: { padding: '20px', color: 'red' }
|
||
}, errorMsg)
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
|
||
// 根路径的子路由去掉前导斜杠
|
||
if (asRootChild && routePath.startsWith('/')) {
|
||
routePath = routePath.substring(1)
|
||
}
|
||
|
||
const hasChildren = view.children && view.children.length > 0
|
||
|
||
if (verbose) {
|
||
console.log('[路由生成] 视图信息:', {
|
||
viewId: view.viewId,
|
||
name: view.name,
|
||
url: view.url,
|
||
component: view.component,
|
||
isIframe,
|
||
hasChildren,
|
||
childrenCount: view.children?.length || 0
|
||
})
|
||
}
|
||
|
||
const route: any = {
|
||
path: routePath,
|
||
name: view.viewId,
|
||
meta: {
|
||
title: view.name || view.viewId,
|
||
icon: view.icon,
|
||
menuId: view.viewId,
|
||
orderNum: view.orderNum,
|
||
requiresAuth: true,
|
||
isIframe,
|
||
iframeUrl: (view as any).iframeUrl
|
||
}
|
||
}
|
||
|
||
// 根据 component 和 children 的情况处理
|
||
if (component && hasChildren) {
|
||
// 有组件且有子视图:组件作为空路径子路由
|
||
route.component = component
|
||
route.children = [
|
||
{
|
||
path: '',
|
||
name: `${view.viewId}_page`,
|
||
component: component,
|
||
meta: route.meta
|
||
}
|
||
]
|
||
|
||
// 添加其他子路由
|
||
view.children!.forEach(childView => {
|
||
const childRoute = generateSimpleRoute(childView, config, {
|
||
asRootChild: false,
|
||
iframePlaceholder,
|
||
verbose
|
||
})
|
||
if (childRoute) {
|
||
route.children.push(childRoute)
|
||
}
|
||
})
|
||
} else if (component && !hasChildren) {
|
||
// 只有组件,没有子视图
|
||
route.component = component
|
||
} else if (!component && hasChildren) {
|
||
// 没有组件,只有子视图(路由容器)
|
||
route.component = () => import('vue').then(({ h, resolveComponent }) => ({
|
||
default: {
|
||
render() {
|
||
const RouterView = resolveComponent('RouterView')
|
||
return h(RouterView)
|
||
}
|
||
}
|
||
}))
|
||
route.children = []
|
||
|
||
// 添加子路由
|
||
view.children!.forEach(childView => {
|
||
const childRoute = generateSimpleRoute(childView, config, {
|
||
asRootChild: false,
|
||
iframePlaceholder,
|
||
verbose
|
||
})
|
||
if (childRoute) {
|
||
route.children.push(childRoute)
|
||
}
|
||
})
|
||
|
||
// 重定向到第一个子路由
|
||
if (route.children.length > 0) {
|
||
const firstChild = route.children[0]
|
||
route.redirect = firstChild.path
|
||
}
|
||
} else {
|
||
// 既没有组件也没有子视图
|
||
if (verbose) {
|
||
console.warn('[路由生成] 视图既无组件也无子视图:', view.name)
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 处理layout:如果视图指定了layout,且不是作为Root的子路由,且有有效组件,需要包裹layout
|
||
const viewLayout = (view as any).layout
|
||
if (viewLayout && !asRootChild && route.component && config.layoutMap[viewLayout]) {
|
||
if (verbose) {
|
||
console.log('[路由生成] 为视图添加布局:', view.name, '布局:', viewLayout, '路径:', routePath)
|
||
}
|
||
|
||
// 创建layout路由,将原路由的组件作为其子路由
|
||
const layoutRoute: RouteRecordRaw = {
|
||
path: routePath,
|
||
name: view.viewId,
|
||
component: config.layoutMap[viewLayout],
|
||
meta: {
|
||
...route.meta,
|
||
layout: viewLayout // 标记使用的布局
|
||
},
|
||
children: [
|
||
{
|
||
path: '',
|
||
name: `${view.viewId}_content`,
|
||
component: route.component,
|
||
meta: route.meta
|
||
}
|
||
]
|
||
}
|
||
|
||
// 如果原路由有其他children(子视图),也添加到layout路由的children中
|
||
if (route.children && route.children.length > 0) {
|
||
// 跳过第一个空路径的子路由(如果存在)
|
||
const otherChildren = route.children.filter((child: any) => child.path !== '')
|
||
if (otherChildren.length > 0) {
|
||
layoutRoute.children!.push(...otherChildren)
|
||
}
|
||
}
|
||
|
||
if (verbose) {
|
||
console.log('[路由生成] Layout路由生成完成:', {
|
||
path: layoutRoute.path,
|
||
name: layoutRoute.name,
|
||
childrenCount: layoutRoute.children?.length
|
||
})
|
||
}
|
||
|
||
return layoutRoute
|
||
}
|
||
|
||
return route
|
||
}
|