Vue3 封装 Element Plus Menu 无限级菜单组件【建议收藏】

vue3 ---- 递归组件生成menu菜单 && 路由守卫鉴权 对于一些有规律的DOM结构,如果我们再一遍遍的编写同样的代码,显然代码是比较繁琐和不科学的,而且自己的工作量会大大增加,那么有没有一种方法来解决这个问题呢?答案是肯定的,我们可以通过方式来生成这个结构,当然在 Vue 模板中也是可以实现的,我们可以在 Vue 的组件中调用自己本身,这样就能达到目的。一个单文件组件可以通过它的文件名被其自己所引用。例如:名为FooBar.vue的组件可以在其模板中用引用它自己。 阅读详情

本文分别使用 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 中的属性我们也希望能够暴露出去使用:

image-20220916165319870

但 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 运行效果

image-20220916180635608

总结:

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

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

vue3.0+ts+element-plus多页签应用模板:侧边导航菜单(下) 目录系列链接一、目录结构二、实现头部logo1. 封装图标组件 (SvgIcon):(1) svg-sprite-loader(2) svgo-loader(3) 开始行动吧 系列链接 第一章——vue3.0+ts+element-plus多页签应用模板:项目搭建篇 第二章——vue3.0+ts+element-plus多页签应用模板:安装vue-router与vuex篇 第三章——vue3.0+ts+element-plus多页签应用模板:element-plus按需引入篇 第四章——vue3.0+ts 阅读详情

相关推荐

VUE3 el-menu菜单设置

【代码】VUE3 el-menu菜单设置。

jieweiwujie的博客 755

element-ui菜单el-menu的使用

el-menu详解。

m0_63040701的博客 2万+

Vue + Element Plus 实现权限管理系统(三):动态渲染菜单

在菜单权限管理开发中,通常需要根据后端返回的菜单列表递归渲染左侧菜单栏以及动态加载路由,这样可以确保用户无法访问没有权限的菜单。为了实现这个功能,我们需要进行以下步骤:获取菜单列表数据:调用菜单接口/menu获取菜单列表数据。渲染左侧菜单栏:使用递归的方式根据菜单列表数据格式来渲染左侧菜单栏(sidebar)。递归可以遍历菜单数据,根据数据的嵌套结构来递归渲染菜单的子菜单。将菜单列表转换为 Vue 路由格式:在渲染菜单的过程中,需要将菜单列表数据转换为符合 Vue 路由的格式。

斌擎科技 790

element-plus中menu的基本知识点

(1)menu组件要有路由导航效果; (2)在页面刷新的时候,选中的导航要保持展开以及高亮状态;

m0_52518047的博客 7344

Vue3 - Element-Plus实现el-menu无限级菜单组件,菜单栏无限多层级分类,导航栏递归动态菜单和动态路由的渲染源码(基于Menu封装左侧菜单栏组件,支持接口获取动态数据、懒加载菜单)

Vue3 Element-plus el-menu无限级菜单组件封装,基于el-menu封装左侧菜单栏组件,递归型菜单组件封装,实现动态菜单和动态路由的渲染,归型菜单组件封装,el-menu递归菜单(含完整代码)vue3+ts el-menu动态多级菜单(递归)实现无线级菜单,数据动态获取怎么办,Vue3中使用ElementPlus以el-menu导航菜单形式展示Tree树形结构,配置路由及侧边栏导航栏,Vue3使用递归组件封装El-Menu多级菜单,Vue3 Element-plus和el-menu无限级

🏆 前端领域优质创作者 5542

QuestaSim使用Xilinx库的设置

安装QuestaSim

miracle_eicont的专栏 3407

Vue3+Ts+Vite项目(第九篇)——基于el-menu封装左侧菜单栏组件,element-plus el-menu二次封装

Vue3+Ts+Vite项目基于el-menu封装左侧菜单栏组件,实现根据路由列表动态渲染导航菜单,递归实现多级菜单渲染

做笔记/记录遇到的前端bug 1万+

电源新手别怕!手把手教你搞定Buck电路的环路补偿(附Type II/III补偿器参数计算)

