菜单组件:菜单项组件MenuItem
MenuItem 从 SubMenu 中独立出来,专注于处理单级菜单项的渲染逻辑。它的核心挑战在于:Element Plus 的 el-menu-item 在不同模式下(水平、垂直侧栏、折叠)要求不同的内部 DOM 结构。这个组件需要根据 collapse 和 icon 两个条件,精确匹配 Element Plus 的三种渲染期望。
为什么要把 MenuItem 独立出来
在初始设计中,MenuItem 的逻辑写在 SubMenu 的模板里。但随着条件分支的增加,模板变得难以维护。独立后的好处是:
- 职责单一:MenuItem 只管"怎么渲染一个菜单项",SubMenu 只管"判断渲染哪种类型"。
- 便于测试:可以单独传入不同的
collapse和icon组合验证渲染结果。 - 代码复用:MenuItem 既被 SubMenu 引用,也可以被 Menu 直接引用(当菜单只有一级时)。
Slot 机制:让用户自定义 Logo
在水平菜单场景中,常见的需求是在左侧放置 Logo,右侧放置菜单,中间用弹性空间撑开。Menu 组件提供了一个名为 icon 的具名插槽来实现这个需求:
<!-- menu.vue -->
<slot name="icon" />
<!-- 后面是菜单区域 -->
vue
用户在使用时:
<Menu :data="menuData">
<template #icon>
<img src="/logo.svg" class="h-8" />
</template>
</Menu>
vue
判断用户是否设置了 slot,使用 Vue 内置的 isRef 方法:
import { isRef } from 'vue'
const slots = useSlots()
const hasIconSlot = isRef(slots.icon)
typescript
当 hasIconSlot 为 true 时,渲染一个 flex-grow 的 div 来撑开左右间距。
MenuItem 的三种渲染场景
MenuItem 接收两个核心 props:
interface MenuItemProps {
data: AppRouteMenuItem
collapse?: boolean
}
typescript
场景一:无图标
最简单的情况,直接显示文本:
<el-menu-item :index="getIndex(data)" :disabled="data.meta?.disabled">
<span>{{ data.meta?.title }}</span>
</el-menu-item>
vue
场景二:有图标 + 侧栏模式(非折叠)
图标和文字并排放置:
<el-menu-item :index="getIndex(data)" :disabled="data.meta?.disabled">
<Iconify :icon="data.meta.icon" />
<span>{{ data.meta?.title }}</span>
</el-menu-item>
vue
场景三:有图标 + 折叠模式
图标在 el-icon 中,文字在 template #title 插槽中:
<el-menu-item :index="getIndex(data)" :disabled="data.meta?.disabled">
<el-icon><Iconify :icon="data.meta.icon" /></el-icon>
<template #title>{{ data.meta?.title }}</template>
</el-menu-item>
vue
折叠模式下的二级菜单仍然使用 template #title 的方式展示标题,而最外层的菜单项则用 el-icon + template #title 的组合。
条件渲染的最终结构
用三个条件分支将三种场景串联:
<el-menu-item :index="getIndex(data)" :disabled="data.meta?.disabled">
<!-- 情况一:无图标 -->
<span v-if="!data.meta?.icon">{{ data.meta?.title }}</span>
<!-- 情况二和三:有图标 -->
<template v-else>
<!-- 折叠模式 -->
<template v-if="collapse">
<el-icon><Iconify :icon="data.meta.icon" /></el-icon>
<template #title>{{ data.meta?.title }}</template>
</template>
<!-- 侧栏模式 -->
<template v-else>
<Iconify :icon="data.meta.icon" />
<span>{{ data.meta?.title }}</span>
</template>
</template>
</el-menu-item>
vue
disabled 属性的透传
disabled 属性从路由 meta 上读取,通过 el-menu-item 的 disabled prop 透传。在管理后台中,禁用菜单的场景通常不多——更多做法是不显示该菜单(hideMenu: true)。但保留 disabled 属性是为了兼容那些需要"灰色显示但不隐藏"的需求。
测试数据与 AI 辅助
在开发阶段,需要构造三层嵌套的 mock 数据来验证递归渲染。这里利用 AI 工具(如 Cursor/Copilot)根据 AppRouteMenuItem 的类型定义自动生成测试数据,显著减少手动编写的工作量:
// Prompt 示例
按照上面的 AppRouteMenuItem 接口,
准备有三个层级的 data 的 mock 数据给我
text
AI 工具会根据类型定义生成包含 path、name、meta(含 title、icon、order)、children 的完整数据结构。
props 传递链路
Menu (接收用户配置)
└── collapse: boolean
└── SubMenu (透传 collapse)
└── MenuItem (接收 collapse)
text
collapse 从 Menu 组件一路透传到 MenuItem。SubMenu 需要显式将 collapse 传递给 MenuItem:
<MenuItem :data="data" :collapse="collapse" />
vue
本节小结
MenuItem 组件的设计体现了"单一职责 + 条件组合"的思路。通过 icon 和 collapse 两个布尔值的组合,精确匹配 Element Plus 在不同模式下的 DOM 结构要求。核心要点:
- 三种场景:无图标、侧栏图标、折叠图标,分别对应不同的内部 DOM 结构。
- 属性透传:
collapse和disabled从父组件逐级传递到el-menu-item。 - Slot 机制:Menu 组件通过
icon插槽让用户自定义 Logo 区域。 - AI 辅助开发:利用 AI 工具生成符合类型定义的 mock 测试数据。
↑