一、需求

要实现的图如下

element button按钮默认选中状态 element按钮居中_tab箭头指向居中


下图中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;
}

我们要实现的箭头居中效果如下:

element button按钮默认选中状态 element按钮居中_自定义_02


箭头始终指向tab的中心。

二、想法

1、去看看tab原生的蓝色线是怎么实现跟随的。看css样式是通过动态改变translateX的值。但在tab的样式改成我这样子,原始的蓝色线也已经不能准确跟随,

element button按钮默认选中状态 element按钮居中_el-tabs_03


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并在视窗内不可同时展示时,会出现左右两个小箭头(如图),这样的话,点击最后一项,箭头就偏移到看不见了。

element button按钮默认选中状态 element按钮居中_自定义_04


3、由于需求的tab不限数量,宽度也不限制,所以只能动态的获取宽度,那么有没有什么方式获取某个tab的在可视窗口的绝对位置。利用 getBoundingClientRect() 可以做到。

getBoundingClientRect 用于获得页面中某个DOM元素的左,上,右和下分别相对浏览器视窗的位置,不包含文档被隐藏的部分。

element button按钮默认选中状态 element按钮居中_箭头跟随_05


上图来自:

该函数返回一个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,但不保证居中。

不居中不罢休!进一步优化!结果如第三部分所示。

三、箭头位置计算

element button按钮默认选中状态 element按钮居中_箭头跟随_06

所以,从图上看最终我们计算箭头角角居中的公式为:

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的一半!!!

element button按钮默认选中状态 element按钮居中_tab箭头指向居中_07


记住这个坑!!!记住这个坑!!!记住这个坑!!!

下面是代码:

// 这里是一个动态数量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;
  }
}

element button按钮默认选中状态 element按钮居中_el-tabs_08