Vue3 封装 Element Plus Menu 无限级菜单组件
本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-menu 组件进行二次封装,实现配置化的菜单,有了配置化的菜单,后续便可以根据路由动态渲染菜单。
1 数据结构定义
1.1 菜单项数据结构
使用 element-plus el-menu 组件实现菜单,主要包括三个组件:
el-menu:整个菜单;
el-sub-menu:含有子菜单的菜单项;
el-sub-menu:没有子菜单的菜单项(最末级);
结合菜单的属性和展示效果,可以得到每个菜单项包括:菜单名称、菜单图标、菜单唯一标识、子菜单列表四个属性。于是可得到菜单项结构定义如下:
/**
* 菜单项
*/
export interface MenuItem {
/**
* 菜单名称
*/
title: string;
/**
* 菜单编码(对应 el-menu-item / el-sub-menu 组件的唯一标识 index 字段)
*/
code: string;
/**
* 菜单的图标
*/
icon?: string;
/**
* 子菜单
*/
children?: MenuItem[]
}
传入 MenuItem 数组,使用该数组渲染出菜单。但有时候数据字段不一定为上面结构定义的属性名,如 菜单名称 字段,上面定义的属性为 title,但实际开发过程中后端返回的是 name,此时字段名不匹配。一种处理方式是前端开发获取到后台返回的数据后,遍历构造上述结构,由于是树形结构,遍历起来麻烦。另一种方式是由用户指定字段的属性名,分别指定菜单名称、菜单编码等分别对应用户传递数据的什么字段。所以需要再定义一个结构,由用户来配置字段名称。
1.2 菜单配置数据结构
首先定义菜单项字段配置的结构:
/**
* 菜单项字段配置结构
*/
export interface MenuOptions {
title?: string;
code?: string;
icon?: string;
children?: string;
}
再定义菜单项结构默认字段名:
/**
* 菜单项默认字段名称
*/
export const defaultMenuOptions: MenuOptions = {
title: 'title',
code: 'code',
icon: 'icon',
children: 'children'
}
2 使用 tsx 实现封装
2.1 tsx 基本结构
通常使用 tsx 封装组件的结构如下:
import { defineComponent } from 'vue'
export default defineComponent({
name: 'yyg-menu',
// 属性定义
props: {
},
setup (props, context) {
console.log(props, context)
return () => (
<div>yyg-menu</div>
)
}
})
2.2 定义 prop
首先定义两个属性:菜单的数据、菜单数据的字段名。
// 属性定义
props: {
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: false,
default: () => ({})
}
},
除了上面定义的两个属性,el-menu 中的属性我们也希望能够暴露出去使用:
但 el-menu 的属性太多,一个个定义不太现实,在 tsx 中可以使用 context.attrs 来获取。
context.attrs 会返回当前组件定义的属性之外、用户传入的其他属性,也就是返回没有在 props 中定义的属性。
2.3 递归实现组件
在 setup 中 递归 实现菜单的无限级渲染。封装函数 renderMenu,该函数接收一个数组,遍历数组:
- 如果没有子节点,则使用 el-menu-item 渲染
- 如果有子节点,先使用 el-sub-menu 渲染,el-sub-menu 中的内容又继续调用 renderMenu 函数继续渲染。
整个组件实现如下 infinite-menu.tsx:
import { DefineComponent, defineComponent, PropType } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { defaultMenuOptions, MenuItem, MenuOptions } from './types'
export default defineComponent({
name: 'yyg-menu-tsx',
// 属性定义
props: {
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: false,
default: () => ({})
}
},
setup (props, context) {
console.log(props, context)
// 合并默认的字段配置和用户传入的字段配置
const options = {
...defaultMenuOptions,
...props.menuOptions
}
// 渲染图标
const renderIcon = (icon?: string) => {
if (!icon) {
return null
}
const IconComp = (ElementPlusIconsVue as { [key: string]: DefineComponent })[icon]
return (
<el-icon>
<IconComp/>
</el-icon>
)
}
// 递归渲染菜单
const renderMenu = (list: any[]) => {
return list.map(item => {
// 如果没有子菜单,使用 el-menu-item 渲染菜单项
if (!item[options.children!] || !item[options.children!].length) {
return (
<el-menu-item index={item[options.code!]}>
{renderIcon(item[options.icon!])}
<span>{item[options.title!]}</span>
</el-menu-item>
)
}
// 有子菜单,使用 el-sub-menu 渲染子菜单
// el-sub-menu 的插槽(title 和 default)
const slots = {
title: () => (
<>
{renderIcon(item[options.icon!])}
<span>{item[options.title!]}</span>
</>
),
default: () => renderMenu(item[options.children!])
}
return <el-sub-menu index={item[options.code!]} v-slots={slots} />
})
}
return () => (
<el-menu {...context.attrs}>
{renderMenu(props.data)}
</el-menu>
)
}
})
3 使用 SFC 实现菜单封装
SFC 即 Single File Component,可以理解为 .vue 文件编写的组件。上面使用 tsx 可以很方便使用递归,模板的方式就不太方便使用递归了,需要使用两个组件来实现。
3.1 封装菜单项的渲染
infinite-menu-item.vue:
<template>
<!-- 没有子节点,使用 el-menu-item 渲染 -->
<el-menu-item v-if="!item[menuOptions.children] || !item[menuOptions.children].length"
:index="item[menuOptions.code]">
<el-icon v-if="item[menuOptions.icon]">
<Component :is="ElementPlusIconsVue[item[menuOptions.icon]]"/>
</el-icon>
<span>{{ item[menuOptions.title] }}</span>
</el-menu-item>
<!-- 有子节点,使用 el-sub-menu 渲染 -->
<el-sub-menu v-else
:index="item[menuOptions.code]">
<template #title>
<el-icon v-if="item[menuOptions.icon]">
<Component :is="ElementPlusIconsVue[item[menuOptions.icon]]"/>
</el-icon>
<span>{{ item[menuOptions.title] }}</span>
</template>
<!-- 循环渲染 -->
<infinite-menu-item v-for="sub in item[menuOptions.children]"
:key="sub[menuOptions.code]"
:item="sub"
:menu-options="menuOptions"/>
</el-sub-menu>
</template>
<script lang="ts" setup>
import { defineProps, PropType } from 'vue'
import { MenuOptions } from './types'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
defineProps({
item: {
type: Object,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: true
}
})
</script>
<style scoped lang="scss">
</style>
3.2 封装菜单组件
infinite-menu-sfc.vue
<template>
<el-menu v-bind="$attrs">
<infinite-menu-item v-for="(item, index) in data"
:key="index"
:item="item"
:menu-options="options"/>
</el-menu>
</template>
<script lang="ts" setup>
import InfiniteMenuItem from './infinite-menu-item.vue'
import { defineProps, onMounted, PropType, ref } from 'vue'
import { defaultMenuOptions, MenuItem, MenuOptions } from './types'
const props = defineProps({
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: false,
default: () => ({})
}
})
const options = ref({})
onMounted(() => {
options.value = {
...defaultMenuOptions,
...props.menuOptions
}
})
</script>
<style scoped lang="scss">
</style>
4 测试组件
4.1 菜单测试数据
menu-mock-data.ts
export const mockData = [{
title: '系统管理',
id: 'sys',
logo: 'Menu',
children: [{
title: '权限管理',
id: 'permission',
logo: 'User',
children: [
{ title: '角色管理', id: 'role', logo: 'User' },
{ title: '资源管理', id: 'res', logo: 'User' }
]
}, {
title: '字典管理', id: 'dict', logo: 'User'
}]
}, {
title: '营销管理', id: '2', logo: 'Menu'
}, {
title: '测试',
id: 'test',
logo: 'Menu',
children: [{
title: '测试-1',
id: 'test-1',
logo: 'Document',
children: [{ title: '测试-1-1', id: 'test-1-1', logo: 'Document', children: [{ title: '测试-1-1-1', id: 'test-1-1-1', logo: 'Document' }]}, { title: '测试-1-2', id: 'test-1-2', logo: 'Document' }]
}]
}]
4.2 测试页面
<template>
<div class="menu-demo">
<div>
<h3>tsx</h3>
<yyg-infinite-menu-tsx
:data="mockData"
active-text-color="red"
default-active="1"
:menu-options="menuOptions"/>
</div>
<div>
<h3>sfc</h3>
<yyg-infinite-menu-sfc
:data="mockData"
active-text-color="red"
default-active="1"
:menu-options="menuOptions"/>
</div>
</div>
</template>
<script lang="ts" setup>
import YygInfiniteMenuTsx from '@/components/infinite-menu'
import YygInfiniteMenuSfc from '@/components/infinite-menu-sfc.vue'
import { mockData } from '@/views/data/menu-mock-data'
const menuOptions = { title: 'title', code: 'id', icon: 'logo' }
</script>
<style scoped lang="scss">
.menu-demo {
display: flex;
> div {
width: 250px;
margin-right: 30px;
}
}
</style>
4.3 运行效果
总结:
- 在之前的文章中有读者问我为什么要使用 tsx,从这个例子可以看出,如果控制流程复杂或有递归等操作时,tsx 会比 sfc 更容易实现;
- tsx 和 sfc 中动态组件的使用;
- tsx 中的 context.attrs 和 sfc 中的 v-bind="$attrs" 的使用。
感谢你阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与大家分享更多干货
Vue3 封装 Element Plus Menu 无限级菜单组件的更多相关文章
- 87.Ext_菜单组件_Ext.menu.Menu
转自:https://blog.csdn.net/lms1256012967/article/details/52574921 菜单组件常用配置: /* Ext.menu.Menu主要配置项表: it ...
- 从零開始学android<Menu菜单组件.三十.>
在Android系统之中.菜单一共同拥有三类:选项菜单(OptionsMenu).上下文菜单(ContextMenu)和子菜单(SubMenu). 今天我们就用几个样例来分别介绍下菜单的使用 acti ...
- 第二百零六节,jQuery EasyUI,Menu(菜单)组件
jQuery EasyUI,Menu(菜单)组件 学习要点: 1.加载方式 2.菜单项属性 3.菜单属性 4.菜单事件 5.菜单方法 本节课重点了解 EasyUI 中 Menu(菜单)组件的使用方法, ...
- Vue 2.0 右键菜单组件 Vue Context Menu
Vue 2.0 右键菜单组件 Vue Context Menu https://juejin.im/entry/5976d14751882507db6e839c
- 封装Vue Element的upload上传组件
本来昨天就想分享封装的这个upload组件,结果刚写了两句话,就被边上的同事给偷窥上了,于是在我全神贯注地写分享的时候他就神不知鬼不觉地突然移动到我身边,腆着脸问我在干啥呢.卧槽你妈,当场就把我吓了一 ...
- element el-table表格的vue组件二次封装(附表格高度自适应)
基于vue的el-table表格二次封装组件方法 前言 在公司实习使用vue+element-ui框架进行前端开发,使用表格el-table较为多,有些业务逻辑比较相似,有些地方使用的重复性高,如果多 ...
- 为什么 Vue3.js / Element+ 组件属性前面有的需要添加冒号,有的不需要?
背景 使用 Element+ Layout 布局: <el-row> <el-col :span="12"><div class="grid ...
- jQuery仿京东无限级菜单HoverTree
官方网址:http://keleyi.com/jq/hovertree/ 效果图: 看了上面效果图,你或许已经明白为什么是仿京东菜单.如果还不明白,请访问http://list.jd.com/list ...
- JS列表的下拉菜单组件(仿美化控件select)
JS列表的下拉菜单组件(仿美化控件select) 2014-01-23 23:51 by 龙恩0707, 1101 阅读, 6 评论, 收藏, 编辑 今天是农历23 也是小年,在这祝福大家新年快乐!今 ...
随机推荐
- Python自动化办公:将文本文档内容批量分类导入Excel表格
序言 (https://jq.qq.com/?_wv=1027&k=GmeRhIX0) 它来了,它又来了. 本文实现用Python将文本文件自动保存到Excel表格里面去. 需求 将锦江区.t ...
- Java ArrayList和LinkedList
目录 集合的概念 集合体系结构 常用list集合 list集合的特点 ArrayList LinkedList 创建对象 常用方法 遍历 ArrayList和LinkedList的区别 集合的概念 ...
- RASP | 远程Java应用的RASP调试教程
远程Java应用的RASP调试教程 介绍 Java RASP是基于Java Agent技术实现的,而Java Agent代码无法独立启动,必须依赖于一个Java运行时程序才能运行. 如何调试一个Jav ...
- 《ASP.NET Core 6框架揭秘》样章[200页/5章]
作为<ASP.NET Core 3 框架揭秘>的升级版,<ASP.NET Core 6框架揭秘>不仅针对ASP.NET Core 6的新特性进行了修订,并添加了若干原来没有的内 ...
- 时空图神经网路:STGNNs
STGNNs:SPATIAL–TEMPORAL GRAPH NEURAL NETWORKS 许多实际应用中的图在图结构和图输入方面都是动态的.STGNNs在捕获图的动态性方面占有重要地位. 这类方法的 ...
- 异常概念&异常体系和异常分类
异常概念 异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止. 在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象.Java处 ...
- spingboot打jar包
pom.xml文件中packaging为jar <packaging>jar</packaging> 安装插件 <plugin> <groupId>or ...
- 使用 spring-security-oauth2 体验 OAuth 2.0 的四种授权模式
目录 背景 相关代码 授权码模式 第一步 访问GET /oauth/authorize 第二步 访问POST /oauth/authorize 第三步 访问POST /oauth/token 简化模式 ...
- 最强人工智能 OpenAI 极简教程
大家好哇,新同学都叫我张北海,老同学都叫我老胡,其实是一个人,只是我特别喜欢章北海这个<三体>中的人物,张是错别字. 上个月安利了一波:机器学习自动补全代(hán)码(shù)神器,然后就 ...
- Linux 更改家目录下的目录为英文
export LANG=en_US xdg-user-dirs-gtk-update