Skip to content

路由和菜单

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

路由配置

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

typescript
export const Layout = () => import("@/layouts/index.vue");

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

动态路由根据用户权限从后端获取,主链路如下:

typescript
// src/store/modules/permission-store.ts
const data = await MenuAPI.getRoutes();
const dynamicRoutes = transformRoutes(data);
routes.value = [...constantRoutes, ...dynamicRoutes];

路由元信息

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

typescript
{
  path: '/system',
  component: Layout,
  meta: {
    title: '系统管理',      // 菜单标题
    icon: 'setting',        // 菜单图标(支持 Element Plus 图标和自定义 SVG)
    hidden: false,          // 是否隐藏菜单
    alwaysShow: true,       // 是否总是显示根菜单
    keepAlive: true,        // 是否缓存页面
    affix: false,           // 是否固定在 tags-view
    breadcrumb: true,       // 是否显示面包屑
    activeMenu: '/system/user' // 激活的菜单路径(用于详情页等特殊场景)
  }
}
属性类型说明
titlestring菜单标题
iconstring菜单图标(支持 Element Plus 图标和自定义图标)
hiddenboolean是否隐藏菜单,默认 false
alwaysShowboolean是否总是显示根菜单,默认 false
keepAliveboolean是否缓存页面,默认 false
affixboolean是否固定在 tags-view,默认 false
breadcrumbboolean是否显示在面包屑,默认 true
activeMenustring激活的菜单路径

多级菜单

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' }
    }
  ]
}

