在使用 UniApp + Vue3 开发微信小程序时,经常会遇到这样一个需求:
顶部是横向滚动的 Tab,点击某个标题,页面自动滚动到对应内容区域。
今天分享一下我自己实现的完整思路与代码。这个案例是以"水果介绍"为例,实现点击水果名称,自动滚动到对应说明区域。
实现效果
-
顶部横向滚动 Tab(固定在顶部)
-
点击某个水果名称
-
页面平滑滚动到对应内容区域
-
当前选中 Tab 高亮
实现思路
核心逻辑其实分为三步:
-
渲染 Tab 和内容列表
-
获取所有内容区域的 top 位置
-
点击 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>
效果:
