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

383 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
}
```