feat(menu): 新增菜单自定义样式配置功能

1. 新增菜单样式表单字段,支持传入JSON格式自定义菜单样式
2. 改造侧边栏组件,实现菜单样式的动态绑定与渲染
3. 添加路由元信息style字段的解析与调试日志
4. 修复侧边栏菜单默认样式冲突问题
This commit is contained in:
2026-06-27 13:12:33 +08:00
parent b94b7823e5
commit 3658728acd
6 changed files with 85 additions and 9 deletions

View File

@@ -91,7 +91,7 @@
.el-menu-item, .el-menu-item,
.el-submenu__title { .el-submenu__title {
// 明确默认状态样式(关键修复) // 明确默认状态样式(关键修复)
background: transparent !important; background: transparent;
box-shadow: none !important; box-shadow: none !important;
position: relative; position: relative;
@@ -318,4 +318,5 @@
font-size: 12px; font-size: 12px;
} }
// 叶子页面无标识 // 叶子页面无标识

View File

@@ -18,6 +18,11 @@ export default {
}, },
render(h, context) { render(h, context) {
const { icon, title, hasChildren } = context.props const { icon, title, hasChildren } = context.props
// 函数式组件中 :style 绑定不会进 props需要从 context.data.style 读取
const customStyle = context.data.style || {}
if (Object.keys(customStyle).length > 0) {
console.log('[Item] ✅ 收到 style:', customStyle, '| title:', title)
}
const vnodes = [] const vnodes = []
if (icon) { if (icon) {
@@ -40,7 +45,25 @@ export default {
) )
} }
return vnodes return (
<span class="menu-item-inner" style={customStyle}>
{vnodes}
</span>
)
} }
} }
</script> </script>
<style lang="scss" scoped>
.menu-item-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
padding: inherit;
box-sizing: border-box;
}
</style>

View File

@@ -4,14 +4,14 @@
<template v-if="hasVisibleChild"> <template v-if="hasVisibleChild">
<app-link :to="subMenuRoute"> <app-link :to="subMenuRoute">
<el-menu-item :index="basePath" :class="{'submenu-title-noDropdown':!isNest}"> <el-menu-item :index="basePath" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="item.meta && item.meta.icon" :title="item.meta && item.meta.title" :has-children="true" /> <item :icon="item.meta && item.meta.icon" :title="item.meta && item.meta.title" :has-children="true" :style="menuStyle" />
</el-menu-item> </el-menu-item>
</app-link> </app-link>
</template> </template>
<template v-else> <template v-else>
<app-link :to="selfRoute"> <app-link :to="selfRoute">
<el-menu-item :index="basePath" :class="{'submenu-title-noDropdown':!isNest}"> <el-menu-item :index="basePath" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="item.meta && item.meta.icon" :title="item.meta && item.meta.title" /> <item :icon="item.meta && item.meta.icon" :title="item.meta && item.meta.title" :style="menuStyle" />
</el-menu-item> </el-menu-item>
</app-link> </app-link>
</template> </template>
@@ -20,14 +20,14 @@
<template v-else-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> <template v-else-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" :style="menuStyle" />
</el-menu-item> </el-menu-item>
</app-link> </app-link>
</template> </template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> <el-submenu v-else :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title"> <template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" :style="menuStyle" />
</template> </template>
<sidebar-item <sidebar-item
v-for="(child, idx) in item.children" v-for="(child, idx) in item.children"
@@ -89,6 +89,23 @@ export default {
}, },
subMenuRoute() { subMenuRoute() {
return this.findFirstLeaf(this.item, this.basePath) return this.findFirstLeaf(this.item, this.basePath)
},
menuStyle() {
console.log('[SidebarItem] 完整 item 对象:', JSON.parse(JSON.stringify(this.item)))
console.log('[SidebarItem] item.meta:', this.item.meta)
console.log('[SidebarItem] item.meta?.style 原始值:', this.item.meta && this.item.meta.style)
if (this.item.meta && this.item.meta.style) {
try {
const parsed = JSON.parse(this.item.meta.style)
console.log('[SidebarItem] ✅ menuStyle 解析成功:', parsed)
return parsed
} catch (e) {
console.warn('[SidebarItem] ❌ JSON.parse 失败:', e)
return {}
}
}
console.log('[SidebarItem] ⚠️ item.meta.style 为空,返回 {}')
return {}
} }
}, },
methods: { methods: {

View File

@@ -13,7 +13,7 @@
class="sub-nav__dropdown" class="sub-nav__dropdown"
@command="handleCommand" @command="handleCommand"
> >
<span class="sub-nav__item sub-nav__item--dropdown" :class="{ 'is-active': isActive(item) }"> <span class="sub-nav__item sub-nav__item--dropdown" :class="{ 'is-active': isActive(item) }" :style="item._style">
{{ item._title }} {{ item._title }}
<i class="el-icon-arrow-down el-icon--right" /> <i class="el-icon-arrow-down el-icon--right" />
</span> </span>
@@ -34,6 +34,7 @@
:to="item._fullPath" :to="item._fullPath"
class="sub-nav__item" class="sub-nav__item"
:class="{ 'is-active': isActive(item) }" :class="{ 'is-active': isActive(item) }"
:style="item._style"
> >
{{ item._title }} {{ item._title }}
</router-link> </router-link>
@@ -160,7 +161,18 @@ export default {
_ownPath: ownPath, _ownPath: ownPath,
_fullPath: routeFullPath, _fullPath: routeFullPath,
_title: title, _title: title,
_hasChildren: hasChildren _hasChildren: hasChildren,
_style: (() => {
const s = c.meta && c.meta.style
if (s) {
try {
return JSON.parse(s)
} catch (e) {
return {}
}
}
return {}
})()
} }
if (hasChildren) { if (hasChildren) {
item._children = this.buildItems(c.children, ownPath) item._children = this.buildItems(c.children, ownPath)

View File

@@ -44,6 +44,18 @@ const permission = {
return new Promise(resolve => { return new Promise(resolve => {
// 向后端请求路由数据 // 向后端请求路由数据
getRouters().then(res => { getRouters().then(res => {
// 调试:检查后端路由数据
const logMetaStyle = (arr, depth = 0) => {
arr.forEach(item => {
const title = (item.meta && item.meta.title) || item.name || ''
const styleVal = item.meta && item.meta.style
console.log('[permission] depth=' + depth, title, 'meta.style=', styleVal)
if (item.children) logMetaStyle(item.children, depth + 1)
})
}
console.log('[permission] === getRouters 原始数据 ===')
logMetaStyle(res.data)
console.log('[permission] === getRouters 数据结束 ===')
const sdata = JSON.parse(JSON.stringify(res.data)) const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data)) const rdata = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata) const sidebarRoutes = filterAsyncRouter(sdata)

View File

@@ -231,6 +231,17 @@
</span> </span>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item prop="style">
<el-input v-model="form.style" placeholder="请输入菜单样式" maxlength="255" />
<span slot="label">
<el-tooltip content='菜单样式JSON格式如`{"backgroundColor":"#ff0000","color":"#fff","fontWeight":"bold"}`' placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
菜单样式
</span>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'"> <el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="isCache"> <el-form-item prop="isCache">
<span slot="label"> <span slot="label">