Vue实现动态滑动标签页组件:带指示器动画的Tabs

在移动端开发中,标签页切换是常见的UI模式。本文将介绍如何使用Vue和Uni-app实现一个带有动态滑动指示器的标签页组件,支持平滑过渡动画和响应式布局。

设计思路

该组件包含以下核心功能:

  • 标签页切换功能

  • 底部指示器随选中项动态滑动

  • 平滑的过渡动画效果

  • 响应式布局适应不同屏幕

完整实现代码

html
<template>
  <view class="my-coupon">
    <!-- 标签页导航 -->
    <view class="tabs">
      <view 
        v-for="(item, index) in tabsList" 
        :key="index"
        class="tab-item"
        :class="{'active': currentTab === index}"
        @click="switchTab(index)"
        :id="`tab-${index}`"
      >
        {{ item.name }}
      </view>
      <!-- 动态滑动指示器 -->
      <view 
        class="tab-line" 
        :style="{
          width: lineWidth,
          transform: `translateX(${lineLeft})`,
          transition: 'transform 0.3s ease, width 0.3s ease'
        }"
      />
    </view>

    <!-- 内容区域 -->
    <view class="content">
      <view v-show="currentTab === 0" class="content-item">
        <view class="coupon-card">
          <text class="coupon-title">满100减20优惠券</text>
          <text class="coupon-desc">适用全平台商品</text>
          <text class="coupon-time">有效期至2023-12-31</text>
        </view>
      </view>
      <view v-show="currentTab === 1" class="content-item">
        <view class="used-coupon">
          <text class="coupon-title">已使用的优惠券</text>
          <text class="coupon-status">已使用</text>
        </view>
      </view>
      <view v-show="currentTab === 2" class="content-item">
        <view class="expired-coupon">
          <text class="coupon-title">已过期的优惠券</text>
          <text class="coupon-status">已过期</text>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      currentTab: 0,
      tabsList: [
        { name: '待使用', type: 0 },
        { name: '已使用', type: 1 },
        { name: '已过期', type: 2 }
      ],
      lineWidth: '60rpx', // 指示器初始宽度
      lineLeft: '0px'     // 指示器初始位置
    }
  },
  mounted() {
    // 组件挂载后初始化指示器位置
    this.$nextTick(() => {
      this.updateLinePosition()
    })
  },
  methods: {
    // 切换标签页
    switchTab(index) {
      this.currentTab = index
      // 确保DOM更新后计算新位置
      this.$nextTick(() => {
        this.updateLinePosition()
      })
    },
    // 更新指示器位置
    updateLinePosition() {
      const query = uni.createSelectorQuery().in(this)
      query.select(`#tab-${this.currentTab}`).boundingClientRect(tabRect => {
        if (tabRect) {
          // 获取容器位置作为参考
          query.select('.tabs').boundingClientRect(tabsRect => {
            if (tabsRect) {
              // 计算指示器相对于容器的位置
              const relativeLeft = tabRect.left - tabsRect.left
              // 计算中心偏移量,使指示器居中
              const centerOffset = (tabRect.width - 30) / 2
              // 更新指示器位置和宽度
              this.lineLeft = `${relativeLeft + centerOffset}px`
            }
          }).exec()
        }
      }).exec()
    }
  }
}
</script>

<style scoped>
/* 整体容器 */
.my-coupon {
  min-height: 100vh;
  background: #f7f9fc;
  font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}

/* 标签页容器 */
.tabs {
  position: relative;
  display: flex;
  width: 100%;
  height: 88rpx;
  background: #fff;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}

/* 单个标签项 */
.tab-item {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 30rpx;
  color: #666;
  position: relative;
  z-index: 1;
  transition: color 0.3s ease;
}

/* 激活状态标签 */
.tab-item.active {
  color: #2B7FFE;
  font-weight: 600;
}

/* 滑动指示器 */
.tab-line {
  position: absolute;
  bottom: 0;
  height: 4rpx;
  background: #2B7FFE;
  border-radius: 4rpx;
  z-index: 0;
}

/* 内容区域 */
.content {
  padding: 30rpx;
}

/* 内容项样式 */
.content-item {
  animation: fadeIn 0.4s ease;
}

/* 优惠券卡片 */
.coupon-card {
  background: linear-gradient(135deg, #ffffff, #f0f7ff);
  border-radius: 16rpx;
  padding: 30rpx;
  box-shadow: 0 4rpx 20rpx rgba(43, 127, 254, 0.1);
  display: flex;
  flex-direction: column;
  border-left: 8rpx solid #2B7FFE;
  margin-bottom: 30rpx;
}

.coupon-title {
  font-size: 32rpx;
  font-weight: 600;
  color: #333;
  margin-bottom: 15rpx;
}

.coupon-desc {
  font-size: 26rpx;
  color: #666;
  margin-bottom: 10rpx;
}

.coupon-time {
  font-size: 24rpx;
  color: #999;
}

.used-coupon, .expired-coupon {
  background: #f9f9f9;
  border-radius: 16rpx;
  padding: 30rpx;
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30rpx;
  border-left: 8rpx solid #ccc;
}

.coupon-status {
  font-size: 28rpx;
  color: #999;
  font-weight: 500;
}

/* 动画效果 */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20rpx);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
</style>

关键实现解析

  1. 标签页结构
<view class="tabs">
  <view 
    v-for="(item, index) in tabsList" 
    :key="index"
    class="tab-item"
    :class="{'active': currentTab === index}"
    @click="switchTab(index)"
    :id="`tab-${index}`"
  >
    {{ item.name }}
  </view>
  <!-- 滑动指示器 -->
  <view class="tab-line" 
        :style="{...}"/>
</view>
  1. 指示器位置计算
    核心方法updateLinePosition使用uni-app的createSelectorQueryAPI获取DOM位置信息:
updateLinePosition() {
  const query = uni.createSelectorQuery().in(this)
  query.select(`#tab-${this.currentTab}`).boundingClientRect(tabRect => {
    if (tabRect) {
      query.select('.tabs').boundingClientRect(tabsRect => {
        if (tabsRect) {
          // 计算相对位置
          const relativeLeft = tabRect.left - tabsRect.left
          // 居中偏移
          const centerOffset = (tabRect.width - 30) / 2
          // 更新指示器位置
          this.lineLeft = `${relativeLeft + centerOffset}px`
        }
      }).exec()
    }
  }).exec()
}
  1. 平滑动画效果
    通过CSS transition实现平滑过渡:
css
.tab-line {
  transition: transform 0.3s ease, width 0.3s ease;
}
  1. 响应式设计
    使用flex布局确保标签页自适应宽度:
css
.tabs {
  display: flex;
  width: 100%;
}

.tab-item {
  flex: 1;
}

功能亮点

  • 动态指示器:底部指示器精确跟随当前选中标签

  • 平滑动画:切换时有流畅的过渡效果

  • 响应式设计:适应不同屏幕尺寸

  • 性能优化:使用transform实现硬件加速

  • 视觉反馈:激活状态有明确的视觉区分

使用场景

该组件适用于:

  • 优惠券管理页面

  • 订单状态筛选

  • 内容分类切换

  • 用户信息分页展示

总结

本文实现了一个功能完善、视觉美观的标签页组件,核心在于精确计算指示器位置和流畅的动画过渡。通过uni-app的DOM查询API,我们可以在小程序环境中实现类似Web的效果。该组件具有良好的可复用性和扩展性,可以根据实际需求调整样式和功能。

posted @ 2025-06-20 14:44  網友攃  阅读(315)  评论(0)    收藏  举报