uniapp 商详吸顶

6 阅读2分钟
<template>
  <view class="product-detail">
    <!-- 商品头部信息区域 -->
    <view class="product-header">...</view>

    <!-- 吸顶Tab栏 -->
    <view v-if="showStickyTab" class="sticky-tabs" :style="{ top: stickyTop + 'px' }">
      <view class="tab-list">
        <view 
          v-for="(tab, index) in tabList" 
          :key="index"
          class="tab-item" 
          :class="{ 'active': activeTabIndex === index }"
          @click="switchTab(index)"
        >
          {{ tab.name }}
        </view>
      </view>
    </view>

    <!-- 页面内容区域 -->
    <scroll-view 
      scroll-y 
      :scroll-top="scrollTop" 
      @scroll="onScroll"
      class="content-scroll-view"
    >
      <!-- 商品介绍区域 -->
      <view id="anchor-product" class="content-section">商品详情内容...</view>
      <!-- 评价区域 -->
      <view id="anchor-comment" class="content-section">评价内容...</view>
      <!-- 详情区域 -->
      <view id="anchor-detail" class="content-section">图文详情内容...</view>
      <!-- 推荐区域 -->
      <view id="anchor-recommend" class="content-section">推荐内容...</view>
    </scroll-view>
  </view>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue';

// Tab列表配置
const tabList = [
  { name: '商品', anchor: 'anchor-product' },
  { name: '评价', anchor: 'anchor-comment' },
  { name: '详情', anchor: 'anchor-detail' },
  { name: '推荐', anchor: 'anchor-recommend' }
];

// 响应式数据
const activeTabIndex = ref(0); // 当前激活的Tab索引
const showStickyTab = ref(false); // 是否显示吸顶Tab栏
const scrollTop = ref(0); // 滚动位置,用于点击Tab时滚动
const sectionTops = ref([]); // 存储各个锚点区域距页面顶部的距离
const isScrollingByClick = ref(false); // 标记是否由点击Tab触发的滚动
onMounted(() => {
  // 等待DOM渲染完成后,计算各区域距离顶部的距离
  nextTick(() => {
    setTimeout(() => {
      calculateAnchorPositions();
    }, 100); // 添加短暂延迟确保所有内容已渲染
  });
});

// 计算所有锚点区域的位置
const calculateAnchorPositions = () => {
  const query = uni.createSelectorQuery().in(this);
  sectionTops.value = []; // 清空旧数据

  tabList.forEach((tab) => {
    query.select(`#${tab.anchor}`).boundingClientRect((rect) => {
      if (rect) {
        // 计算元素顶部距离页面顶部的绝对距离
        sectionTops.value.push(Math.floor(rect.top));
      }
    }).exec();
  });
};
// 滚动事件处理函数
const onScroll = (event) => {
  // 如果当前滚动是由点击Tab触发的,则不进行自动切换判断,避免冲突
  if (isScrollingByClick.value) return;

  const scrollTop = event.detail.scrollTop;
  
  // 控制吸顶Tab的显示/隐藏(例如在滚动超过200px后显示)
  showStickyTab.value = scrollTop > 200;

  // 从后向前遍历,找到第一个滚动位置超过其顶部的区域
  for (let i = sectionTops.value.length - 1; i >= 0; i--) {
    // 添加一个偏移量(如80),让切换触发更自然
    if (scrollTop >= sectionTops.value[i] - 80) {
      activeTabIndex.value = i;
      break;
    }
  }
};
// 切换Tab函数
const switchTab = (index) => {
  // 更新激活状态
  activeTabIndex.value = index;
  
  // 设置标志位,防止滚动事件干扰
  isScrollingByClick.value = true;
  
  // 使用uni.pageScrollTo实现平滑滚动
  uni.pageScrollTo({
    scrollTop: sectionTops.value[index], // 滚动到目标区域顶部
    duration: 300, // 动画时长300ms
    success: () => {
      // 滚动完成后,在短暂延迟后清除标志位
      // 这个延迟是为了避免滚动惯性带来的误触发
      setTimeout(() => {
        isScrollingByClick.value = false;
      }, 400);
    },
    fail: () => {
      // 即使失败也清除标志位
      isScrollingByClick.value = false;
    }
  });
};
<style scoped>
.sticky-tabs {
  position: sticky;
  z-index: 999;
  background-color: #ffffff;
  border-bottom: 1rpx solid #eeeeee;
}

.tab-list {
  display: flex;
  height: 80rpx;
}

.tab-item {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28rpx;
  color: #666;
  transition: all 0.3s;
}

.tab-item.active {
  color: #e4393c; /* 激活状态颜色 */
  font-weight: bold;
}

.content-section {
  min-height: 100vh; /* 确保每个区域有足够高度 */
  padding: 30rpx;
  box-sizing: border-box;
}
</style>
</script>