// 三级菜单
{
  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
{
  path: '/external',
  component: Layout,
  children: [
    {
      path: 'https://www.baidu.com',
      meta: { title: '百度', icon: 'link' }
    }
  ]
}

隐藏路由

不在侧边栏显示,但可以访问(常用于详情页):

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

路由传参

typescript
// 动态路由参数
router.push({ path: '/user/detail/1' })
const id = route.params.id

// Query 参数
router.push({ path: '/user/detail', query: { id: 1 } })
const id = route.query.id

页面缓存

使用 keep-alive 缓存页面,组件必须设置 name 属性且与路由 name 一致:

typescript
{
  path: '/system/user',
  name: 'User',
  component: () => import('@/views/system/user/index.vue'),
  meta: { title: '用户管理', keepAlive: true }
}
vue
<script setup lang="ts">
defineOptions({ name: "User" });
</script>

路由守卫

typescript
// src/router/guards/permission.ts
export function setupPermissionGuard() {
  const whiteList = ["/login"];

  router.beforeEach(async (to, from, next) => {
    const isLoggedIn = useUserStore().isLoggedIn();

    if (!isLoggedIn) {
      whiteList.includes(to.path) ? next() : next(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
      return;
    }

    if (to.path === "/login") {
      next({ path: "/" });
      return;
    }

    const permissionStore = usePermissionStore();
    if (!permissionStore.isRouteGenerated) {
      const dynamicRoutes = await permissionStore.generateRoutes();
      dynamicRoutes.forEach((route) => router.addRoute(route));
      next({ ...to, replace: true });
      return;
    }

    next();
  });
}

后台菜单配置

菜单管理不是简单地“填一行菜单”。在 vue3-element-admin 中,菜单数据会同时影响侧边栏、动态路由、页面缓存、按钮权限和角色授权。下面按实际使用场景说明怎么配置。

进入方式:登录后台 →「系统管理」→「菜单管理」。新增或修改完成后,记得去「角色管理」给角色分配菜单权限,然后重新登录或刷新权限缓存。

配置前先看这 4 条

  1. 目录只负责分组和承载布局,不填写页面组件,不直接对应 src/views 下的页面。
  2. 菜单才对应真实页面,需要填写访问路径和页面组件。
  3. 顶级页面不要直接建成菜单。如果要做一个侧边栏一级菜单,需要先建顶级目录,再在目录下建唯一子菜单。
  4. 只有一个可见子菜单的目录默认会折叠显示子菜单。这就是「首页」「代码生成」这类看起来是一级菜单、实际由“目录 + 子菜单”组成的配置方式。

内置「首页」是静态路由,通常不需要在后台重复创建。新增类似「代码生成」这样的顶级一级菜单时,按下面的“顶级目录 + 唯一子菜单”方式配置即可。

路径填写规则:

场景访问路径怎么填
顶级目录填完整路径,以 / 开头,例如 /system/codegen
子级目录只填当前层级片段,例如 configreport
页面菜单只填当前页面片段,例如 userindexbasic
最终访问地址由父级路径和当前路径拼接,例如 /system + user = /system/user

页面组件填写规则:

字段值实际文件
system/user/indexsrc/views/system/user/index.vue
codegen/indexsrc/views/codegen/index.vue
demo/multi-level/level-one/level-two/level-three-a/indexsrc/views/demo/multi-level/level-one/level-two/level-three-a/index.vue

新增已有目录下的页面菜单

适合这种效果:

text
系统管理
└── 岗位管理

第一步,先创建页面文件:

text
src/views/system/post/index.vue

如果需要页面缓存,在页面中声明组件名称:

vue
<script setup lang="ts">
defineOptions({ name: "Post" });
</script>

第二步,在后台新增菜单:

  1. 进入「菜单管理」
  2. 找到「系统管理」这一行,点击「新增」
  3. 菜单类型选择「菜单」
  4. 按下面填写并保存
字段
父级菜单系统管理
菜单名称岗位管理
菜单类型菜单
访问路径post
页面组件system/post/index
页面缓存按需开启
页面标识开启缓存时填写 Post
显示状态显示
图标按需选择
排序同级菜单中的显示顺序

最终效果:

text
路由地址:/system/post
页面文件:src/views/system/post/index.vue
侧边栏:系统管理 → 岗位管理

最后到「角色管理」给角色勾选「岗位管理」,重新登录后即可看到菜单。

新增顶级一级菜单

适合这种效果:

text
代码生成

虽然侧边栏只显示一个「代码生成」,但推荐配置不是直接建一个顶级菜单,而是建成下面这种结构:

text
代码生成目录(目录,负责挂载 Layout,不作为菜单项显示)
└── 代码生成(菜单,真正显示在侧边栏并打开页面)

这样做的原因是:顶级路由需要布局组件承载,真实页面放在它的子路由中。侧边栏渲染时,如果一个目录只有 1 个可见子菜单,并且「单子级显示」选择「显示子级」,系统会直接把这个子菜单显示成一级菜单。

第一步,创建页面文件:

text
src/views/codegen/index.vue

需要缓存时,页面中声明:

vue
<script setup lang="ts">
defineOptions({ name: "Codegen" });
</script>

第二步,新增顶级目录:

  1. 在「菜单管理」点击页面左上角「新增」
  2. 菜单类型选择「目录」
  3. 按下面填写并保存
字段
父级菜单顶级菜单
菜单名称代码生成
菜单类型目录
访问路径/codegen
默认跳转/codegen/index
单子级显示显示子级
显示状态显示
图标code
排序按需填写

这里的「单子级显示 = 显示子级」对应路由 meta 中的 alwaysShow: false,也就是旧说法里的“始终显示为否”。这样侧边栏会使用唯一子菜单作为实际显示节点。

第三步,在目录下新增唯一子菜单:

  1. 找到刚创建的「代码生成」目录
  2. 点击这一行后的「新增」
  3. 菜单类型选择「菜单」
  4. 按下面填写并保存
字段
父级菜单代码生成
菜单名称代码生成
菜单类型菜单
访问路径index
页面组件codegen/index
页面缓存开启
页面标识Codegen
显示状态显示
图标code

最终效果:

text
路由地址:/codegen/index
页面文件:src/views/codegen/index.vue
侧边栏:代码生成

如果你想让侧边栏显示成「代码生成」目录并展开子菜单,把父级目录的「单子级显示」改成「始终显示本级」。

新增多级菜单

适合这种效果:

text
多级菜单
└── 一级菜单
    └── 二级菜单
        ├── 三级菜单 A
        └── 三级菜单 B

配置多级菜单时,当前项目推荐和 mock 保持一致:顶层和中间层用目录,最终可访问页面用菜单

这里的“中间层用目录”不是泛泛而谈。vue3-element-admin 的多级菜单演示中,多级菜单一级菜单二级菜单 都是目录,只有 三级菜单 A三级菜单 B 是菜单:

text
多级菜单(目录 C,component = Layout)
└── 一级菜单(目录 C,component = Layout)
    └── 二级菜单(目录 C,component = Layout)
        ├── 三级菜单 A(菜单 M,component = demo/multi-level/level-one/level-two/level-three-a/index)
        └── 三级菜单 B(菜单 M,component = demo/multi-level/level-one/level-two/level-three-b/index)

前端转换动态路由时,会保留顶级目录的 Layout,但会把非顶层目录的 Layout 组件置空。也就是说,中间目录只负责生成菜单层级和路由嵌套,不渲染自己的业务页面。

演示菜单要让用户一眼看出“这是多级菜单”,所以菜单标题保留 一级菜单二级菜单三级菜单 A/B。代码命名则使用可读、可推导的英文路径,避免 multi-level1children/children 这类随意命名:

对象推荐规则示例
访问路径使用小写 kebab-case,只写当前层级片段level-onelevel-twolevel-three-a
页面组件和路由层级保持镜像,放到 src/viewsdemo/multi-level/level-one/level-two/level-three-a/index
页面标识使用 PascalCase,从完整路径推导,保持唯一MultiLevelLevelThreeA

第一步,新增顶级目录「多级菜单」:

字段
父级菜单顶级菜单
菜单名称多级菜单
菜单类型目录
访问路径/multi-level
默认跳转/multi-level/level-one/level-two/level-three-a
单子级显示始终显示本级
显示状态显示

第二步,在「多级菜单」下新增子目录「一级菜单」:

字段
父级菜单多级菜单
菜单名称一级菜单
菜单类型目录
访问路径level-one
默认跳转/multi-level/level-one/level-two/level-three-a
单子级显示始终显示本级
显示状态显示

第三步,在「一级菜单」下新增子目录「二级菜单」:

字段
父级菜单一级菜单
菜单名称二级菜单
菜单类型目录
访问路径level-two
默认跳转/multi-level/level-one/level-two/level-three-a
单子级显示始终显示本级
显示状态显示

第四步,在「二级菜单」下新增页面菜单「三级菜单 A」:

字段
父级菜单二级菜单
菜单名称三级菜单 A
菜单类型菜单
访问路径level-three-a
页面组件demo/multi-level/level-one/level-two/level-three-a/index
页面缓存开启
页面标识MultiLevelLevelThreeA
显示状态显示

第五步,新增页面菜单「三级菜单 B」:

字段
父级菜单二级菜单
菜单名称三级菜单 B
菜单类型菜单
访问路径level-three-b
页面组件demo/multi-level/level-one/level-two/level-three-b/index
页面缓存开启
页面标识MultiLevelLevelThreeB
显示状态显示

对应页面文件:

text
src/views/demo/multi-level/level-one/level-two/level-three-a/index.vue
src/views/demo/multi-level/level-one/level-two/level-three-b/index.vue

最终路由:

text
/multi-level/level-one/level-two/level-three-a
/multi-level/level-one/level-two/level-three-b

当前项目 mock 的多级菜单对应关系如下:

后台菜单类型访问路径页面组件是否缓存
多级菜单目录/multi-level不填
一级菜单目录level-one不填
二级菜单目录level-two不填
三级菜单 A菜单level-three-ademo/multi-level/level-one/level-two/level-three-a/index可开启
三级菜单 B菜单level-three-bdemo/multi-level/level-one/level-two/level-three-b/index可开启

多级菜单缓存

当前项目支持多级菜单最终页面缓存。缓存只对真实页面菜单生效,不对中间目录生效。

以「三级菜单 A」页面为例:

配置项
菜单类型菜单
访问路径level-three-a
页面组件demo/multi-level/level-one/level-two/level-three-a/index
页面缓存开启
页面标识MultiLevelLevelThreeA

页面组件中声明同名 name

vue
<script setup lang="ts">
defineOptions({ name: "MultiLevelLevelThreeA" });
</script>

缓存机制说明:

  1. 菜单接口返回 meta.keepAlive = true 后,页面进入标签栏时会加入缓存列表。
  2. 后台「页面标识」仍按页面组件 name 填写,例如 MultiLevelLevelThreeA
  3. 当前项目底层使用 route.fullPath 包装缓存组件,因此多级路由、带 query 的页面都可以独立缓存。
  4. 中间目录没有业务页面,不会进入 keep-alive 缓存;只需要给最终页面菜单开启缓存。
  5. src/views/demo/multi-level/components/LevelOnePanel.vueLevelTwoPanel.vue 这类文件在示例中是被三级页面手动引入展示的组件,不是中间目录自己的路由页面。

如果某个目录只有一个可见子菜单:

想要的显示效果单子级显示
直接显示子菜单显示子级
保留目录层级始终显示本级

开启页面缓存

页面缓存用于保留列表查询条件、分页、滚动位置或表单状态。后台只打开「页面缓存」还不够,页面标识也要配好。

后台配置:

字段填写方式
菜单类型菜单
页面缓存开启
页面标识唯一的 PascalCase 名称,例如 UserCodegenMultiLevelLevelThreeA

页面代码:

vue
<script setup lang="ts">
defineOptions({ name: "MultiLevelLevelThreeA" });
</script>

检查顺序:

  1. 菜单的「页面缓存」是否开启
  2. 菜单的「页面标识」是否已填写
  3. 页面组件是否声明了相同的 name
  4. 页面是否通过菜单路由进入,而不是手动跳转到一个未注册的临时路径

新增按钮权限

适合控制页面内按钮,例如新增、编辑、删除、导出。

操作方式:

  1. 在「菜单管理」找到对应页面菜单,例如「用户管理」
  2. 点击这一行后的「新增」
  3. 菜单类型选择「按钮」
  4. 填写按钮名称和权限标识
  5. 保存后到「角色管理」给角色分配按钮权限

示例:

字段
父级菜单用户管理
菜单类型按钮
菜单名称用户新增
权限标识sys:user:create

页面中这样使用:

vue
<el-button v-hasPerm="['sys:user:create']" type="primary">
  新增
</el-button>

常用权限命名:

操作示例
查询sys:user:list
新增sys:user:create
编辑sys:user:update
删除sys:user:delete
导入sys:user:import
导出sys:user:export

新增外链菜单

外链有两种打开方式:

打开方式效果
新标签页点击菜单后打开浏览器新标签页
系统内嵌在系统内容区通过 iframe 打开

新标签页示例:

字段
父级菜单文档中心
菜单类型外链
菜单名称项目文档
外链地址https://example.com/docs
打开方式新标签页

系统内嵌示例:

字段
父级菜单接口文档
菜单类型外链
菜单名称Apifox
外链地址https://example.com
打开方式系统内嵌
系统路径apifox
页面缓存按需开启
页面标识开启缓存时填写 Apifox

如果内嵌后页面空白,大概率是目标网站禁止 iframe 嵌入,请改成「新标签页」。

保存后看不到怎么办

按这个顺序查:

  1. 角色管理中是否给当前角色分配了新菜单
  2. 是否重新登录或刷新了权限缓存
  3. 菜单「显示状态」是否为显示
  4. 顶级一级菜单的父级目录是否只有一个可见子菜单
  5. 父级目录「单子级显示」是否选了正确的模式
  6. 页面菜单「访问路径」是否只填当前片段,没有误填完整路径
  7. 页面组件是否存在,且填写时省略了 src/views/.vue

常见问题

菜单添加后前端看不到?

  • 优先按 保存后看不到怎么办 逐项确认。
  • 最常见原因是角色未分配菜单、当前账号未刷新权限缓存,或菜单显示状态为隐藏。

路由跳转报错找不到组件?

  • 检查「页面组件」字段是否填写 src/views 下的页面路径,并省略 src/views/ 前缀和 .vue 后缀。
  • 确认真实文件存在,例如填写 system/user/index 时,应存在 src/views/system/user/index.vue

菜单高亮/面包屑不正确?

  • 检查路由 meta 中的 activeMenu 配置

外链内嵌后空白?

  • 检查外链地址是否允许 iframe 内嵌;如果目标站点禁止内嵌,请改用「新标签页」打开方式。

相关链接

基于 MIT 许可发布 · 如需部署协助或二开定制,请查看 支持与合作