<template> <view class="tabs"> <scroll-view ref="tabbar1" id="tab-bar" class="tab-bar" :scroll="false" :scroll-x="true" :show-scrollbar="false" :scroll-into-view="scrollInto"> <view style="flex-direction: column;"> <view style="flex-direction: row;"> <view class="uni-tab-item" v-for="(tab,index) in tabList" :key="tab.id" :id="tab.id" :ref="'tabitem'+index" :data-id="index" :data-current="index" @click="ontabtap"> <text class="uni-tab-item-title" :class="tabIndex==index ? 'uni-tab-item-title-active' : ''">{{tab.name}}</text> </view> </view> <view class="scroll-view-indicator"> <view ref="underline" class="scroll-view-underline" :class="isTap ? 'scroll-view-animation':''" :style="{left: indicatorLineLeft + 'px', width: indicatorLineWidth + 'px'}"></view> </view> </view> </scroll-view> <view class="tab-bar-line"></view> <swiper class="tab-view" ref="swiper1" id="tab-bar-view" :current="tabIndex" :duration="300" @change="onswiperchange" @transition="onswiperscroll" @animationfinish="animationfinish" @onAnimationEnd="animationfinish"> <swiper-item class="swiper-item" v-for="(page, index) in tabList" :key="index"> <!-- #ifndef MP-ALIPAY --> <swiperPage class="swiper-page" :pid="page.pageid" ref="page"></swiperPage> <!-- #endif --> <!-- #ifdef MP-ALIPAY --> <swiperPage class="swiper-page" :pid="page.pageid" :ref="'page' + index"></swiperPage> <!-- #endif --> </swiper-item> </swiper> </view> </template> <script> // #ifdef APP-NVUE const dom = weex.requireModule('dom'); // #endif // 缓存每页最多 const MAX_CACHE_DATA = 100; // 缓存页签数量 const MAX_CACHE_PAGE = 3; const TAB_PRELOAD_OFFSET = 1; import swiperPage from './swiper-page.nvue'; export default { components: { swiperPage }, data() { return { tabList: [], tabIndex: 0, cacheTab: [], scrollInto: "", indicatorLineLeft: 0, indicatorLineWidth: 0, isTap: false } }, onLoad() { for (var i = 0; i < 6; i++) { this.tabList.push({ id: "tab" + i, name: 'Tab ' + (i + 1), pageid: i + 1 }) } }, onReady() { this._lastTabIndex = 0; this.swiperWidth = 0; this.tabbarWidth = 0; this.tabListSize = {}; this._touchTabIndex = 0; // #ifndef MP-ALIPAY this.pageList = this.$refs.page; // #endif // #ifdef MP-ALIPAY this.pageList = []; for (var i = 0; i < this.tabList.length; i++) { this.pageList.push(this.$refs['page' + i][0]); } // #endif this.switchTab(this.tabIndex); this.getTabbarItemsSize(); }, methods: { ontabtap(e) { let index = e.target.dataset.current || e.currentTarget.dataset.current; // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-QQ this.isTap = true; var currentSize = this.tabListSize[index]; this.updateIndicator(currentSize.left, currentSize.width); this._touchTabIndex = index; // #endif this.switchTab(index); }, onswiperchange(e) { // 注意:百度小程序会触发2次 // #ifndef APP-PLUS || H5 || MP-WEIXIN || MP-QQ let index = e.target.current || e.detail.current; this.switchTab(index); // #endif }, onswiperscroll(e) { if (this.isTap) { return; } var offsetX = e.detail.dx; var preloadIndex = this._lastTabIndex; if (offsetX > TAB_PRELOAD_OFFSET) { preloadIndex++; } else if (offsetX < -TAB_PRELOAD_OFFSET) { preloadIndex--; } if (preloadIndex === this._lastTabIndex || preloadIndex < 0 || preloadIndex > this.pageList.length - 1) { return; } if (this.pageList[preloadIndex].dataList.length === 0) { this.loadTabData(preloadIndex); } /// 计算 tabbar 底线 // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-QQ var percentage = Math.abs(this.swiperWidth / offsetX); var currentSize = this.tabListSize[this._lastTabIndex]; var preloadSize = this.tabListSize[preloadIndex]; var lineL = currentSize.left + (preloadSize.left - currentSize.left) / percentage; var lineW = currentSize.width + (preloadSize.width - currentSize.width) / percentage; this.updateIndicator(lineL, lineW); // #endif }, animationfinish(e) { // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-QQ let index = e.detail.current; if (this._touchTabIndex === index) { this.isTap = false; } this._lastTabIndex = index; this.switchTab(index); this.updateIndicator(this.tabListSize[index].left, this.tabListSize[index].width); // #endif }, getTabbarItemsSize() { // #ifdef APP-NVUE // 查询 tabbar 宽度 uni.createSelectorQuery().in(this).select('#tab-bar').boundingClientRect().exec(rect => { this.tabbarWidth = rect[0].width; }); // 查询 tabview 宽度 uni.createSelectorQuery().in(this).select('#tab-bar-view').boundingClientRect().exec(rect => { this.swiperWidth = rect[0].width; }); // 因 nvue 暂不支持 class 查询 var queryTabSize = uni.createSelectorQuery().in(this); for (var i = 0; i < this.tabList.length; i++) { queryTabSize.select('#' + this.tabList[i].id).boundingClientRect(); } queryTabSize.exec(rects => { console.log(JSON.stringify(rects)); rects.forEach((rect) => { this.tabListSize[rect.dataset.id] = rect; }) }); // #endif // #ifdef MP-WEIXIN || H5 || MP-QQ uni.createSelectorQuery().in(this).select('.tab-view').fields({ dataset: true, size: true, }, (res) => { this.swiperWidth = res.width; }).exec(); uni.createSelectorQuery().in(this).selectAll('.uni-tab-item').boundingClientRect((rects) => { rects.forEach((rect) => { this.tabListSize[rect.dataset.id] = rect; }) }).exec(); // #endif // #ifdef APP-NVUE || H5 || MP-WEIXIN || MP-QQ setTimeout(() => { this.updateIndicator(this.tabListSize[this.tabIndex].left, this.tabListSize[this.tabIndex].width); }, 100) // #endif }, updateIndicator(left, width) { this.indicatorLineLeft = left; this.indicatorLineWidth = width; }, switchTab(index) { if (this.pageList[index].dataList.length === 0) { this.loadTabData(index); } if (this.tabIndex === index) { return; } // 缓存 tabId if (this.pageList[this.tabIndex].dataList.length > MAX_CACHE_DATA) { let isExist = this.cacheTab.indexOf(this.tabIndex); if (isExist < 0) { this.cacheTab.push(this.tabIndex); } } this.tabIndex = index; // #ifdef APP-NVUE this.scrollTabTo(index); // #endif // #ifndef APP-NVUE this.scrollInto = this.tabList[index].id; // #endif // 释放 tabId if (this.cacheTab.length > MAX_CACHE_PAGE) { let cacheIndex = this.cacheTab[0]; this.clearTabData(cacheIndex); this.cacheTab.splice(0, 1); } }, scrollTabTo(index) { const el = this.$refs['tabitem' + index][0]; let offset = 0; // TODO fix ios offset if (index > 0) { offset = this.tabbarWidth / 2 - this.tabListSize[index].width / 2; if (this.tabListSize[index].right < this.tabbarWidth / 2) { offset = this.tabListSize[0].width; } } dom.scrollToElement(el, { offset: -offset }); }, loadTabData(index) { this.pageList[index].loadData(); }, clearTabData(index) { this.pageList[index].clear(); } } } </script> <style> /* #ifndef APP-PLUS */ page { width: 100%; min-height: 100%; display: flex; } /* #endif */ .tabs { flex: 1; flex-direction: column; overflow: hidden; background-color: #ffffff; /* #ifdef MP-ALIPAY || MP-BAIDU */ height: 100vh; /* #endif */ } .tab-bar { width: 750rpx; height: 84rpx; flex-direction: row; /* #ifndef APP-PLUS */ white-space: nowrap; /* #endif */ } /* #ifndef APP-NVUE */ .tab-bar ::-webkit-scrollbar { display: none; width: 0 !important; height: 0 !important; -webkit-appearance: none; background: transparent; } /* #endif */ .scroll-view-indicator { position: relative; height: 2px; background-color: transparent; } .scroll-view-underline { position: absolute; top: 0; bottom: 0; width: 0; background-color: #007AFF; } .scroll-view-animation { transition-duration: 0.2s; transition-property: left; } .tab-bar-line { height: 1rpx; background-color: #cccccc; } .tab-view { flex: 1; } .uni-tab-item { /* #ifndef APP-PLUS */ display: inline-block; /* #endif */ flex-wrap: nowrap; padding-left: 25px; padding-right: 25px; } .uni-tab-item-title { color: #555; font-size: 30rpx; height: 80rpx; line-height: 80rpx; flex-wrap: nowrap; /* #ifndef APP-PLUS */ white-space: nowrap; /* #endif */ } .uni-tab-item-title-active { color: #007AFF; } .swiper-item { flex: 1; flex-direction: column; } .swiper-page { flex: 1; flex-direction: row; position: absolute; left: 0; top: 0; right: 0; bottom: 0; } </style>