Skip to content

路由系统

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>

元信息字段

字段类型说明
titlestring页面标题
i18nKeystring国际化键名
iconstring菜单图标(Iconify)
ordernumber菜单排序
keepAliveboolean是否缓存页面
constantboolean是否为常量路由(无需登录)
permissionsstring[]权限标识
hideInMenuboolean是否在菜单中隐藏
activeMenustring激活的菜单项
multiTabboolean是否支持多标签页
fixedIndexInTabnumber固定在标签页的位置

生成路由

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();
  });
}

关键逻辑

  1. 常量路由放行:登录页、错误页等无需权限
  2. 登录检查:未登录跳转到登录页,携带 redirect 参数
  3. 路由初始化:首次进入时动态添加权限路由
  4. 权限验证:检查用户是否有访问该页面的权限

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>

相关文档