背景
前不久,剛完成了一個商品列表+購物車功能的頁面,因為一級商品分類在頂部tab中顯示,可滑動,間距可定制,如下圖所示:

定制的tab需求如下:
1. 每個tab-item的間距是相同的,可定制
2. 每一個tab-item的寬度是隨着文字的增多而寬度增大
3. 當tab-item小於等於4個時,tab-item填滿當前屏幕,平分剩余空間;當tab-item超過4個時,tab可滑動選擇
4. 點擊tab-item時,底部橫線居中顯示,跟隨在點擊的tab-item底部
5. 從上一個頁面點擊一級分類,進入此頁面,顯示上一頁面點擊的一級分類名稱,居中顯示,樣式高亮
成品效果如下:

這個組件基本滿足上面5點需求
難點
1)使用vux的可滑動的tab,修改組件css,如何令到每一個tab的間距為響應式的。
2)這個組件最核心的就是底部bar的精准定位跟隨
3)從前一個頁面點擊一級分類進入商品列表頁,自動選中並在屏幕居中顯示被選中的tab-item
前期知識點
1)offsetLeft:子元素相對於父元素最左上角側的橫向偏離位置
2)offsetWidth: 元素的寬度
3)scrollLeft: 滑動到對應的x坐標
4)定位元素style.left的運用
5)vux組件之滑動tab的運用 (需要用到組件自帶的onItemClick()方法,通過dom,可以起到點擊該tab-item的作用)
難點逐一破解
1)使用vux的可滑動的tab,修改組件css,如何令到每一個tab的間距為響應式的。
原本vux的可滑動的tab是根據scrollWidth的長度來自動計算每一個tab-item的寬度的,因為包含這tab-item的tabBox這個div使用的是flex布局,而tab-item是它的子元素,它會自動沾滿tabBox。如果文字超出了tab-item的寬度,文字就會被隱藏。
可以通過修改vux-tab-item這個樣式來自定義樣式,把子元素的彈性屬性去除,並且設置他的padding,這樣可以呈現出文字能顯示全,並且每個tab-item間距相同的效果,css如下:
/*改變原來tabBox的flex布局*/
.tab-component .vux-tab .vux-tab-item {
display: inline-block;
width: auto;
height: 100%;
padding: 0 10px;
flex: none;
background-color: #f2f4f5;
}
2)這個組件最核心之一的就是底部bar的精准定位跟隨
因為上面的1)改變了布局,所以導致底部bar跟隨不准確的情況,我們可以定制bar。在vux里面,bar是一個div,它有滑動的動畫,我的做法是這樣的,首先通過right讓它置於tab的最左側,然后通過按鈕點擊事件獲得相對應的tab-item元素的下標,然后使用for循環從第一tab-item開始尋找,如果不為改元素,則把它的元素寬度進行累加,直到找到該需激活的tab-item,然后通過數學計算可把bar定位在該元素的底部並且居中,代碼如下:
onItemClick(keyword, index) {
console.log('on item click:', index)
let barLeft = 0;
document.getElementsByClassName('vux-tab-ink-bar')[0].style.right = '100%';
for (let i = 0; i < this.list.length;) {
if (document.getElementsByClassName('vux-tab-item')[i].innerText === keyword) {
console.log('document.getElementsByClassName(\'vux-tab-item\')[' + index + '].offsetWidth = ' + document.getElementsByClassName('vux-tab-item')[i].offsetWidth)
barLeft += document.getElementsByClassName('vux-tab-item')[i].offsetWidth / 2
//為什么是22.5?因為底部bar長度為44px,這樣做可以讓bar的中心對齊tab-item的中心
barLeft -= 22.5
break;
}
barLeft += document.getElementsByClassName('vux-tab-item')[i].offsetWidth;
i += 1;
}
document.getElementsByClassName('vux-tab-ink-bar')[0].style.left = (barLeft + 'px');
},
3)從前一個頁面點擊一級分類進入商品列表頁,自動選中並在屏幕居中顯示被選中的tab-item
思路是這樣的:
當tab-item的數量為4個或者以下的時候,獲取當前屏幕寬度,然后評分長度,計算之后,平均分給tab-item,因為每一個tab-item自己的樣式中有設置的padding屬性,所以間距相同,不需要額外為間距分配空間。
當tab-item的數量超過4個,則不需要分配寬度,因為是flex布局的子元素,每一個tab-item會根據自己的文字得到自己的寬度。
最重要最核心的來了,如何讓選中的tab-item居中顯示,例如,屏幕為320px, 需要居中顯示的tab-item(簡稱SItem)距離屏幕最右側1000px,SItem本身長度為60,問現在如何讓SItem居中在長度為320px的屏幕當中?

