<template>
  <view class="tab-container">
    <!-- Tab 标签滚动容器 -->
    <scroll-view scroll-x="true" class="tab-scroll" scroll-with-animation="true">
      <view class="tab-list">
        <view
          v-for="(item, index) in tabs"
          :key="index"
          class="tab-item"
          :class="{ active: currentIndex === index }"
          @click="changeTab(index)"
        >
          {{ item.name }}
        </view>
      </view>
    </scroll-view>

    <!-- 内容区域 -->
    <swiper
      :current="currentIndex"
      class="swiper-content"
      @change="onSwiperChange"
      :duration="300"
      scroll-x="true"
    >
      <swiper-item v-for="(item, index) in tabs" :key="index">
        <view class="swiper-item-content">
          <!-- 内容区域的具体内容 -->
          <view>{{ item.name }} Content</view>
        </view>
      </swiper-item>
    </swiper>
  </view>
</template>
<script lang="ts">
import { ref } from 'vue';

export default {
  setup() {
    const tabs = ref([
      { name: 'Tab 1' },
      { name: 'Tab 2' },
      { name: 'Tab 3' },
	  { name: 'Tab 4' },
	  // { name: 'Tab 5' },
	  // { name: 'Tab 6' },
	  // { name: 'Tab 7' },
	  // { name: 'Tab 8' },
	  // { name: 'Tab 9' },
      // ... 更多 Tab
    ]);

    const currentIndex = ref(0);

    const changeTab = (index: number) => {
      currentIndex.value = index;
    };

    const onSwiperChange = (e: any) => {
      currentIndex.value = e.detail.current;
    };

    return {
      tabs,
      currentIndex,
      changeTab,
      onSwiperChange,
    };
  },
};
</script>
<style>
.tab-container {
  width: 100%;
}

.tab-scroll {
  white-space: nowrap;
  overflow-x: auto;
}

.tab-list {
  display: flex;
}

.tab-item {
  padding: 10px 20px;
  cursor: pointer;
  position: relative;
}

.tab-item.active {
  color: #f10606;
}

.swiper-content {
  height: 300px; /* 设置内容区域的高度 */
}

.swiper-item-content {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 20px;
}
.active::after {
	content: '';
	position: absolute;
	/* background-image: url(../../static/checkDetails/action.png); */
	background-size: 100%;
	background: #f00;
	background-repeat: no-repeat;
	width: 40px;
	height: 4px;
	top: 38px;
	left: 30px;
}
</style>

基本思路

1横向滚动的动画效果主要依靠scrollview自带的croll-with-animation="true"来设置

2 而底部下划线则是给当前激活的tab设置伪元素来实现

2:通过v-model实现索引值的绑定

以上我们只实现了基础的tab标签页效果,如果要实现常见的v-model如何实现?

<template>
  <view class="tab-container">
    <!-- Tab 标签滚动容器 -->
    <scroll-view scroll-x="true" :scroll-left="scrollLeft" class="tab-scroll" scroll-with-animation="true">
      <view class="tab-list">
        <view
          :id="'tab' + index"
          v-for="(item, index) in list"
          :key="index"
          class="tab-item"
          :class="{ active: index == activeIndex }"
          @click="handleTabClick(item, index)"
        >
          <text class="tab_name"> {{ item.sku }}</text>
          <text class="tab_num">{{ item.actualWeight }} / {{ item.outboundWeight }} </text>
        </view>
      </view>
    </scroll-view>
  </view>
</template>
<script setup lang="ts">
import { onLoad, onShow, onReady } from '@dcloudio/uni-app'
import { getCurrentInstance, computed } from 'vue'
// const currentTabId = ref('')
const tabWidth = ref(0)
const emits = defineEmits(['change', 'update:modelValue'])

const props = defineProps({
  list: {
    type: Array,
    default: () => []
  },
  modelValue: {
    type: Number,
    default: 0
  }

})



let activeIndex = ref(props.modelValue)
watch(
  () => props.modelValue,
  (newVal) => {
    console.log('内部newVal', newVal)
    console.log('内部newVal--type', typeof newVal)
    activeIndex.value = newVal
  }
)

const handleTabClick = (data, index) => {
  activeIndex.value = index
  let curData = props.list[index]
  // currentTabId.value = data.skuId + index
  emits('update:modelValue', activeIndex.value)
  emits('change', { data: curData, index: activeIndex.value })
}
</script>
<style scoped lang="scss">
.tab-scroll {
  white-space: nowrap;
  overflow-x: auto;
}

