混合模式双菜单与菜单样式调整
概述
多导航模式下菜单系统真正的难点,不在于某个 v-if,而在于三件事:结构上一级菜单和二级菜单要不要拆、数据上二级菜单到底从哪来、视觉上两级导航怎样拉开层次。本节先建立四种模式的显示条件矩阵并用 computed 收口,再讲清混合模式的核心是「一级/二级菜单数据拆分」而非复制两份菜单,最后落到双菜单并排布局、一级菜单纵向样式覆盖与层级配色,并补上 select 事件透传与路由联动的闭环。
学习目标
- 用模式矩阵 + computed 收口四种导航模式的菜单显示条件
- 拆分一级菜单(模块切换)与二级菜单(当前激活一级的 children)的数据源
- 用外层 flex 容器解决双菜单直接平铺互相撑满的问题
- 覆盖 el-menu-item 默认 height/line-height 实现图标在上、文字在下的纵向结构
- 通过背景色与文字色做一/二级菜单视觉分级,并把 select 事件上升到 DefaultLayout 接管路由
一、先用模式矩阵收口显示条件
四种模式分别显示哪些区域,必须先把矩阵想明白,后面的 v-if 才不会越写越乱:
| 模式 | 顶部一级菜单 | 左侧一级菜单 | 左侧二级菜单 |
|---|---|---|---|
sidebar | 否 | 是 | 否 |
mixTop | 是 | 否 | 是 |
top | 是 | 否 | 否 |
mixedBar | 否 | 是 | 是 |
建议把所有显示条件都抽成 computed,模板会清晰很多,也避免父级 v-if 限制模式、子级又写重复矛盾条件的陷阱:
const showTopMenu = computed(() =>
settings.navMode === "top" || settings.navMode === "mixTop"
)
const showPrimarySidebar = computed(() =>
settings.navMode === "sidebar" || settings.navMode === "mixedBar"
)
const showSecondarySidebar = computed(() =>
settings.navMode === "mixTop" || settings.navMode === "mixedBar"
)二、混合模式核心是数据拆分而非复制菜单
混合模式里出现两个菜单,不代表「同一份菜单渲染两遍」就结束了。一级菜单负责模块切换,二级菜单负责展示当前一级菜单下的子项——二级菜单数据应来自「当前激活一级菜单的 children」,而不是整棵菜单树:
const secondaryMenus = computed(() => {
const current = primaryMenus.value.find(
item => item.path === activePrimaryPath.value
)
return current?.children ?? []
})一级菜单点击更新 activePrimaryPath,二级菜单随之重算。注意 mixTop 与 mixedBar 的区别不是有没有二级菜单,而是一级菜单放在顶部还是左侧。
三、双菜单并排需要外层布局容器
两个菜单都显示出来后,如果直接平铺,它们各自按「独占型块级结构」渲染,会把布局撑乱。课程里用 el-row 包裹是可行的阶段性修正,但纯结构并排更适合用 flex:
.side-menus { display: flex; height: 100%; }
.primary-menu { width: 96px; }
.secondary-menu { flex: 1; }双菜单布局要提前想好宽度分配,否则后面折叠和钉住交互会很难加。
四、一级菜单纵向布局要覆盖 Element Plus 默认样式
把一级菜单改成「图标在上、文字在下」后图标异常,原因不是 flex-direction: column 失效,而是 el-menu-item 自带的 height、line-height、对齐方式仍按横向菜单项工作。只改 flex-direction 不够,要一起覆盖:
.primary-menu :deep(.el-menu-item) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: auto;
line-height: 1.2;
padding-top: 12px;
padding-bottom: 12px;
}横向菜单遗留的 mr-3 间距在这里应移除,改成 margin-bottom,否则图标和文字仍会被默认样式挤压。
五、一/二级菜单的视觉分级
二级菜单颜色应比一级更淡:一级菜单承担模块切换、视觉权重更高;二级菜单承担模块内导航、强调度低一级。例如一级背景 #0f172a、二级背景 #111827 且文字用浅色半透明。层次感是视觉语义上的主次区分,要和暗黑模式、主题色方案一起考虑,避免颜色策略互相打架。
六、折叠、钉住是双菜单的自然下一步
课程提前埋了二级菜单可折叠隐藏、可「钉住」防自动收起两个后续交互。说明当前双菜单结构是在为更完整的后台导航铺路。要先把宽度和布局关系设计清楚,再抽成 PrimaryMenu / SecondaryMenu 两个职责明确的组件,否则后续折叠/钉住很难维护。
七、select 事件透传与路由联动
双菜单从「界面切换」走向「真实路由联动」的最后一环,是让菜单基础组件只做事件出口,真正的跳转交给 DefaultLayout。菜单组件内部先根据 index 通过递归 getItem() 找回完整节点(name / path / meta / children),再 emit('select', item)——只抛 index 业务层很难消费。
export function getItem(menu: AppRouteMenuItem[], index: string) {
for (const current of menu) {
if (current.meta?.key === index) return current
if (Array.isArray(current.children)) {
const target = getItem(current.children, index)
if (target) return target
}
}
}DefaultLayout 统一监听 @select,用 useRouter() 接管导航:节点具备 name 时优先 router.push({ name: item.name }),拿到完整路径再考虑 router.push(item.path)。混合模式下点击一级菜单往往同时影响高亮与二级菜单数据,所以 select 处理函数要统一收口跳转与激活态,避免「已跳转但高亮停在旧菜单」的状态脱节。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 条件渲染越写越绕 | 顶层与子层 v-if 逻辑重叠 | 先列模式矩阵,再统一抽 computed |
| 双菜单平铺撑乱布局 | 缺少双列容器,各自独占块级 | 增加外层 flex / el-row 容器 |
| 二级菜单显示错误内容 | 直接把整棵菜单树再渲染一遍 | 二级菜单取自当前激活一级的 children |
| 一级菜单纵向后图标被挤压 | 只改 flex-direction 没覆盖默认高度行高 | 同时重写 height、line-height、对齐 |
| 一/二级菜单层次不明显 | 两层颜色强调度过于接近 | 用背景色、文字色做视觉分级 |
| 点击菜单不跳转 | @select 只更新激活态没进路由层 | DefaultLayout 补 handleSelect + useRouter |
| 页面跳了高亮却不对 | 路由联动与状态联动分开写 | 统一收口 select 处理函数 |