-----------------------------------------------------------------------------------------------
通過下面這段代碼
// 偽代碼
this.$refs.tabBoxOuter.scrollLeft = SItem.offsetLeft
可以把上面這種狀態變為下圖:

-------------------------------------------------------------------------------
通過下面這段代碼,就可以把上圖的兩黑點中心在垂直方向上重合,並且滾動顯示在屏幕上面
this.$refs.tabBoxOuter.scrollLeft = SItem.offsetLeft - tabConter
完整代碼
(只要配置了vux環境,就能直接運行此組件)
<template>
<div class="tab-component">
<divider>tab組件</divider>
<div ref="tabBoxOuter" style="width: 100%;overflow:scroll;-webkit-overflow-scrolling:touch;">
<tab ref="tabBox" style="background-color: #f2f4f5;font-size: 14px" bar-active-color="#149c81" :line-width="4"
:custom-bar-width="getBarWidth" :style="{width: tabWidth + 'px'}">
<tab-item v-for="(item,index) in list" :key="index" @on-item-click="onItemClick(item, index)">{{item}}
</tab-item>
</tab>
</div>
<br/>
<div class="box">
<x-button @click.native="clickTabItemById(2)" type="primary">go to 2</x-button>
<x-button @click.native="clickTabItemById(3)" type="primary">go to 3</x-button>
<x-button @click.native="clickTabItemById(4)" type="primary">go to 4</x-button>
<x-button @click.native="clickTabItemById(5)" type="primary">go to 5</x-button>
<x-button @click.native="clickTabItemById(6)" type="primary">go to 6</x-button>
<x-button @click.native="clickTabItemById(7)" type="primary">go to 7</x-button>
</div>
</div>
</template>
<script>
import { Tab, TabItem, Divider, XButton } from 'vux'
export default {
components: {
Tab,
TabItem,
Divider,
XButton
},
data () {
return {
// tab標簽div長度
tabWidth: document.body.clientWidth,
list: ['打印機', '復印機', '打印紙', '訂書機11111111', '打印機2222222222222222', '復印機3333333333333', '打印紙444444444444', '訂書機5']
}
},
mounted () {
this.setTabWidth()
this.clickFirstItem()
},
methods: {
onItemClick (keyword, index) {
console.log('on item click:', index)
let barLeft = 0
document.getElementsByClassName('vux-tab-ink-bar')[0].style.right = '100%'
for (let i = 0; i < this.list.length;) {
if (document.getElementsByClassName('vux-tab-item')[i].innerText === keyword) {
console.log('document.getElementsByClassName(\'vux-tab-item\')[' + index + '].offsetWidth = ' + document.getElementsByClassName('vux-tab-item')[i].offsetWidth)
barLeft += document.getElementsByClassName('vux-tab-item')[i].offsetWidth / 2
barLeft -= 22.5
break
}
barLeft += document.getElementsByClassName('vux-tab-item')[i].offsetWidth
i += 1
}
document.getElementsByClassName('vux-tab-ink-bar')[0].style.left = (barLeft + 'px')
},
// 函數控制tab-bar的寬度,如果tab標簽頁數量為1,則隱藏tab-bar
getBarWidth () {
if (this.list && this.list.length === 1) {
return '0px'
}
return '45px'
},
setTabWidth () {
// 頁面完成刷新之后
this.$nextTick(() => {
let ofwidth = 0
let efwidth = 0
// efwidth為每一個tab-item的長度總和,因為tab-item的父級為flex布局,而tab-item的flex: none,所以初始化的時候,tab-item會根據自己的字體長度,自動擴張寬度。
for (let i = 0; i < this.$refs.tabBox.$children.length;) {
efwidth += this.$refs.tabBox.$children[i].$el.offsetWidth
i += 1
}
// 同樣是計算初始化的時候,每一個tab-item的總寬度,但當tab-item總長度大於tab的總長度時,立馬退出程序
for (let i = 0; i < this.$refs.tabBox.$children.length;) {
ofwidth += this.$refs.tabBox.$children[i].$el.offsetWidth
if (ofwidth > (document.body.clientWidth)) {
break
}
i += 1
}
// 假如tab-item的總寬度小於顯示tabwidth,則評分tab的剩余空間,加到每一個tab-item中
if (ofwidth < (document.body.clientWidth)) {
for (let i = 0; i < this.$refs.tabBox.$children.length;) {
this.$refs.tabBox.$children[i].$el.style.width = (this.$refs.tabBox.$children[i].$el.clientWidth + (((document.body.clientWidth) - ofwidth) / this.$refs.tabBox.$children.length)) + 'px'
console.log(((((document.body.clientWidth) - ofwidth) / this.$refs.tabBox.$children.length)) + 'px')
i += 1
}
this.tabWidth = (document.body.clientWidth)
} else {
this.tabWidth = efwidth
}
}, 1000)
},
clickFirstItem () {
setTimeout(() => {
this.$refs.tabBox.$children[0].onItemClick()
}, 200)
},
clickTabItemById (index) {
// 模擬點擊事件
this.$refs.tabBox.$children[index].onItemClick()
// 滑動到對應的點擊標簽頁
// 這里值得注意的是,為什么tabBoxOut的寬度明明只有屏幕的寬度,而里面的tabBox是超過屏幕的寬度的,所有才
// 可以滑動,滑動的是tabBox這個div,而真正滑動的事件卻是綁定在tabBoxOut這個div當中。所以,當你使用scrollLeft
// 這個屬性的時候,是要用在tabBoxOut這個div上,而不是在tabBox這個div上。
// ----------------------------------------------------------------
// 接下來可以運用offsetLeft計算tab-item在父div tabBox橫軸偏移量、scrollLeft滑動到對應的tab-item,然后運用數學公式來把激活的tab-item滾動到tabBoxOuter這個div
// 的中心
let tabConter = (document.body.clientWidth - this.$refs.tabBox.$children[index].$el.offsetWidth) / 2
this.$refs.tabBoxOuter.scrollLeft = this.$refs.tabBox.$children[index].$el.offsetLeft - tabConter
}
}
}
</script>
<!--此style用來設置組件去除橫向滾動條顯示-->
<style scoped>
/*定義滾動條高寬及背景 高寬分別對應橫豎滾動條的尺寸,在這里設置滾動條寬度為0px*/
::-webkit-scrollbar {
width: 0px;
display: none;
background-color: #fff;
}
/*定義滾動條軌道 內陰影+圓角*/
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
background-color: #fff;
}
/*定義滑塊 內陰影+圓角*/
::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #fff;
}
</style>
<!--此style用來設置vux組件的部分樣式調整-->
<style>
.tab-component .vux-tab-bar-inner {
border-radius: 10px !important;
}
/*改變原來tabBox的flex布局*/
.tab-component .vux-tab .vux-tab-item {
display: inline-block;
width: auto;
height: 100%;
padding: 0 10px;
flex: none;
background-color: #f2f4f5;
}
/*定義tab-item選中時的樣式*/
.tab-component .vux-tab .vux-tab-item.vux-tab-selected {
font-size: 16px;
color: #149c81;
border-bottom: 3px solid #04BE02;
}
.box {
padding: 15px;
}
</style>
