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

news2025/1/22 8:14:15

文章目录

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

学习链接

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

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

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这个dom
        let currentTarget = e.currentTarget

        // 使用dataset自定义属性, 将当前目录所对应的子节点是否为展开状态, 记录到data-expanded属性当中, 作为一个标记
        // 如果它是打开状态, 那么就需要关闭它
        if(currentTarget.dataset['expanded'] == 'true') {
            console.log(1);
            
            // 获取目录的下一个节点ul
            let 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 = null
                    
                    console.log(currentTarget.dataset['expanded'],123);
                }

                // 在动画结束后,直接func函数
                ul.addEventListener('transitionend', func)
            },0)
        } else {

            // 如果它是关闭状态, 那么就需要打开它
            // 打开它的话,就必须要知道它有多高,才能产生动画,实现0到指定高度的变化
            console.log(2);

            // 拿到目录标题dom的下一个节点ul
            let 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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/522318.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Sqoop: Hadoop数据传输的利器【Sqoop实战】【上进小菜猪大数据系列】

我是上进小菜猪&#xff0c;沈工大软件工程专业&#xff0c;爱好敲代码&#xff0c;持续输出干货&#xff0c;欢迎关注。 Sqoop: Hadoop数据传输的利器, 在大数据领域&#xff0c;数据的传输和集成是至关重要的任务之一。Sqoop&#xff08;SQL to Hadoop&#xff09;作为Apache…

ChatGPT的前世今生,到如今AI领域的竞争格局,本文带你一路回看!

73年前&#xff0c;“机器思维”的概念第一次被计算机科学之父艾伦图灵&#xff08;Alan Turing&#xff09;提出&#xff0c;从此&#xff0c;通过图灵测试成为了人类在AI领域为之奋斗的里程碑目标。 73年后的今天&#xff0c;在AI历经了数十年的不断进化、迭代后&#xff0c…

【第二章:数据的表示和运算】

目录 知识框架No.0 引言No.1 数制与编码一、进位计数制及其相互转换二、BCD码三、无符号的整数在计算机内部表示和运算1、表示2、加法、减法实现 四、带符号的整数在计算机内部表示和运算1、表示1.1、原码表示1.2、原码形式实现加减法运算不行1.3 补码表示1.4 补码实现加法运算…

分享一个图片展示特效

先上效果图&#xff1a; 备注&#xff1a;这个效果图太大了&#xff0c;压缩了一下效果有点不咋好看。感兴趣同学们可以自己运行代码看一下&#xff0c;保证不会失望~ 再上代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta cha…

mysql数据库的表操作 --3

表操作 3.1&#xff1a;创建表 语法&#xff1a; CREATE TABLE table_name ( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collate 校验规则 engine 存储引擎; 说明&#xff1a; field 表示列名 datatype 表示列的…

Java 数组与List转换

int[] 与 List<Integer> 转换 刷题常见 int[] 转 List<Integer> // int[] 转 List<Integer> int[] arr {1, 2, 3, 4, 5}; List<Integer> list Arrays.stream(arr).boxed().collect(Collectors.toList());解释&#xff1a; Arrays.stream(arr) /…

基于Ant DesignPro Vue + SpringBoot 前后端分离 - 部署后解决跨域的问题

基于Ant DesignPro Vue SpringBoot 前后端分离 - 部署后解决跨域的问题 通过Ant DesignPro Vue SpringBoot 搭建的后台管理系统后&#xff0c;实现了前后端分离&#xff0c;并实现了登录认证&#xff0c;认证成功后返回该用户相应权限范围内可见的菜单&#xff1b;但时将服务…

剑指 Offer II 105. 岛屿的最大面积代码注释

题目&#xff1a; 给定一个由 0 和 1 组成的非空二维数组 grid &#xff0c;用来表示海洋岛屿地图。 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合&#xff0c;这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0&#xff08;代表…

第一章 集合框架

文章目录 什么是集合框架集合接口Set和List的区别 集合实现类&#xff08;集合类&#xff09;集合算法 什么是集合框架 官方教程 Java 集合框架 Java Collection Framework &#xff0c;又被称为容器container&#xff0c;是定义在java.util包下的一组接口 interfaces和其实现…

【华为HCIP | 高级网络工程师】刷题日记(8)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大二在校生 &#x1f43b;‍❄️个人主页&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;落798. &#x1f54a;️系列专栏&#xff1a;零基础学java ----- 重识c语言 ---- 计算机网络 &#x1f413;每日一…

68.建立手风琴组件第一部分

本节目标 使用的文件 本次使用的文件可私信我获取&#xff0c;本次就只有两张图片 ● 我们导入两个照片至我们的项目文件夹&#xff0c;并新建一个HTML文件 ● 之后我们输入感叹号&#xff0c;让他自动为什么生成初始代码 ● 之后我们修改下title&#xff0c;导入字体、…

springboot+vue家具网站(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的家具网站。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风歌&#…

kafka之消费者进阶

一、几个概念 1. 消费者组 消费者组&#xff1a;一个消费者组包含多个消费者。同一个消费组的消费者&#xff0c;分别消费不同的partition&#xff0c;便于加快消费。 kafka约定在一个消费者组中&#xff0c;对于同一个topic&#xff0c;每个consumer会分配不同partition&am…

Unity搭建VR全景图

VR全景图片显示和相机旋转 ** 如果需要内置面材质球文件&#xff0c;可以私信下我 ** 场景构建 创建项目后拖进所需文件 文件有内置面材质球、图片等 创建文件 拖拽内置面材质球进入场景&#xff0c;并设置相机在球内部 再创建一个材质球&#xff0c;命名和图片相同 选…

springcloud+springboot+vue学生信息管理系统(选课,成绩,奖惩,奖学金,缴费)xnt81

后端语言&#xff1a;Java 框架&#xff1a;springcloudspringboot 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 学生信息管理系统主要实现角色有管理员和学生,教师,管理员在后台管理学生模块、学籍信息模块、选择课程模块、用户表模块、收藏表模块、课程信息模块…

国产航顺HK32F030M: 448byte EEPROM

EEPROM (~Electrically Erasable Programmable read only memory~)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。 HK32F030M用户手册V1.1.9.pdf bsp_eeprom.c #include "bsp_eeprom.h"/*****************************************************…

springboot+vue房产销售平台(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的房产销售平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风歌&a…

切换以太网接口MAC地址的批处理脚本

MAC&#xff08;媒体访问控制&#xff09;地址是网络设备的唯一标识符&#xff0c;用于网络通信。MAC 地址通常在设备制造时被分配并写入硬件。然而&#xff0c;在某些情况下&#xff0c;你可能需要或想要更改设备的 MAC 地址。以下是一些可能的理由&#xff1a; 1. **隐私和安…

Linux期末复习总结

一、Linux基础及安装 LINUX是在UNIX基础上开发,具有UNIX全部功能。 **Linux特点&#xff1a;**开放性、多用户、多任务、出色的稳定性、良好的用户界面、设备独立性、丰富的网络功能、安全性、可移植性 Linux由4个主要部分组成&#xff1a;内核、Shell、文件系统、应用程序 …

【Python入门篇】——Python中循环语句(while循环的基础语法和基础案例)

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; Python入门&#xff0c;本专栏主要内容为Python的基础语法&#xff0c;Python中的选择循环语句…