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

ElementUI动画进阶:从零封装一个平滑的左右抽屉式折叠组件

1. 为什么需要左右抽屉式折叠组件

在后台管理系统开发中,"左树右表"的布局几乎成了标配设计。左侧是导航树或菜单栏,右侧是内容展示区。这种布局最大的痛点就是当左侧菜单项过多时,会挤压右侧内容的显示空间。这时候如果能像抽屉一样把左侧菜单收起来,用户体验会好很多。

ElementUI自带的折叠动画组件el-collapse-transition只能实现上下方向的折叠效果。但实际项目中,左右方向的折叠需求更为常见。比如:

  • 后台管理系统的侧边栏收缩
  • 电商平台商品筛选面板的隐藏/显示
  • 聊天应用的联系人列表折叠

我去年负责一个数据可视化平台项目时,就遇到了这个问题。产品经理要求左侧的指标选择面板要能平滑收起,给图表区更多展示空间。当时试了几种方案都不理想,最后决定基于ElementUI的动画原理自己封装一个。

2. 理解ElementUI的过渡动画原理

2.1 Vue的过渡系统基础

Vue提供了<transition>组件来实现元素进入/离开的过渡效果。它会在适当的时候添加/删除CSS类名,主要包括:

  • v-enter:进入动画的初始状态
  • v-enter-active:进入动画的激活状态
  • v-enter-to:进入动画的结束状态
  • v-leave:离开动画的初始状态
  • v-leave-active:离开动画的激活状态
  • v-leave-to:离开动画的结束状态

ElementUI的el-collapse-transition组件就是基于这套机制实现的。它通过JavaScript钩子函数精确控制过渡过程,比纯CSS方案更灵活。

2.2 分析el-collapse-transition源码

关键点在于它实现了6个JavaScript钩子:

  1. beforeEnter:进入动画开始前
  2. enter:进入动画执行中
  3. afterEnter:进入动画完成后
  4. beforeLeave:离开动画开始前
  5. leave:离开动画执行中
  6. afterLeave:离开动画完成后

这些钩子函数主要做了三件事:

  • 保存元素原始样式(如width、padding)
  • 应用过渡样式(如设置width为0)
  • 恢复原始样式

3. 实现左右折叠动画组件

3.1 组件基础结构

我们先创建一个名为HorizontalCollapseTransition.vue的文件:

<template> <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" > <slot></slot> </transition> </template> <script> export default { name: 'HorizontalCollapseTransition', methods: { // 这里实现各个动画钩子 } } </script>

3.2 核心动画钩子实现

重点在于width和padding的处理。下面是完整实现:

