自定义标签切换动画

张开发
2026/5/12 4:10:37 15 分钟阅读

分享文章

自定义标签切换动画
子组件template !-- Tab组件的根容器 -- div classmy-tab refmyTabRef !-- 遍历tabs数组生成每个tab项 -- div classmy-tab-item v-for(item, index) in tabs :ref(el) (itemRefs[index] el) :class{ active: activeName item.name } clicktabClick(item) span classtab-text{{ item.label }}/span /div !-- 滑块元素使用计算属性动态设置样式 -- div classslider :stylesliderStyle/div /div /template script setup // 导入Vue组合式API函数 import { ref, computed, nextTick, onMounted, watch } from vue // 定义组件props const props defineProps({ // tabs数组包含tab的配置信息 tabs: { type: Array, required: true, default: () [], }, // 默认激活的tab名称 defaultActive: { type: String, default: , }, }) // 定义组件自定义事件 const emit defineEmits([change]) // Tab容器元素的引用 const myTabRef ref(null) // 存储每个tab项元素的引用数组 const itemRefs ref([]) // 当前激活的tab名称 const activeName ref() // 监听defaultActive属性的变化 watch( () props.defaultActive, (val) { // 如果有值更新当前激活的tab if (val) activeName.value val }, { immediate: true } ) // 组件挂载后的生命周期钩子 onMounted(() { // 如果没有默认激活的tab且有tabs数据默认激活第一个 if (!activeName.value props.tabs.length) { activeName.value props.tabs[0].name } // 在DOM更新后滚动到激活的tab中心位置 nextTick(() scrollToCenter()) }) // tab点击事件处理函数 const tabClick async (item) { // 更新当前激活的tab名称 activeName.value item.name // 触发change事件传递当前点击的tab对象 emit(change, item) // 等待DOM更新 await nextTick() // 滚动到激活的tab中心位置 scrollToCenter() } // 将激活的tab滚动到可视区域中心 const scrollToCenter () { // 获取tab容器元素 const nav myTabRef.value // 如果容器不存在则返回 if (!nav) return // 查找当前激活tab的索引 const index props.tabs.findIndex((t) t.name activeName.value) // 获取当前激活tab的元素 const item itemRefs.value[index] // 如果元素不存在则返回 if (!item) return // 计算滚动位置使tab居中显示 const to item.offsetLeft - (nav.offsetWidth - item.offsetWidth) / 2 // 设置容器的滚动位置 nav.scrollLeft to } // 计算滑块样式的计算属性 const sliderStyle computed(() { // 查找当前激活tab的索引 const index props.tabs.findIndex((t) t.name activeName.value) // 获取当前激活tab的元素 const el itemRefs.value[index] // 如果元素不存在返回透明度为0的样式 if (!el) return { opacity: 0 } // 返回滑块的样式对象 return { // 设置滑块左边距为tab的offsetLeft left: el.offsetLeft px, // 设置滑块宽度为tab的宽度 width: el.offsetWidth px, // 设置滑块透明度 opacity: 1, } }) /script style langless scoped // Tab组件容器样式 .my-tab { // 相对定位作为滑块的定位参考 position: relative; // 容器高度 height: 44px; // 背景颜色为白色 background: #fff; // 使用flex布局子元素横向排列且垂直居中 display: flex; align-items: center; // 允许横向滚动 overflow-x: auto; // 强制文本不换行 white-space: nowrap; // 平滑滚动效果 scroll-behavior: smooth; // 容器内边距 padding: 0 10px; // 底部边框 border-bottom: 2px solid rgba(229, 229, 229); // 隐藏滚动条 ::-webkit-scrollbar { // 滚动条宽度 width: 0; // 滚动条高度 height: 0; // 隐藏滚动条 display: none; } } // Tab项样式 .my-tab-item { // 不参与flex伸缩 flex: none; // tab项高度 height: 44px; // 行高使文本垂直居中 line-height: 44px; // 水平内边距 padding: 0 14px; // 文本颜色 color: #999; // 字体大小 font-size: 14px; // 相对定位 position: relative; // 层级高于滑块 z-index: 1; // 鼠标样式为手型 cursor: pointer; // 所有属性变化时的过渡动画 transition: all 0.3s ease-in-out; // 激活状态的tab项样式 .active { // 文本颜色改为白色 color: #ffffff; // 字体加粗 font-weight: bold; // 文本向下移动10px transform: translateY(10px); } } // 文本样式 .tab-text { // 行内块级元素 display: inline-block; } // 滑块样式 .slider { // 绝对定位 position: absolute; // 左边距 left: 0; // 底部定位 bottom: 0; // 滑块高度 height: 45px; // 滑块背景颜色 background-color: #64ccc5; // 上圆角为20px border-radius: 20px 20px 0 0; // 透明度 opacity: 0; // 所有属性变化时的过渡动画 transition: all 0.3s ease-in-out; // 层级低于tab项 z-index: 0; } /style ## 父组件 vue template div stylewidth: 500px; margin: 20px; SliderTabs :tabslist default-activetabThree changehandleChange / /div /template script setup import { ref } from vue import SliderTabs from ./ZiZuJian.vue // 父组件传递列表 const list ref([ { name: tabOne, label: tab切换显示1 }, { name: tabTwo, label: tab切换显示2 }, { name: tabThree, label: tab切换显示3 }, { name: tabFour, label: tab切换显示4 }, { name: tabFive, label: tab切换显示5 }, { name: tabSix, label: tab切换显示6 }, { name: tabSeven, label: tab切换显示7 }, { name: tabEight, label: tab切换显示8 }, { name: tabNine, label: tab切换显示9 }, { name: tabTen, label: tab切换显示10 }, { name: tabEleven, label: tab切换显示11 }, { name: tabTwelve, label: tab切换显示12 }, { name: tabThirteen, label: tab切换显示13 }, { name: tabFourteen, label: tab切换显示14 }, { name: tabFifteen, label: tab切换显示15 }, ]) // 切换事件 const handleChange (index) { console.log(当前选中索引, index) } /script ## html结构 html !DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleDocument/title style *{ margin: 0; padding: 0; box-sizing: border-box; font-family: sans-serif; } body{ /* background-color: #001c30; */ height: 100vh; display: flex; justify-content: center; align-items: center; /* color: #eee; */ } .container{ min-width: 600px; padding: 30px; box-shadow: 0 2px 16px #fff; border-radius: 22px; overflow: hidden; } .nav-box{ width: 100%; display: flex; justify-content: space-around; align-items: center; border-bottom: 2px solid rgba(229, 229, 229); font-size: 18px; font-weight: 600; position: relative; } .nav-box .nav-item{ padding: 18px; z-index: 2; cursor: pointer; transition: all 0.3s ease-in-out; } .nav-box .nav-item.active{ color: #ffffff; transform: translateY(10px); } .slider{ position: absolute; left: 14px; bottom: 0; width: 72px; z-index: 1; height: 45px; background-color: #64ccc5; /* border-radius: 2px; */ border-radius: 20px 20px 0 0; transition: all 0.3s ease-in-out; } keyframes fadeIn{ from{ transform: translateX(50px); opacity: 0; } to{ transform: translateX(0); opacity: 1; } } /style /head body div classcontainer div classnav-box div classnav-item active第一个/div div classnav-itemashld阿松大/div div classnav-item阿松大安东将军就澳视度/div div classnav-item156啊十大金牌阿松大aasd1/div div classslider/div /div /div script const tabs document.querySelectorAll(.nav-item); tabs.forEach((item,index){ item.addEventListener(click,e{ tabs.forEach((item){ item.classList.remove(active); }) item.classList.add(active); const slider document.querySelector(.slider); slider.style.width e.target.offsetWidth px; slider.style.left e.target.offsetLeft px; }) }) /script /body /html

更多文章