开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
最近在使用 Element UI V2 的时候,遇到了菜单的问题。默认的 NavMenu 不支持 submenu 点击,不支持宽度过长自动隐藏。此文通过一些取巧的方式,相对的实现动态菜单的一些功能。
前期准备
- Element UI NavMenu 文档地址: https://element.eleme.cn/2.0/#/zh-CN/component/menu
Vue2 + Element UI 2项目 Gitee 代码自取
需求理解
NavMenu 本身已具备的功能
- 横向菜单
- 设置菜单背景色和文字颜色等
- 父子菜单,选中之后,会添加
is-active类名
动态菜单
- 通过树状结构数组实现动态菜单
submenu 可点击,可选中
Navmenu中submenu是不可以点击也不能单独选中的。我们需要可以点击并选中
横向菜单太长,自动隐藏到“更多”菜单中
- 横向空间有限,菜单多的时候,无法完全放的下。添加 “更多” 菜单显示剩余菜单
代码思路
动态菜单
使用 Vue 中自循环组件,通过传入menus,递归渲染 submenu 和 menu-item。
需要注意的点:
- 要多包一层
div - 循环组件上别忘记
v-on="$listeners",否则@click事件传递不出来
submenu 可点击,可选中
可点击:submenu 的 title 上添加 @click 事件。
可选中:遇到 submenu 就给其下添加一个隐藏的 menu-item,用于选中。设置 submenu 和 menu-item 的 key 不一致
横向菜单太长,自动隐藏
处理传入的 menus 数组,通过计算菜单实际所占的宽度,将超过宽度的菜单填充为“更多”菜单的 children
效果图
具体代码实现
使用 vue-cli 创建项目
参考文档: https://cli.vuejs.org/zh/guide/installation.html
vue create dynamic-navmenu
安装必需的包
注意 sass 的版本,如果太高的话,会报错
npm install element-ui@2.15.12 -S
# 防止 sass 版本过高,报错
npm install -D node-sass@1.57.1 sass-loader@8.0.2
完整版如下:
> npm list
dynamic-navmenu@0.1.0 /Users/zhen/Desktop/items
├── @vue/cli-plugin-babel@4.5.19
├── @vue/cli-plugin-eslint@4.5.19
├── @vue/cli-service@4.5.19
├── babel-eslint@10.1.0
├── core-js@3.26.1
├── element-ui@2.15.12
├── eslint-plugin-vue@6.2.2
├── eslint@6.8.0
├── sass-loader@8.0.2
├── sass@1.57.0
├── vue-template-compiler@2.7.14
└── vue@2.7.14
main.js 增加 Element UI
可以参考官方文档:element.eleme.cn/2.0/#/zh-CN…
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false
Vue.use(ElementUI);
new Vue({
render: h => h(App),
}).$mount('#app')
编写循环组件 DynamicMenu.vue
该组件只用于循环生成
menu-item和submenu,真正的el-menu还需要套一层
具体代码如下,其中有几个需要注意的点:
- 1、设置组件的
name为DynamicMenus - 2、添加
div组件,包裹v-for生成的menu-item和submenu - 3、
submenu设置不同的key和index - 4、根据
menu id,如果是 "更多" 菜单,显示特殊的样式 - 5、在
title和menu-item上的添加@click事件,用于响应点击 - 6、添加隐藏的
menu-item,为了submenu能够选中 - 7、如果有子级数据使用递归组件,注意别忘了加
v-on="$listeners", 否则@click会不响应
<template>
<!-- 2、添加 div -->
<div class="dynamic-menu">
<template v-for="item in menus">
<!-- 3、有子菜单,设置不同的 key 和 index -->
<el-submenu
v-if="item.hasOwnProperty('children') && item.children.length > 0"
:key="'submenu' + item.id"
:index="'submenu' + item.id"
style="dislay: initial"
>
<!-- 4、更多菜单,设置不同样式 -->
<template v-if="item.id === 'menu_more'" slot="title">
<i class="el-icon-more"></i>
</template>
<!-- 5、title 上添加 @click -->
<template v-else slot="title">
<!-- <i class="el-icon-menu"></i> -->
<span @click="handleClick(item)" style="display: inline-block">
{{ item.text }}
</span>
</template>
<!-- 6、此处添加 el-menu-item 是为了 submenu 能够选中 -->
<el-menu-item v-show="false" :index="item.id" :key="item.id">
<span>{{ item.text }}</span>
</el-menu-item>
<!-- 7、如果有子级数据使用递归组件 -->
<DynamicMenus v-on="$listeners" :menus="item.children"></DynamicMenus>
</el-submenu>
<!-- 5、没有子菜单,添加 @click -->
<el-menu-item
v-else
:index="item.id"
:key="item.id"
@click="handleClick(item)"
>
<!-- <i :class="item.icon"></i> -->
<span>{{ item.text }}</span>
</el-menu-item>
</template>
</div>
</template>
<script>
export default {
// 1、 设置名称
name: 'DynamicMenus',
components: {},
props: {
menus: {
type: Array,
default: () => [],
},
onClick: {
type: Function,
default: () => {},
},
},
methods: {
handleClick(item) {
this.$emit('onClick', item);
},
},
};
</script>
<style scoped lang="scss">
$fontSize: 14px;
.dynamic-menu {
display: inline-flex;
:deep(.el-menu-item),
:deep(.el-submenu__title) {
font-size: $fontSize;
}
:deep(.el-submenu__title) i {
color: #fff;
position: initial;
display: initial;
margin-left: 5px;
}
}
.el-menu--popup {
.dynamic-menu {
display: initial;
:deep(.el-menu-item),
:deep(.el-submenu__title) {
font-size: $fontSize;
}
:deep(.el-submenu__title) i {
position: absolute;
display: inline-block;
}
}
}
</style>
编写 Header.vue 组件
Header 组件就比较正常了,按照
Element官方文档提供的示例进行编写
代码如下,需要注意的点:
- 1、不能点击的
menu-item要设置为disabled,否则is-active会设置在其上 - 2、设置样式,按需修改
<template>
<div class="preview-header">
<el-menu
class="preview-header-menu"
mode="horizontal"
background-color="#578cff"
text-color="#e0e0e0"
active-text-color="#fff"
:default-active="menuActive"
:unique-opened="true"
>
<!-- 1、设置为 disabled,防止用户点中 -->
<el-menu-item
index="name"
class="preview-header-name disable-menu-item"
disabled
>
动态菜单
</el-menu-item>
<DynamicMenus :menus="menus" @onClick="handleClick"></DynamicMenus>
<el-menu-item
index="user"
disabled
class="disable-menu-item"
style="float: right"
>
您好,{{ currentUser.userName }}
</el-menu-item>
<el-menu-item
index="help"
class="disable-menu-item"
style="float: right; cursor: pointer"
disabled
>
体验说明
</el-menu-item>
</el-menu>
</div>
</template>
<script>
import DynamicMenus from './DynamicMenu.vue';
export default {
name: 'Header',
components: {
DynamicMenus,
},
props: {
menus: {
type: Array,
default: () => [],
},
select: {
type: Function,
default: () => {},
},
},
mounted() {
console.log('this', this.menus);
},
data() {
return {
currentUser: { userName: 'XXX' },
menuActive: this.menus.length > 0 ? this.menus[0].id : '',
};
},
methods: {
handleClick(item) {
this.$emit('select', item);
this.menuActive = item.id;
},
},
};
</script>
<style scoped lang="scss">
$menuHeight: 46px;
// 2、设置样式,按需修改
.preview-header {
width: 100%;
height: $menuHeight;
line-height: $menuHeight;
&-name {
font-size: 22px;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-menu {
:deep(.el-menu-item),
:deep(.el-submenu .el-submenu__title) {
height: $menuHeight;
line-height: $menuHeight;
// border: none !important;
padding: 0 10px;
}
:deep(.is-active) {
border-bottom: 2px solid #fff !important;
}
}
}
// 将 disabled 的颜色设置为白色
.disable-menu-item {
color: #fff !important;
opacity: 1 !important;
cursor: default !important;
}
</style>
使用 Header.vue,在此之前需要通过处理菜单 menus,实现 “更多” 功能
utils/index.js
该文件主要是通过计算菜单的宽度,然后将剩余的菜单设置到 “更多” 里面,就可以实现
需要注意的点:
getTextWidth方法会受到字体等影响,需要注意菜单字体的大小transferMenus方法中的fullWidth会受到外部menu-item、padding, marign, border等的影响,同时别忘记了更多 本身也会占空间,此处计算不是很精细,需要测试一下看看
// utils/index.js
export function getTextWidth(str, fontSize = "14px") {
let width = 0;
const html = document.createElement("span");
// html.style.display = "none";
html.style.position = "absolute";
html.style.bottom = 0;
html.style.zIndex = -10000;
html.style.fontSize = fontSize;
html.innerText = str;
html.setAttribute("id", "getTextWidth");
document.querySelector("#app").appendChild(html);
const dom = document.querySelector("#getTextWidth");
width = dom.offsetWidth;
// 删除 dom
dom.remove();
return width;
}
export function transferMenus(
appData,
fullWidth = window.innerWidth - 150 - 104 - 76 - 65,
fontSize = "14px",
paddingWidth = 20
) {
const dealMenus = [];
const moreMenus = {
id: "menu_more",
text: "更多",
type: "list",
api: "",
table: "",
children: [],
menuData: {
modelId: "",
modelVersion: "",
pageId: "",
},
disabled: true,
};
let totalWidth = 0;
appData.menuInfo.forEach((m) => {
const textWidth = getTextWidth(m.text, fontSize);
// 左右边距 20
let realWidth = textWidth + paddingWidth;
// 如果有子元素,需要多加 5 + 12
if(Array.isArray(m.children) && m.children.length > 0) {
realWidth += (5 + 12)
}
totalWidth += realWidth
if (totalWidth >= fullWidth) {
moreMenus.children.push(m);
} else {
dealMenus.push(m);
}
});
return moreMenus.children.length > 0 ? [...dealMenus, moreMenus] : dealMenus;
}
utils/menus.js
该文件主要是模拟动态生成菜单
// utils/menus.js
export const AllMenus = generateMenus(20);
function generateMenus(len = 10) {
const menus = []
for (let index = 0; index < len; index++) {
const menu = {
id: `menu_${index + 1}`,
text: `第 ${index + 1} 个菜单`,
children: [
{
id: `menu_${index + 1}_children_1`,
text: `第${index + 1}-1个子菜单`,
children: [
{
id: `menu_${index + 1}_children_1_1`,
text: `第${index + 1}-1-1个子菜单`,
children: []
}
]
},
{
id: `menu_${index + 1}_children_1_2`,
text: `第${index + 1}-1-2个子菜单`,
children: []
}
]
}
menus.push(menu)
}
return menus
}
App.vue
使用 Header 组件,将转换过的 menus 传入组件中
<template>
<div>
<Header :menus="menus" @select="handleSelectMenu" />
<img
style="margin-top: 100px"
alt="Vue logo"
src="https://vuejs.org/images/logo.png"
/>
</div>
</template>
<script>
import Header from "./Header.vue";
import { transferMenus } from "./utils/index.js";
import { AllMenus } from "./utils/menus.js";
export default {
name: "App",
components: {
Header,
},
data() {
return {
menus: transferMenus(AllMenus),
};
},
methods: {
handleSelectMenu(item) {
console.log("App.vue select menu ", item.text);
},
},
};
</script>
<style>
body {
margin: 0;
}
</style>
结束
# 运行起来,可查看效果了
npm run serve