# Platform 路由集成指南 ## 快速开始 ### TL;DR 1. **shared 提供**:路由生成工具、菜单处理、设备检测 2. **platform 定义**:布局组件映射、视图加载器 3. **platform 生成**:调用 `generateRoutes()` 生成自己的路由 ```typescript // 1. 导入工具 import { generateRoutes, type RouteGeneratorConfig } from 'shared/utils/route' import type { SysMenu } from 'shared/types' // 2. 配置生成器 const config: RouteGeneratorConfig = { layoutMap: { /* 布局映射 */ }, viewLoader: (path) => { /* 组件加载 */ } } // 3. 生成路由 const routes = generateRoutes(menus, config) // 4. 添加到路由 routes.forEach(route => router.addRoute(route)) ``` ## 架构说明 Platform 使用 shared 提供的路由生成工具来动态生成路由。架构如下: ``` ┌─────────────────────────────────────────────────────────────┐ │ shared │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ utils/route/route-generator.ts │ │ │ │ - generateRoutes() 路由生成 │ │ │ │ - buildMenuTree() 菜单树构建 │ │ │ │ - filterMenusByPermissions() 权限过滤 │ │ │ │ - findMenuByPath() 路径查找 │ │ │ │ - getMenuPath() 面包屑路径 │ │ │ │ - getFirstAccessibleMenuUrl() 首页跳转 │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ utils/device.ts │ │ │ │ - getDeviceType() 设备类型检测 │ │ │ │ - isMobile() 移动端判断 │ │ │ │ - useDevice() 响应式 Hook │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ types/sys/menu.ts & types/enums.ts │ │ │ │ - SysMenu 菜单接口 │ │ │ │ - MenuType 菜单类型枚举 │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Module Federation (远程模块) │ │ - shared/utils/route │ │ - shared/utils/device │ │ - shared/types │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ platform │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ src/router/index.ts │ │ │ │ 1. 定义布局组件映射 (platformLayoutMap) │ │ │ │ 2. 定义视图组件加载器 (viewLoader) │ │ │ │ 3. 调用 generateRoutes() 生成动态路由 │ │ │ │ 4. 添加到 Vue Router │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ src/layouts/ │ │ │ │ - SidebarLayout.vue 侧边栏布局 │ │ │ │ - NavigationLayout.vue 导航布局(如需) │ │ │ │ - BasicLayout.vue 基础布局(如需) │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ src/views/ │ │ │ │ - 各种页面组件 │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ## 使用步骤 ### 1. 在 router/index.ts 中配置路由生成器 ```typescript import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { generateRoutes, type RouteGeneratorConfig } from 'shared/utils/route' import type { SysMenu } from 'shared/types' import { SidebarLayout } from '../layouts' // 1. 定义布局组件映射 const platformLayoutMap: Record Promise> = { 'SidebarLayout': () => Promise.resolve({ default: SidebarLayout }), 'NavigationLayout': () => Promise.resolve({ default: SidebarLayout }), // 可复用或自定义 'BasicLayout': () => Promise.resolve({ default: SidebarLayout }), } // 2. 定义视图组件加载器 const VIEW_MODULES = import.meta.glob('../views/**/*.vue') function viewLoader(componentPath: string): (() => Promise) | null { // 将后台路径转换为实际路径 let path = componentPath if (!path.startsWith('../')) { if (!path.startsWith('/')) { path = '/' + path } path = '../views' + path } if (!path.endsWith('.vue')) { path += '.vue' } const loader = VIEW_MODULES[path] return loader ? (loader as () => Promise) : null } // 3. 创建路由生成器配置 const routeConfig: RouteGeneratorConfig = { layoutMap: platformLayoutMap, viewLoader, staticRoutes: routes, // 可选:静态路由 notFoundComponent: () => import('../views/public/404.vue') // 可选 } // 4. 动态添加路由的函数 export function addDynamicRoutes(menus: SysMenu[]) { const dynamicRoutes = generateRoutes(menus, routeConfig) dynamicRoutes.forEach(route => { router.addRoute(route) }) console.log('✅ 动态路由已添加', dynamicRoutes.length, '个') } ``` ### 2. 在应用初始化时调用 ```typescript // main.ts 或登录成功后 import { addDynamicRoutes } from './router' import { menuAPI } from 'shared/api' // 获取用户菜单 const response = await menuAPI.getUserMenus() if (response.success && response.data) { // 添加动态路由 addDynamicRoutes(response.data) // 跳转到首页或指定页面 router.push('/home') } ``` ## 菜单数据格式 ```typescript interface SysMenu { menuID: string // 菜单ID,作为路由 name parentID?: string // 父菜单ID,'0' 表示根菜单 name: string // 菜单名称 url?: string // 路由路径,如 '/user/profile' type: MenuType // 菜单类型 icon?: string // 图标 component?: string // 组件路径,如 'user/profile/ProfileView' layout?: string // 布局名称,如 'SidebarLayout' orderNum?: number // 排序号 permission?: string // 权限标识 hidden?: boolean // 是否隐藏 children?: SysMenu[] // 子菜单 } enum MenuType { NAVIGATION = 'navigation', // 导航菜单(顶部导航) SIDEBAR = 'sidebar', // 侧边栏菜单 MENU = 'menu', // 普通菜单 PAGE = 'page', // 页面(独立路由) BUTTON = 'button' // 按钮(不生成路由) } ``` ## 示例菜单数据 ```typescript const menus: SysMenu[] = [ { menuID: 'user-center', parentID: '0', name: '用户中心', url: '/user', type: MenuType.NAVIGATION, icon: 'User', layout: 'SidebarLayout', orderNum: 1, children: [ { menuID: 'user-profile', parentID: 'user-center', name: '个人信息', url: '/user/profile', type: MenuType.MENU, component: 'user/profile/ProfileView', orderNum: 1 }, { menuID: 'user-settings', parentID: 'user-center', name: '账号设置', url: '/user/settings', type: MenuType.MENU, component: 'user/settings/SettingsView', orderNum: 2 } ] } ] ``` ## 布局组件要求 布局组件必须包含 `` 用于渲染子路由: ```vue ``` ## 响应式布局(可选) 如果需要移动端适配,可以使用 shared 的设备检测工具: ```typescript import { getDeviceType, DeviceType } from 'shared/utils/device' const deviceType = getDeviceType() if (deviceType === DeviceType.MOBILE) { // 移动端逻辑 } ``` ## 工具方法说明 所有工具方法从 `shared/utils/route` 导入: ```typescript import { generateRoutes, buildMenuTree, filterMenusByPermissions, findMenuByPath, getMenuPath, getFirstAccessibleMenuUrl, type RouteGeneratorConfig } from 'shared/utils/route' import type { SysMenu, MenuType } from 'shared/types' ``` ### generateRoutes(menus, config) 根据菜单生成路由配置数组 **参数:** - `menus: SysMenu[]` - 菜单列表 - `config: RouteGeneratorConfig` - 路由生成器配置 **返回:** `RouteRecordRaw[]` ### buildMenuTree(menus, staticRoutes?) 将扁平菜单列表转换为树形结构 **参数:** - `menus: SysMenu[]` - 菜单列表 - `staticRoutes?: RouteRecordRaw[]` - 静态路由(可选) **返回:** `SysMenu[]` ### filterMenusByPermissions(menus, permissions) 根据权限过滤菜单 **参数:** - `menus: SysMenu[]` - 菜单列表 - `permissions: string[]` - 权限列表 **返回:** `SysMenu[]` ### findMenuByPath(menus, path) 根据路径查找菜单项 **参数:** - `menus: SysMenu[]` - 菜单列表 - `path: string` - 路由路径 **返回:** `SysMenu | null` ### getMenuPath(menus, targetMenuId) 获取菜单路径数组(用于面包屑导航) **参数:** - `menus: SysMenu[]` - 菜单列表 - `targetMenuId: string` - 目标菜单ID **返回:** `SysMenu[]` ### getFirstAccessibleMenuUrl(menus) 获取第一个可访问的菜单URL(用于登录后跳转) **参数:** - `menus: SysMenu[]` - 菜单列表 **返回:** `string | null` ## 注意事项 1. **shared 服务必须先启动**:因为使用 Module Federation,platform 依赖 shared 的远程模块 2. **布局组件必须包含 router-view**:否则子路由无法渲染 3. **组件路径映射**:确保 `viewLoader` 能正确加载组件 4. **静态路由优先**:如果菜单标记为 `__STATIC_ROUTE__`,不会重复生成路由 5. **路由守卫**:记得在 `router.beforeEach` 中添加权限检查 ## 调试技巧 1. **查看生成的路由**: ```typescript console.log('所有路由:', router.getRoutes()) console.log('路由数量:', router.getRoutes().length) ``` 2. **查看菜单树结构**: ```typescript import { buildMenuTree } from 'shared/utils/route' const tree = buildMenuTree(menus) console.log('菜单树:', JSON.stringify(tree, null, 2)) ``` 3. **查看当前路由信息**: ```typescript console.log('当前路由:', router.currentRoute.value) console.log('路由路径:', router.currentRoute.value.path) console.log('路由参数:', router.currentRoute.value.params) console.log('路由元数据:', router.currentRoute.value.meta) ``` 4. **测试菜单查找**: ```typescript import { findMenuByPath, getMenuPath } from 'shared/utils/route' // 根据路径查找菜单 const menu = findMenuByPath(menus, '/user/profile') console.log('找到的菜单:', menu) // 获取面包屑路径 const breadcrumb = getMenuPath(menus, 'user-profile') console.log('面包屑:', breadcrumb.map(m => m.name).join(' > ')) ``` 5. **检查视图组件加载**: ```typescript // 在 viewLoader 中添加日志 function viewLoader(componentPath: string) { console.log('尝试加载组件:', componentPath) const path = /* 转换逻辑 */ const loader = VIEW_MODULES[path] console.log('找到的加载器:', loader ? '✅' : '❌') return loader ? (loader as () => Promise) : null } ```