Files
urbanLifeline/urbanLifelineWeb/packages/platform/ROUTE_GUIDE.md
2025-12-17 15:32:58 +08:00

14 KiB
Raw Blame History

Platform 路由集成指南

快速开始

TL;DR

  1. shared 提供:路由生成工具、菜单处理、设备检测
  2. platform 定义:布局组件映射、视图加载器
  3. platform 生成:调用 generateRoutes() 生成自己的路由
// 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                菜单接口                    │ │
│  │  - ViewType               视图类型枚举                │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                              ↓
        ┌─────────────────────────────────────┐
        │  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 中配置路由生成器

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<string, () => Promise<any>> = {
    '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<any>) | 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<any>) : 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. 在应用初始化时调用

// 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')
}

菜单数据格式

interface SysMenu {
    menuID: string          // 菜单ID作为路由 name
    parentID?: string       // 父菜单ID'0' 表示根菜单
    name: string           // 菜单名称
    url?: string           // 路由路径,如 '/user/profile'
    type: ViewType         // 视图类型0=目录 1=菜单 2=按钮 3=页面)
    icon?: string          // 图标
    component?: string     // 组件路径,如 'user/profile/ProfileView'
    layout?: string        // 布局名称,如 'SidebarLayout'
    orderNum?: number      // 排序号
    permission?: string    // 权限标识
    hidden?: boolean       // 是否隐藏
    children?: SysMenu[]   // 子菜单
}

enum ViewType {
    NAVBAR = 0,     // 导航栏/目录
    SIDEBAR = 1,    // 侧边栏/菜单
    BUTTON = 2,     // 按钮(权限控制,不生成路由)
    ROUTE = 3       // 空白页/路由页面
}

示例菜单数据

const menus: SysMenu[] = [
    {
        menuID: 'user-center',
        parentID: '0',
        name: '用户中心',
        url: '/user',
        type: ViewType.NAVBAR,
        icon: 'User',
        layout: 'SidebarLayout',
        orderNum: 1,
        children: [
            {
                menuID: 'user-profile',
                parentID: 'user-center',
                name: '个人信息',
                url: '/user/profile',
                type: ViewType.SIDEBAR,
                component: 'user/profile/ProfileView',
                orderNum: 1
            },
            {
                menuID: 'user-settings',
                parentID: 'user-center',
                name: '账号设置',
                url: '/user/settings',
                type: ViewType.SIDEBAR,
                component: 'user/settings/SettingsView',
                orderNum: 2
            }
        ]
    }
]

布局组件要求

布局组件必须包含 <router-view /> 用于渲染子路由:

<template>
    <div class="sidebar-layout">
        <aside class="sidebar">
            <!-- 侧边栏内容 -->
        </aside>
        <main class="content">
            <router-view />  <!-- 重要用于渲染页面组件 -->
        </main>
    </div>
</template>

响应式布局(可选)

如果需要移动端适配,可以使用 shared 的设备检测工具:

import { getDeviceType, DeviceType } from 'shared/utils/device'

const deviceType = getDeviceType()
if (deviceType === DeviceType.MOBILE) {
    // 移动端逻辑
}

工具方法说明

所有工具方法从 shared/utils/route 导入:

import { 
    generateRoutes,
    buildMenuTree,
    filterMenusByPermissions,
    findMenuByPath,
    getMenuPath,
    getFirstAccessibleMenuUrl,
    type RouteGeneratorConfig
} from 'shared/utils/route'

import type { SysMenu } from 'shared/types'
import { ViewType } from 'shared/types/enums'

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 Federationplatform 依赖 shared 的远程模块
  2. 布局组件必须包含 router-view:否则子路由无法渲染
  3. 组件路径映射:确保 viewLoader 能正确加载组件
  4. 静态路由优先:如果菜单标记为 __STATIC_ROUTE__,不会重复生成路由
  5. 路由守卫:记得在 router.beforeEach 中添加权限检查

调试技巧

  1. 查看生成的路由
console.log('所有路由:', router.getRoutes())
console.log('路由数量:', router.getRoutes().length)
  1. 查看菜单树结构
import { buildMenuTree } from 'shared/utils/route'

const tree = buildMenuTree(menus)
console.log('菜单树:', JSON.stringify(tree, null, 2))
  1. 查看当前路由信息
console.log('当前路由:', router.currentRoute.value)
console.log('路由路径:', router.currentRoute.value.path)
console.log('路由参数:', router.currentRoute.value.params)
console.log('路由元数据:', router.currentRoute.value.meta)
  1. 测试菜单查找
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(' > '))
  1. 检查视图组件加载
// 在 viewLoader 中添加日志
function viewLoader(componentPath: string) {
    console.log('尝试加载组件:', componentPath)
    const path = /* 转换逻辑 */
    const loader = VIEW_MODULES[path]
    console.log('找到的加载器:', loader ? '✅' : '❌')
    return loader ? (loader as () => Promise<any>) : null
}