核心原理:CSS 变量的继承范围 + ElementUI popup 的挂载方式
要理解为什么"挂 body 上就可以",你必须知道两件事:
① CSS 变量(--xxx)是"继承型属性"
CSS 变量的继承规则:
-
子元素可以继承父元素的 CSS 变量
-
但 只能继承祖先节点的变量
-
如果祖先链上没有这个变量 → 变量无效
举例:
<div style="--color: red">
<span>能继承</span>
</div>
但如果结构是:
<div style="--color: red"></div>
<body>
<span>不能继承</span>
</body>
span 不会继承 div 的变量,因为它不是 div 的子孙节点。
② ElementUI 折叠后的菜单 popup 是挂载到 body 下的
折叠后 ElementUI 会把菜单弹窗挂到 body:
<body>
<ul class="el-menu el-menu--popup-right-start">
<li class="el-menu-item">...</li>
</ul>
</body>
注意:
-
popup 不在你的组件内部
-
popup 不是 .sidebar-menu 的子元素
-
popup 不在 scoped 样式作用域内
所以:
❌ 你写在组件上的 CSS 变量,popup 根本继承不到
❌ 你写在 scoped 样式里的 hover,popup 根本命不中
❌ v-bind() 在全局样式里无效
❌ popup hoverColor 永远不会生效
这就是你之前所有方法都失败的原因。
那为什么挂到 body 上就可以?
因为 popup 是 body 的子元素:
body
└── ul.el-menu--popup
所以当你这样写:
document.body.style.setProperty('--hover-color', this.hoverColor)
popup 就能继承:
body
└── popup(继承 --hover-color)
然后你在全局样式里写:
body .el-menu--popup .el-menu-item:hover {
background-color: var(--hover-color) !important;
}
popup 就能正确使用 hoverColor。
一句话总结原理
因为 popup 不在你的组件里,所以它继承不到组件上的 CSS 变量。 但 popup 是 body 的子元素,所以把 CSS 变量挂到 body 上,它就能继承了。
再补一句你会恍然大悟的话
你之前写的:
<div class="sidebar-menu" :style="{ '--hover-color': hoverColor }">
对 popup 来说等价于:
一个完全无关的 div 上设置了变量
popup 根本看不到
所以当然不生效。
代码示例
<template>
<div class="sidebar-menu">
<!-- 顶部 logo 区域 -->
<div class="menu-header">
<el-menu
:default-active="$route.path"
class="el-menu-vertical-demo"
router
:collapse="isCollapse"
:text-color="textColor"
:active-text-color="activeColor"
:background-color="backgroundColor"
>
<el-menu-item disabled>
<div class="logo">
<img src="@/assets/images/horse.png" />
</div>
<span :style="{ color: textColor }">后台管理系统</span>
</el-menu-item>
</el-menu>
</div>
<!-- 菜单项区域 -->
<div class="menu-scroll">
<el-menu
v-if="isLightOn"
:default-active="$route.path"
class="el-menu-vertical-demo"
router
:collapse="isCollapse"
:text-color="textColor"
:active-text-color="activeColor"
>
<menu-item
v-for="item in menu_list"
:key="item.path"
:item="item"
@menu-click="updateMenu"
/>
</el-menu>
<el-menu
v-else
:default-active="$route.path"
class="el-menu-vertical-demo"
router
:collapse="isCollapse"
:text-color="textColor"
:active-text-color="activeColor"
:background-color="backgroundColor"
>
<menu-item
v-for="item in menu_list"
:key="item.path"
:item="item"
@menu-click="updateMenu"
/>
</el-menu>
</div>
</div>
</template>
<script>
import MenuItem from "@/components/Layout/MenuItem.vue";
import { mapActions, mapGetters } from "vuex";
export default {
name: "SidebarMenu",
components: { MenuItem },
props: ["isCollapse"],
computed: {
...mapGetters("PermissionModule", ["menuPermissions"]),
...mapGetters("LightModule", [
"isLightOn",
"backgroundColor",
"textColor",
"activeColor",
"hoverColor",
]),
menu_list() {
return this.menuPermissions || [];
},
},
mounted() {
document.body.style.setProperty("--hover-color", this.hoverColor);
this.updateMenuFromRoute(this.$route);
},
watch: {
hoverColor(val) {
document.body.style.setProperty("--hover-color", val);
},
},
methods: {
...mapActions("MenuStateModule", ["setCurrentMenuaddTabMenu"]),
updateMenuFromRoute(route) {
this.setCurrentMenuaddTabMenu(route.path);
},
updateMenu(path) {
this.setCurrentMenuaddTabMenu(path);
},
},
};
</script>
<style lang="less" scoped>
/* ===========================
外层统一背景(主题切换核心)
=========================== */
.sidebar-menu {
display: flex;
flex-direction: column;
height: 100vh;
background-color: v-bind(backgroundColor);
/* 顶部区域同步背景 */
.menu-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 10;
background-color: v-bind(backgroundColor);
::v-deep .el-menu-item.is-disabled {
opacity: 1;
cursor: pointer;
background-color: transparent !important;
color: inherit !important;
&:hover {
background-color: transparent !important;
}
}
}
/* ===========================
菜单滚动区域
=========================== */
.menu-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background-color: v-bind(backgroundColor);
::v-deep .el-menu,
::v-deep .el-submenu__title,
::v-deep .el-menu-item {
background-color: transparent !important;
}
::v-deep .el-menu-item:hover,
::v-deep .el-submenu__title:hover {
background-color: v-bind(hoverColor) !important;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background-color: #f5f5f5;
}
}
}
/* logo 样式 */
.logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: white;
border-radius: 50%;
margin-right: 8px;
img {
width: 30px;
height: 30px;
object-fit: cover;
}
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
}
</style>
<style lang="less">
/* popup 根节点背景 */
// body .el-menu--popup,
// body .el-menu--popup-right-start {
// background-color: var(--bg-color) !important;
// }
/* popup hover(关键) */
/* 折叠 popup hover */
body .el-menu--popup .el-menu-item:hover,
body .el-menu--popup-right-start .el-menu-item:hover {
background-color: var(--hover-color) !important;
// color: var(--active-color) !important;
}
</style>