UniApp + Vue3 实现 Tab 点击滚动定位(微信小程序)

在使用 UniApp + Vue3 开发微信小程序时,经常会遇到这样一个需求:

顶部是横向滚动的 Tab,点击某个标题,页面自动滚动到对应内容区域。

今天分享一下我自己实现的完整思路与代码。这个案例是以"水果介绍"为例,实现点击水果名称,自动滚动到对应说明区域。

实现效果

  • 顶部横向滚动 Tab(固定在顶部)

  • 点击某个水果名称

  • 页面平滑滚动到对应内容区域

  • 当前选中 Tab 高亮


实现思路

核心逻辑其实分为三步:

  1. 渲染 Tab 和内容列表

  2. 获取所有内容区域的 top 位置

  3. 点击 Tab 时调用 uni.pageScrollTo 滚动


技术栈

  • 框架:Vue3(script setup 语法)

  • 平台:UniApp

  • 运行端:微信小程序

  • API:uni.createSelectorQuery() + uni.pageScrollTo()

1️⃣ 数据结构

我们定义一个数组 resultListData

javascript 复制代码
let resultListData = ref([
  {
    title: "苹果",
    content: "苹果介绍..."
  },
  {
    title: "香蕉",
    content: "香蕉介绍..."
  }
]);

每一项:

  • title → 作为顶部 Tab

  • content → 作为下面的内容区域

2️⃣ 页面结构

顶部 Tab:

javascript 复制代码
<view class="topTab-con">
  <view
    class="tabItem"
    v-for="(item,index) in resultListData"
    @click="pickerResFn(item,index)"
    :class="activityIndex == index?'activited': ''"
    :key="index"
  >
    {{item.title}}
  </view>
</view>

内容区域:

javascript 复制代码
<view class="res-item"
  v-for="(item,index) in resultListData"
  :key="index"
>
  <view class="res-card">
    <view class="title-con">{{item.title}}</view>
    <view class="res-con">{{item.content}}</view>
  </view>
</view>

核心逻辑解析


① 记录每个内容块的位置

我们使用:

javascript 复制代码
uni.createSelectorQuery().selectAll().boundingClientRect()

获取所有 .res-item 的 top 值。

javascript 复制代码
const sectionTops = ref([]);

const getMyElementFn = () => {
  const query = uni.createSelectorQuery();
  query
    .selectAll(".res-item")
    .boundingClientRect(data => {
      if (data && data.length) {
        sectionTops.value = data.map(item => item.top);
      }
    })
    .exec();
};

⚠ 为什么要在 nextTick 里执行?

因为必须等 DOM 渲染完成才能获取到真实位置。

javascript 复制代码
onMounted(() => {
  nextTick(() => {
    getMyElementFn();
  });
});

② 点击 Tab 进行滚动

javascript 复制代码
const pickerResFn = (item, index) => {
  activityIndex.value = index;

  const targetTop = sectionTops.value[index];

  uni.pageScrollTo({
    scrollTop: targetTop - 80,
    duration: 250
  });
};

为什么要减 80?

因为顶部是固定定位的 Tab,高度会遮挡内容。

css 复制代码
.topTab-con {
  position: fixed;
  top: 0;
  height: 160rpx;
}

所以需要减去固定头部高度(根据实际情况调整)。

样式关键点

1️⃣ 顶部固定横向滚动

css 复制代码
.topTab-con {
  position: fixed;
  top: 0;
  overflow-x: auto;
  display: flex;
}

2️⃣ 隐藏滚动条

复制代码
css 复制代码
&::-webkit-scrollbar {
  display: none;
}

3️⃣ 激活状态

复制代码
css 复制代码
.tabItem {
  background-color: #f6f6f6;

  &.activited {
    background-color: #007aff;
    color: #ffffff;
  }
}

实现原理总结

功能 使用技术
获取元素位置 uni.createSelectorQuery()
页面滚动 uni.pageScrollTo()
高亮切换 Vue3 响应式 ref
DOM 完成监听 nextTick()

注意事项

1️⃣ 必须等 DOM 渲染完再获取位置

否则拿到的是 undefined


2️⃣ 如果数据是接口异步加载

需要在数据赋值后再重新调用:

html 复制代码
nextTick(() => { getMyElementFn(); });

最终效果

✔ 横向滑动 Tab

✔ 点击跳转对应内容

✔ 平滑滚动

✔ 当前项高亮

整个实现逻辑清晰,性能也不错,非常适合在内容类页面中使用。


结语

这个功能在实际项目中非常常见,比如:

  • 商品详情页

  • 菜单分类页

  • 文章目录导航

  • 医疗说明页

  • 课程章节跳转

如果你也在使用 UniApp + Vue3 开发微信小程序,这个方案可以直接落地使用。

完整代码如下:

