{T}

菜单激活态恢复与主题色 CSS 变量联动

概述

这一节修复了两个后台布局体验问题。其一,左侧菜单刷新后激活态丢失——根因不是样式没写,而是 el-menudefault-active 没有在首屏根据当前路由重新对齐。其二,主题色选择器改了值页面却没反应——根因不是色值没选中,而是状态没有真正下发到 Element Plus 的 CSS 变量系统。两者共同指向一个结论:导航状态和主题状态都必须从“某个值”继续往下分发到对应的消费端。

这一节的核心方法论是「状态要向下分发到消费端」——无论是菜单激活索引还是主题色变量,停留在数据层都不等于界面生效。很多主题/导航 bug 的真相不是「值没算对」,而是「值算对了却没人把它喂给真正渲染的组件或 CSS 变量」。

学习目标

  • 理解 default-active 吃的是菜单索引 key,刷新恢复必须回到菜单树里反查。
  • 在嵌套路由场景下,用 route.name 恢复激活态比直接比 path 更稳。
  • 用递归 findKey 把当前路由重新映射回菜单树的 meta.key
  • 区分“算出首屏初始值”与“复杂响应式联动”的优先级。
  • 掌握主题色联动的两层:菜单激活色(--el-menu-active-color)与全局主色(--el-color-primary)。

一、刷新后激活态丢失,根因是 default-active 没和路由重新对齐

页面正常点击时高亮正常,是因为 el-menu 内部已经知道当前选中了哪个 index。但浏览器一刷新,这份“当前激活项”就没了,如果没有重新根据路由算出来,菜单就只剩默认样式。所以恢复的不是一个 class,而是首屏进入时重新给 el-menu 一个正确的菜单索引字符串。

Vue SFC
<el-menu :default-active="defaultActive" />

default-active 接收的是菜单项 index,不是随便一个路径字符串。这个问题的本质是“导航状态恢复”,不是单纯“补高亮样式”。

二、default-active 识别的是菜单 key,不是标题或路径

el-menu 真正识别的是菜单项的 index。这套项目里菜单 index 不是 Element Plus 自动生成的,而是之前在菜单系统里通过 generateMenuKeys 之类的方法手动算出来的层级索引(如 3-3-1)。所以恢复激活态时必须沿用同一套生成规则,不能把 default-active 想成“路径专用字段”。

这个 key 已经不只是“显示顺序标记”,而是菜单系统的定位 ID:点击时标记激活项、刷新时重新定位、后续还可能参与默认展开和递归查找。只要它被 default-active、展开逻辑、路由恢复逻辑依赖,就不能随便换生成规则——重构菜单系统时优先保证 key 稳定。

三、嵌套路由下用 route.name 恢复比 path 更稳

越深层的子路由,path 越可能只是当前记录的局部片段,直接拿 path 去比容易遇到父级前缀缺失、自动路由生成的局部路径不完整、同层级结构比对不稳定。更稳妥的方式是:先拿当前路由 name,再到菜单树里递归匹配 item.name,最后命中后返回 meta.keydefault-active。这与面包屑点击优先走命名路由是同一套思路,前提是所有菜单项和路由项都配置了稳定的 name

四、递归 findKey 把当前路由映射回菜单 key

ts
function findKey(menus: AppRouteMenuItem[], routeName?: string): string {
  let key = ""
  menus.forEach((item) => {
    if (item.name === routeName) {
      key = item.meta?.key as string
    }
    if (item.children?.length) {
      const childKey = findKey(item.children, routeName)
      if (childKey) key = childKey
    }
  })
  return key
}

递归函数的真正返回目标是 meta.key,不是把菜单项对象整个拿回来。只要子节点还能继续嵌套,就要保留递归,不要写死层级;meta.key 若类型上可选,调用处要做好兜底值处理。

五、首屏问题优先算准初始值,而非先做复杂响应式

这一节的第一目标不是做一个极其复杂的响应式联动系统,而是把初始 defaultActive 算准。只要初始值正确,刷新恢复这个体验问题就已经解决。

