el-menu:浩瀚无垠
el-menu:浩瀚无垠
0. 缘起
遇到了一个需要展现多级列表的需求,之前写的版本就是我嗯写死的二级菜单,现在想来个Updated版本,所以需要我这里调整。抄了组长整的无极列表组件,不过我还需要对项目兼容进行一些调整。
1. v-if与v-for的循环产生el-menu-item与el-submenu
这个版本的可以实现想要的无极效果,不过时不时element.js爆个堆栈溢出的错误,看的那叫个不爽啊。组长表示可以用高端的函数式组件,直接生成对应render模板就好。事实上那个方法确实比这个版本的好很多~~
Element NavMenu 无限级菜单 - tianyawhl的个人空间 - OSCHINA - 中文开源技术交流社区
2. 函数式组件
函数式组件,我们可以理解为没有内部状态,没有生命周期钩子函数,没有 this(不需要实例化的组件)
2.1 何时用到函数式组件?
在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:
- 程序化地在多个组件中选择一个来代为渲染;
- 在将
children
、props
、data
传递给子组件之前操作它们。
2.2 数据传输
组件需要的一切都是通过
context
参数传递,它是一个包括如下字段的对象:
props
:提供所有 prop 的对象children
:VNode 子节点的数组slots
:一个函数,返回了包含所有插槽的对象scopedSlots
:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。data
:传递给组件的整个数据对象,作为createElement
的第二个参数传入组件parent
:对父组件的引用listeners
:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是data.on
的一个别名。injections
:(2.3.0+) 如果使用了inject
选项,则该对象包含了应当被注入的 property。
2.3 封装组件
<script>
import Vue from "vue";
import "element-ui/lib/theme-chalk/index.css";
import { Menu, MenuItem, Submenu, Icon } from "element-ui";
Vue.use(Menu).use(MenuItem).use(Submenu).use(Icon);
/// 渲染导航栏
const renderNavBar = (h, context) => {
// routers格式自定义,menu中有description标题文字,hidden是否隐藏
// ATTENTION: 注意下方routerChildren是内部子路由,根据项目修改
// option是传入的样式,这里的不全,只是定义透明背景与文字颜色
// 还需要加入全局menu样式,来控制hover与focus时候的背景色(直接用了!important)
const { routers, option, maxLength, className, id } = context.props;
let routersCopy = JSON.parse(JSON.stringify(routers));
if (routers.length > maxLength) {
let emptyRoute = {
path: "",
name: "emptyRoute",
children: routersCopy.slice(maxLength, routers.length),
};
// 隐藏路由黏在正常显示的后面
routersCopy = [...routersCopy.slice(0, maxLength), emptyRoute];
}
return h(
"el-menu",
{
class: {
"navbar-menu": true,
[`${className}`]: true,
},
attrs: {
id: id,
},
props: {
mode: option.mode || "horizontal",
router: true,
backgroundColor: option.backgroundColor || "#545c64",
activeTextColor: option.activeTextColor || "#ffd04b",
textColor: option.textColor || "#fff",
uniqueOpened: option.uniqueOpened || false,
},
},
(routersCopy || [])
.map((item) => {
// 大菜单完了,到小菜单的生成
return renderChildItem(h, item, context);
})
.filter((it) => it)
);
};
/// 渲染子菜单
const renderChildItem = (h, item, context, popperClass = "menu-popup") => {
// popper-class是element中对el-menu下拉出现的选项框的自定义类名
// const { popperClass } = context.props;
let haveRouterChildren =
Array.isArray(item.routerChildren) && item.routerChildren.length;
// 将有路由节点的且非隐藏情况的显示
if (haveRouterChildren && !item.meta?.hidden) {
return !(item.meta && item.meta.hidden)
? h(
"el-submenu",
{
props: {
// el-menu的路由模式,根据index跳转对应地址,这里item.path例如'/home'
index: item.path || "",
popperClass: popperClass,
},
class: {
"empty-menu": item.name === "emptyRoute",
},
},
[
h(
"template",
{
slot: "title",
},
[
item.meta && item.meta.icon
// 存在Icon时显示对应图标
? h("em", { class: { [item.meta.icon]: true } }, "")
: null,
h(
"span",
{ slot: "title" },
// 显示标题span
item.meta && item.meta.description
? item.meta.description
: ""
),
].filter((it) => it)
),
// 子路由进行下一次循环
...item.routerChildren.map((child) => {
return renderChildItem(h, child, context);
}),
]
)
: null;
} else {
// !(item.meta && item.meta.hidden)
// ?
return h(
"el-menu-item",
{
props: {
index: `${item.path || ""}`,
},
},
[
item.meta && item.meta.icon
? h("em", { class: { [item.meta.icon]: true } }, "")
: null,
h(
"span",
{ slot: "title" },
item.meta && item.meta.description ? item.meta.description : ""
),
].filter((it) => it)
);
// : null;
}
};
export default {
name: "MenuBar",
functional: true,
props: {
id: {
type: String,
default: () => "",
},
className: {
type: String,
default: () => "",
},
// 子菜单弹窗类
popperClass: {
type: String,
default: () => "",
},
// 路由列表
routers: {
type: Array,
default: () => [],
},
// el-menu option
option: {
type: Object,
default: () => {
return {};
},
},
// 允许展示的导航数最大值
maxLength: {
type: Number,
default: () => 5,
},
},
render(createElement, context) {
return renderNavBar(createElement, context);
},
};
</script>
<style lang="scss">
.navbar-menu {
min-height: 76px;
.empty-menu {
.el-icon-arrow-down {
font-size: 20px;
transform: rotate(-90deg);
}
.el-icon-arrow-down:before {
content: "\e7a9";
}
.el-submenu__title {
padding: 0 10px 0 0;
text-align: center;
}
&.is-opened {
.el-submenu__title .el-icon-arrow-down {
transform: rotate(-270deg);
}
}
}
}
</style>
2.4 使用组件
注意这里需要将扁平结构转化为树形
routers: {
get() {
let routers = this.$store.state.routers.map((key) => ({
name: key.router.toUpperCase(),
path: "/" + key.router,
id: key.uiWebId,
parentId: key.parentId,
meta: {
hidden: false,
link: true,
description: key.menuName,
},
}));
let transferRouters = convertMenus(routers, "parentId");
console.log('transferRouters: ', transferRouters);
return transferRouters;
function convertMenus(list, pid = "pid", id = "id") {
let info = list.reduce(
(map, node) => (
(map[node[`${id}`]] = node), (node.routerChildren = []), map
),
{}
);
return list.filter((node) => {
info[node[`${pid}`]] &&
info[node[`${pid}`]].routerChildren.push(node);
return !node[`${pid}`];
});
}
},
},
2.5 补充的样式代码
CSS苦手,写这个真的头皮发麻,所以用了! important
,丑陋至极!
.frame_window_out {
.el-menu-demo {
margin-left: 34%;
}
.el-menu.el-menu--horizontal {
border: 0;
}
.el-submenu:focus .el-submenu__title,
.el-submenu:hover .el-submenu__title {
color: #ffffff;
background-color: transparent;
}
.el-menu {
background-color: transparent;
.el-submenu:focus .el-submenu__title,
.el-submenu:hover .el-submenu__title {
background-color: rgba(23, 78, 112, 0.7) !important;
}
.el-menu-item,
.el-submenu .el-submenu__title {
color: #ffffff;
height: 40px;
margin: 8px;
border-radius: 4px;
line-height: 40px;
i {
color: #ffffff;
}
}
.el-menu-item:hover,
.el-menu-item:focus {
background-color: transparent !important;
background-position: center;
}
.el-menu-item-group {
.el-menu-item-group__title {
color: #ffffff;
}
}
.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,
.el-menu--horizontal .el-menu-item:not(.is-disabled):hover {
background-color: #088dc1;
}
}
}
.menu-popup .el-menu--popup .el-menu-item {
background-color: rgba(23, 78, 112, 0.7) !important;
&:hover,
&:focus {
background-color: rgba(16, 130, 201, 0.7) !important;
}
}
3. JSX
人生到处知何似,应似飞鸿踏雪泥。