html 复制代码
<template>
  <view class="fruit-page">
    <view class="container">
      <view class="topTab-con">
        <view
          class="tabItem"
          v-for="(item,index) in resultListData"
          @click="pickerResFn(item,index)"
          :class="activityIndex == index?'activited': ''"
          :key="index"
        >{{item.title}}</view>
      </view>

      <!-- 主要区域 -->
      <view class="result-content">
        <view
          class="res-item"
          v-for="(item,index) in resultListData"
          :key="index"
          :id="'section'+index"
        >
          <view class="res-card">
            <view class="title-con">{{item.title}}</view>
            <view class="res-con">{{item.content}}</view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

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

import { onLoad } from "@dcloudio/uni-app";

let resultListData = ref([
  {
    title: "苹果",
    content:
      "学名Malus pumila Mill.,别称西洋苹果、柰,属于蔷薇科苹果属的植物。苹果是全球最广泛种植和销售的水果之一,具有悠久的栽培历史和广泛的分布范围。苹果的原始种群主要起源于中亚的天山山脉附近,尤其是现代哈萨克斯坦的阿拉木图地区,提供了所有现代苹果品种的基因库。苹果通过早期的贸易路线,如丝绸之路,从中亚向外扩散到全球各地。"
  },
  {
    title: "香蕉",
    content:
      "英文名(banana)芭蕉科芭蕉属多年生草本植物,植株丛生,有匐匍茎;假茎浓绿有黑色斑点;叶片长圆形,上面为深绿色,无白粉,下面浅绿色;花朵为乳白色或淡紫色;果实呈弯曲的弓状,有棱,果皮为青绿色,成熟后变黄;果肉松软,黄白色,味甜香味浓,无种子"
  },
  {
    title: "葡萄",
    content:
      "(学名:Vitis vinifera L.)是葡萄科葡萄属高大缠绕藤本,幼茎秃净或略被绵毛;叶片为纸质,圆卵形或圆形;花序大而长;萼很小,为黄绿色的杯状;花柱很短,为圆锥形;浆果为卵圆形至卵状长圆形,成熟时为紫黑色或红而带青色。 花期4-5月,果期8-9月。 李时珍在《本草纲目》中说:葡萄,《汉书》作蒲桃,可以造酒,人哺饮之,则陶然而醉,故有是名。"
  },
  {
    title: "梨子",
    content:
      "蔷薇科梨属乔木植物, 树冠开展;小枝粗壮,幼时有柔毛:二年生的枝紫褐色,具稀疏皮孔;托叶膜质,边缘具腺齿;叶片卵形或椭圆形,先端渐尖或急尖,初时两面有绒毛,老叶无毛;伞形总状花序,总花梗和花梗幼时有绒毛;果实卵形或近球形,微扁,褐色;花为白色;花期4月;果期8-9月。种子黑色或黑褐色,种皮软骨质,子叶平凸。"
  },
  {
    title: "量天尺",
    content:
      "是仙人掌科蛇鞭柱属的攀援肉质灌木,别名火龙果、三角柱、三棱箭、霸王鞭等 。茎粗壮,有常呈翅状的棱,深绿色至淡蓝绿色;小窠沿棱排列,有硬刺。花漏斗状,瓣状花被片白色。浆果红色,长球形,果肉白色。花期7~12月。 "
  },
  {
    title: "杏",
    content:
      "是蔷薇科李属落叶乔木,高约5-12米 ,别名归勒斯、杏子、野杏树等。杏是落叶乔木,高约8-12米。树冠开阔,呈圆球形或扁球形;叶呈宽卵形或圆卵形;花单生,两性花,花瓣呈白色或带红色;果实球形,白色、黄色至黄红色;种仁味苦或甜。花期3~4月,果期6~7月"
  },
  {
    title: "桃",
    content:
      "蔷薇科李属落叶小乔木植物;枝条圆柱形,光滑;叶互生,卵状披针形或长圆状披针形,有细齿,托叶线形;花萼被短柔毛,花瓣粉红色;核果宽卵状球形,密被短柔毛;核坚木质;种子扁卵状心形; 花期3-4月,果实成熟期因品种而异,通常为8-9月。"
  },
  {
    title: "樱桃",
    content:
      "属于乔木,株高达3-8米,树皮红褐色;嫩枝无毛或被疏柔毛;冬芽无毛;叶卵形或长圆状倒卵形,先端渐尖或尾尖,基部圆,有尖锐重锯齿,齿端有小腺体,上面近无毛,下面淡绿色,沿脉或脉间有稀疏柔毛;叶柄被疏柔毛,先端有1或2个大腺体,托叶早落,披针形,有羽裂腺齿;花序伞房状或近伞形,先叶开花;总苞倒卵状椭圆形,花梗被疏柔毛;萼筒钟状,长外面被疏柔毛,萼片三角状卵形或卵状长圆形,全缘,长为萼筒一半或近半;花瓣白色,卵形,先端下凹或2裂;花柱与雄蕊近等长,无毛;核果近球形,熟时红色,可食用。"
  },
  {
    title: "凤梨",
    content:
      "别名菠萝、露兜子、波罗等,是凤梨科凤梨属一种多年生草本果树。 该植物植株高约1米。茎短粗,呈褐色,基部有吸芽抽出。叶多数,莲座式排列,剑形;穗状花序于叶丛中抽出,状如松球;聚花果球状,果肉黄色多汁;种子细小,质地坚硬,紫黑色,为尖卵形。 花期夏季,果期5-7月。 "
  },
  {
    title: "芒果",
    content:
      "是漆树科杧果属高大乔木,为热带和亚热带果树。常绿大乔木。树皮灰褐色,小枝褐色,无毛。叶薄革质,略具光泽,常集生枝顶,叶形和大小变化较大,通常为长圆形或长圆状披针形,叶侧脉20-25对,斜升,两面突起,网脉不显,叶柄长2-6厘米,上面具槽,基部膨大。圆锥花序,多花密集,被灰黄色微柔毛,分枝开展,苞片披针形,花小,杂性,黄色或淡黄色,花梗具节,萼片卵状披针形,花瓣长圆形或长圆状披针形,花盘膨大,肉质,雄蕊仅1个发育,花药卵圆形;子房斜卵形。中果皮肉质,肥厚,鲜黄色,味甜。果核坚硬且大,肾形,成熟时黄色"
  },
  {
    title: "草莓",
    content:
      "为蔷薇科草莓属多年生草本植物 ,又名凤梨草莓,洋莓,红莓等。草莓植株一般高10~40厘米;茎低于叶或近相等;小叶具短柄,倒卵形或菱形,叶柄密被开展黄色柔毛;聚伞花序,花瓣白色,近圆形或倒卵椭圆形;雄蕊20枚,雌蕊极多。聚合果大,呈鲜红色;瘦果尖卵形,光滑 [2]。种子呈螺旋状排列在果肉上,长圆形,黄色或黄绿色。花期4~5月,果期6~7月"
  }
]); // 可以自行去模拟相关数据

