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 菜单接口 │ │
|
2025-12-17 15:32:58 +08:00
|
|
|
|
│ │ - ViewType 视图类型枚举 │ │
|
2025-12-12 18:17:38 +08:00
|
|
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
↓
|
|
|
|
|
|
┌─────────────────────────────────────┐
|
|
|
|
|
|
│ 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'
|
2025-12-17 15:32:58 +08:00
|
|
|
|
type: ViewType // 视图类型(0=目录 1=菜单 2=按钮 3=页面)
|
2025-12-12 18:17:38 +08:00
|
|
|
|
icon?: string // 图标
|
|
|
|
|
|
component?: string // 组件路径,如 'user/profile/ProfileView'
|
|
|
|
|
|
layout?: string // 布局名称,如 'SidebarLayout'
|
|
|
|
|
|
orderNum?: number // 排序号
|
|
|
|
|
|
permission?: string // 权限标识
|
|
|
|
|
|
hidden?: boolean // 是否隐藏
|
|
|
|
|
|
children?: SysMenu[] // 子菜单
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 15:32:58 +08:00
|
|
|
|
enum ViewType {
|
|
|
|
|
|
NAVBAR = 0, // 导航栏/目录
|
|
|
|
|
|
SIDEBAR = 1, // 侧边栏/菜单
|
|
|
|
|
|
BUTTON = 2, // 按钮(权限控制,不生成路由)
|
|
|
|
|
|
ROUTE = 3 // 空白页/路由页面
|
2025-12-12 18:17:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 示例菜单数据
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
const menus: SysMenu[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
menuID: 'user-center',
|
|
|
|
|
|
parentID: '0',
|
|
|
|
|
|
name: '用户中心',
|
|
|
|
|
|
url: '/user',
|
2025-12-17 15:32:58 +08:00
|
|
|
|
type: ViewType.NAVBAR,
|
2025-12-12 18:17:38 +08:00
|
|
|
|
icon: 'User',
|
|
|
|
|
|
layout: 'SidebarLayout',
|
|
|
|
|
|
orderNum: 1,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{
|
|
|
|
|
|
menuID: 'user-profile',
|
|
|
|
|
|
parentID: 'user-center',
|
|
|
|
|
|
name: '个人信息',
|
|
|
|
|
|
url: '/user/profile',
|
2025-12-17 15:32:58 +08:00
|
|
|
|
type: ViewType.SIDEBAR,
|
2025-12-12 18:17:38 +08:00
|
|
|
|
component: 'user/profile/ProfileView',
|
|
|
|
|
|
orderNum: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
menuID: 'user-settings',
|
|
|
|
|
|
parentID: 'user-center',
|
|
|
|
|
|
name: '账号设置',
|
|
|
|
|
|
url: '/user/settings',
|
2025-12-17 15:32:58 +08:00
|
|
|
|
type: ViewType.SIDEBAR,
|
2025-12-12 18:17:38 +08:00
|
|
|
|
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'
|
|
|
|
|
|
|
2025-12-17 15:32:58 +08:00
|
|
|
|
import type { SysMenu } from 'shared/types'
|
|
|
|
|
|
import { ViewType } from 'shared/types/enums'
|
2025-12-12 18:17:38 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 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<any>) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|