自定义标签切换动画

子组件

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 小时前
Vue2 实现 1.5s 十连击监听(连续点击若干次),封装通用可复用点击检测工具,不用 data!Vue 封装通用连击监听方法,支持自定义时长与点击次数
前端·javascript·vue.js
半个落月3 小时前
用 LangChain.js 手写一个能读写文件和执行命令的 Mini Cursor
javascript·人工智能·后端
天外天-亮3 小时前
HBuilder X 使用 uview-plus 方式
开发语言·javascript·hbuilder x·uview-plus
多加点辣也没关系7 小时前
JavaScript|第9章:对象 — 基础
开发语言·javascript·ecmascript
后台开发者Ethan8 小时前
setState组件更新
前端·javascript·react
蜡台9 小时前
uniapp vue2 转 vue3
前端·javascript·uni-app·vue3·vue2
用户059540174469 小时前
把前端内存泄漏排查从2小时压到5分钟,我们把它集成进了CI
前端·css
WindfallSheng10 小时前
从Vibe Coding到Spec Coding:一套可落地的AI-SDD企业级研发实战工程
javascript·vue.js
Asize10 小时前
Agent 入门:从 LLM 与 Agent 的区别到 Function Calling
javascript·人工智能
我有满天星辰10 小时前
Vue 指令完全指南:从 Vue 2 到 Vue 3 的演进与实战
前端·javascript·vue.js