![]()
子组件
<template> <!-- Tab组件的根容器 --> <div class="my-tab" ref="myTabRef"> <!-- 遍历tabs数组,生成每个tab项 --> <div class="my-tab-item" v-for="(item, index) in tabs" :ref="(el) => (itemRefs[index] = el)" :class="{ active: activeName === item.name }" @click="tabClick(item)" > <span class="tab-text">{{ item.label }}</span> </div> <!-- 滑块元素,使用计算属性动态设置样式 --> <div class="slider" :style="sliderStyle"></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 lang="less" 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 style="width: 500px; margin: 20px;"> <SliderTabs :tabs="list" default-active="tabThree" @change="handleChange" /> </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 lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</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 class="container"> <div class="nav-box"> <div class="nav-item active">第一个</div> <div class="nav-item">ashld阿松大</div> <div class="nav-item">阿松大安东将军就澳视度</div> <div class="nav-item">156啊十大金牌阿松大aasd1</div> <div class="slider"></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> ```