自定义标签切换动画

子组件

vue 复制代码
<template>
  <div class="my-tab" ref="myTabRef">
    <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>
import { ref, computed, nextTick, onMounted, watch } from 'vue'

const props = defineProps({
  tabs: {
    type: Array,
    required: true,
    default: () => [],
  },
  defaultActive: {
    type: String,
    default: '',
  },
})

const emit = defineEmits(['change'])

const myTabRef = ref(null)
const itemRefs = ref([])
const activeName = ref('')

watch(
  () => props.defaultActive,
  (val) => {
    if (val) activeName.value = val
  },
  { immediate: true }
)

onMounted(() => {
  if (!activeName.value && props.tabs.length) {
    activeName.value = props.tabs[0].name
  }
  nextTick(() => scrollToCenter())
})

const tabClick = async (item) => {
  activeName.value = item.name
  emit('change', item)
  await nextTick()
  scrollToCenter()
}

const scrollToCenter = () => {
  const nav = myTabRef.value
  if (!nav) return
  const index = props.tabs.findIndex((t) => t.name === activeName.value)
  const item = itemRefs.value[index]
  if (!item) return

  const to = item.offsetLeft - (nav.offsetWidth - item.offsetWidth) / 2
  nav.scrollLeft = to
}

const sliderStyle = computed(() => {
  const index = props.tabs.findIndex((t) => t.name === activeName.value)
  const el = itemRefs.value[index]
  if (!el) return { opacity: 0 }

  return {
    left: el.offsetLeft + 'px',
    width: el.offsetWidth + 'px',
    opacity: 1,
  }
})
</script>

<style lang="less" scoped>
.my-tab {
  position: relative;
  height: 44px;
  background: #fff;
  display: flex;
  align-items: center;
  overflow-x: auto;
  white-space: nowrap;
  scroll-behavior: smooth;
  padding: 0 10px;

  &::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
  }
}

.my-tab-item {
  flex: none;
  height: 44px;
  line-height: 44px;
  padding: 0 14px;
  color: #999;
  font-size: 14px;
  position: relative;
  z-index: 1;
  cursor: pointer;

  &.active {
    color: #333;
    font-weight: bold;
  }
}

.tab-text {
  display: inline-block;
}

.slider {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3px;
  background: linear-gradient(180deg, #649afc 0%, #5180f4 100%);
  border-radius: 2px;
  opacity: 0;
  transition: all 0.3s ease;
  z-index: 0;
}
.slider::before{
  content: '';
  position: absolute;
  left: 0;
  bottom: 50%;
  height: 50%;
  background: #649afc;
  border-radius: 50px;
  opacity: 0;
  transition: all 0.3s ease;
  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>
```
相关推荐
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
kyriewen5 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
weedsfly7 小时前
迭代器、生成器与异步迭代——让数据“按需流动”的艺术
前端·javascript
假如让我当三天老蒯7 小时前
前端跨域解决方案(学习用)
前端·javascript·面试
铁皮饭盒9 小时前
Bun 哪比 Node.js 快?
javascript·后端
JieE21217 小时前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
candyTong20 小时前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构
_柳青杨1 天前
深入理解 JavaScript 事件循环
前端·javascript
大家的林语冰1 天前
ES5 凉凉,Babel 8 正式发布,默认不再编译为 ES5 和 CJS......
前端·javascript·前端工程化
weedsfly1 天前
异步编程全景与事件循环——彻底搞懂 JS 执行机制
前端·javascript