mock数据,AI对话,全部应用

This commit is contained in:
2025-12-12 18:17:38 +08:00
parent 8b211fbad6
commit 0a72416365
41 changed files with 5667 additions and 205 deletions

View File

@@ -1,5 +1,6 @@
import { api } from '@/api/index'
import type { LoginParam, LoginDomain } from '@/types'
import type { LoginParam, LoginDomain, ResultDomain } from '@/types'
/**
* 认证 API
@@ -14,16 +15,18 @@ export const authAPI = {
* @param loginParam 登录参数
* @returns 登录结果(包含 token 和用户信息)
*/
login(loginParam: LoginParam) {
return api.post<LoginDomain>(`${this.baseUrl}/login`, loginParam)
async login(loginParam: LoginParam): Promise<ResultDomain<LoginDomain>> {
const response = await api.post<LoginDomain>(`${this.baseUrl}/login`, loginParam)
return response.data
},
/**
* 用户登出
* @returns 登出结果
*/
logout() {
return api.post<LoginDomain>(`${this.baseUrl}/logout`)
async logout(): Promise<ResultDomain<null>> {
const response = await api.post<null>(`${this.baseUrl}/logout`)
return response.data
},
/**
@@ -31,16 +34,18 @@ export const authAPI = {
* @param loginParam 登录参数(包含验证码类型)
* @returns 验证码结果
*/
getCaptcha(loginParam: LoginParam) {
return api.post<LoginDomain>(`${this.baseUrl}/captcha`, loginParam)
async getCaptcha(loginParam: LoginParam): Promise<ResultDomain<any>> {
const response = await api.post<any>(`${this.baseUrl}/captcha`, loginParam)
return response.data
},
/**
* 刷新 Token
* @returns 新的登录信息
*/
refreshToken() {
return api.post<LoginDomain>(`${this.baseUrl}/refresh`)
async refreshToken(): Promise<ResultDomain<LoginDomain>> {
const response = await api.post<LoginDomain>(`${this.baseUrl}/refresh`)
return response.data
},
/**
@@ -48,8 +53,9 @@ export const authAPI = {
* @param email 邮箱地址
* @returns 发送结果
*/
sendEmailCode(email: string) {
return api.post<LoginDomain>(`${this.baseUrl}/send-email-code`, { email })
async sendEmailCode(email: string): Promise<ResultDomain<null>> {
const response = await api.post<null>(`${this.baseUrl}/send-email-code`, { email })
return response.data
},
/**
@@ -57,8 +63,9 @@ export const authAPI = {
* @param phone 手机号
* @returns 发送结果
*/
sendSmsCode(phone: string) {
return api.post<LoginDomain>(`${this.baseUrl}/send-sms-code`, { phone })
async sendSmsCode(phone: string): Promise<ResultDomain<null>> {
const response = await api.post<null>(`${this.baseUrl}/send-sms-code`, { phone })
return response.data
},
/**
@@ -66,7 +73,7 @@ export const authAPI = {
* @param registerData 注册数据
* @returns 注册结果(成功后自动登录,返回 token
*/
register(registerData: {
async register(registerData: {
registerType: 'username' | 'phone' | 'email'
username?: string
phone?: string
@@ -78,15 +85,17 @@ export const authAPI = {
smsSessionId?: string
emailSessionId?: string
studentId?: string
}) {
return api.post<LoginDomain>(`${this.baseUrl}/register`, registerData)
}): Promise<ResultDomain<LoginDomain>> {
const response = await api.post<LoginDomain>(`${this.baseUrl}/register`, registerData)
return response.data
},
/**
* 健康检查
* @returns 健康状态
*/
health() {
return api.get<string>(`${this.baseUrl}/health`)
async health(): Promise<ResultDomain<string>> {
const response = await api.get<string>(`${this.baseUrl}/health`)
return response.data
}
}

View File

@@ -17,25 +17,52 @@ export interface LoginParam {
}
// LoginDomain - 登录信息
import type {
TbSysUserDTO,
TbSysUserInfoDTO,
TbSysUserRoleDTO
} from '@/types/sys/user'
import type {
TbSysDeptDTO,
TbSysPermissionDTO,
TbSysViewDTO
} from '@/types/sys/permission'
/**
* 登录返回的领域对象
*/
export interface LoginDomain {
/** 用户ID */
userId?: string
/** 用户名 */
username?: string
/** 邮箱 */
email?: string
/** 手机 */
phone?: string
/** 访问令牌 */
accessToken?: string
/** 刷新令牌 */
refreshToken?: string
/** 令牌类型 */
tokenType?: string
/** 过期时间(秒) */
expiresIn?: number
/** 用户权限列表 */
permissions?: string[]
/** 用户角色列表 */
roles?: string[]
/** 用户基本信息 */
user?: TbSysUserDTO
/** 用户详细信息 */
userInfo?: TbSysUserInfoDTO
/** 用户角色列表 */
userRoles?: TbSysUserRoleDTO[]
/** 用户部门列表 */
userDepts?: TbSysDeptDTO[]
/** 用户权限列表 */
userPermissions?: TbSysPermissionDTO[]
/** 用户视图列表(视图即菜单,用于生成路由和侧边栏) */
userViews?: TbSysViewDTO[]
/** 访问令牌 */
token?: string
/** 令牌过期时间 */
tokenExpireTime?: string | Date
/** 登录时间 */
loginTime?: string
/** IP地址 */
ipAddress?: string
/** 登录类型 */
loginType?: string
}

View File

@@ -0,0 +1,10 @@
/**
* 菜单类型枚举
*/
export enum MenuType {
NAVIGATION = 'navigation', // 导航菜单
SIDEBAR = 'sidebar', // 侧边栏菜单
MENU = 'menu', // 普通菜单
PAGE = 'page', // 页面
BUTTON = 'button' // 按钮
}

View File

@@ -2,6 +2,7 @@ export * from "./response"
export * from "./page"
export * from "./base"
export * from "./sys"
export * from "./enums"
// 服务 types
export * from "./auth"

View File

@@ -120,16 +120,22 @@ export interface TbSysViewDTO extends BaseDTO {
url?: string;
/** 组件 */
component?: string;
/** iframe URL */
iframeUrl?: string;
/** 图标 */
icon?: string;
/** 类型 */
type?: number;
/** 视图类型 route\iframe*/
viewType?: string;
/** 布局 */
layout?: string;
/** 排序 */
orderNum?: number;
/** 描述 */
description?: string;
/** 子视图列表(用于构建树形结构) */
children?: TbSysViewDTO[];
}
// TbSysPermissionDTO - 系统权限DTO

View File

@@ -0,0 +1,109 @@
import { ref, onMounted, onUnmounted } from 'vue'
/**
* 设备类型枚举
*/
export enum DeviceType {
MOBILE = 'mobile', // h5移动端
DESKTOP = 'desktop' // web桌面端
}
/**
* 屏幕尺寸断点
*/
export const BREAKPOINTS = {
mobile: 768, // 小于768px为移动端(h5)
desktop: 768 // 大于等于768px为桌面端(web)
}
/**
* 检测当前设备类型
*/
export function getDeviceType(): DeviceType {
const width = window.innerWidth
if (width < BREAKPOINTS.mobile) {
return DeviceType.MOBILE // h5移动端
} else {
return DeviceType.DESKTOP // web桌面端
}
}
/**
* 检测是否为移动端
*/
export function isMobile(): boolean {
return getDeviceType() === DeviceType.MOBILE
}
/**
* 检测是否为桌面端
*/
export function isDesktop(): boolean {
return getDeviceType() === DeviceType.DESKTOP
}
/**
* 响应式设备类型 Hook
*/
export function useDevice() {
const deviceType = ref<DeviceType>(getDeviceType())
const isMobileDevice = ref(isMobile())
const isDesktopDevice = ref(isDesktop())
const updateDeviceType = () => {
deviceType.value = getDeviceType()
isMobileDevice.value = isMobile()
isDesktopDevice.value = isDesktop()
}
onMounted(() => {
window.addEventListener('resize', updateDeviceType)
})
onUnmounted(() => {
window.removeEventListener('resize', updateDeviceType)
})
return {
deviceType,
isMobileDevice,
isDesktopDevice
}
}
/**
* 根据设备类型获取对应的组件路径
*/
export function getComponentPath(basePath: string, deviceType?: DeviceType): string {
const currentDeviceType = deviceType || getDeviceType()
// 如果是移动端(h5),尝试加载移动端版本
if (currentDeviceType === DeviceType.MOBILE) {
const mobilePath = basePath.replace('.vue', '.mobile.vue')
return mobilePath
}
// 默认返回桌面版本(web)
return basePath
}
/**
* 动态导入组件,支持回退机制
*/
export async function importResponsiveComponent(basePath: string) {
const deviceType = getDeviceType()
// 尝试加载设备特定的组件
if (deviceType === DeviceType.MOBILE) {
try {
const mobilePath = basePath.replace('.vue', '.mobile.vue')
return await import(/* @vite-ignore */ mobilePath)
} catch {
// 移动端组件不存在,回退到默认组件
}
}
// 加载默认组件(桌面端/web)
return await import(/* @vite-ignore */ basePath)
}

View File

@@ -1,6 +1,7 @@
/**
* Utils 统一导出
*/
export * from './file'
export * from './crypto'
export * from './device'
export * from './file'
export * from './route'

View File

@@ -0,0 +1,19 @@
// 导出所有路由生成相关的函数和类型
export * from './route-generator'
// 显式导出新增的函数和类型
export type {
RouteGeneratorConfig,
GenerateSimpleRoutesOptions
} from './route-generator'
export {
generateRoutes,
generateSimpleRoutes,
buildMenuTree,
filterMenusByPermissions,
findMenuByPath,
getMenuPath,
getFirstAccessibleMenuUrl,
loadViewsFromStorage
} from './route-generator'

View File

@@ -0,0 +1,789 @@
/**
* @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 || (() => Promise.resolve({ default: { template: '<div>404</div>' } }))
}
} else {
// 使用路由占位组件
route.component = () => Promise.resolve({
default: {
template: '<router-view />'
}
})
}
}
// 处理子路由
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 类型:使用占位组件
component = iframePlaceholder || (() => Promise.resolve({
default: {
template: '<div class="iframe-placeholder"></div>'
}
}))
} else if (view.component) {
// route 类型:加载实际组件
component = config.viewLoader(view.component)
if (!component) {
if (verbose) console.warn('[路由生成] 组件加载失败:', view.component)
}
}
// 根路径的子路由去掉前导斜杠
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 = () => Promise.resolve({
default: {
template: '<router-view />'
}
})
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
}
return route
}