ts
const defaultActive = computed(() => findKey(filterMenus.value, route.name as string))

当前 Element Plus 文档里 Menu 还暴露了 updateActiveIndex 方法,若后续确实需要命令式更新激活项可继续扩展,但本节的关注点在首屏恢复。链路稳定之后,再去考虑更复杂的菜单状态同步。

六、主题色改了值却不生效,是没接入组件变量系统

之前虽已有 ColorPicker 能改出新的颜色值,但页面没反应,说明现在只是“状态变了”,而不是“组件库真的消费了这个状态”。配置项能改,不代表 UI 已经消费了它。主题系统的关键是“变量下发”,不是“状态存在”。后台布局里最明显的联动区域通常是菜单、按钮、链接和高亮态。

七、active-text-color 已废弃,改用 --el-menu-active-color

el-menu 上直接传 active-text-color 当前仍能工作,能把激活菜单项改成主题色,但根据当前 Element Plus 官方 Menu 文档,它已被标记为 deprecated,官方推荐改用 CSS 变量 --el-menu-active-color

Vue SFC
<el-menu class="menu-theme" :style="menuStyle" />
ts
const menuStyle = computed(() => ({ "--el-menu-active-color": settings.theme }))

这类“当前还能用但已废弃”的 API 要在笔记里明确标出。复现老师代码可先沿用,自己项目则优先直接切到 CSS 变量方案;同类的 background-colortext-color 在 Menu 文档里也已被标记 deprecated。

八、真正的全局联动核心是 --el-color-primary

最关键的动作是把最外层布局容器的 --el-color-primary 绑定到 settings.theme。一旦这个变量变了,Element Plus 中大量依赖主色的组件都会一起响应:按钮主色、链接色、激活态、交互反馈色。这一步才真正让主题色从“菜单局部颜色”升级成“整页主题色”。

Vue SFC
<div class="default-view" :style="{ '--el-color-primary': settings.theme }">

根据 Element Plus 主题文档,动态改主色最直接的方式就是改 --el-color-primary;把变量挂在某个作用域类上通常比直接改全局 :root 更推荐。如果只想局部主题生效,就不要无脑写到 document.documentElement

九、菜单激活色与全局主色是两层联动

--el-menu-active-color 解决的是“当前菜单项高亮怎么显示”,--el-color-primary 解决的是“整个 Element Plus 主题主色怎么联动”。只做前者会看到左侧菜单变了、按钮链接没变;只做后者可能局部菜单样式没按预期覆盖。完整的方案是把两层都接上:主题色联动不是单点配置,而是一层一层向下分发的。

十、主题色联动要配合可访问性基线

主题色选择器虽然自由,但任意主色都可能影响对比度:过浅的主色配白底会导致按钮文字看不清,过深则失去层级区分。接入 --el-color-primary 后,最好同时约束一下可选色域或提供一组经过对比度校验的预设色,而不是完全开放任意十六进制值。主题系统最终要兼顾「好看」和「可读」,不能只解决联动技术问题而忽略基础可访问性,否则换了个顺眼的主色反而让操作区难以辨认。


常见问题

问题原因解决方案
刷新后左侧菜单高亮丢失default-active 没根据当前路由重新计算用当前路由去菜单树递归反查 meta.key,传回 default-active
route.path 对比命中不稳定深层路由 path 可能只是局部片段优先用 route.nameitem.name 匹配
default-active 传了路径却没生效el-menu 识别的是菜单 index传递之前生成好的菜单 key
颜色选择器改值后页面没变化主题色状态没绑定到 Element Plus CSS 变量绑定 --el-color-primary 及菜单相关变量
active-text-color 能用但提示不推荐官方已标记 deprecated新项目优先改用 --el-menu-active-color
只有左侧菜单变色,其他区域不变只接了菜单局部色,没接全局主色同时把 --el-color-primary 绑到外层容器

延伸阅读