let activityIndex = ref(0);

const sectionTops = ref([]); // 所有需要跳转的区域块 需要用类查找

const pickerResFn = (item, index) => {
  activityIndex.value = index;

  const targetTop = sectionTops.value[index];
  uni.pageScrollTo({
    scrollTop: targetTop - 80,
    duration: 250
  });
};

const getMyElementFn = () => {
  const query = uni.createSelectorQuery();
  query
    .selectAll(".res-item")
    .boundingClientRect(data => {
      if (data && data.length) {
        sectionTops.value = data.map(item => item.top);
      } else {
        console.log("未找到元素");
      }
    })
    .exec();
};

onMounted(() => {
  nextTick(() => {
    getMyElementFn();
  });
});

onLoad(options => {});
</script>

<style lang="scss">
.fruit-page {
  background-color: #ffffff;
  min-height: 100vh;

  .container {
    padding-bottom: 40rpx;
    padding-top: 160rpx;

    .topTab-con {
      position: fixed;
      top: 0;
      background-color: #ffffff;
      height: 160rpx;
      width: 100%;
      overflow-x: auto;
      display: flex;
      gap: 12rpx;
      padding: 40rpx;
      scrollbar-width: none;
      -ms-overflow-style: none;

      &::-webkit-scrollbar {
        display: none;
      }

      .tabItem {
        flex: 0 0 auto;
        width: 170rpx;
        height: 80rpx;
        border-radius: 40rpx;
        background-color: #f6f6f6;
        color: #383838;
        text-align: center;
        line-height: 80rpx;
        font-size: 24rpx;

        &.activited {
          background-color: #007aff;
          color: #ffffff;
        }
      }
    }

    .result-content {
      padding: 0 40rpx;

      .res-item {
        margin-top: 40rpx;

        .res-card {
          padding: 32rpx;
          border-radius: 28rpx;
          box-shadow: 0px 0px 2px 0px #171a1f14;

          .title-con {
            font-size: 36rpx;
            font-weight: 700;
          }

          .res-con {
            margin-top: 20rpx;
            color: #5f5f5f;
            line-height: 46rpx;
            font-size: 28rpx;
            font-weight: 400;
          }
        }
      }
    }
  }
}
</style>

效果:

相关推荐
大黄说说2 小时前
小程序商城哪个平台好?码云数智、有赞、微盟对比
小程序
游戏开发爱好者83 小时前
完整教程:App上架苹果App Store全流程指南
android·ios·小程序·https·uni-app·iphone·webview
潆润千川科技6 小时前
中老年同城社交小程序功能梳理与应用分析
小程序
予你@。6 小时前
uni-app progress 组件使用详解
uni-app
iOS阿玮6 小时前
春节提审高峰来袭!App Store 审核时长显著延长。
uni-app·app·apple
说私域7 小时前
数字化运营视角下用户留存体系构建与实践研究——以AI智能客服商城小程序为载体
人工智能·小程序·产品运营·流量运营·私域运营
码云数智-大飞7 小时前
小程序商城哪个平台好?小程序商城制作平台深度对比
微信小程序
code袁7 小时前
基于Springboot+Vue的家教小程序的设计与实现
vue.js·spring boot·小程序·vue·家教小程序
一 乐9 小时前
健身房预约|基于java+ vue健身房预约小程序系统(源码+数据库+文档)
java·vue.js·spring boot·小程序·论文·毕设·健身房预约小程序