路由和菜单
了解 vue3-element-admin 的路由配置和菜单管理。
路由配置
在 src/router/index.ts 中配置静态路由:
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,
},
},
],
},
];2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
动态路由根据用户权限从后端获取,主链路如下:
// src/store/modules/permission-store.ts
const data = await MenuAPI.getRoutes();
const dynamicRoutes = transformRoutes(data);
routes.value = [...constantRoutes, ...dynamicRoutes];2
3
4
路由元信息
在路由的 meta 中配置菜单相关信息:
{
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' // 激活的菜单路径(用于详情页等特殊场景)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
| 属性 | 类型 | 说明 |
|---|---|---|
title | string | 菜单标题 |
icon | string | 菜单图标(支持 Element Plus 图标和自定义图标) |
hidden | boolean | 是否隐藏菜单,默认 false |
alwaysShow | boolean | 是否总是显示根菜单,默认 false |
keepAlive | boolean | 是否缓存页面,默认 false |
affix | boolean | 是否固定在 tags-view,默认 false |
breadcrumb | boolean | 是否显示在面包屑,默认 true |
activeMenu | string | 激活的菜单路径 |
多级菜单
// 二级菜单
{
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' }
}
]
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
外链与内嵌
// 外部链接
{
path: 'https://www.baidu.com',
meta: { title: '百度', icon: 'link' }
}
// 内嵌 iframe
{
path: '/external',
component: Layout,
children: [
{
path: 'https://www.baidu.com',
meta: { title: '百度', icon: 'link' }
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
隐藏路由
不在侧边栏显示,但可以访问(常用于详情页):
{
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' // 激活用户管理菜单
}
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
路由传参
// 动态路由参数
router.push({ path: '/user/detail/1' })
const id = route.params.id
// Query 参数
router.push({ path: '/user/detail', query: { id: 1 } })
const id = route.query.id2
3
4
5
6
7
页面缓存
使用 keep-alive 缓存页面,组件必须设置 name 属性且与路由 name 一致:
{
path: '/system/user',
name: 'User',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '用户管理', keepAlive: true }
}2
3
4
5
6
<script setup lang="ts">
defineOptions({ name: "User" });
</script>2
3
路由守卫
// 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();
});
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
后台菜单配置
菜单管理不是简单地“填一行菜单”。在 vue3-element-admin 中,菜单数据会同时影响侧边栏、动态路由、页面缓存、按钮权限和角色授权。下面按实际使用场景说明怎么配置。
进入方式:登录后台 →「系统管理」→「菜单管理」。新增或修改完成后,记得去「角色管理」给角色分配菜单权限,然后重新登录或刷新权限缓存。
配置前先看这 4 条
- 目录只负责分组和承载布局,不填写页面组件,不直接对应
src/views下的页面。 - 菜单才对应真实页面,需要填写访问路径和页面组件。
- 顶级页面不要直接建成菜单。如果要做一个侧边栏一级菜单,需要先建顶级目录,再在目录下建唯一子菜单。
- 只有一个可见子菜单的目录默认会折叠显示子菜单。这就是「首页」「代码生成」这类看起来是一级菜单、实际由“目录 + 子菜单”组成的配置方式。
内置「首页」是静态路由,通常不需要在后台重复创建。新增类似「代码生成」这样的顶级一级菜单时,按下面的“顶级目录 + 唯一子菜单”方式配置即可。
路径填写规则:
| 场景 | 访问路径怎么填 |
|---|---|
| 顶级目录 | 填完整路径,以 / 开头,例如 /system、/codegen |
| 子级目录 | 只填当前层级片段,例如 config、report |
| 页面菜单 | 只填当前页面片段,例如 user、index、basic |
| 最终访问地址 | 由父级路径和当前路径拼接,例如 /system + user = /system/user |
页面组件填写规则:
| 字段值 | 实际文件 |
|---|---|
system/user/index | src/views/system/user/index.vue |
codegen/index | src/views/codegen/index.vue |
demo/multi-level/level-one/level-two/level-three-a/index | src/views/demo/multi-level/level-one/level-two/level-three-a/index.vue |
新增已有目录下的页面菜单
适合这种效果:
系统管理
└── 岗位管理2
第一步,先创建页面文件:
src/views/system/post/index.vue如果需要页面缓存,在页面中声明组件名称:
<script setup lang="ts">
defineOptions({ name: "Post" });
</script>2
3
第二步,在后台新增菜单:
- 进入「菜单管理」
- 找到「系统管理」这一行,点击「新增」
- 菜单类型选择「菜单」
- 按下面填写并保存
| 字段 | 值 |
|---|---|
| 父级菜单 | 系统管理 |
| 菜单名称 | 岗位管理 |
| 菜单类型 | 菜单 |
| 访问路径 | post |
| 页面组件 | system/post/index |
| 页面缓存 | 按需开启 |
| 页面标识 | 开启缓存时填写 Post |
| 显示状态 | 显示 |
| 图标 | 按需选择 |
| 排序 | 同级菜单中的显示顺序 |
最终效果:
路由地址:/system/post
页面文件:src/views/system/post/index.vue
侧边栏:系统管理 → 岗位管理2
3
最后到「角色管理」给角色勾选「岗位管理」,重新登录后即可看到菜单。
新增顶级一级菜单
适合这种效果:
代码生成虽然侧边栏只显示一个「代码生成」,但推荐配置不是直接建一个顶级菜单,而是建成下面这种结构:
代码生成目录(目录,负责挂载 Layout,不作为菜单项显示)
└── 代码生成(菜单,真正显示在侧边栏并打开页面)2
这样做的原因是:顶级路由需要布局组件承载,真实页面放在它的子路由中。侧边栏渲染时,如果一个目录只有 1 个可见子菜单,并且「单子级显示」选择「显示子级」,系统会直接把这个子菜单显示成一级菜单。
第一步,创建页面文件:
src/views/codegen/index.vue需要缓存时,页面中声明:
<script setup lang="ts">
defineOptions({ name: "Codegen" });
</script>2
3
第二步,新增顶级目录:
- 在「菜单管理」点击页面左上角「新增」
- 菜单类型选择「目录」
- 按下面填写并保存
| 字段 | 值 |
|---|---|
| 父级菜单 | 顶级菜单 |
| 菜单名称 | 代码生成 |
| 菜单类型 | 目录 |
| 访问路径 | /codegen |
| 默认跳转 | /codegen/index |
| 单子级显示 | 显示子级 |
| 显示状态 | 显示 |
| 图标 | code |
| 排序 | 按需填写 |
这里的「单子级显示 = 显示子级」对应路由 meta 中的 alwaysShow: false,也就是旧说法里的“始终显示为否”。这样侧边栏会使用唯一子菜单作为实际显示节点。
第三步,在目录下新增唯一子菜单:
- 找到刚创建的「代码生成」目录
- 点击这一行后的「新增」
- 菜单类型选择「菜单」
- 按下面填写并保存
| 字段 | 值 |
|---|---|
| 父级菜单 | 代码生成 |
| 菜单名称 | 代码生成 |
| 菜单类型 | 菜单 |
| 访问路径 | index |
| 页面组件 | codegen/index |
| 页面缓存 | 开启 |
| 页面标识 | Codegen |
| 显示状态 | 显示 |
| 图标 | code |
最终效果:
路由地址:/codegen/index
页面文件:src/views/codegen/index.vue
侧边栏:代码生成2
3
如果你想让侧边栏显示成「代码生成」目录并展开子菜单,把父级目录的「单子级显示」改成「始终显示本级」。
新增多级菜单
适合这种效果:
多级菜单
└── 一级菜单
└── 二级菜单
├── 三级菜单 A
└── 三级菜单 B2
3
4
5
配置多级菜单时,当前项目推荐和 mock 保持一致:顶层和中间层用目录,最终可访问页面用菜单。
这里的“中间层用目录”不是泛泛而谈。vue3-element-admin 的多级菜单演示中,多级菜单、一级菜单、二级菜单 都是目录,只有 三级菜单 A、三级菜单 B 是菜单:
多级菜单(目录 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)2
3
4
5
前端转换动态路由时,会保留顶级目录的 Layout,但会把非顶层目录的 Layout 组件置空。也就是说,中间目录只负责生成菜单层级和路由嵌套,不渲染自己的业务页面。
演示菜单要让用户一眼看出“这是多级菜单”,所以菜单标题保留 一级菜单、二级菜单、三级菜单 A/B。代码命名则使用可读、可推导的英文路径,避免 multi-level1、children/children 这类随意命名:
| 对象 | 推荐规则 | 示例 |
|---|---|---|
| 访问路径 | 使用小写 kebab-case,只写当前层级片段 | level-one、level-two、level-three-a |
| 页面组件 | 和路由层级保持镜像,放到 src/views 下 | demo/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 |
| 显示状态 | 显示 |
对应页面文件:
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.vue2
最终路由:
/multi-level/level-one/level-two/level-three-a
/multi-level/level-one/level-two/level-three-b2
当前项目 mock 的多级菜单对应关系如下:
| 后台菜单 | 类型 | 访问路径 | 页面组件 | 是否缓存 |
|---|---|---|---|---|
| 多级菜单 | 目录 | /multi-level | 不填 | 否 |
| 一级菜单 | 目录 | level-one | 不填 | 否 |
| 二级菜单 | 目录 | level-two | 不填 | 否 |
| 三级菜单 A | 菜单 | level-three-a | demo/multi-level/level-one/level-two/level-three-a/index | 可开启 |
| 三级菜单 B | 菜单 | level-three-b | demo/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:
<script setup lang="ts">
defineOptions({ name: "MultiLevelLevelThreeA" });
</script>2
3
缓存机制说明:
- 菜单接口返回
meta.keepAlive = true后,页面进入标签栏时会加入缓存列表。 - 后台「页面标识」仍按页面组件
name填写,例如MultiLevelLevelThreeA。 - 当前项目底层使用
route.fullPath包装缓存组件,因此多级路由、带 query 的页面都可以独立缓存。 - 中间目录没有业务页面,不会进入
keep-alive缓存;只需要给最终页面菜单开启缓存。 src/views/demo/multi-level/components/LevelOnePanel.vue、LevelTwoPanel.vue这类文件在示例中是被三级页面手动引入展示的组件,不是中间目录自己的路由页面。
如果某个目录只有一个可见子菜单:
| 想要的显示效果 | 单子级显示 |
|---|---|
| 直接显示子菜单 | 显示子级 |
| 保留目录层级 | 始终显示本级 |
开启页面缓存
页面缓存用于保留列表查询条件、分页、滚动位置或表单状态。后台只打开「页面缓存」还不够,页面标识也要配好。
后台配置:
| 字段 | 填写方式 |
|---|---|
| 菜单类型 | 菜单 |
| 页面缓存 | 开启 |
| 页面标识 | 唯一的 PascalCase 名称,例如 User、Codegen、MultiLevelLevelThreeA |
页面代码:
<script setup lang="ts">
defineOptions({ name: "MultiLevelLevelThreeA" });
</script>2
3
检查顺序:
- 菜单的「页面缓存」是否开启
- 菜单的「页面标识」是否已填写
- 页面组件是否声明了相同的
name - 页面是否通过菜单路由进入,而不是手动跳转到一个未注册的临时路径
新增按钮权限
适合控制页面内按钮,例如新增、编辑、删除、导出。
操作方式:
- 在「菜单管理」找到对应页面菜单,例如「用户管理」
- 点击这一行后的「新增」
- 菜单类型选择「按钮」
- 填写按钮名称和权限标识
- 保存后到「角色管理」给角色分配按钮权限
示例:
| 字段 | 值 |
|---|---|
| 父级菜单 | 用户管理 |
| 菜单类型 | 按钮 |
| 菜单名称 | 用户新增 |
| 权限标识 | sys:user:create |
页面中这样使用:
<el-button v-hasPerm="['sys:user:create']" type="primary">
新增
</el-button>2
3
常用权限命名:
| 操作 | 示例 |
|---|---|
| 查询 | 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 嵌入,请改成「新标签页」。
保存后看不到怎么办
按这个顺序查:
- 角色管理中是否给当前角色分配了新菜单
- 是否重新登录或刷新了权限缓存
- 菜单「显示状态」是否为显示
- 顶级一级菜单的父级目录是否只有一个可见子菜单
- 父级目录「单子级显示」是否选了正确的模式
- 页面菜单「访问路径」是否只填当前片段,没有误填完整路径
- 页面组件是否存在,且填写时省略了
src/views/和.vue
常见问题
菜单添加后前端看不到?
- 优先按 保存后看不到怎么办 逐项确认。
- 最常见原因是角色未分配菜单、当前账号未刷新权限缓存,或菜单显示状态为隐藏。
路由跳转报错找不到组件?
- 检查「页面组件」字段是否填写
src/views下的页面路径,并省略src/views/前缀和.vue后缀。 - 确认真实文件存在,例如填写
system/user/index时,应存在src/views/system/user/index.vue。
菜单高亮/面包屑不正确?
- 检查路由 meta 中的
activeMenu配置
外链内嵌后空白?
- 检查外链地址是否允许 iframe 内嵌;如果目标站点禁止内嵌,请改用「新标签页」打开方式。