.tab-item {
  padding: 20rpx 40rpx; /* 使用px */
  display: inline-block;
  position: relative;
  font-size: 26rpx;
  color: #343b52;
  cursor: pointer;
  vertical-align: top;
}
.tab_num {
  color: #76c802;
  display: inline-block;
}
.active {
  color: #694feb;
}
.active::after {
  content: '';
  position: absolute;
  /* background-image: url(../../static/checkDetails/action.png); */
  background-size: 100%;
  background: #694feb;
  background-repeat: no-repeat;
  width: 100rpx;
  height: 4rpx;
  top: 64rpx;
  left: 40rpx;
}
</style>

父组件中

 <Tabs
          :list="detailData.skuList"
          v-model="currentIndex"
          @change="changeTab"
          v-if="detailData.skuList && detailData.skuList.length > 0"
        />

const currentIndex=ref(0)
const changeTab = (data) => {
  console.log('changetab回调', data)

  currentSkuData.value = data.data
  tableParams.value.materialId = data.data.skuId
  // console.log('currentSkuData--', currentSkuData.value)
  // currentIndex.value = data.index
  if (data.data.hasCode == 1) {
    getTableList()
  }
}

3实现内容区滚动tab标签页滚动定位

核心:获取tab的宽度。每次滚动内容区的时候通过计算 const tabLeft = tabWidth.value * activeIndex.value,然后给scroview的 :scroll-left="scrollLeft"设置scrollLeft值既可

<template>
  <view class="tab-container">
    <!-- Tab 标签滚动容器 -->
    <scroll-view scroll-x="true" :scroll-left="scrollLeft" class="tab-scroll" scroll-with-animation="true">

        <view
          :id="'tab' + index"
          v-for="(item, index) in list"
          :key="index"
          class="tab-item"
          :class="{ active: index == activeIndex }"
          @click="handleTabClick(item, index)"
        >
          <text class="tab_name"> {{ item.sku }}</text>
          <text class="tab_num">{{ item.actualWeight }} / {{ item.outboundWeight }} </text>
        </view>
      </view>
    </scroll-view>
  </view>
</template>
<script setup lang="ts">
import { onLoad, onShow, onReady } from '@dcloudio/uni-app'
import { getCurrentInstance, computed } from 'vue'
// const currentTabId = ref('')
const tabWidth = ref(0)
const emits = defineEmits(['change', 'update:modelValue'])

const props = defineProps({
  list: {
    type: Array,
    default: () => []
  },
  modelValue: {
    type: Number,
    default: 0
  }


onMounted(async () => {
  let res = await getEle()
  tabWidth.value = res[1].width
  console.log('getEle--res-', res)
  console.log('getEle--res-wid', res[0].width)
})

const getEle = () => {
  const { proxy } = getCurrentInstance()
  return new Promise((resolve, reject) => {
    let view = uni.createSelectorQuery().in(proxy)
    view
      .selectAll('.tab-item')
      .boundingClientRect((data) => {
        resolve(data)
      })
      .exec()
  })
}
const scrollLeft = computed(() => {
  const tabLeft = tabWidth.value * activeIndex.value
  return tabLeft
})

let activeIndex = ref(props.modelValue)
watch(
  () => props.modelValue,
  (newVal) => {
    console.log('内部newVal', newVal)
    console.log('内部newVal--type', typeof newVal)
    activeIndex.value = newVal
  }
)

const handleTabClick = (data, index) => {
  activeIndex.value = index
  let curData = props.list[index]
  // currentTabId.value = data.skuId + index
  emits('update:modelValue', activeIndex.value)
  emits('change', { data: curData, index: activeIndex.value })
}
</script>
<style scoped lang="scss">
.tab-scroll {
  white-space: nowrap;
  overflow-x: auto;
}

.tab-item {
  padding: 20rpx 40rpx; /* 使用px */
  display: inline-block;
  position: relative;
  font-size: 26rpx;
  color: #343b52;
  cursor: pointer;
  vertical-align: top;
}
.tab_num {
  color: #76c802;
  display: inline-block;
}
.active {
  color: #694feb;
}
.active::after {
  content: '';
  position: absolute;
  /* background-image: url(../../static/checkDetails/action.png); */
  background-size: 100%;
  background: #694feb;
  background-repeat: no-repeat;
  width: 100rpx;
  height: 4rpx;
  top: 64rpx;
  left: 40rpx;
}
</style>

更多推荐