本文为电源设计新手详细解析Buck电路环路补偿技术,重点介绍Type II和Type III补偿器的参数计算与设计方法。通过实例演示和实用技巧,帮助工程师快速掌握环路补偿原理,解决电源振荡、响应慢等问题,提升电路稳定性与性能。

weixin_30607029的博客 617

Vue3 Element-plus el-menu无限级菜单组件封装

对于element中提供给我们的el-menu组件最多可以实现三层嵌套,如果多一层数据只能自己通过变量去加一层,如果加了两层、三层这种往往是行不通的,所以只能进行封装

^_^ 6275

碎碎念android eMMC【转】

本文转载自:https://blog.youkuaiyun.com/Fybon/article/details/44242549 一、/dev/blockroot@:/dev/block #lsbootdevicemmcblk0mmcblk0p1mmcblk0p10…….mmcblk0p18mmcblk0p19mmcblk0p2mmcblk0p20……mmcblk0p29mmcblk0p3mmcblk0p3...

zzb2760715357的博客 377

【实战】ESP32 + LN298N 实现电动推杆的闭环位置伺服控制

本文详细介绍了如何使用ESP32微控制器和LN298N电机驱动模块,为普通电动推杆增加编码器反馈,实现精准的闭环位置伺服控制。文章从硬件选型与接线讲起,深入解析了编码器四倍频计数原理与位置换算,并重点阐述了PID控制算法的核心思想、代码实现及参数整定方法,旨在帮助读者从零构建一个反应迅速、定位精准的智能执行器系统。

weixin_29195615的博客 318

vue3-封装菜单组件

在项目之中,一般我们的后台管理正常结构部分都是包括菜单部分,左侧菜单是必不可少的,菜单部分一般由以下几个部分组成菜单标题菜单图标菜单子项菜单折叠。

qq_35485206的博客 879

Rust 与 C/C++:Rust 比 C/C++ 更好还是只是“技能问题”?

对于许多嵌入式系统开发人员而言,的崛起似乎掀起了一股不小的浪潮,特别是在相较于传统的语言,Rust的“内存安全”优势让人眼前一亮。然而,是否可以将Rust作为C/C++的替代品?或者说,Rust是否真的比C/C++更好,抑或它只是一个“技能问题”呢?在嵌入式开发的领域,我观察到,尽管Rust在一些高性能、内存安全敏感的场景中受到了广泛推崇,但它在实际应用中的普及度,特别是在嵌入式领域,依然有限。今天,我会从多个角度探讨这个问题,并且分享一些对于Rust、C/C++以及嵌入式系统开发的个人见解。

qq_63315166的博客 1300

【VUE3】基于Vue3和Element Plus的递归组件实现多级导航栏

本文基于 Vue3 和 Element Plus,通过递归组件实现了可动态渲染的多级导航栏,利用自引用数据结构和深度控制避免无限循环,同时结合 TypeScript 规范数据类型并优化组件封装

Arabys的博客 2156

LSCM最小二乘保角参数化

LSCM最小二乘保角参数化 最小二乘思想 已知一组点的数据(x,y) x x1x_1x1​ x2x_2x2​ x3x_3x3​ … xnx_nxn​ y y1y_1y1​ y2y_2y2​ y3y_3y3​ … yny_nyn​ 可以近似为线性关系 y=ax+by=ax+by=ax+b ,目标为确定常数 aaa , bbb ; 解法:找到 aaa , bbb ,使得所有 yiy_iyi​ 与 axi+bax_i+baxi​+b 偏差的平方和 Q=∑i=1n(yi−axi−b)2 Q=\s

weixin_49732532的博客 4252

从网口变压器到TVS管:手把手设计通过4KV浪涌的以太网电路

本文详细解析工业级以太网接口4KV浪涌防护设计,从网口变压器选型到TVS管布局,提供通过IEC61000-4-5标准测试的完整方案。重点介绍Bob Smith电路优化和PCB设计技巧,帮助工程师有效应对雷电感应和设备开关瞬态干扰,确保工业设备稳定运行。

study的专栏 283

