604 lines
12 KiB
Markdown
604 lines
12 KiB
Markdown
# Nova Admin 路由系统文档
|
|
|
|
## 概述
|
|
|
|
Nova Admin 基于 Vue Router 4 构建了一套完整的路由管理系统,支持静态路由和动态路由,具备完善的路由守卫、权限控制、菜单生成等功能。
|
|
|
|
## 🏗️ 路由架构
|
|
|
|
### 核心组件
|
|
|
|
1. **路由配置** (`src/router/`)
|
|
2. **路由守卫** (`src/router/guard.ts`)
|
|
3. **路由状态管理** (`src/store/router/`)
|
|
4. **菜单生成** (`src/store/router/helper.ts`)
|
|
5. **标签页管理** (`src/store/tab.ts`)
|
|
|
|
### 文件结构
|
|
|
|
```
|
|
src/router/
|
|
├── index.ts # 路由主配置文件
|
|
├── guard.ts # 路由守卫
|
|
├── routes.inner.ts # 内置路由(登录、错误页等)
|
|
└── routes.static.ts # 静态路由配置
|
|
|
|
src/store/router/
|
|
├── index.ts # 路由状态管理
|
|
└── helper.ts # 路由处理辅助函数
|
|
```
|
|
|
|
## 🛣️ 路由类型
|
|
|
|
### 1. 内置路由 (Inner Routes)
|
|
|
|
位置:`src/router/routes.inner.ts`
|
|
|
|
包含系统基础路由:
|
|
- 根路由 (`/`)
|
|
- 登录页 (`/login`)
|
|
- 错误页 (`/403`, `/404`, `/500`)
|
|
- 通配符路由 (404 处理)
|
|
|
|
```typescript
|
|
export const routes: RouteRecordRaw[] = [
|
|
{
|
|
path: '/',
|
|
name: 'root',
|
|
redirect: '/appRoot',
|
|
},
|
|
{
|
|
path: '/login',
|
|
name: 'login',
|
|
component: () => import('@/views/login/index.vue'),
|
|
meta: {
|
|
title: '登录',
|
|
withoutTab: true,
|
|
},
|
|
},
|
|
// ... 错误页路由
|
|
]
|
|
```
|
|
|
|
### 2. 静态路由 (Static Routes)
|
|
|
|
位置:`src/router/routes.static.ts`
|
|
|
|
业务功能路由配置:
|
|
|
|
```typescript
|
|
export const staticRoutes: AppRoute.RowRoute[] = [
|
|
{
|
|
name: 'monitor',
|
|
path: '/dashboard/monitor',
|
|
title: '仪表盘',
|
|
requiresAuth: true,
|
|
icon: 'icon-park-outline:anchor',
|
|
menuType: 'page',
|
|
componentPath: '/dashboard/monitor/index.vue',
|
|
id: 3,
|
|
pid: null,
|
|
},
|
|
// ... 其他业务路由
|
|
]
|
|
```
|
|
|
|
### 3. 动态路由 (Dynamic Routes)
|
|
|
|
通过 API 从后端获取的路由配置,结构与静态路由相同。
|
|
|
|
## 📋 路由数据结构
|
|
|
|
### AppRoute.RowRoute 接口
|
|
|
|
```typescript
|
|
interface AppRoute.RowRoute {
|
|
/** 路由名称 */
|
|
name: string
|
|
/** 路由路径 */
|
|
path: string
|
|
/** 页面标题 */
|
|
title: string
|
|
/** 是否需要认证 */
|
|
requiresAuth?: boolean
|
|
/** 访问角色 */
|
|
roles?: Entity.RoleType[]
|
|
/** 图标 */
|
|
icon?: string
|
|
/** 菜单类型 */
|
|
menuType?: 'dir' | 'page'
|
|
/** 组件路径 */
|
|
componentPath?: string | null
|
|
/** 路由ID */
|
|
id: number
|
|
/** 父路由ID */
|
|
pid: number | null
|
|
/** 是否隐藏 */
|
|
hide?: boolean
|
|
/** 排序权重 */
|
|
order?: number
|
|
/** 外链地址 */
|
|
href?: string
|
|
/** 激活菜单 */
|
|
activeMenu?: string
|
|
/** 不显示标签页 */
|
|
withoutTab?: boolean
|
|
/** 固定标签页 */
|
|
pinTab?: boolean
|
|
/** 是否缓存 */
|
|
keepAlive?: boolean
|
|
}
|
|
```
|
|
|
|
### 路由元信息 (Meta)
|
|
|
|
```typescript
|
|
interface RouteMeta {
|
|
/** 页面标题 */
|
|
title: string
|
|
/** 图标 */
|
|
icon?: string
|
|
/** 是否需要认证 */
|
|
requiresAuth?: boolean
|
|
/** 访问角色 */
|
|
roles?: Entity.RoleType[]
|
|
/** 是否缓存 */
|
|
keepAlive?: boolean
|
|
/** 是否隐藏 */
|
|
hide?: boolean
|
|
/** 排序权重 */
|
|
order?: number
|
|
/** 外链地址 */
|
|
href?: string
|
|
/** 激活菜单 */
|
|
activeMenu?: string
|
|
/** 不显示标签页 */
|
|
withoutTab?: boolean
|
|
/** 固定标签页 */
|
|
pinTab?: boolean
|
|
/** 菜单类型 */
|
|
menuType?: 'dir' | 'page'
|
|
}
|
|
```
|
|
|
|
## 🔄 路由处理流程
|
|
|
|
### 1. 路由初始化流程
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant App as 应用启动
|
|
participant Router as 路由系统
|
|
participant Guard as 路由守卫
|
|
participant Store as RouteStore
|
|
participant API as 后端API
|
|
|
|
App->>Router: 创建路由实例
|
|
Router->>Guard: 安装路由守卫
|
|
App->>Store: 初始化路由状态
|
|
|
|
Note over Router,Store: 用户访问路由时
|
|
Router->>Guard: beforeEach 触发
|
|
Guard->>Store: 检查路由是否初始化
|
|
Store->>Store: initAuthRoute()
|
|
|
|
alt 动态路由模式
|
|
Store->>API: fetchUserRoutes()
|
|
API-->>Store: 返回用户路由数据
|
|
else 静态路由模式
|
|
Store->>Store: 使用 staticRoutes
|
|
end
|
|
|
|
Store->>Store: createRoutes() 生成路由
|
|
Store->>Router: addRoute() 注册路由
|
|
Store->>Store: createMenus() 生成菜单
|
|
Guard->>Router: next() 继续导航
|
|
```
|
|
|
|
### 2. 路由守卫执行流程
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[路由跳转] --> B{外链路由?}
|
|
B -->|是| C[打开新窗口]
|
|
B -->|否| D[开始 LoadingBar]
|
|
|
|
D --> E{登录页?}
|
|
E -->|是| F[直接放行]
|
|
E -->|否| G{requiresAuth=false?}
|
|
G -->|是| F
|
|
G -->|否| H{需要认证且未登录?}
|
|
H -->|是| I[重定向到登录页]
|
|
H -->|否| J{路由已初始化?}
|
|
|
|
J -->|否| K[初始化路由]
|
|
K --> L{是404页面?}
|
|
L -->|是| M[重新导航到正确路径]
|
|
L -->|否| N[继续导航]
|
|
|
|
J -->|是| O{已登录访问登录页?}
|
|
O -->|是| P[重定向到首页]
|
|
O -->|否| N
|
|
|
|
F --> N
|
|
N --> Q[设置菜单高亮]
|
|
Q --> R[添加标签页]
|
|
R --> S[设置页面标题]
|
|
S --> T[结束 LoadingBar]
|
|
```
|
|
|
|
## 🎯 路由守卫详解
|
|
|
|
### beforeEach 守卫
|
|
|
|
位置:`src/router/guard.ts`
|
|
|
|
#### 主要功能
|
|
|
|
1. **外链处理**: 检测并打开外部链接
|
|
2. **进度条控制**: 开始 LoadingBar
|
|
3. **登录验证**: 检查用户认证状态
|
|
4. **路由初始化**: 动态加载和注册路由
|
|
5. **重定向处理**: 处理各种重定向场景
|
|
|
|
#### 核心逻辑
|
|
|
|
```typescript
|
|
router.beforeEach(async (to, from, next) => {
|
|
// 1. 外链处理
|
|
if (to.meta.href) {
|
|
window.open(to.meta.href)
|
|
next(false)
|
|
return
|
|
}
|
|
|
|
// 2. 开始进度条
|
|
appStore.showProgress && window.$loadingBar?.start()
|
|
|
|
// 3. 登录验证
|
|
const isLogin = Boolean(local.get('accessToken'))
|
|
|
|
if (to.meta.requiresAuth === true && !isLogin) {
|
|
const redirect = to.name === '404' ? undefined : to.fullPath
|
|
next({ path: '/login', query: { redirect } })
|
|
return
|
|
}
|
|
|
|
// 4. 路由初始化
|
|
if (!routeStore.isInitAuthRoute) {
|
|
await routeStore.initAuthRoute()
|
|
if (to.name === '404') {
|
|
next({ path: to.fullPath, replace: true })
|
|
return
|
|
}
|
|
}
|
|
|
|
// 5. 登录页重定向
|
|
if (to.name === 'login' && isLogin) {
|
|
next({ path: '/' })
|
|
return
|
|
}
|
|
|
|
next()
|
|
})
|
|
```
|
|
|
|
### beforeResolve 守卫
|
|
|
|
```typescript
|
|
router.beforeResolve((to) => {
|
|
// 设置菜单高亮
|
|
routeStore.setActiveMenu(to.meta.activeMenu ?? to.fullPath)
|
|
// 添加标签页
|
|
tabStore.addTab(to)
|
|
// 设置当前标签页
|
|
tabStore.setCurrentTab(to.fullPath)
|
|
})
|
|
```
|
|
|
|
### afterEach 守卫
|
|
|
|
```typescript
|
|
router.afterEach((to) => {
|
|
// 设置页面标题
|
|
document.title = `${to.meta.title} - ${title}`
|
|
// 结束进度条
|
|
appStore.showProgress && window.$loadingBar?.finish()
|
|
})
|
|
```
|
|
|
|
## 🏪 路由状态管理
|
|
|
|
### RouteStore 状态
|
|
|
|
```typescript
|
|
interface RoutesStatus {
|
|
/** 路由是否已初始化 */
|
|
isInitAuthRoute: boolean
|
|
/** 菜单数据 */
|
|
menus: MenuOption[]
|
|
/** 原始路由数据 */
|
|
rowRoutes: AppRoute.RowRoute[]
|
|
/** 当前激活菜单 */
|
|
activeMenu: string | null
|
|
/** 缓存路由 */
|
|
cacheRoutes: string[]
|
|
}
|
|
```
|
|
|
|
### 核心方法
|
|
|
|
#### initAuthRoute()
|
|
|
|
初始化认证路由的完整流程:
|
|
|
|
```typescript
|
|
async initAuthRoute() {
|
|
this.isInitAuthRoute = false
|
|
|
|
// 1. 获取路由数据
|
|
const rowRoutes = await this.initRouteInfo()
|
|
if (!rowRoutes) return
|
|
|
|
this.rowRoutes = rowRoutes
|
|
|
|
// 2. 生成实际路由并注册
|
|
const routes = createRoutes(rowRoutes)
|
|
router.addRoute(routes)
|
|
|
|
// 3. 生成侧边菜单
|
|
this.menus = createMenus(rowRoutes)
|
|
|
|
// 4. 生成路由缓存
|
|
this.cacheRoutes = generateCacheRoutes(rowRoutes)
|
|
|
|
this.isInitAuthRoute = true
|
|
}
|
|
```
|
|
|
|
#### initRouteInfo()
|
|
|
|
根据配置模式获取路由信息:
|
|
|
|
```typescript
|
|
async initRouteInfo() {
|
|
if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') {
|
|
// 动态路由:从 API 获取
|
|
const userInfo = local.get('userInfo')
|
|
const { data } = await fetchUserRoutes({ id: userInfo.id })
|
|
return data
|
|
} else {
|
|
// 静态路由:使用本地配置
|
|
this.rowRoutes = staticRoutes
|
|
return staticRoutes
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🎨 菜单生成
|
|
|
|
### 菜单生成流程
|
|
|
|
```typescript
|
|
export function createMenus(routes: AppRoute.RowRoute[]) {
|
|
const { hasPermission } = usePermission()
|
|
|
|
// 1. 权限过滤
|
|
let resultMenus = routes.filter(i => hasPermission(i.roles))
|
|
|
|
// 2. 隐藏项过滤
|
|
resultMenus = resultMenus.filter(i => !i.hide)
|
|
|
|
// 3. 转换菜单格式
|
|
const menus = resultMenus.map(transformRouteToMenu)
|
|
|
|
// 4. 生成菜单树
|
|
return arrayToTree(menus)
|
|
}
|
|
```
|
|
|
|
### 菜单数据结构
|
|
|
|
```typescript
|
|
interface MenuOption {
|
|
label: string // 菜单标题
|
|
key: string // 菜单键值
|
|
icon?: () => VNode // 菜单图标
|
|
children?: MenuOption[] // 子菜单
|
|
disabled?: boolean // 是否禁用
|
|
}
|
|
```
|
|
|
|
## 🏷️ 标签页系统
|
|
|
|
### TabStore 管理
|
|
|
|
标签页系统与路由系统紧密集成:
|
|
|
|
```typescript
|
|
// 添加标签页
|
|
tabStore.addTab(route)
|
|
|
|
// 设置当前标签页
|
|
tabStore.setCurrentTab(route.fullPath)
|
|
|
|
// 关闭标签页
|
|
tabStore.closeTab(fullPath)
|
|
```
|
|
|
|
### 特殊标签页类型
|
|
|
|
1. **固定标签页** (`pinTab: true`): 不可关闭的标签页
|
|
2. **无标签页** (`withoutTab: true`): 不显示在标签栏的页面
|
|
|
|
## ⚙️ 配置选项
|
|
|
|
### 环境变量
|
|
|
|
```bash
|
|
# 路由加载模式
|
|
VITE_ROUTE_LOAD_MODE=static|dynamic
|
|
|
|
# 默认首页路径
|
|
VITE_HOME_PATH=/dashboard/monitor
|
|
|
|
# 应用名称
|
|
VITE_APP_NAME=Nova Admin
|
|
```
|
|
|
|
### 路由模式配置
|
|
|
|
```typescript
|
|
// vite.config.ts 或 .env 文件
|
|
const { VITE_ROUTE_MODE = 'hash', VITE_BASE_URL } = import.meta.env
|
|
|
|
export const router = createRouter({
|
|
history: VITE_ROUTE_MODE === 'hash'
|
|
? createWebHashHistory(VITE_BASE_URL)
|
|
: createWebHistory(VITE_BASE_URL),
|
|
routes,
|
|
})
|
|
```
|
|
|
|
## 🛠️ 开发指南
|
|
|
|
### 1. 添加新路由
|
|
|
|
#### 静态路由方式
|
|
|
|
在 `src/router/routes.static.ts` 中添加:
|
|
|
|
```typescript
|
|
{
|
|
name: 'newPage',
|
|
path: '/new-page',
|
|
title: '新页面',
|
|
requiresAuth: true,
|
|
icon: 'icon-park-outline:new',
|
|
menuType: 'page',
|
|
componentPath: '/new/page/index.vue',
|
|
id: 100,
|
|
pid: null,
|
|
}
|
|
```
|
|
|
|
#### 动态路由方式
|
|
|
|
通过后端 API 返回路由配置,格式与静态路由相同。
|
|
|
|
### 2. 路由权限配置
|
|
|
|
```typescript
|
|
{
|
|
name: 'adminPage',
|
|
path: '/admin',
|
|
title: '管理页面',
|
|
requiresAuth: true,
|
|
roles: ['admin', 'super'], // 权限控制
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### 3. 嵌套路由
|
|
|
|
```typescript
|
|
// 父路由
|
|
{
|
|
name: 'parent',
|
|
path: '/parent',
|
|
title: '父级菜单',
|
|
menuType: 'dir',
|
|
componentPath: null,
|
|
id: 1,
|
|
pid: null,
|
|
}
|
|
|
|
// 子路由
|
|
{
|
|
name: 'child',
|
|
path: '/parent/child',
|
|
title: '子级菜单',
|
|
menuType: 'page',
|
|
componentPath: '/parent/child/index.vue',
|
|
id: 2,
|
|
pid: 1, // 指向父路由ID
|
|
}
|
|
```
|
|
|
|
### 4. 路由缓存
|
|
|
|
```typescript
|
|
{
|
|
name: 'cachedPage',
|
|
path: '/cached',
|
|
title: '缓存页面',
|
|
keepAlive: true, // 启用路由缓存
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## 🐛 常见问题
|
|
|
|
### 1. 动态路由不生效
|
|
|
|
检查:
|
|
- 路由数据格式是否正确
|
|
- 组件路径是否存在
|
|
- 权限配置是否正确
|
|
|
|
### 2. 菜单不显示
|
|
|
|
检查:
|
|
- `hide` 属性是否为 `true`
|
|
- 权限验证是否通过
|
|
- `menuType` 是否正确设置
|
|
|
|
### 3. 404 页面循环
|
|
|
|
确保:
|
|
- 静态路由已正确配置
|
|
- 路由初始化完成
|
|
- 通配符路由放在最后
|
|
|
|
### 4. 标签页问题
|
|
|
|
检查:
|
|
- `withoutTab` 设置
|
|
- 路由路径是否正确
|
|
- TabStore 状态是否正常
|
|
|
|
## 🎯 最佳实践
|
|
|
|
### 1. 路由命名规范
|
|
|
|
```typescript
|
|
// 推荐命名方式
|
|
name: 'userManagement' // 驼峰命名
|
|
name: 'user-management' // 中划线命名
|
|
name: 'user_management' // 下划线命名
|
|
```
|
|
|
|
### 2. 路径规范
|
|
|
|
```typescript
|
|
// 路径应该语义化
|
|
path: '/user/management' // ✅ 好的
|
|
path: '/user/mgmt' // ❌ 避免缩写
|
|
path: '/u/m' // ❌ 避免单字符
|
|
```
|
|
|
|
### 3. 权限设计
|
|
|
|
```typescript
|
|
// 明确的权限配置
|
|
roles: ['admin'] // ✅ 单角色
|
|
roles: ['admin', 'manager'] // ✅ 多角色
|
|
requiresAuth: true // ✅ 明确需要认证
|
|
```
|
|
|
|
## 📚 相关文档
|
|
|
|
- [登录鉴权文档](./登录鉴权系统文档)
|
|
- [权限系统文档](./权限管理系统文档)
|
|
- [整体架构文档](./整体架构文档)
|