Files
urbanLifeline/urbanLifelineWeb/packages/platform/ROUTE_GUIDE.md

383 lines
14 KiB
Markdown
Raw Normal View History

2025-12-12 18:17:38 +08:00
# 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<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. 在应用初始化时调用
```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
}
]
}
]
```
## 布局组件要求
布局组件必须包含 `<router-view />` 用于渲染子路由:
```vue
<template>
<div class="sidebar-layout">
<aside class="sidebar">
<!-- 侧边栏内容 -->
</aside>
<main class="content">
<router-view /> <!-- 重要!用于渲染页面组件 -->
</main>
</div>
</template>
```
## 响应式布局(可选)
如果需要移动端适配,可以使用 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 Federationplatform 依赖 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<any>) : null
}
```