文章目录
- 学习链接
- 效果图
- 代码
- 要点
- 简单模拟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这个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>
复制