前言
在移動端應用開發中,Tab 標簽導航是一種常見的交互模式。本文將詳細介紹如何在 UniApp 中實現一個功能完善的智能吸頂 Tab 導航組件,該組件具有以下特性:
- 🎯 智能顯示:根據滾動位置動態顯示/隱藏
- 📌 吸頂效果:Tab 欄固定在頂部,不隨頁面滾動
- 🔄 自動切換:根據滾動位置自動高亮對應 Tab
- 📱 平滑滾動:點擊 Tab 平滑滾動到對應內容區域
- ? 性能優化:節流防抖,確保流暢體驗
效果預覽
當用戶向下滾動超過 200px 時,Tab 導航欄會出現并吸頂顯示。隨著繼續滾動,Tab 會自動切換高亮狀態,點擊 Tab 可以快速定位到對應內容。
核心實現
1. 組件結構設計
首先,我們需要設計基礎的 HTML 結構:
<template><view class="page-container"><!-- 吸頂Tab欄 --><view v-if="showTabs" class="sticky-tabs" id="tabs"><u-tabs :current="currentTab" :list="tabList" @click="clickTab"lineColor="#1482DC":inactiveStyle="{ color: '#969799', fontSize: '28rpx' }":activeStyle="{ color: '#323233', fontSize: '28rpx', fontWeight: 'bold' }"/></view><!-- 頁面內容區域 --><scroll-view class="content-area"scroll-y@scroll="onScroll"><!-- 基本信息模塊 --><view class="content-section" id="baseInfo"><view class="section-title">基本信息</view><!-- 內容... --></view><!-- 帶看/跟進模塊 --><view class="content-section" id="followRecord"><view class="section-title">帶看/跟進</view><!-- 內容... --></view><!-- 相似房源模塊 --><view class="content-section" id="similarHouses"><view class="section-title">相似房源</view><!-- 內容... --></view></scroll-view></view>
</template>
2. 數據結構定義
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, // 是否正在點擊TabclickingTabTimer: null, // 點擊超時定時器targetTab: -1, // 目標Tab索引// 閾值配置showTabsThreshold: 200, // 顯示Tab的滾動閾值hideTabsThreshold: 120, // 隱藏Tab的滾動閾值}}
}
3. 核心方法實現
3.1 滾動監聽處理
// 滾動監聽 - 使用節流優化性能
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欄// 從后往前遍歷,找到當前應該高亮的Tabfor (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;}
}
3.2 Tab位置計算
// 計算各內容模塊的位置
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);});});
}
3.3 Tab點擊處理
// 點擊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);}});
}
4. 生命周期管理
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;
}
5. 樣式定義
<style lang="scss" scoped>
.page-container {height: 100vh;background-color: #f5f5f6;
}// 吸頂Tab樣式
.sticky-tabs {position: sticky;top: calc(var(--status-bar-height) + 88rpx);z-index: 970;background-color: #fff;width: 100%;box-shadow: 0 2rpx 6rpx 0 rgba(153, 153, 153, 0.2);// Tab項平均分布/deep/ .u-tabs__wrapper__nav__item {flex: 1;}
}// 內容區域
.content-area {height: 100%;padding-bottom: 120rpx;
}// 內容模塊
.content-section {margin: 20rpx;padding: 30rpx;background-color: #fff;border-radius: 20rpx;.section-title {font-size: 32rpx;font-weight: 500;color: #1b243b;margin-bottom: 20rpx;}
}
</style>
使用 Mescroll 組件的適配
如果項目中使用了 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 });}
}
性能優化建議
1. 節流優化
// 使用 lodash 的 throttle
import { throttle } from 'lodash';onScroll: throttle(function(e) {// 滾動處理邏輯
}, 16)
2. 緩存計算結果
// 緩存系統信息
created() {this.systemInfo = uni.getSystemInfoSync();this.headerHeight = this.systemInfo.statusBarHeight + 88;
}
3. 條件渲染
// 只在需要時渲染Tab
<view v-if="showTabs && tabList.length > 0" class="sticky-tabs">
常見問題解決
1. Tab閃爍問題
通過設置合理的顯示/隱藏閾值,形成緩沖區域:
showTabsThreshold: 200, // 顯示閾值
hideTabsThreshold: 120 // 隱藏閾值(小于顯示閾值)
2. 點擊Tab時消失
使用 isClickingTab
標識防止點擊過程中Tab被隱藏。
3. 位置計算不準確
確保在 DOM 渲染完成后計算位置,使用 $nextTick
和適當的延遲。
總結
本文介紹的智能吸頂 Tab 導航組件通過精細的狀態管理和優化策略,實現了流暢的用戶體驗。關鍵技術點包括:
- ? 動態顯示控制,提升頁面空間利用率
- ? 防抖節流優化,確保滾動性能
- ? 智能狀態管理,避免交互沖突
- ? 兼容性處理,支持多種滾動組件
完整的代碼已經過實際項目驗證,可以直接用于生產環境。希望這個方案能夠幫助到有類似需求的開發者。