在移动端应用开发中,Tab 标签导航是一种常见的交互模式。本文将详细介绍如何在 UniApp 中实现一个功能完善的智能吸顶 Tab 导航组件,该组件具有以下特性:
当用户向下滚动超过 200px 时,Tab 导航栏会出现并吸顶显示。随着继续滚动,Tab 会自动切换高亮状态,点击 Tab 可以快速定位到对应内容。
首先,我们需要设计基础的 HTML 结构:
基本信息
带看/跟进
相似房源
export default {
data() {
return {
// Tab配置
tabList: [
{ id: 'baseInfo', name: '基本信息' },
{ id: 'followRecord', name: '带看/跟进' },
{ id: 'similarHouses', name: '相似房源' }
],
// 状态控制
showTabs: false, // Tab显示状态
currentTab: -1, // 当前选中的Tab索引
distanceArr: [], // 各内容模块的位置信息
// 滚动控制
scrollTop: 0, // 当前滚动位置
lastScrollTop: undefined, // 上次滚动位置
scrollTimer: null, // 滚动节流定时器
// 点击控制
isClickingTab: false, // 是否正在点击Tab
clickingTabTimer: null, // 点击超时定时器
targetTab: -1, // 目标Tab索引
// 阈值配置
showTabsThreshold: 200, // 显示Tab的滚动阈值
hideTabsThreshold: 120, // 隐藏Tab的滚动阈值
}
}
}
// 滚动监听 - 使用节流优化性能
onScroll(e) {
const scrollTop = e.detail.scrollTop;
// 检测用户主动滚动
if (this.isClickingTab && this.lastScrollTop !== undefined) {
const scrollDiff = Math.abs(scrollTop - this.lastScrollTop);
if (scrollDiff > 200) {
// 用户主动滚动,清除点击标识
this.isClickingTab = false;
this.targetTab = -1;
}
}
this.lastScrollTop = scrollTop;
// 使用节流处理Tab显示和切换逻辑
if (this.scrollTimer) clearTimeout(this.scrollTimer);
this.scrollTimer = setTimeout(() => {
this.handleTabVisibility(scrollTop);
this.handleTabSwitch(scrollTop);
}, 16); // 约60fps
},
// 处理Tab显示/隐藏
handleTabVisibility(scrollTop) {
if (scrollTop >= this.showTabsThreshold) {
if (!this.showTabs) {
this.showTabs = true;
if (this.currentTab < 0) {
this.currentTab = 0;
}
}
} else if (scrollTop <= this.hideTabsThreshold) {
// 点击Tab时不隐藏
if (!this.isClickingTab) {
this.showTabs = false;
}
}
},
// 处理Tab自动切换
handleTabSwitch(scrollTop) {
if (!this.isClickingTab && this.distanceArr.length > 0) {
let newTab = 0;
// 计算偏移量(考虑导航栏高度)
const systemInfo = uni.getSystemInfoSync();
const headerHeight = systemInfo.statusBarHeight + 44 + 44; // 状态栏 + 导航栏 + Tab栏
// 从后往前遍历,找到当前应该高亮的Tab
for (let i = this.distanceArr.length - 1; i >= 0; i--) {
if (scrollTop >= (this.distanceArr[i] - headerHeight)) {
newTab = i;
break;
}
}
if (newTab !== this.currentTab) {
this.currentTab = newTab;
}
} else if (this.isClickingTab && this.targetTab >= 0) {
// 点击期间锁定Tab状态
this.currentTab = this.targetTab;
}
}
// 计算各内容模块的位置
calculateTabPositions() {
return new Promise((resolve) => {
this.distanceArr = [];
const queries = this.tabList.map((tab, index) => {
return new Promise((resolveQuery) => {
// 延迟确保DOM渲染完成
setTimeout(() => {
const query = uni.createSelectorQuery().in(this);
query.select(`#${tab.id}`).boundingClientRect();
query.selectViewport().scrollOffset();
query.exec(([element, viewport]) => {
if (element) {
// 计算元素相对于页面顶部的绝对位置
const absoluteTop = element.top + (viewport?.scrollTop || 0);
resolveQuery({ index, top: absoluteTop });
} else {
resolveQuery({ index, top: 0 });
}
});
}, 50);
});
});
Promise.all(queries).then(results => {
// 按索引排序并提取位置值
results.sort((a, b) => a.index - b.index);
this.distanceArr = results.map(item => item.top);
resolve(this.distanceArr);
});
});
}
// 点击Tab
clickTab(item, index) {
// 获取正确的索引
const tabIndex = typeof item === 'number' ? item :
(typeof index === 'number' ? index :
this.tabList.findIndex(tab => tab.id === item.id));
// 设置点击标识
this.isClickingTab = true;
this.targetTab = tabIndex;
this.currentTab = tabIndex;
// 设置超时保护
if (this.clickingTabTimer) clearTimeout(this.clickingTabTimer);
this.clickingTabTimer = setTimeout(() => {
this.isClickingTab = false;
this.targetTab = -1;
}, 2000);
// 检查位置数据
if (this.distanceArr.length === 0) {
// 重新计算位置
this.calculateTabPositions().then(() => {
this.scrollToTab(tabIndex);
});
} else {
this.scrollToTab(tabIndex);
}
},
// 滚动到指定Tab
scrollToTab(index) {
if (index < 0 || index >= this.distanceArr.length) return;
const systemInfo = uni.getSystemInfoSync();
const headerHeight = systemInfo.statusBarHeight + 44 + 44;
// 计算目标滚动位置
let targetScrollTop = this.distanceArr[index] - headerHeight + 20;
targetScrollTop = Math.max(0, targetScrollTop);
// 平滑滚动
uni.pageScrollTo({
scrollTop: targetScrollTop,
duration: 300,
complete: () => {
// 延迟清除点击标识
setTimeout(() => {
this.isClickingTab = false;
this.targetTab = -1;
}, 500);
}
});
}
mounted() {
// 初始化时计算位置
this.$nextTick(() => {
setTimeout(() => {
this.calculateTabPositions();
}, 500);
});
},
// 数据更新后重新计算
updated() {
this.$nextTick(() => {
this.calculateTabPositions();
});
},
// 页面卸载时清理
beforeDestroy() {
// 清理定时器
if (this.scrollTimer) {
clearTimeout(this.scrollTimer);
this.scrollTimer = null;
}
if (this.clickingTabTimer) {
clearTimeout(this.clickingTabTimer);
this.clickingTabTimer = null;
}
// 重置状态
this.isClickingTab = false;
this.targetTab = -1;
this.lastScrollTop = undefined;
}
如果项目中使用了 mescroll-uni 组件,需要进行相应的适配:
// 使用mescroll时的滚动监听
onScroll(mescroll, y) {
const scrollTop = mescroll.getScrollTop ? mescroll.getScrollTop() : y;
// 后续处理逻辑相同...
},
// 使用mescroll的滚动方法
scrollToTab(index) {
if (this.mescroll) {
const targetScrollTop = Math.max(0, this.distanceArr[index] - headerHeight + 20);
this.mescroll.scrollTo(targetScrollTop, 300);
} else {
// 降级使用原生方法
uni.pageScrollTo({ scrollTop: targetScrollTop, duration: 300 });
}
}
// 使用 lodash 的 throttle
import { throttle } from 'lodash';
onScroll: throttle(function(e) {
// 滚动处理逻辑
}, 16)
// 缓存系统信息
created() {
this.systemInfo = uni.getSystemInfoSync();
this.headerHeight = this.systemInfo.statusBarHeight + 88;
}
// 只在需要时渲染Tab
通过设置合理的显示/隐藏阈值,形成缓冲区域:
showTabsThreshold: 200, // 显示阈值
hideTabsThreshold: 120 // 隐藏阈值(小于显示阈值)
使用 isClickingTab
标识防止点击过程中Tab被隐藏。
确保在 DOM 渲染完成后计算位置,使用 $nextTick
和适当的延迟。
本文介绍的智能吸顶 Tab 导航组件通过精细的状态管理和优化策略,实现了流畅的用户体验。关键技术点包括:
完整的代码已经过实际项目验证,可以直接用于生产环境。希望这个方案能够帮助到有类似需求的开发者。