Skip to content

路由和菜单

了解 vue3-element-admin 的路由配置和菜单管理。

路由配置

静态路由

src/router/routes.ts 中配置静态路由:

typescript
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'Dashboard',
        meta: { title: '首页', icon: 'homepage', affix: true }
      }
    ]
  }
]

动态路由

动态路由根据用户权限从后端获取:

typescript
// store/modules/permission.ts
export const usePermissionStore = defineStore('permission', () => {
  const routes = ref<RouteRecordRaw[]>([])

  async function generateRoutes(roles: string[]) {
    // 获取后端返回的路由
    const res = await getRoutes()
    const asyncRoutes = res.data

    // 过滤有权限的路由
    const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)

    routes.value = constantRoutes.concat(accessedRoutes)

    return accessedRoutes
  }

  return { routes, generateRoutes }
})

路由元信息

在路由的 meta 中配置菜单相关信息:

typescript
{
  path: '/system',
  component: Layout,
  meta: {
    title: '系统管理',      // 菜单标题
    icon: 'setting',        // 菜单图标
    hidden: false,          // 是否隐藏菜单
    alwaysShow: true,       // 是否总是显示根菜单
    roles: ['ADMIN'],       // 允许访问的角色
    perms: ['sys:menu'],    // 需要的权限
    keepAlive: true,        // 是否缓存页面
    affix: false,           // 是否固定在 tags-view
    noCache: false,         // 是否不缓存
    breadcrumb: true,       // 是否显示面包屑
    activeMenu: '/system/user' // 激活的菜单路径
  }
}

meta 属性说明

属性类型说明
titlestring菜单标题
iconstring菜单图标(支持 Element Plus 图标和自定义图标)
hiddenboolean是否隐藏菜单,默认 false
alwaysShowboolean是否总是显示根菜单,默认 false
rolesstring[]允许访问的角色
permsstring[]需要的权限标识
keepAliveboolean是否缓存页面,默认 false
affixboolean是否固定在 tags-view,默认 false
noCacheboolean是否不缓存,默认 false
breadcrumbboolean是否显示在面包屑,默认 true
activeMenustring激活的菜单路径

菜单图标

Element Plus 图标

typescript
{
  path: '/user',
  meta: {
    title: '用户管理',
    icon: 'user' // Element Plus 图标名称
  }
}

自定义 SVG 图标

typescript
{
  path: '/system',
  meta: {
    title: '系统管理',
    icon: 'system' // 对应 src/assets/icons/system.svg
  }
}

多级菜单

二级菜单

typescript
{
  path: '/system',
  component: Layout,
  redirect: '/system/user',
  meta: { title: '系统管理', icon: 'setting' },
  children: [
    {
      path: 'user',
      component: () => import('@/views/system/user/index.vue'),
      meta: { title: '用户管理', icon: 'user' }
    },
    {
      path: 'role',
      component: () => import('@/views/system/role/index.vue'),
      meta: { title: '角色管理', icon: 'role' }
    }
  ]
}

三级菜单

typescript
{
  path: '/nested',
  component: Layout,
  redirect: '/nested/menu1',
  meta: { title: '多级菜单', icon: 'nested' },
  children: [
    {
      path: 'menu1',
      component: () => import('@/views/nested/menu1/index.vue'),
      redirect: '/nested/menu1/menu1-1',
      meta: { title: '菜单1', icon: 'menu' },
      children: [
        {
          path: 'menu1-1',
          component: () => import('@/views/nested/menu1/menu1-1/index.vue'),
          meta: { title: '菜单1-1' }
        }
      ]
    }
  ]
}

外链菜单

外部链接

typescript
{
  path: 'https://www.baidu.com',
  meta: { title: '百度', icon: 'link' }
}

内嵌 iframe

typescript
{
  path: '/external',
  component: Layout,
  children: [
    {
      path: 'https://www.baidu.com',
      meta: {
        title: '百度',
        icon: 'link',
        iframe: true // 内嵌 iframe
      }
    }
  ]
}

隐藏菜单

隐藏路由

不在侧边栏显示,但可以访问:

typescript
{
  path: '/user',
  component: Layout,
  meta: { hidden: true },
  children: [
    {
      path: 'profile',
      component: () => import('@/views/user/profile.vue'),
      meta: { title: '个人中心' }
    }
  ]
}

详情页面

详情页面通常隐藏在菜单中:

typescript
{
  path: '/user',
  component: Layout,
  children: [
    {
      path: 'index',
      component: () => import('@/views/user/index.vue'),
      meta: { title: '用户管理' }
    },
    {
      path: 'detail/:id',
      component: () => import('@/views/user/detail.vue'),
      meta: {
        title: '用户详情',
        hidden: true,
        activeMenu: '/user/index' // 激活用户管理菜单
      }
    }
  ]
}

路由传参

动态路由参数

typescript
// 路由配置
{
  path: 'detail/:id',
  component: () => import('@/views/user/detail.vue')
}

// 跳转
router.push({ path: '/user/detail/1' })

