当前位置: 首页 > news >正文

原生js手动实现一个多级树状菜单效果(高度可过渡变化) + 模拟el-menu组件实现(简单版)

文章目录

    • 学习链接
    • 效果图
    • 代码
      • 要点
    • 简单模拟el-menu实现
      • TestTree.vue
      • Menu.vue
      • SubMenu.vue

学习链接

vue实现折叠展开收缩动画 - 自己的链接

elment-ui/plus不定高度容器收缩折叠动画组件 - 自己的链接

vue的过渡与动画理解

Vue transition 折叠类动画自动获取隐藏层高度以及手风琴效果实现

vue transition动画钩子- vue官网

vue transition 过渡动画

基于vue渐变展开收起盒子动画(盒子高度不定)

效果图

在这里插入图片描述

代码

要点

  • 需要注意这个dom结构,
  • 过渡动画一定要有开始和结束值才能产生动画,并且在js里面修改的时候,不能连着修改,要把第二次修改放到setTimeout里面
  • 为了让菜单能够不是一次性过渡(让它可以一直产生过渡动画),需要在动画结束后,清理掉设置的高度,这个设置的高度只需要在动画的时候生效。
  • 以上的操作参考了elementui的el-menu 和 iview里面的menu
  • 使用下面这种原生的方式实现之后,再对比vue的transition组件的的钩子函数感觉好类似阿(可参考:vue项目中实现折叠面板动画效果),就是不知道,我这样用setTimeout到底属不属于正常操作。不过,感觉理解了下面这个之后,再去看vue的transition过渡钩子好像就比较容易理解了