methods: { beforeEnter(el) { el.classList.add('horizontal-transition') if (!el.dataset) el.dataset = {} // 保存原始样式 el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight // 设置初始状态 el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' }, enter(el) { // 保存overflow状态 el.dataset.oldOverflow = el.style.overflow if (el.scrollWidth !== 0) { el.style.width = el.scrollWidth + 'px' el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } else { el.style.width = '' el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } el.style.overflow = 'hidden' }, afterEnter(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow }, beforeLeave(el) { if (!el.dataset) el.dataset = {} // 保存当前样式 el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight el.dataset.oldOverflow = el.style.overflow // 设置初始状态 el.style.width = el.scrollWidth + 'px' el.style.overflow = 'hidden' }, leave(el) { if (el.scrollWidth !== 0) { el.classList.add('horizontal-transition') el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' } }, afterLeave(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } }

3.3 添加过渡样式

在组件的<style>部分添加过渡效果:

.horizontal-transition { transition: width 0.3s ease-in-out, padding-left 0.3s ease-in-out, padding-right 0.3s ease-in-out; }

4. 在项目中使用组件

4.1 全局注册组件

在main.js中全局注册:

import HorizontalCollapseTransition from '@/components/HorizontalCollapseTransition' Vue.component('horizontal-collapse-transition', HorizontalCollapseTransition)

4.2 实现侧边栏折叠

典型的使用场景:

<template> <div class="layout"> <div class="sidebar" :style="{width: isCollapse ? '0' : '250px'}"> <horizontal-collapse-transition> <div class="menu-content" v-show="!isCollapse"> <!-- 菜单内容 --> </div> </horizontal-collapse-transition> </div> <div class="main-content"> <button @click="isCollapse = !isCollapse"> {{ isCollapse ? '展开' : '折叠' }} </button> <!-- 主内容区 --> </div> </div> </template> <script> export default { data() { return { isCollapse: false } } } </script> <style> .layout { display: flex; } .sidebar { overflow: hidden; } .menu-content { width: 250px; padding: 20px; } .main-content { flex: 1; } </style>

5. 进阶优化技巧

5.1 支持动态宽度

有时候折叠区域的宽度可能是动态的。我们可以通过props接收宽度值:

props: { width: { type: String, default: '250px' } }, methods: { enter(el) { // ...其他代码 if (el.scrollWidth !== 0) { el.style.width = this.width // 使用props传入的宽度 // ...其他代码 } } }

使用时:

<horizontal-collapse-transition width="300px"> <!-- 内容 --> </horizontal-collapse-transition>

5.2 性能优化建议

  1. 避免频繁重排:在动画过程中,浏览器需要不断重排。可以通过以下方式优化:

    • 对动画元素使用will-change: width
    • 确保动画元素不是复杂DOM结构的一部分
  2. 硬件加速:可以尝试添加transform: translateZ(0)来启用GPU加速

  3. 节流处理:快速连续点击折叠按钮时,可以添加防抖逻辑

data() { return { isCollapse: false, isAnimating: false } }, methods: { toggleCollapse() { if (this.isAnimating) return this.isAnimating = true this.isCollapse = !this.isCollapse setTimeout(() => { this.isAnimating = false }, 300) // 与动画持续时间一致 } }

5.3 与ElementUI组件集成

我们的组件可以完美配合ElementUI的菜单组件使用:

<el-menu :collapse="isCollapse" class="sidebar-menu" > <horizontal-collapse-transition> <div v-show="!isCollapse"> <el-menu-item index="1">首页</el-menu-item> <el-menu-item index="2">用户管理</el-menu-item> <el-menu-item index="3">订单管理</el-menu-item> </div> </horizontal-collapse-transition> </el-menu>

6. 常见问题与解决方案

6.1 动画卡顿问题

如果发现动画不够流畅,可以尝试以下解决方案:

  1. 检查CSS属性:确保没有在过渡期间改变会触发重排的属性,如height、margin等

  2. 简化DOM结构:动画元素内部的DOM结构越简单越好

  3. 使用requestAnimationFrame:修改enter和leave方法:

enter(el, done) { requestAnimationFrame(() => { // 动画逻辑 el.style.width = this.width el.addEventListener('transitionend', done) }) }

6.2 内容闪动问题

在组件初始渲染时,可能会出现内容闪动。解决方法:

  1. 确保初始状态一致:在mounted钩子中设置初始样式
mounted() { if (this.isCollapse) { this.$el.style.width = '0' this.$el.style.paddingLeft = '0' this.$el.style.paddingRight = '0' } }
  1. 使用v-if替代v-show:对于初始不可见的内容

6.3 浏览器兼容性问题

虽然现代浏览器都支持CSS过渡,但需要注意:

  1. IE兼容性:如果需要支持IE,可能需要添加polyfill

  2. 移动端适配:在移动设备上可能需要调整过渡时间

@media (max-width: 768px) { .horizontal-transition { transition-duration: 0.2s; } }

7. 完整组件代码

以下是经过优化的完整组件代码:

<template> <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" :css="false" > <slot></slot> </transition> </template> <script> export default { name: 'HorizontalCollapseTransition', props: { width: { type: String, default: '250px' }, duration: { type: Number, default: 300 } }, methods: { beforeEnter(el) { el.classList.add('horizontal-transition') if (!el.dataset) el.dataset = {} el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' }, enter(el, done) { requestAnimationFrame(() => { el.dataset.oldOverflow = el.style.overflow if (el.scrollWidth !== 0) { el.style.width = this.width el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } else { el.style.width = '' el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } el.style.overflow = 'hidden' setTimeout(done, this.duration) }) }, afterEnter(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow }, beforeLeave(el) { if (!el.dataset) el.dataset = {} el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight el.dataset.oldOverflow = el.style.overflow el.style.width = el.scrollWidth + 'px' el.style.overflow = 'hidden' }, leave(el, done) { requestAnimationFrame(() => { if (el.scrollWidth !== 0) { el.classList.add('horizontal-transition') el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' } setTimeout(done, this.duration) }) }, afterLeave(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } } } </script> <style> .horizontal-transition { transition-property: width, padding-left, padding-right; transition-timing-function: ease-in-out; will-change: width, padding-left, padding-right; } </style>

这个优化版本增加了props配置,使用了requestAnimationFrame提升性能,并添加了will-change优化提示。

http://www.jsqmd.com/news/572143/

相关文章:

  • 3个核心优势解决离线文本提取难题:Umi-OCR如何重塑本地OCR工作流
  • 从MDK到VSCode:为STM32H743搭建一个高效双开发环境工程模板(含ARM Compiler V5/V6选择指南)
  • 如何彻底掌控你的微信聊天记录:WeChatMsg本地数据管理终极指南
  • Java-Redis
  • 实战应用:基于快马平台开发完整权限监控应用,保障用户隐私
  • JAVA-Web端学习6 ElementPlus
  • 银河麒麟系统下JDK安装全攻略:在线与离线两种方式详解(ARM版)
  • Doris集群部署避坑指南:3FE+3BE配置全流程(含Java环境配置与常见问题解决)
  • Jetson AGX Orin上编译报错‘找不到 -lnvidia-ml’?别急着重装系统,先检查这个源文件
  • 突破阅读限制:Tomato-Novel-Downloader让小说阅读不受束缚
  • 实战应用:在快马平台复现claude code教程中的电商列表页开发案例
  • 纯前端架构深度解析:jsontop.cn,JSON 格式化与全栈开发效率平台
  • 深度探索MAA:揭秘明日方舟全自动游戏助手的创新架构与实战应用
  • 深入浅出:NVIDIA BlueField DPU的BFB到底是什么?从原理到实践
  • 【T型三电平仿真】SPWM调制中的单双极性载波特性对比
  • VU13P FPGA板卡多卡级联实战:用光纤口实现200Gbps数据汇聚与处理
  • 3步搞定QQ机器人开发难题:LuckyLilliaBot OneBot实战指南
  • Modbus RTU通信常见问题排查:以三菱FX5U和CK系列读卡器为例
  • AI官网生成器:让你的想法在10分钟内成为官网
  • java面试小白福音:用快马ai生成带详解的渐进式学习应用
  • RadHAR实战:基于毫米波雷达点云的人类活动识别技术解析
  • 国产铷原子钟 快稳铷原子钟突破铷钟启动时长痛点 铷钟 特种铷原子钟
  • CasRel镜像部署指南:多租户隔离的关系抽取服务架构设计
  • 洛谷-入门6-函数与结构体2
  • OpenClaw 的模型训练中,是否使用了课程学习(Curriculum Learning)?
  • Qwen3.5-9B效果展示:强逻辑推理与代码生成惊艳案例集
  • 小红书自动评论的‘伪需求’与真风险:聊聊RPA工具养号背后的封号逻辑与合规玩法
  • 大三下期末突击指南:从编译原理到大数据,这6门课我是怎么一周内搞定的
  • 离线语音智能处理平台Buzz:本地化音频转文本全攻略
  • 告别CPU高负载!在RK3588开发板上用FFMedia实现H.264硬件编解码的保姆级教程