// 获取参数
const route = useRoute()
const id = route.params.id

Query 参数

typescript
// 跳转
router.push({
  path: '/user/detail',
  query: { id: 1 }
})

// 获取参数
const route = useRoute()
const id = route.query.id

页面缓存

使用 keep-alive 缓存页面:

typescript
{
  path: '/user',
  component: () => import('@/views/user/index.vue'),
  meta: {
    title: '用户管理',
    keepAlive: true // 开启缓存
  }
}

注意:组件必须设置 name 属性,且与路由的 name 保持一致。

vue
<script setup lang="ts">
defineOptions({
  name: 'User' // 与路由 name 一致
})
</script>

面包屑

面包屑自动根据路由生成:

typescript
{
  path: '/system',
  meta: { title: '系统管理', breadcrumb: true },
  children: [
    {
      path: 'user',
      meta: { title: '用户管理', breadcrumb: true }
    }
  ]
}

显示效果:系统管理 / 用户管理

路由守卫

全局前置守卫

typescript
router.beforeEach(async (to, from, next) => {
  // 设置页面标题
  document.title = to.meta.title || '默认标题'

  // 判断是否登录
  const token = getToken()
  if (token) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      // 判断是否已获取用户信息
      const hasRoles = store.state.user.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // 获取用户信息
          await store.dispatch('user/getUserInfo')
          // 生成动态路由
          const accessRoutes = await store.dispatch('permission/generateRoutes')
          // 动态添加路由
          accessRoutes.forEach(route => {
            router.addRoute(route)
          })
          next({ ...to, replace: true })
        } catch (error) {
          await store.dispatch('user/logout')
          next({ path: '/login' })
        }
      }
    }
  } else {
    // 白名单直接访问
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next({ path: '/login', query: { redirect: to.path } })
    }
  }
})

菜单管理实践

说明

本节介绍如何在后台管理系统中操作菜单,包括新增菜单、一级菜单、多级菜单以及开启页面缓存功能。

新增菜单

在系统中,菜单通常呈现为目录 + 菜单的二级结构。首先需要创建一个目录,然后在该目录下添加菜单。

1. 新增目录

系统管理 > 菜单管理页面,点击"新增"按钮,进行以下配置:

操作步骤

  1. 选择顶级菜单作为父级菜单
  2. 设置菜单名称(例如:系统管理)
  3. 选择目录作为菜单类型
  4. 路由路径需要以 / 开头(例如:/system
  5. 选择图标
  6. 设置排序值
  7. 其他使用默认设置即可

新增目录

对应的路由配置

typescript
{
  path: '/system',
  component: Layout,
  meta: {
    title: '系统管理',
    icon: 'setting',
    alwaysShow: true
  }
}

2. 新增菜单

在目录下新增菜单,选择刚添加的目录作为父级菜单。

操作步骤

  1. 选择刚添加的目录作为父级菜单
  2. 填写菜单名称(例如:用户管理)
  3. 填写路由名称(例如:User)
  4. 填写路由路径(例如:user,不需要 / 开头)
  5. 填写组件路径(例如:system/user/index,与实际页面组件路径一致)
  6. 选择显示状态为"显示"
  7. 设置排序值

新增菜单

对应的路由配置

typescript
{
  path: '/system',
  component: Layout,
  children: [
    {
      path: 'user',
      component: () => import('@/views/system/user/index.vue'),
      name: 'User',
      meta: {
        title: '用户管理',
        icon: 'user'
      }
    }
  ]
}

注意事项

  • ✅ 确保路由名称(name)唯一
  • ✅ 组件路径与页面组件的实际路径一致(不需要 @/views/ 前缀和 .vue 后缀)
  • ✅ 配置显示状态和排序值
  • ✅ 如果是菜单(非目录),必须填写组件路径

新增一级菜单

系统支持创建一级菜单,但不直接支持将菜单作为根菜单。需要通过特殊配置实现。

核心技巧

通过将目录的"始终显示"设置为,使其唯一的子菜单呈现为一级菜单。

操作步骤

1. 添加一级目录

在菜单管理页面创建一级目录,关键配置:

  • 父级菜单:选择顶级菜单
  • 菜单名称:随意填写(例如:首页目录)
  • 菜单类型:选择目录
  • 路由路径:例如 /dashboard
  • 始终显示:设置为(关键配置)

新增一级目录

2. 添加子菜单

在刚才创建的目录下添加子菜单:

  • 父级菜单:选择刚创建的目录
  • 菜单名称:例如"首页"
  • 路由名称:例如 Dashboard
  • 路由路径:例如 index
  • 组件路径:例如 dashboard/index

添加子菜单

3. 效果展示

最终效果:侧边栏只显示"首页",不显示父级目录,呈现为一级菜单。

一级菜单效果

对应的路由配置

typescript
{
  path: '/dashboard',
  component: Layout,
  redirect: '/dashboard/index',
  meta: {
    alwaysShow: false  // 关键:不总是显示父菜单
  },
  children: [
    {
      path: 'index',
      component: () => import('@/views/dashboard/index.vue'),
      name: 'Dashboard',
      meta: {
        title: '首页',
        icon: 'dashboard',
        affix: true  // 固定在标签栏
      }
    }
  ]
}

注意事项

  • 一级目录下只能有一个子菜单,否则会显示为二级结构
  • 必须将目录的 alwaysShow 设置为 false
  • 子菜单的图标和名称会直接显示在侧边栏

新增多级菜单

多级菜单的操作与普通的二级菜单类似,首先需要创建一级目录,然后增加二级目录,相对应的在多级目录下创建菜单即可。

操作步骤

1. 创建一级目录

  • 父级菜单:顶级菜单
  • 菜单类型:目录
  • 示例:系统管理

2. 创建二级目录

  • 父级菜单:选择刚创建的一级目录
  • 菜单类型:目录
  • 示例:权限管理

3. 创建三级菜单

  • 父级菜单:选择二级目录
  • 菜单类型:菜单
  • 填写组件路径
  • 示例:用户管理、角色管理

路由配置示例

三级菜单结构

typescript
{
  path: '/system',
  component: Layout,
  meta: {
    title: '系统管理',
    icon: 'setting'
  },
  children: [
    {
      path: 'permission',
      redirect: '/system/permission/user',
      meta: {
        title: '权限管理',
        icon: 'permission'
      },
      children: [
        {
          path: 'user',
          component: () => import('@/views/system/permission/user/index.vue'),
          name: 'PermissionUser',
          meta: { title: '用户管理' }
        },
        {
          path: 'role',
          component: () => import('@/views/system/permission/role/index.vue'),
          name: 'PermissionRole',
          meta: { title: '角色管理' }
        }
      ]
    }
  ]
}

提示

  • 多级菜单理论上支持无限层级,但建议不超过 3 级,以保持良好的用户体验
  • 二级目录需要设置 redirect 属性,指向默认显示的子菜单
  • 每个菜单的 name 必须唯一

开启页面缓存

项目支持菜单页面的缓存(keep-alive)功能,可以在切换菜单时保留页面状态,提升用户体验。

缓存原理

Vue 的 keep-alive 通过组件名称(name)来缓存组件实例。因此,路由名称必须与组件名称完全一致。

后台配置步骤

在菜单管理页面进行以下配置:

1. 开启缓存选项

在新增或编辑菜单时,勾选"是否缓存"选项:

开启页面缓存

2. 确保路由名称与组件名称一致

  • 菜单的路由名称:例如 User
  • 组件的 name 属性:必须也是 User

代码配置

路由配置

typescript
{
  path: 'user',
  component: () => import('@/views/system/user/index.vue'),
  name: 'User',  // 路由名称
  meta: {
    title: '用户管理',
    keepAlive: true  // 开启缓存
  }
}

组件配置

vue
<script setup lang="ts">
// 组件名称必须与路由名称一致
defineOptions({
  name: 'User'  // 与路由 name 保持一致
})

// 页面逻辑...
</script>

<template>
  <div>
    <!-- 页面内容 -->
  </div>
</template>

验证缓存是否生效

  1. 访问需要缓存的页面,进行一些操作(例如滚动、输入内容)
  2. 切换到其他页面
  3. 再次返回该页面,检查页面状态是否保留

注意事项

  • ✅ 路由 name 必须与组件 defineOptions 中的 name 完全一致(大小写敏感)
  • ✅ 组件名称建议使用大驼峰命名(PascalCase),例如:UserManagement
  • ✅ 缓存的组件在切换时会保留表单输入、滚动位置等状态
  • ⚠️ 如果名称不一致,缓存将不会生效
  • ⚠️ 组件必须使用 defineOptions 定义名称,export default 方式不支持

常见问题

问:为什么我开启了缓存,但页面还是会重新加载?

答:请检查以下几点:

  1. 路由的 name 和组件的 name 是否完全一致
  2. 组件是否使用了 defineOptions({ name: 'XXX' })
  3. 路由的 meta.keepAlive 是否设置为 true

问:缓存的组件如何刷新数据?

答:可以使用以下生命周期钩子:

typescript
import { onActivated } from 'vue'

// 每次激活时刷新数据
onActivated(() => {
  loadData()
})

常见问题

1. 菜单不显示

可能原因

  • meta.hidden 设置为 true
  • 没有权限访问该菜单
  • 路由配置错误

解决方案

  • 检查路由配置中的 hidden 属性
  • 确认用户角色是否有权限
  • 验证路由路径和组件路径是否正确

2. 页面缓存不生效

可能原因

  • 路由 name 与组件 name 不一致
  • 未设置 keepAlive: true
  • 组件未使用 defineOptions 定义名称

解决方案

  • 确保路由和组件的名称一致
  • 在路由 meta 中添加 keepAlive: true
  • 使用 defineOptions 定义组件名称

3. 面包屑显示异常

可能原因

  • 路由层级结构不正确
  • meta.title 未配置

解决方案

  • 检查路由的父子关系
  • 为每个路由配置 meta.title

相关链接

基于 MIT 许可发布