<style lang="scss" scoped>
@import url(//at.alicdn.com/t/c/font_4065865_kb7oyb2wje9.css);
ul,
li {margin: 0;list-style: none;padding: 0;
}.tree-wrapper {width: 200px;border: 1px solid #ccc;border-radius: 5px;user-select: none;
}.menu-title {padding: 7px 12px;cursor: pointer;display: flex;align-items: center;justify-content: space-between;&:hover{background-color: #eee;}i.iconfont.icon-jiantou {font-size: 26px;display: inline-block;transition: transform 0.3s;}
}// 箭头展开样式
.menu-opened > .menu-title > i.icon-jiantou {transform: rotate(180deg);
}// 子菜单高度使用过渡
ul.menu {transition: all 0.3s;overflow: hidden;
}</style><template><div class="tree-wrapper"><ul class="menu"><li class="menu-submenu menu-opened" ><div class="menu-title" data-expanded="true" @click="clickMenu($event)"><div><span>目录1</span></div><i class="iconfont icon-jiantou"></i></div><ul class="menu" id="t1-u"><li class="menu-submenu menu-opened" ><div class="menu-title" data-expanded="true" style="padding-left: 43px;"  @click="clickMenu($event)"><div><span>目录1-1</span></div><i class="iconfont icon-jiantou"></i></div><ul class="menu"><li class="menu-item"  data-expanded="true"><div class="menu-title" style="padding-left: 67px;"><div><span>菜单1-1-1</span></div></div></li></ul></li><li class="menu-item"><div class="menu-title" style="padding-left: 43px;"><div><span>菜单1-2</span></div></div></li></ul></li><li class="menu-submenu menu-opened" ><div class="menu-title" data-expanded="true"  @click="clickMenu($event)"><div><span>目录2</span></div><i class="iconfont icon-jiantou"></i></div><ul class="menu"><li class="menu-item"><div class="menu-title" style="padding-left: 43px;"><div><span>菜单2-1</span></div></div></li><li class="menu-item"><div class="menu-title" style="padding-left: 43px;"><div><span>菜单2-2</span></div></div></li></ul></li><li class="menu-item"><div class="menu-title"><div><span>菜单4</span></div></div></li></ul></div>
</template><script setup>function clickMenu(e) {// console.log(e.target,'e.target'); // 获取的是发生事件的对象,有可能是子元素// console.log(e.currentTarget,'e.currentTarget'); // 获取的是绑定了事件的对象, 这里用的是这个!// console.log(e.currentTarget.dataset); // 自定义的dataset属性// console.log(e.currentTarget.nextSibling); // 下一个兄弟节点// 获取绑定了点击事件的对象, 即目录的那个menu-title这个domlet currentTarget = e.currentTarget// 使用dataset自定义属性, 将当前目录所对应的子节点是否为展开状态, 记录到data-expanded属性当中, 作为一个标记// 如果它是打开状态, 那么就需要关闭它if(currentTarget.dataset['expanded'] == 'true') {console.log(1);// 获取目录的下一个节点ullet ul  =  currentTarget.nextSibling// 移除掉父节点的menu-opened类(这个类用来控制三角形的旋转状态)ul.parentNode.classList.remove('menu-opened')// 在打开状态下,先去获取ul的scrollHeight值作为ul的height值(里面有个细节,如果ul中还有未展开的节点,那么此时获取ul的scrollHeight是不包括未展开节点的高度的)// 获取这个高度的目的是因为://     1. 我们知道关闭的时候的高度是0,但是不知道打开状态下的高度是多少(不能是auto,写auto的话,高度是正常了,但是没有过渡动画),所以拿scrollHeight作为高度//     2. 我们一定要保持在动画完毕时, 高度要清理掉, 否则后面的动画无法继续下去。所以不能直接设置style.height,然后就不管了, 动画完成后要清理掉style.height。ul.style.height = ul.scrollHeight + 'px'// 这里的setTimeout不能省略, 虽然延迟时间为0。// 上面设置了起始高度,如果要产生过渡动画的话,那就要另一个高度值,关闭的时候,结束高度显然是0px,但是不能直接立马设置为0px,// 需要放在虾米那这个setTimeout里面去。setTimeout(()=>{console.log(ul);// 设置结束高度ul.style.height = '0px'const func = ()=>{// 这里的意思就是想在动画结束后,把高度清空,然后将ul给隐藏掉,保持干净// 动画都结束了,将ul隐藏掉ul.style.display = 'none'// 解绑事件函数ul.removeEventListener('transitionend',func)// 记录当前目录是关闭状态currentTarget.dataset['expanded'] = 'false'// 将高度置为空(这个很重要,动画结束后,这个高度一定要清空掉,因为这个高度不能写死,//             如果写死了,万一它里面还有子节点的话,子节点一旦展开,那这个高度肯定不够,//             我们需要的只是在过渡的时候需要它的高度)ul.style.height = nullconsole.log(currentTarget.dataset['expanded'],123);}// 在动画结束后,直接func函数ul.addEventListener('transitionend', func)},0)} else {// 如果它是关闭状态, 那么就需要打开它// 打开它的话,就必须要知道它有多高,才能产生动画,实现0到指定高度的变化console.log(2);// 拿到目录标题dom的下一个节点ullet ul  =  currentTarget.nextSibling// 三角形打开状态ul.parentNode.classList.add('menu-opened')// 开始是0px(过渡的起始值)ul.style.height = '0px'// 可见状态ul.style.display = 'block'// 修改ul的高度必须要写在setTimeout里面,不能在setTimeout外面立马改掉setTimeout(()=>{// 设置过渡的结束值ul.style.height = ul.scrollHeight + 'px'const func = ()=>{// 解除事件绑定ul.removeEventListener('transitionend',func)// 记录当前是打开状态currentTarget.dataset['expanded'] = 'true'// 将高度置为空(这个很重要,动画结束后,这个高度一定要清空掉,因为这个高度不能写死,//             如果写死了,万一它里面还有子节点的话,子节点一旦展开,那这个高度肯定不够,//             我们需要的只是在过渡的时候需要它的高度)ul.style.height = null}// 动画结束后,收尾工作ul.addEventListener('transitionend', func)})}}
</script>

简单模拟el-menu实现

  • 生成的结构与上面完全一致,所以还是要先把想要的样子先写出来,规划好,然后再通过vue拆分组件去实现。

  • 获得的效果与上面一致,但是写法更加的简单,并且使用到了element-ui的CollapseTransition组件(需要从el的源码中导入)

  • 注意下面在拆分组件的时候的技巧:把当前的菜单标题和这个菜单下的子菜单拆成一个组件SubMenu,这个组件专门负责生成子菜单

  • vue是可以支持两个组件之间相互引用的,下面的Menu组件和SubMenu组件就是相互引用了

  • 通过vue实现,比上面写起来简单多了

在这里插入图片描述

TestTree.vue

<template><Menu :menu-list="menuList" style="width: 200px;border: 1px solid #ddd;border-radius: 4px;"></Menu>
</template><script setup>
import { ref,reactive } from 'vue'import Menu from './Menu.vue'let menuList = ref([{id: 1,title: '目录1',type: 1,children: [{id: 2,title: '目录1-1',type: 1,children: [{id: 3,title: '菜单1-1-1',type: 2,}]},{id: 4,title: '菜单1-2',type: 2,}]},{id: 5,title: '目录2',type: 1,children: [{id: 6,title: '菜单2-1',type: 2,},{id: 7,title: '菜单2-2',type: 2,}]},{id: 8,title: '菜单4',type: 2,}
])</script><style lang="scss"></style>

Menu.vue

<template><ul class="menu"><template v-for="(menu) in menuList" :key="menu.id"><SubMenu v-if="menu.type == 1" :menu="menu" :level="level"></SubMenu><li v-else class="menu-item"><div class="menu-title" :level="level" :style="{'padding-left': level == 1?'7px': 30 * (level - 1) + 'px' }"><div><span>{{ menu.title }}</span></div></div></li></template></ul>
</template><script setup>
import { ref, reactive } from 'vue'
import SubMenu from './SubMenu.vue'const props = defineProps({level: {type: Number,default: 1},menuList: {type: Array,}
})const menuShow = ref(true)</script><style lang="scss" scoped>
@import url(//at.alicdn.com/t/c/font_4065865_kb7oyb2wje9.css);ul,
li {margin: 0;list-style: none;padding: 0;
}.tree-wrapper {width: 200px;border: 1px solid #ccc;border-radius: 5px;user-select: none;
}.menu-title {padding: 7px 12px;cursor: pointer;display: flex;align-items: center;justify-content: space-between;&:hover {background-color: #eee;}i.iconfont.icon-jiantou {font-size: 26px;display: inline-block;transition: transform 0.3s;}
}// 箭头展开样式
.menu-opened>.menu-title>i.icon-jiantou {transform: rotate(180deg);
}// 子菜单高度使用过渡
ul.menu {transition: all 0.3s;overflow: hidden;
}
</style>

SubMenu.vue

<template><li :class="['menu-submenu',{'menu-opened': submenuShow}]"><div class="menu-title" :level="level" :style="{'padding-left': level == 1?'7px': 30 * (level - 1) + 'px' }" @click="submenuShow = !submenuShow"><div><span>{{ menu.title }}</span></div><i class="iconfont icon-jiantou"></i></div><template v-if="menu.children && menu.children.length > 0"><CollapseTransition><Menu v-show="submenuShow" :menu-list="menu.children" :level = "level + 1"></Menu></CollapseTransition></template></li>
</template><script setup>
import { ref,reactive } from 'vue'
import CollapseTransition from 'element-plus/lib/components/collapse-transition/src/collapse-transition';
import Menu from './Menu.vue'
const props = defineProps({menu: {type:Object},level: {type: Number,}
})const submenuShow = ref(true)</script><style lang="scss" scoped>
@import url(//at.alicdn.com/t/c/font_4065865_kb7oyb2wje9.css);
ul,
li {margin: 0;list-style: none;padding: 0;
}.tree-wrapper {width: 200px;border: 1px solid #ccc;border-radius: 5px;user-select: none;
}.menu-title {padding: 7px 12px;cursor: pointer;display: flex;align-items: center;justify-content: space-between;&:hover{background-color: #eee;}i.iconfont.icon-jiantou {font-size: 26px;display: inline-block;transition: transform 0.3s;}
}// 箭头展开样式
.menu-opened > .menu-title > i.icon-jiantou {transform: rotate(180deg);
}// 子菜单高度使用过渡
ul.menu {transition: all 0.3s;overflow: hidden;
}
</style>

相关文章:

原生js手动实现一个多级树状菜单效果(高度可过渡变化) + 模拟el-menu组件实现(简单版)

文章目录 学习链接效果图代码要点 简单模拟el-menu实现TestTree.vueMenu.vueSubMenu.vue 学习链接 vue实现折叠展开收缩动画 - 自己的链接 elment-ui/plus不定高度容器收缩折叠动画组件 - 自己的链接 vue的过渡与动画理解 Vue transition 折叠类动画自动获取隐藏层高度以及…...

RK3568平台开发系列讲解(Linux内存篇)Linux内存管理框架

🚀返回专栏总目录 文章目录 一、内核态内存分配二、用户态内存分配三、内存篇章更新哪些内容沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们一起将整个内存管理的体系串起来。 对于内存的分配需求,可能来自内核态,也可能来自用户态。 一、内核态内存分配…...

你的编程能力从什么时候开始突飞猛进?

关于编程能力突飞猛进的原因和如何突破自己&#xff0c;以下是我的建议。 在过去的几年中&#xff0c;编程领域发生了很多变化。新的语言和技术不断涌现&#xff0c;使得程序员们需要不断学习和提高。作为一名程序员&#xff0c;编程能力的提高是非常重要的&#xff0c;有助于…...

滨州高企认定条件

认定为高新技术企业必须同时满足以下条件&#xff1a; (一)企业在申请认定时需要注册一年以上。 (二)公司通过自主开发、转让、赠与、并购等方式&#xff0c;获得对其主要产品(服务)在技术上发挥核心支持作用的知识产权所有权。 &#xff08;三&#xff09;对企业主要产品(服…...

Azkaban学习——单机版安装与部署

目录 1.解压改名 2.修改装有mysql的虚拟机的my.cnf文件 3.重启装有mysql的虚拟机 4.Datagrip创建azkaban数据库&#xff0c;执行脚本文件 5.修改/opt/soft/azkaban-exec/conf/azkaban.properties文件 6.修改commonprivate.properties 7.传入mysql-connector-java-8.0.29…...

table标签-移动端适配

封装一个组件&#xff0c;该组件需要根据不同设备屏幕宽度自适应调整展示方式。对于 PC 端&#xff0c;以类似 el-table 的形式展示数据&#xff0c;而移动端则以一个类似 item 的形式展示每行数据。 可以先在组件中判断设备类型&#xff0c;如以下示例代码所示&#xff1a; …...

Yolov8改进---注意力机制:DoubleAttention、SKAttention,SENet进阶版本

目录 🏆🏆🏆🏆🏆🏆Yolov8魔术师🏆🏆🏆🏆🏆🏆 1. DoubleAttention 2. SKAttention 3.总结...

【逆向工程核心原理:TLS回调函数】

TLS 代码逆向分析领域中&#xff0c;TLS&#xff08;Thread Local Storage&#xff0c;线程局部存储&#xff09;回调函数&#xff08;Callback Function&#xff09;常用反调试。TLS回调函数的调用运行要先于EP代码的执行&#xff0c;该特征使它可以作为一种反调试技术的使用…...

“Shell“Awk命令

文章目录 一.Awk二.Awk按行输出文本三.Awk按字段输出文本四.通过管道&#xff0c;双引号调用shell命令五.总结&#xff1a; 一.Awk Awk的工作原理&#xff1a; 逐行读取文本&#xff0c;默认以空格或tab键为分隔符进行分隔&#xff0c;将分隔所得的各个字段保存到内建变量中&a…...

射频放大器的原理和作用(射频放大器和功率放大器的区别)

射频放大器是一种电子电路&#xff0c;用于将输入信号增强到足够高的电平以驱动射频输出负载。其原理和作用如下&#xff1a; 射频放大器的工作原理是利用晶体管的三极管效应&#xff0c;将输入信号放大到足够的电平以驱动输出负载。在射频放大器中&#xff0c;输入信号经过输入…...

揭秘KubeEdge边缘网络项目EdgeMesh:如何打造高速、安全、低延迟的互联网连接

KubeEdge是由百度主导的边缘计算项目&#xff0c;旨在为物联网设备提供一种高效、安全的互联网连接方式。EdgeMesh是KubeEdge的核心组件之一&#xff0c;它是一种基于OpenDaylight的边缘网络协议&#xff0c;能够在物联网设备之间提供高速、可靠的互联网连接。 EdgeMesh的设计目…...

Java设计模式 14-访问者模式

访问者模式 这个模式用的很少&#xff0c;《设计模式》的作者评价为&#xff1a; 大多情况下&#xff0c;你不需要使用访问者模式&#xff0c;但是一旦需要使用它时&#xff0c;那就真的需要使用了 一、测评系统的需求 1)将观众分为男人和女人&#xff0c;对歌手进行测评&…...

【数据结构】线性表之链表

目录 前言一、链表的定义二、链表的分类1. 单向和双向2. 带头和不带头3. 循环和不循环4. 常用&#xff08;无头单向非循环链表和带头双向循环链表&#xff09; 三、无头单向非循环链表的接口及实现1. 单链表的接口2. 接口的实现 四、带头双向循环链表接口的及实现1. 双向链表的…...

微服架构基础设施环境平台搭建 -(四)在Kubernetes集群基础上搭建Kubesphere平台

微服架构基础设施环境平台搭建 -&#xff08;四&#xff09;在Kubernetes集群基础上搭建Kubesphere平台 通过采用微服相关架构构建一套以KubernetesDocker为自动化运维基础平台&#xff0c;以微服务为服务中心&#xff0c;在此基础之上构建业务中台&#xff0c;并通过Jekins自动…...

Linux开发板安装Python环境

1. 环境介绍 硬件&#xff1a;STM32MP157&#xff0c;使用的是野火出的开发板。 软件&#xff1a;Debian ARM 架构制作的 Linux 发行版&#xff0c;版本信息如下&#xff1a; Linux发行版本&#xff1a;Debian GNU/Linux 10 内核版本&#xff1a;4.19.94 2. Python 简介…...

ChatGPT 聊天接口API 使用

一、准备工作 1.准备 OPENAI_ACCESS_TOKEN 2.准备好PostMan 软件 二、测试交流Demo 本次使用POSTMAN工具进行快速测试&#xff0c;旨在通过ChatGPT API实现有效的上下文流。在测试过程中&#xff0c;我们发现了三个问题&#xff1a;    1.如果您想要进行具有上下文的交流&…...

软件测试月薪2万,需要技术达到什么水平?

最近跟朋友在一起聚会的时候&#xff0c;提了一个问题&#xff0c;说一个软件测试工程师如何能月薪达到二万&#xff0c;技术水平需要达到什么程度&#xff1f;人回答说这只能是大企业或者互联网企业工程师才能拿到。也许是的&#xff0c;小公司或者非互联网企业拿二万的不太可…...

从入门到进阶,Vue框架让Web开发更简单高效

Vue是现代前端开发中最为流行的JavaScript框架之一&#xff0c;它具有轻量、易学、易用的特点&#xff0c;能够帮助开发者构建出高效、交互丰富的Web应用。在本文中&#xff0c;我们将会深入探索Vue框架的各个方面&#xff0c;包括Vue组件、Vue路由、Vue状态管理等&#xff0c;…...

怎么缩小照片的kb,压缩照片kb的几种方法

缩小照片的KB大小是我们日常工作生活中遇到的常见问题。虽然听起来十分专业&#xff0c;但其实很简单。照片的KB是指照片文件的大小&#xff0c;通常以“KB”为单位表示。缩小照片的KB就是减小照片文件的大小&#xff0c;以便占用更少的磁盘空间或更快地上传和下载照片。在实际…...

2. 注解Annotation

Java注解(Annotation)又称为Java标注,是JDK5.0引入的一种注释机制.注解是原数据的一种形式,提供有关于程序但不属于程序本身的数据.注解对他们注解的代码的操作没有直接的影响. 声明方式 注解的声明方式使用interface关键字,举例说明: public interface MyInject{ }元注解 Ta…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

Pinocchio 库详解及其在足式机器人上的应用

Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库&#xff0c;专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性&#xff0c;并提供了一个通用的框架&…...

IP如何挑?2025年海外专线IP如何购买?

你花了时间和预算买了IP&#xff0c;结果IP质量不佳&#xff0c;项目效率低下不说&#xff0c;还可能带来莫名的网络问题&#xff0c;是不是太闹心了&#xff1f;尤其是在面对海外专线IP时&#xff0c;到底怎么才能买到适合自己的呢&#xff1f;所以&#xff0c;挑IP绝对是个技…...

QT3D学习笔记——圆台、圆锥

类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体&#xff08;对象或容器&#xff09;QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质&#xff08;定义颜色、反光等&#xff09;QFirstPersonC…...

Xela矩阵三轴触觉传感器的工作原理解析与应用场景

Xela矩阵三轴触觉传感器通过先进技术模拟人类触觉感知&#xff0c;帮助设备实现精确的力测量与位移监测。其核心功能基于磁性三维力测量与空间位移测量&#xff0c;能够捕捉多维触觉信息。该传感器的设计不仅提升了触觉感知的精度&#xff0c;还为机器人、医疗设备和制造业的智…...

goreplay

1.github地址 https://github.com/buger/goreplay 2.简单介绍 GoReplay 是一个开源的网络监控工具&#xff0c;可以记录用户的实时流量并将其用于镜像、负载测试、监控和详细分析。 3.出现背景 随着应用程序的增长&#xff0c;测试它所需的工作量也会呈指数级增长。GoRepl…...

2.2.2 ASPICE的需求分析

ASPICE的需求分析是汽车软件开发过程中至关重要的一环&#xff0c;它涉及到对需求进行详细分析、验证和确认&#xff0c;以确保软件产品能够满足客户和用户的需求。在ASPICE中&#xff0c;需求分析的关键步骤包括&#xff1a; 需求细化&#xff1a;将从需求收集阶段获得的高层需…...