一、需求
要实现的图如下
下图中1-3是3个tab页,通过对element样式的修改,去掉了选中状态的蓝线和灰色线:
// 蓝线
::v-deep .el-tabs__active-bar{
height: 0;
}
// 灰线
::v-deep .el-tabs__nav-wrap::after{
height: 0;
}
// 去掉tab与content之间的空隙 便于后续自定义
::v-deep .el-tabs__header{
margin: 0;
}
我们要实现的箭头居中效果如下:
箭头始终指向tab的中心。
二、想法
1、去看看tab原生的蓝色线是怎么实现跟随的。看css样式是通过动态改变translateX的值。但在tab的样式改成我这样子,原始的蓝色线也已经不能准确跟随,
2、获取每个tab的宽度,然后每次计算:(前面tab的宽度+margin+padding+border总和 + 当前tab/2)
就是箭头的位置
// 获取每个tab 项的总的宽
getAllTabWidth(){
const paddingWidth = 28 , magleftWidth = 45 , borderWidth = 2
return this.$refs.dataTabDiv.map(item => item.offsetWidth + paddingWidth + magleftWidth + borderWidth)
},
handleArrow(tabVal){
const widthList = this.getAllTabWidth()
const beforeWidthArr = widthList.slice(0,index)
this.arrowLocation = beforeWidthArr.reduce((prev,curr) => prev+curr) + widthList[tabVal.index]/2
}
但这个存在一个问题,el-tab 如果是视窗内固定的几个可以用上述方法,但tab数量不确定时,就不适用了。el-tabs组件在有多个tabs并在视窗内不可同时展示时,会出现左右两个小箭头(如图),这样的话,点击最后一项,箭头就偏移到看不见了。
3、由于需求的tab不限数量,宽度也不限制,所以只能动态的获取宽度,那么有没有什么方式获取某个tab的在可视窗口的绝对位置。利用 getBoundingClientRect() 可以做到。
getBoundingClientRect 用于获得页面中某个DOM元素的左,上,右和下分别相对浏览器视窗的位置,不包含文档被隐藏的部分。
上图来自:
该函数返回一个Object对象:
DOMRect {
bottom: 237.91193389892578
height: 157.9829559326172
left: 423.57952880859375
right: 593.5795288085938
top: 79.9289779663086
width: 170
x: 423.57952880859375
y: 79.9289779663086
}
那么初步有了下面的代码
// 处理箭头位置
handleArrow(tabVal){
const index = tabVal.index
const absoluteLocations = this.$refs.dataTabTitle.map(item => item.getBoundingClientRect())
// 不能动态计算 absoluteLocations[0].left的值,第一项隐藏时位置就会出错
this.arrowLocation = absoluteLocations[index].left - 455 //455 是在第一个tab位置的基础上加的偏差值
},
但这个很明显不准确,只能保证箭头指向某个tab,但不保证居中。
不居中不罢休!进一步优化!结果如第三部分所示。
三、箭头位置计算
所以,从图上看最终我们计算箭头角角居中的公式为:
arrowLocation = left - leftOffset + width/2 - 箭头宽度/2
这里我给箭头设置了个初始位移 arrowDefaultLeft:102px,所以需要减去他
arrowLocation = left - leftOffset + width/2 - 箭头宽度/2 - arrowDefaultLeft
这样按道理就可以了!
然鹅!一缩小屏幕,哦豁,箭头移位了,原因就是我在项目中运用了px2rem , 所有css中的px都转rem了,但是我计算公式中的px没有啊,看公式,需要进行转换的有2个值:箭头宽度、arrowDefaultLeft。
转换就简单的计算,我当前屏幕像素宽度为1920。
getRemWidth(num){
return num*document.documentElement.clientWidth/1920
},
4、最终代码
到具体实现时,发现上面公式计算的箭头并没有居中,然后就发现我们看上去的箭头的一半宽度其实等于设置的border-width,而不是border-width的一半!!!
记住这个坑!!!记住这个坑!!!记住这个坑!!!
下面是代码:
// 这里是一个动态数量tab的例子,重点在@tab-click 和 ref="dataTabDiv"
<el-tabs v-model="activeTabId" @tab-click="handleArrow" ref="tabsComp">
<el-tab-pane
v-for="(item,index) in dataIndList"
:key="index"
:lazy="true"
:name="item.indicatorId"
>
<div slot="label" class="data__tab" ref="dataTabDiv">
tab 自定义头部项
</div>
</el-tab-pane>
</el-tabs>
/** ---------------------js 部分------重要部分-----------------*/
// 由于项目使用了px2rem,所以这里计算px也手动转成rem,1920是我屏幕像素宽度
getRemWidth(num){
return num*document.documentElement.clientWidth/1920
},
// 处理箭头位置
handleArrow(tabVal){
// 当前点击的第几个tab 从0开始
const index = tabVal.index
// 获取每个tab的宽度 ,如果你是固定宽就不需要了。 实际上下面getBoundingClientRect可以获取宽度,并且两者的关系为
//tabWidth[index] === Math.floor(absoluteLocations[index].width)
//const tabWidth = this.$refs.dataTabDiv.map(item => item.offsetWidth)
// 计算每个tab相对于视窗的位置
const absoluteLocations = this.$refs.dataTabDiv.map(item => item.getBoundingClientRect())
// leftOffset 是getBoundingClientRect()算出tabs组件最左侧与左边界的距离
const leftOffset = this.$refs.tabsComp.$el.getBoundingClientRect().left
// 箭头默认初始偏移值,箭头宽度一半 === border-width
const arrowDefaultLeft = 102, arrowWidth = 10
const otherOffset = this.getRemWidth( - arrowDefaultLeft - arrowWidth)
this.arrowLocation = absoluteLocations[index].left - leftOffset + absoluteLocations[index].width/2 + otherOffset
},
/** ---------------------css 部分 : 改写element UI 的tab padding为16px-----------------------*/
::v-deep .el-tabs__item{
padding: 0 16px;
}
::v-deep .el-tabs--top .el-tabs__item.is-top:nth-child(2){
padding: 0 16px;
}
5、箭头的实现代码:
箭头就是两个三角形拼在一起,蓝色在下面,白色在上面,通过拼接,凑出了一个角角,然后通过设置translateX来移动箭头。
默认设置了箭头的初始位置为102px,这个在css中的left设置的,后期可优化。
<div class="box__arrow" :style="{'transform': 'translateX('+arrowLocation+'px)'}">
<span class="tra-arrow"></span>
<span class="tra-top"></span>
</div>
.box__arrow {
position: relative;
z-index: 10;
height: 15px;
.tra-top {
position: absolute;
border-width: 10px;
border-style: dashed dashed solid;
border-color: transparent transparent #fff;
left: 102px; // 箭头的初始位置
top: -3px;
}
.tra-arrow {
@extend .tra-top;
border-color: transparent transparent #e8e8e8;
top: -5px;
}
}