路由系统
CondorAdmin 采用基于文件系统的路由方案(Elegant Router),结合动态路由和路由守卫,实现灵活的权限控制和页面管理。
路由架构
路由类型
路由类型
├── 常量路由(Constant Routes)
│ ├── 登录页
│ ├── 404 页面
│ ├── 403 页面
│ └── 500 页面
└── 权限路由(Auth Routes)
├── 首页
├── 系统管理
│ ├── 管理员管理
│ ├── 角色管理
│ └── 菜单管理
└── ...目录结构
src/router/
├── index.ts # 路由入口
├── guard/ # 路由守卫
│ ├── index.ts # 守卫注册
│ ├── route.ts # 路由权限守卫
│ ├── progress.ts # 进度条守卫
│ └── title.ts # 页面标题守卫
├── routes/ # 路由配置
│ ├── builtin.ts # 内置路由(404/403/500)
│ └── index.ts # 路由导出
└── elegant/ # Elegant Router
├── routes.ts # 生成的路由
├── imports.ts # 组件导入
└── transform.ts # 路由转换Elegant Router
文件路由映射
Elegant Router 根据 src/views/ 下的文件结构自动生成路由。
文件结构:
src/views/
├── home/
│ └── index.vue → /home
├── system/
│ ├── admin/
│ │ └── index.vue → /system/admin
│ ├── role/
│ │ └── index.vue → /system/role
│ └── menu/
│ └── index.vue → /system/menu
└── _builtin/
├── login/
│ └── index.vue → /login
├── 404/
│ └── index.vue → /404
└── 403/
└── index.vue → /403命名规则:
| 文件/目录 | 路由路径 | 说明 |
|---|---|---|
home/index.vue | /home | 普通路由 |
system_admin/index.vue | /system/admin | 下划线转斜杠 |
user-center/index.vue | /user-center | 连字符保留 |
_builtin/login/ | /login | 下划线开头表示特殊目录 |
[id]/index.vue | /:id | 动态路由参数 |
路由元信息
在页面组件中定义路由元信息:
vue
<!-- src/views/system/admin/index.vue -->
<script setup lang="ts">
defineOptions({
name: 'SystemAdmin'
});
</script>
<route lang="json">
{
"meta": {
"title": "管理员管理",
"i18nKey": "route.system_admin",
"icon": "mdi:account-supervisor",
"order": 1,
"keepAlive": true,
"constant": false,
"permissions": ["admin:index"]
}
}
</route>
<template>
<div>管理员管理页面</div>
</template>元信息字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| title | string | 页面标题 |
| i18nKey | string | 国际化键名 |
| icon | string | 菜单图标(Iconify) |
| order | number | 菜单排序 |
| keepAlive | boolean | 是否缓存页面 |
| constant | boolean | 是否为常量路由(无需登录) |
| permissions | string[] | 权限标识 |
| hideInMenu | boolean | 是否在菜单中隐藏 |
| activeMenu | string | 激活的菜单项 |
| multiTab | boolean | 是否支持多标签页 |
| fixedIndexInTab | number | 固定在标签页的位置 |
生成路由
bash
# 手动生成路由
pnpm sa gen-route
# 自动生成(开发模式下文件变化时自动触发)
pnpm dev生成的路由文件:
src/router/elegant/routes.ts- 路由配置src/router/elegant/imports.ts- 组件导入src/typings/elegant-router.d.ts- 类型定义
路由守卫
守卫执行顺序
用户导航到新页面
↓
1. createProgressGuard() # 显示进度条
↓
2. createRouteGuard() # 权限验证
├─ 检查登录状态
├─ 初始化路由
├─ 验证页面权限
└─ 处理重定向
↓
3. createDocumentTitleGuard() # 设置页面标题
↓
页面渲染完成
↓
进度条隐藏1. 进度条守卫
位置:src/router/guard/progress.ts
typescript
import NProgress from 'nprogress';
export function createProgressGuard(router: Router) {
router.beforeEach(() => {
NProgress.start();
});
router.afterEach(() => {
NProgress.done();
});
}2. 路由权限守卫
位置:src/router/guard/route.ts
typescript
export function createRouteGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();
const routeStore = useRouteStore();
// 常量路由(登录页、404等)直接放行
if (to.meta.constant) {
next();
return;
}
// 未登录,跳转到登录页
if (!authStore.isLogin) {
const redirect = to.fullPath;
next({ name: 'login', query: { redirect } });
return;
}
// 已登录但未初始化路由
if (!routeStore.isInitAuthRoute) {
await routeStore.initAuthRoute();
// 重新导航到目标页面
next({ ...to, replace: true });
return;
}
// 404 页面直接放行
if (to.name === 'not-found') {
next();
return;
}
// 检查权限
const hasPermission = authStore.hasPermission(to.meta.permissions);
if (!hasPermission) {
next({ name: '403' });
return;
}
next();
});
}关键逻辑:
- 常量路由放行:登录页、错误页等无需权限
- 登录检查:未登录跳转到登录页,携带
redirect参数 - 路由初始化:首次进入时动态添加权限路由
- 权限验证:检查用户是否有访问该页面的权限
3. 页面标题守卫
位置:src/router/guard/title.ts
typescript
export function createDocumentTitleGuard(router: Router) {
router.afterEach((to) => {
const { i18nKey, title } = to.meta;
const appName = import.meta.env.VITE_APP_TITLE;
const pageTitle = i18nKey ? $t(i18nKey) : title;
document.title = pageTitle ? `${pageTitle} - ${appName}` : appName;
});
}动态路由
初始化权限路由
流程:
typescript
// src/store/modules/route/index.ts
async function initAuthRoute() {
if (isInitAuthRoute.value) return;
// 1. 获取后端返回的菜单数据
const { data: menuData } = await fetchGetUserRoutes();
// 2. 转换为 Vue Router 路由
const routes = transformMenuToRoutes(menuData);
// 3. 添加到 Router
routes.forEach(route => {
router.addRoute(route);
});
// 4. 生成菜单树
menus.value = transformMenus(routes);
// 5. 标记已初始化
isInitAuthRoute.value = true;
}后端菜单数据格式
json
{
"code": "0000",
"data": [
{
"id": 1,
"pid": 0,
"type": "menu",
"title": "系统管理",
"path": "/system",
"name": "system",
"component": "layout.base",
"icon": "mdi:cog",
"order": 1,
"children": [
{
"id": 2,
"pid": 1,
"type": "menu",
"title": "管理员管理",
"path": "/system/admin",
"name": "system_admin",
"component": "view.system_admin",
"icon": "mdi:account-supervisor",
"order": 1
}
]
}
]
}路由转换
typescript
function transformMenuToRoutes(menus: Api.Route.MenuRoute[]): RouteRecordRaw[] {
return menus.map(menu => {
const route: RouteRecordRaw = {
name: menu.name,
path: menu.path,
component: getComponent(menu.component),
meta: {
title: menu.title,
icon: menu.icon,
order: menu.order,
permissions: menu.permissions
}
};
if (menu.children?.length) {
route.children = transformMenuToRoutes(menu.children);
}
return route;
});
}路由模式
静态路由模式
所有路由在前端定义,后端仅返回权限列表,前端根据权限过滤路由。
配置:
bash
# .env
VITE_AUTH_ROUTE_MODE=static优点:
- 前端完全控制路由
- 无需等待后端接口
缺点:
- 新增路由需要前端发版
- 不够灵活
动态路由模式(推荐)
路由由后端返回,前端动态添加。
配置:
bash
# .env
VITE_AUTH_ROUTE_MODE=dynamic优点:
- 后端控制路由,无需前端发版
- 灵活性高
缺点:
- 依赖后端接口
- 首次加载稍慢
路由跳转
编程式导航
typescript
import { useRouter } from 'vue-router';
const router = useRouter();
// 跳转到指定路径
router.push('/system/admin');
// 跳转到命名路由
router.push({ name: 'system_admin' });
// 携带参数
router.push({
name: 'system_admin',
query: { id: 1 }
});
// 替换当前历史记录
router.replace('/system/admin');
// 后退
router.back();
// 前进
router.forward();声明式导航
vue
<template>
<!-- 路径跳转 -->
<router-link to="/system/admin">
管理员管理
</router-link>
<!-- 命名路由跳转 -->
<router-link :to="{ name: 'system_admin' }">
管理员管理
</router-link>
<!-- 携带参数 -->
<router-link :to="{ name: 'system_admin', query: { id: 1 } }">
编辑管理员
</router-link>
</template>路由 Hook
typescript
import { useRouterPush } from '@/hooks/common/router';
const { routerPush, routerBack } = useRouterPush();
// 跳转并关闭当前标签页
routerPush('/system/admin');
// 后退
routerBack();路由缓存
keep-alive
在路由元信息中配置 keepAlive: true:
vue
<route lang="json">
{
"meta": {
"keepAlive": true
}
}
</route>实现:
vue
<!-- src/layouts/base-layout/index.vue -->
<template>
<router-view v-slot="{ Component, route }">
<keep-alive :include="cacheRoutes">
<component :is="Component" :key="route.name" />
</keep-alive>
</router-view>
</template>
<script setup lang="ts">
import { useTabStore } from '@/store';
const tabStore = useTabStore();
const cacheRoutes = computed(() => tabStore.cacheRoutes);
</script>清除缓存
typescript
// 移除单个缓存
tabStore.removeCacheRoute('system_admin');
// 清空所有缓存
tabStore.clearCacheRoutes();路由参数
Query 参数
typescript
// 跳转时传递
router.push({
name: 'system_admin',
query: { id: 1, status: 'active' }
});
// 获取参数
const route = useRoute();
const id = route.query.id; // '1'
const status = route.query.status; // 'active'Params 参数
typescript
// 定义动态路由
{
path: '/user/:id',
name: 'user-detail',
component: () => import('@/views/user/detail.vue')
}
// 跳转时传递
router.push({
name: 'user-detail',
params: { id: 1 }
});
// 获取参数
const route = useRoute();
const id = route.params.id; // '1'菜单生成
从路由生成菜单
typescript
function transformRoutesToMenus(routes: RouteRecordRaw[]): App.Global.Menu[] {
return routes
.filter(route => !route.meta?.hideInMenu)
.map(route => ({
key: route.name as string,
label: route.meta?.title || '',
icon: route.meta?.icon,
order: route.meta?.order || 0,
children: route.children
? transformRoutesToMenus(route.children)
: undefined
}))
.sort((a, b) => a.order - b.order);
}菜单激活
typescript
// 当前激活的菜单项
const activeMenu = computed(() => {
const route = useRoute();
return route.meta.activeMenu || route.name;
});面包屑
生成面包屑
typescript
function getBreadcrumbs(route: RouteLocationNormalizedLoaded) {
const matched = route.matched.filter(item => item.meta?.title);
return matched.map(item => ({
key: item.name as string,
label: item.meta.title || '',
path: item.path
}));
}使用面包屑
vue
<template>
<n-breadcrumb>
<n-breadcrumb-item
v-for="item in breadcrumbs"
:key="item.key"
@click="router.push(item.path)"
>
{{ item.label }}
</n-breadcrumb-item>
</n-breadcrumb>
</template>
<script setup lang="ts">
const route = useRoute();
const breadcrumbs = computed(() => getBreadcrumbs(route));
</script>最佳实践
1. 路由命名规范
typescript
// ✅ 使用下划线分隔
{ name: 'system_admin' }
{ name: 'user_detail' }
// ❌ 使用驼峰或连字符
{ name: 'systemAdmin' }
{ name: 'user-detail' }2. 路由懒加载
typescript
// ✅ 懒加载
{
component: () => import('@/views/system/admin/index.vue')
}
// ❌ 同步加载
{
component: import('@/views/system/admin/index.vue')
}3. 路由重定向
typescript
// 首页重定向
{
path: '/',
redirect: '/home'
}
// 子路由默认重定向
{
path: '/system',
redirect: '/system/admin',
children: [...]
}4. 404 处理
typescript
// 捕获所有未匹配路由
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/_builtin/404/index.vue')
}常见问题
Q1: 路由跳转后页面空白?
检查路由是否已添加到 Router:
typescript
console.log(router.getRoutes());Q2: 权限路由不生效?
确保 initAuthRoute() 已执行:
typescript
console.log(routeStore.isInitAuthRoute);Q3: keep-alive 缓存失效?
检查组件是否定义了 name 选项:
vue
<script setup lang="ts">
defineOptions({
name: 'SystemAdmin' // 必须与路由 name 一致
});
</script>相关文档
- 📖 状态管理
- 🔐 权限系统
- 🎨 主题系统
- 📦 Packages 详解