Vue3 封装 Element Plus Menu 无限级菜单组件

  • Vue3 封装 Element Plus Menu 无限级菜单组件已关闭评论
  • 11 次浏览
  • A+
所属分类:Web前端
摘要

本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-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 中的属性我们也希望能够暴露出去使用:

Vue3 封装 Element Plus 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 运行效果

Vue3 封装 Element Plus Menu 无限级菜单组件

总结:

  1. 在之前的文章中有读者问我为什么要使用 tsx,从这个例子可以看出,如果控制流程复杂或有递归等操作时,tsx 会比 sfc 更容易实现;
  2. tsx 和 sfc 中动态组件的使用;
  3. tsx 中的 context.attrs 和 sfc 中的 v-bind="$attrs" 的使用。

感谢你阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与大家分享更多干货