vue3【实战】菜单【组件封装】Menu 支持无限嵌套(含组件递归的使用),切换展开层级,搜索等

效果预览 技术方案 vue3 + vite + Element Plus + VueRouter + UnoCSS + TS + vueUse + AutoImport 技术要点 自行生成菜单索引 index /** * 初始化菜单列表(组装index,删除空children) * * @param menu_list 菜单列表 * @param parent_index 父级菜单的索引,默认为空字符串 * @returns 初始化后的菜单列表 */ function init_

朝阳39的博客 1641

Vue3 Element Plus 应用开发实战2

Vue3 Element Plus 应用开发实战2 本节实战内容:对 Element Plus 的菜单组件进行封装,实现一个无限级菜单组件。 首先,我们来分析一下 Element Plus 中菜单的基本使用方法。 1.Element Plus 菜单组件 <el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo...

chenhaiy的博客 1508

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Element Plus完整入门指南:5分钟掌握Vue 3企业级UI组件库

Element Plus是专为Vue 3打造的企业级UI组件库,为开发者提供了一套完整的前端界面解决方案。无论你是Vue新手还是经验丰富的开发者,Element Plus都能帮助你快速构建美观、功能强大的Web应用。这个基于Vue 3的UI框架不仅支持TypeScript,还提供了丰富的组件库和灵活的主题定制能力,是构建现代化企业应用的理想选择。 ## 🚀 为什么选择Element Plus进

gitblog_00712的博客 670

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

基于Element-plus + JS二次封装左侧菜单栏(1)

)meta: {title: '知识文章',},meta: {title: '菜单1-1'},meta: {title: '菜单1-2'},meta: {title: '我的',},注意;路由定义时使用绝对路径,防止后面多级路由嵌套时,进行跳转出现问题;isShow决定是否在菜单栏上显示。isShow写在父级上就行结果图:有点丑哈~~~我是来学习的,不喜勿喷,如果有什么错误可以在评论区说,互相学习,Thanks♪(・ω・)ノ。

m0_74064516的博客 190

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Vue:Elemenu-Plus递归型菜单组件封装

Vue:Elemenu-Plus递归型菜单组件封装,含:示例效果与完整组件代码。

席木木的博客 1439

ThingsBoard 日志管理三档方案:3 步告别磁盘爆满

凌晨磁盘告警炸响,`df -h` 一查,100G 卷被日志吃掉了 80G。平台卡顿、写入变慢,第一反应就是删日志止血。但光删不解决根因:ThingsBoard 各组件的日志一直在增长,不治理迟早再爆一次。做 ThingsBoard 日志管理,顺序其实很简单——先找日志在哪,再按规模分档上治理手段。 ## 一、日志都住哪儿:先体检,再治疗 ThingsBoard 的日志不是堆在一个地方。主应用的

gitblog_00615的博客 440

vue3结合element-plus封装ems菜单栏组件

效果如下: ok,开始代码 nav-menu.vue <template> <div class="nav-menu"> <div class="logo"> <img class="img" src="~@/assets/img/logo.svg" alt="logo" /> <span v-if="!collapse" class="title">Vue3+TS</span> </

weixin_45389051的博客 1441

vue3-element-admin 菜单管理实现:树形结构与权限控制

你是否在开发后台管理系统时遇到过这些问题:多级菜单渲染异常、权限控制与菜单展示脱节、不同用户看到相同菜单结构?vue3-element-admin 作为基于 Vue3 + Element-Plus 的企业级前端框架,提供了完整的菜单管理解决方案。本文将从数据结构设计、树形组件实现、权限动态过滤三个维度,详解如何构建灵活可控的菜单系统。 读完本文你将掌握: - 菜单树形结构的前后端数据交互设计 -...

gitblog_00467的博客 1143
上一篇: 基于 vite 创建 vue3 全家桶项目(vite + vue3 + tsx + pinia)
下一篇: JS 模块化- 01 模块化前传 (举例:高质量男和拼团名媛相亲)
程序员优雅哥
博客等级 码龄4年 52粉丝 58原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员优雅哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值