antd框架
——实现自定义菜单功能
最近在写后台管理系统,看到同事写的antd
的框架,是在antd
原有框架的基础上进行的修改。由于原有的框架中的菜单是menu.js
的写法,类似于react
的形式,后面要进行改动样式,因此自定义了一个menuNew
的菜单组件,方便用于样式的修改。
1.左侧菜单可以通过a-layout-sider
组件来展示
代码如下:
1.1 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>
1.2 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>
1.3 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;
// }
2.menuItem
组件部分
2.1 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>
2.2 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>
这个代码中有以下相关内容:
2.2.1 vue项目——鼠标移入时判断是否含有某个类名
e.currentTarget.getAttribute('class').includes('menu-item-wrap')
2.2.2 vue项目——鼠标移入时判断当前元素的父级元素是否含有某个类名
当鼠标移入时,需要判断当前元素是否含有指定类名,如果不含有,则需要判断该元素的父级元素是否含有指定类名
e.currentTarget.parentElement.getAttribute('class').includes('menu-item-wrap')
判断当前元素的父级元素是否含有某个类名
2.2.3 vue项目——判断父级元素距离页面顶部的距离
let doms = e.currentTarget.parentElement;//获取当前元素的父级元素
let pageY = doms.getBoundingClientRect().top
//通过doms.getBoundingClientRect()可以获取该元素的很多参数,包含宽度高度 距离顶部 距离左边的大小等。
判断父级元素距离页面顶部的距离
2.2.4 vue项目——鼠标移入时判断当前元素距离页面顶部的距离
let pageY = e.clientY - e.offsetY
判断当前元素距离页面订单的距离
2.2.5 vue项目——判断当前元素含有sub-item-content
的元素的高度
let domHeight = doms.getElementsByClassName('sub-item-content')[0].getBoundingClientRect().height
判断当前元素的父级元素且类名为sub-item-content的元素的高度,如果该元素距离页面顶部的距离+元素高度超过了屏幕的高度,则该元素距离页面顶部的距离要改成 屏幕的可视高度-元素的高度,也就是元素与底部贴合,否则元素就是根据距离顶部的距离来渲染。
2.2.6 vue项目——获取浏览器的可视区域的高度
let height = window.innerHeight || document.body.clientHeight
获取浏览器的可视区域的高度
2.3 css
部分代码
<style lang="less" scoped>
.menu-wrap {
position: relative;
padding-top: 13px;
.menu-item {
width: 64px;
height: 64px;
padding-top: 12px;
margin: 0 auto 10px;
border-radius: 8px;
text-align: center;
cursor: pointer;
.menu-icon {
font-size: 20px;
color: #fff;
}
.menu-text {
color: #fff;
font-size: 14px;
line-height: 19px;
}
.menu-sub-item {
display: none;
}
}
.menu-item-wrap {
&:hover {
.menu-item {
background-color: #2b3e51;
}
.menu-sub-item {
display: block;
position: fixed;
}
}
}
.menu-item.active {
background-color: #2b3e51;
}
}
</style>
3.SubItem
组件代码
3.1 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>
3.2 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>
3.3 css
部分代码
<style lang="less" scoped>
.sub-item-wrap {
position: absolute;
top: 10px;
.sub-item-content {
width: 200px;
// height: calc(100vh - 84px);
max-height: 500px;
padding: 8px 10px;
box-sizing: border-box;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.16);
border-radius: 8px;
overflow: auto;
background-color: #fff;
border-radius: 10px;
position: fixed;
}
.sub-item-close {
position: absolute;
top: 12px;
right: 14px;
}
.sub-item {
height: 40px;
padding: 0 38px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
}
.parent-node {
font-size: 14px;
color: @primary-color;
justify-content: flex-start;
color: #999999;
padding-left: 20px;
&::before {
content: '';
display: block;
width: 5px;
height: 5px;
background: #999999;
border-radius: 50%;
margin-right: 6px;
}
}
.hide-node {
display: none;
}
.child-node {
color: #333;
cursor: pointer;
padding-left: 44px;
&:hover {
color: #f90 !important;
background-color: #f5f5f5;
border-radius: 4px;
}
}
.sub-item.active {
color: #f90 !important;
// background-color: #f90;
border-radius: 4px;
}
}
</style>
效果图如下:
完成!!!多多积累,多多收获!!!