二级菜单折叠图标策略与 mixTop 布局修正
概述
这一节继续收口 mixedBar 与 mixTop 两种混合模式:二级菜单折叠后能否保留 icon-only 形态,不再只看布局模式,而要看菜单数据是否完整;mixTop 则要明确拆成「顶部一级 + 左侧二级」的结构,并让左侧二级菜单回归正常宽度。同时修了一个典型样式污染 bug——纵向菜单的强覆盖误伤了顶部横向菜单,引出多模式下必须按模式 class 隔离样式的工程纪律。
这一节的难点不在于某一条样式规则,而在于「混合模式一旦增多,任何一处全局覆盖都会变成定时炸弹」。把布局拆分和样式隔离当成同一件事来思考,是本节的真正收口点。
学习目标
- 用 every 判断二级菜单顶层项是否全部有图标,决定是否允许 icon-only 折叠
- 区分 every(全部满足)与 some(至少一个)的语义差异,避免逻辑差一点
- 让二级菜单折叠宽度策略跟随图标完整性动态切换
- 把 mixTop 调整为顶部一级 + 左侧二级,并让左侧回归正常 menuWidth
- 用模式 class 限制样式作用范围,防止纵/横菜单覆盖互相污染
- 在拆分一二级菜单时同步处理空数据边界,避免左侧二级区渲染异常
一、折叠保留图标取决于数据完整性
mixedBar 下二级菜单折叠时,如果每个顶层项都有 icon,保留图标是合理的;但只要有一项没有图标,折叠后就会出现「露一截」「残缺占位」的怪异效果。这说明二级菜单能不能进入 icon-only 折叠态,不只是布局问题,而是数据完整性问题。判断的是「是否全部具备图标」,不是「是否至少有一个」;当菜单数据不完整时,宁可不折叠成 icon-only,也不要让界面残缺。
const subMenuHasFullIcons = computed(() => {
return subMenus.value.every((item) => {
return typeof item.meta?.icon === "string" && item.meta.icon.length > 0
})
})二、这里必须用 every 不能用 some
课程里调试到一半发现判断用了 some,但当前语义是「所有二级菜单顶层项都必须有图标,才允许进入图标折叠态」。some 表示「只要有一个满足就行」,和当前需求完全不同;every 才表示「每一个都必须满足」。这类数组判断一旦写错,页面往往不是直接报错,而是「逻辑看起来差一点」,更难排查。
// 错误语义:只要有一个菜单项有图标就返回 true
subMenus.value.some((item) => !!item.meta?.icon)
// 正确语义:所有菜单项都必须有图标
subMenus.value.every((item) => !!item.meta?.icon)三、折叠宽度策略跟随图标完整性
新增的 computed 不只是显示一个布尔值,而是要真正影响二级菜单折叠后的宽度策略:全部有图标时折叠后进入窄宽度,任一项缺图标就不要进入 icon-only 折叠态、保留完整宽度。
const secondaryMenuWidth = computed(() => {
if (settings.navMode !== "mixedBar") return `${settings.menuWidth}px`
if (localSettings.collapse && subMenuHasFullIcons.value) return "64px"
return `${settings.menuWidth}px`
})核心不是某个固定宽度值,而是「折叠是否允许发生」。折叠策略不应该只依赖布局模式,也要依赖菜单元信息的完整性。
四、mixTop 拆成顶部一级 + 左侧二级
mixTop 的正确结构是:顶部菜单只负责一级菜单,左侧不再显示一级菜单、只显示当前一级菜单对应的二级菜单。它不是「顶部一份菜单、左侧再来一份完整菜单」,而是明确拆成顶部一级导航、左侧二级导航。
const topMenus = computed(() => {
if (settings.navMode === "mixTop") return getTopMenus(menuTree.value)
return menuTree.value
})左侧一级菜单在 mixTop 下应隐藏,否则会和顶部一级菜单重复;二级菜单数据应来自当前激活的顶部一级菜单,而不是整棵菜单树。
五、mixTop 左侧宽度回归正常 menuWidth
mixedBar 和 mixTop 都会出现二级菜单,但左侧区视觉语义不同:mixedBar 左侧是「一级导航条 + 二级菜单区」,mixTop 左侧只有「二级菜单区」。既然 mixTop 左侧只剩下二级菜单,宽度就应回到正常侧边菜单宽度,而不是继续沿用一级导航条那种窄列逻辑。宽度策略和显示策略最好一起计算,别分散在多个模板判断里。
六、纵向菜单样式不能误伤横向菜单
课程后半段修了一个细节 bug:切到顶部菜单模式后,菜单项右侧小箭头位置不正常。最终发现原因不是顶部菜单本身,而是之前为垂直菜单写的某个 padding-right 覆盖,被错误地作用到了横向菜单。横向和纵向菜单虽然都叫 Menu,但样式覆盖范围绝不能混着写。
.menu--vertical :deep(.el-sub-menu__title) {
padding-right: 0;
}只要是为某一模式写的样式,最好都挂上模式级 class 限制范围;!important 尤其要谨慎,一旦写成全局覆盖,非常容易误伤其他模式。
七、模式越多越要按模式隔离样式
这一节看似只修了箭头位置,实则点出了一个更本质的工程点:菜单模式一多,样式覆盖一旦写成全局,就会出现「修好了 A,打坏了 B」的连锁反应。当项目同时支持 sidebar / mixTop / top / mixedBar 时,样式必须尽快进入「按模式分层」的写法。
.menu--vertical { /* 垂直菜单专属 */ }
.menu--horizontal { /* 顶部菜单专属 */ }
.menu--mixed-bar { /* 双列混合菜单专属 */ }不要只靠注释区分,最好真的在 DOM 结构上挂模式 class。这类约束不只是样式整洁问题,更是后续维护成本问题。
八、拆分一级二级时顺手处理数据边界
顶部只取一级、左侧只取二级的写法依赖 getTopMenus 这类数据函数正确返回。当菜单树为空、或当前一级菜单没有子节点时,左侧二级区应当显示空态或兜底,而不是渲染出 undefined 列表。模式拆分越清晰,越要确认每个分支在数据异常时不会崩溃——把数据边界和布局拆分一起考虑,可以避免线上偶发的白屏,也避免调试时误以为是布局判断写错。
九、折叠策略统一收敛到单一出口
二级菜单宽度由多个条件决定(模式、collapse、图标完整性),如果散落在模板多处判断,切换时容易出现闪烁或互相覆盖。更好的做法是把所有条件收敛到 secondaryMenuWidth 一个 computed 出口,模板只绑定这一个值。状态出口越少,行为越可预测,调试也只需盯一处,而不是在模板里逐条比对条件分支。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 二级菜单折叠后露半截很怪 | 有菜单项缺 icon 却进入 icon-only | 用 every 判断全部有图标,不满足禁止该形态 |
| 用 some 看起来能跑但逻辑差一点 | some 是至少一个满足,语义不对 | 当前场景必须用 every |
| mixTop 顶部和左侧都像在显示一级 | 没重新分配一/二级职责 | 顶部只留一级,左侧只留二级 |
| mixTop 左侧宽度不对 | 沿用双列窄列逻辑 | 回归正常 menuWidth |
| 顶部菜单箭头位置异常 | 垂直菜单样式覆盖到横向菜单 | 用模式 class 限制作用范围 |
| 修一个模式另一个又坏 | 样式写成全局无隔离 | 为各模式建专属 class 与样式范围 |
| 顶部一级菜单为空时左侧空白 | 没处理当前一级无子节点边界 | 二级区加空态或默认回退首项 |
| 折叠宽度切来切去闪烁 | computed 多分支互相覆盖 | 把宽度策略收敛到单一 computed 出口 |