antd框架——实现自定义菜单功能最近在写后台管理系统,看到同事写的antd的框架,是在antd原有框架的基础上进行的修改。由于原有的框架中的菜单是menu.js的写法,类似于react的形式,后面要进行改动样式,因此自定义了一个menuNew的菜单组件,方便用于样式的修改。

a-layout-sider组件来展示
代码如下:
html部分的代码<a-layout-sider
:theme="sideTheme"
:class="['side-menu', isMobile ? null : 'shadow']"
width="220px"
:collapsible="collapsible"
v-model="collapsed"
:trigger="null"
>
<div :class="['logo', theme]">
<router-link to="/dashboard/workplace">
<img src="@/assets/img/logo.png" />
<h1>{{ systemName }}</h1>
</router-link>
</div>
<div :class="['side-menu-content', 'beauty-scroll']">
<MenuItem
:theme="theme"
:collapsed="collapsed"
:options="menuData"
@select="onSelect"
class="menu"
/>
</div>
</a-layout-sider>
js部分的代码<script>
import MenuItem from './MenuItem';
import { mapState } from 'vuex';
export default {
name: 'SideMenu',
components: { MenuItem },
props: {
collapsible: {
type: Boolean,
required: false,
default: false,
},
collapsed: {
type: Boolean,
required: false,
default: false,
},
menuData: {
type: Array,
required: true,
},
theme: {
type: String,
required: false,
default: 'dark',
},
},
computed: {
sideTheme() {
return this.theme == 'light' ? this.theme : 'dark';
},
...mapState('setting', ['isMobile', 'systemName']),
},
methods: {
onSelect(obj) {
this.$emit('menuSelect', obj);
},
},
};
</script>
css部分的代码// .shadow{
// box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
// }
.side-menu{
min-height: 100vh;
// overflow-y: auto;
z-index: 9;
.logo{
height: 64px;
position: relative;
line-height: 64px;
padding-left: 24px;
-webkit-transition: all .3s;
transition: all .3s;
overflow: hidden;
background-color: #0B2540;
&.light{
background-color: #0B2540;
h1{
color: @primary-color;
}
}
h1{
color: @menu-dark-highlight-color;
font-size: 20px;
margin: 0 0 0 12px;
display: inline-block;
vertical-align: middle;
}
img{
width:159px;
vertical-align: middle;
}
}
.side-menu-content{
overflow-y: auto;
height: calc(100vh - 64px);
}
}
.ant-layout-sider{background: #213346;}
// .menu{
// padding: 16px 0;
// background-color: #fff;
// }
menuItem组件部分html部分代码<template>
<div class="menu-wrap">
<div
class="menu-item-wrap"
v-for="(item, index) in menuData"
:key="`menu-${index}`"
>
<div
class="menu-item"
:class="{ active: item.active }"
@click="handleRouter($event, item)"
@mouseover="mouseover($event, item)"
>
<SvgIcon
class="menu-icon"
v-if="item.meta && item.meta.icon"
:iconClass="item.meta.icon"
/>
<div class="menu-text">{{ item.name }}</div>
<SubItem
class="menu-sub-item"
:style="{ left: '80px', top: pageY + 'px' }"
v-if="
item.children &&
item.children.length > 0 &&
item.meta.type != 'link'
"
:list="item.children"
:index="index"
:collapsed="collapsed"
/>
</div>
</div>
</div>
</template>
js部分代码<script>
import SubItem from './SubItem'
export default {
components: {
SubItem
},
props: {
options: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
collapsed: {
type: Boolean,
required: false,
default: false
}
},
watch: {
$route() {
this.menuData.forEach(item => {
let active = false
if (this.$route.fullPath.startsWith(item.fullPath)) {
active = true
}
item.active = active
})
}
},
data() {
return {
menuData: [],
openSelect: false,
pageY: undefined,
currentItem: null
}
},
created() {
this.menuData = JSON.parse(JSON.stringify(this.options))
.map(item => {
let active = false
if (this.$route.fullPath.startsWith(item.fullPath)) {
active = true
}
return {
...item,
active: active
}
})
.filter(item => {
if (item.meta.type == 'link') {
return true
}
if (item.meta.invisible) {
return false
}
if (item.children?.length > 0) {
return this.isShow(item.children)
} else if (item.children) {
return false
} else {
return true
}
})
},
methods: {
isShow(list) {
return list.some(item => {
if (item.children?.length > 0) {
return this.isShow(item.children)
} else {
return !item.meta.invisible
}
})
},
mouseover(e, item) {
if (this.currentItem == item.name) {
return
}
let height = window.innerHeight || document.body.clientHeight
let pageY
let doms
if (e.currentTarget.getAttribute('class').includes('menu-item-wrap')) {
pageY = e.clientY - e.offsetY
} else if (
e.currentTarget.parentElement
.getAttribute('class')
.includes('menu-item-wrap')
) {
doms = e.currentTarget.parentElement
pageY = doms.getBoundingClientRect().top
}
let domHeight
if (doms && doms.getElementsByClassName('sub-item-content')[0]) {
domHeight = doms
.getElementsByClassName('sub-item-content')[0]
.getBoundingClientRect().height
} else {
domHeight = 0
}
if (domHeight + pageY > height) {
this.pageY = height - domHeight
} else if (pageY < 64) {
this.pageY = 64
} else {
this.pageY = pageY
}
this.currentItem = item.name
},
handleRouter(e, item) {
if (item.redirect) {
this.$router.push(item.fullPath)
} else {
if (item.meta.type == 'link') {
this.$router.push(item.fullPath)
}
}
}
}
}
</script>
这个代码中有以下相关内容:
e.currentTarget.getAttribute('class').includes('menu-item-wrap')
当鼠标移入时,需要判断当前元素是否含有指定类名,如果不含有,则需要判断该元素的父级元素是否含有指定类名
e.currentTarget.parentElement.getAttribute('class').includes('menu-item-wrap')
判断当前元素的父级元素是否含有某个类名
let doms = e.currentTarget.parentElement;//获取当前元素的父级元素
let pageY = doms.getBoundingClientRect().top
//通过doms.getBoundingClientRect()可以获取该元素的很多参数,包含宽度高度 距离顶部 距离左边的大小等。
判断父级元素距离页面顶部的距离
let pageY = e.clientY - e.offsetY
判断当前元素距离页面订单的距离
sub-item-content的元素的高度let domHeight = doms.getElementsByClassName('sub-item-content')[0].getBoundingClientRect().height
判断当前元素的父级元素且类名为sub-item-content的元素的高度,如果该元素距离页面顶部的距离+元素高度超过了屏幕的高度,则该元素距离页面顶部的距离要改成 屏幕的可视高度-元素的高度,也就是元素与底部贴合,否则元素就是根据距离顶部的距离来渲染。



let height = window.innerHeight || document.body.clientHeight
获取浏览器的可视区域的高度
css部分代码
SubItem组件代码html部分代码<template>
<div
class="sub-item-wrap"
:style="{ left: collapsed ? '80px' : '220px' }"
v-if="visible"
>
<!-- :style="{top: top + 'px',left:collapsed ? '80px' : '220px'}" -->
<div class="sub-item-content">
<div class="item-box">
<div
class="item-list"
v-for="(item, index) in subMenuData"
:key="index"
>
<div
class="sub-item"
:class="{
'parent-node': subItem.hasChild,
'child-node': !subItem.hasChild,
'hide-node': subItem.childFlag,
active: $route.path == subItem.fullPath
}"
v-for="(subItem, i) in item"
:key="i"
@click.stop="handleRouter(subItem)"
>
<span>{{ subItem.name }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
js部分代码<script>
export default {
props: {
collapsed: {
type: Boolean,
required: false,
default: false
},
list: Array,
index: Number
},
data() {
return {
visible: false,
subMenuData: [],
top: 77
}
},
mounted() {
this.showSubItem(this.list, this.index)
},
methods: {
showSubItem(data, index) {
let top = 64 + 13 + index * 74 // 头部高度+搜索高度+当前点击下标*菜单高度
const arr = this.getMenuData(JSON.parse(JSON.stringify(data)))
const num = arr.length
const contentHeight = 40 * num // 子菜单内容高度
const clientHeight =
document.documentElement.clientHeight || document.body.clientHeight // 可视区高度
if (top + contentHeight + 16 + 5 <= clientHeight) {
this.top = top
} else {
if (contentHeight + 16 + 64 <= clientHeight) {
this.top = clientHeight - contentHeight - 16 - 5
} else {
this.top = 69
}
}
this.visible = true
// 处理多列子菜单
let subMenuData = []
let i = 0
subMenuData[i] = []
arr.forEach((item, index) => {
subMenuData[i].push(item)
if ((index + 1) % 6 == 0) {
i++
if (arr[index + 1]) {
subMenuData[i] = []
}
}
})
this.subMenuData = subMenuData
},
getMenuData(data) {
const arr = []
data.forEach((item, i) => {
if (!item.meta?.invisible) {
arr.push({
...item,
hasChild: item.children?.length > 0
})
if (item.children && item.children.length > 0) {
arr.push(...this.getMenuData(item.children))
}
}
})
return arr
},
handleRouter(v) {
if (v.hasChild) return
if (this.$route.fullPath == v.fullPath) {
return false
}
this.$router.push(v.fullPath)
}
}
}
</script>
css部分代码
效果图如下:
完成!!!多多积累,多多收获!!!