自定义标签切换动画

子组件

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>
```
相关推荐
福兮说6 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
凤城老人8 小时前
从 PyQt6 到 Electron:给 Edge TTS 做一个“多角色配音机“的踩坑手记
javascript·typescript·electron
Dovis(誓平步青云)8 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan8 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_9 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
yivifu10 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
weixin_4482902511 小时前
书庐开发实战教学
javascript·css·html
默_笙11 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript
一木 之林11 小时前
DeepSeek Agent 开发(一)
开发语言·前端·javascript
樱花落木兰13 小时前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存