没有组件库支持可滑动切换Tabs?自己封装一个高级组件SlidingTab
前言
前段时间我在写个人项目的时候需要一个组件,就是在移动端非常常见的功能----可滑动tabs栏,但是我找了一下Vue3的组件库,主要是我常用的 京东NutUI 和 有赞Vant 都没有类似组件,但是又非常想实现那样的效果,在这个万般无奈的背景下,开始了自己的挖坑之路。
预览最终情况
开始构思
首先,要确定的是,tabs组件有上下两部分一个是上面的tab标签,另一个就是每个标签所对应的内容,在这个大前提下,我们先做最常见的也是组件库里有的上面的tabs标签,之后再来实现下面包含着的每个tabs的内容。
开始封装
传入的props
后面要用到 提前告诉大家 都是字面意思
/**传入的props*/
const props =defineProps({
/**
* tabs显示文字的数组
*@typeArray
*/
tabList: {
type:Array,
required: true
},
dataList: {
type:Array,
default: []
},
width: {
type: String,
default: "90vw"
},
tabWidth: {
type: String,
default: "60px"
}
})
封装头部Tabs
思路:总体构思利用原生CSS中有的属性overflow-x:scroll
即超过容器时滚动,以此来实现能够左右滑动
Tabs样式
注意点: 需要包三层,最外层设置该属性,最大宽度即为最外层宽或者外层的外层。
scrollbar-width: none
取消滚动条
其他属性可有可无 大家可以自己去测试查阅
代码如下:
//scss
.SlidingTab-header {
font-size: medium;
color: $grayColor;
margin: 10px;
overflow: scroll;
&::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.SlidingTab-header-item {
/*解决ios上滑动不流畅*/
-webkit-overflow-scrolling: touch;
white-space: nowrap; /* 合并空白和回车 */
flex: none;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
text-align: center;
margin: 0 auto;
.tab-item {
display: inline-block;
text-align: center;
width: v-bind(tabWidth);
margin: 10px 10px 5px 10px;
cursor: pointer;
}
}
}
可移动的小红条的样式
一个active
一般用来表示
transition: left .5s;
表示小红条变换移动时间为0.5秒
transition-delay: .25s;
表示文字颜色转换延迟时间
上两条属性是为了使变换流程,符合美学!
//scss 小红条样式如下
.active-line {
position: relative;
width: v-bind(lineWidth);
height: 0.3rem;
margin-left: v-bind(lineMarginWidth);//tab-item's margin 10px + tab-item's width * 10%
border-radius: 1.5rem;
border: 1px solid $theme-red;
background-color: $theme-red;
transition: left .5s;
}
.active {
color: $theme-red;
font-weight: bolder;
transition-delay: .25s;
}
Tab部的HTML
<div class="SlidingTab-header">
<div class="SlidingTab-header-item">
<div
class="tab-item"
v-for="(item, index) in tabList"
:class="[currentIndex === index ? 'active':'']"
@click="changeTab(index)"
>{{ item }}
</div>
<div class="active-line"></div>
</div>
</div>
Tab部的JavaScript
真的很简单就这样
// setup语法
<script setup>
/** 改变tab*/
function changeTab(indexTab) {
index = indexTab;
currentIndex.value = index;
setMove(indexTab); //此处是为了改变content内容(后其优化涉及到偏移改变tab的位置使其居中)
}
<script/>
封装内容Content
思路:
- 首先也是需要采用超出容器的属性但是超出后我们选择隐藏即
overflow-x:hidden
以此来只显示一个tab下的content - 左滑右滑,滑动一下后马上滑动到下一个,并且点击tab也要马上到滑动到对应的content中
- 滑动效果类似于轮播图,直接滑动到下一块,并非如Tabs可以只滑动一半
- 区分滑动事件和点击事件
- 降低耦合性,让各种不同的content都能用
Content的HTML&&SCSS
没什么好说的,slot
内是为了降低耦合性增加复用,默认封了一个我自己的Cell单元格组件。
<div class="SlidingTab-content">
<div class="SlidingTab-content-box"
@touchstart="touchStart"
@touchmove="touchmove"
@touchend="touchend">
<slot>
<div
v-for="(item, index) in dataList"
:key="index"
>
<div class="absoluteCenterCol" style="height: 100%">
<template v-for="(value,key) in item">
<SingCellGroup
v-if="value"
text-center
:group-title="key"
class="item">
<SingCell
style="width: auto;"
v-for="subItem in value"
@click="toDetailInfo(subItem.id)"
:left-main="subItem.singerName"
:center="true"></SingCell>
</SingCellGroup>
<div
v-if="(item.AM === null && item.PM === null && key === 'AM') "
class="nullSingers"
>今日暂无演唱歌手
</div>
</template>
</div>
</div>
</slot>
</div>
</div>
.SlidingTab-content-box {
position: relative;
width: 100%;
height: 100%;
min-height: 280px;
display: flex;
flex-flow: nowrap;
transition: left .5s;
:slotted(.SlidingTab-content-item) {
flex-shrink: 0;
display: inline-block;
width: $content-item-width;
padding: 10px;
float: left;
background-color: #ffffff;
touch-action: none;
.item {
width: auto;
margin: 0 0 15px 0;
}
}
.nullSingers {
color: $grayColor;
text-align: center;
}
}
Content的JS
首先是基础的触摸事件,利用JS的触摸事件来进行判断移动的方向,并且多加了一个事件判断是否为点击事件(若至于touchStart事件,没用touchmove事件则判断为点击事件)
/**触摸开始事件*/
function touchStart(e) {
startX = e.touches[0].clientX;
}
/**用于判断使点击事件还是触摸移动事件*/
let isClick = true;
/**触摸移动事件*/
function touchmove(e) {
moveX = e.touches[0].clientX;
isClick = false;
}
/**触摸结束事件*/
function touchend() {
if (isClick) {
console.log("is click");
return null;
} else {
isClick = true
/**根据触摸位置判断滑动方向*/
if (moveX - startX > 0) { //手指从左往右滑动(往前)
index = index - 1;
currentIndex.value = index;
if (index >= 0) { //判断是否tab下标超限
setMove(index);
} else { //循环 小于0则跳到数组最后
index = dataLength - 1;
currentIndex.value = index;
setMove(index);
}
} else { //从右往左滑(往后)
index = index + 1;
if (index > dataLength - 1) { //判断是否tab下标超限
index = 0; //循环 大于最大下标则跳到最前
}
currentIndex.value = index;
setMove(index);
}
}
}
最主要的事件:setMove事件
BUG:IOS上滑动会不流畅即smooth
失效需要引入smoothscroll
npm上引入,不会可以百度一下
/**
*设置偏移
*/
function setMove(index) {
//获取每个item的宽度用于计算
let itemWidth =document.querySelector(".SlidingTab-content-item").offsetWidth;
//计算红色移动调的距离每次改变时都能在对应tab最中间
document.querySelector(".active-line").style.left = ((tabWidthNum + 20) * (index)) + 'px';
//计算content内容的移动距离 对应每次选择的tabs
document.querySelector(".SlidingTab-content-box").style.left = -(itemWidth * (index)) + 'px';
smoothscroll.polyfill(); //ios流畅滑动
//去计算调整tab那一块的滚动条(已经被不可见)让所有被选择的且可以被居中的tab居中现实(最左和最右可能不能够居中,所以是能居中的则居中,不能居中的正常显示)
headerScrollLength = (tabWidthNum + 20) * index - (document.querySelector(".SlidingTab-header").offsetWidth - tabWidthNum - 20) / 2;//可见框的宽度
document.querySelector(".SlidingTab-header").scrollTo({top: 0, left: headerScrollLength, behavior: 'smooth'});
}
做了一些更加让用户舒服且符合业务的优化
-
上一代码块中的 最后一块内容
//去计算调整tab那一块的滚动条(已经被不可见)让所有被选择的且可以被居中的tab居中现实(最左和最右可能不能够居中,所以是能居中的则居中,不能居中的正常显示) headerScrollLength = (tabWidthNum + 20) * index - (document.querySelector(".SlidingTab-header").offsetWidth - tabWidthNum - 20) / 2;//可见框的宽度 document.querySelector(".SlidingTab-header").scrollTo({top: 0, left: headerScrollLength, behavior: 'smooth'});
-
因为tabs是星期,这边挂载时默认选择了当天,还有各种默认滚动的距离等等
-
因为有 slot 插槽存在 所以不能把类写死,我们是querySelector(类名)来滚的
-
内容长度以TabsList的长度为准
具体挂载代码以及变量如下
/**与tabsList长度相同*/
const dataLength = props.tabList.length;
//分割tabWidth成数字和字母两个数组 然后计算线的宽度等等
/**单个tab宽度*/
const tabWidthNum = Number(props.tabWidth.match(/[a-z]+|[^a-z]+/gi)[0]); //tab宽度
const tabWidthUnit = props.tabWidth.match(/[a-z]+|[^a-z]+/gi)[1]; //tab的计量单位
const lineWidth = (tabWidthNum * 0.8) + tabWidthUnit; //拼接移动线的宽
const lineMarginWidth = (tabWidthNum * 0.1) + 10 + tabWidthUnit; //拼接移动线的左margin(为了居中 10为tab的margin)
const today = dayjs().day(); //计算今天是星期几
let currentIndex = ref(today); //游标 保证下划线和选中的tab一致
let index = today; //目前下标
/**判断移动方向*/
let startX = 0; //开始的X坐标
let moveX = 0; //移动中的X坐标
let headerScrollLength = undefined; //为了使被选择的tab居中主动去滚动滚动条的距离
/**挂载钩子*/
onMounted(() => {
/**动态的把‘SlidingTab-content-box’赋值给展示框的每个元素*/
for (let i = 0; i < dataLength; i++) {
document.querySelector(".SlidingTab-content-box").children[i].className = "SlidingTab-content-item";
}
let itemWidth =document.querySelector(".SlidingTab-content-item").offsetWidth; //单个内容长度
document.querySelector(".SlidingTab-content-box").style.width = itemWidth * dataLength + 'px'; //定内容box长为所有内容之和
document.querySelector(".SlidingTab-header-item").style.width = (tabWidthNum + 20) * props.tabList.length + 'px'; //同上定tabs
document.querySelector(".SlidingTab-content-box").style.left = -(itemWidth * (index)) + 'px'; //默认定位为今天
document.querySelector(".active-line").style.left = ((tabWidthNum + 20) * (index)) + 'px'; //下移动条位置
/**滚动距离=使星期X到最左边(最初距离) 再往回(左边)-(减去)[整个header -单个tab -单个tab的左右margin ]/2 */
headerScrollLength = (tabWidthNum + 20) * index - (document.querySelector(".SlidingTab-header").offsetWidth - tabWidthNum - 20) / 2;//可见框的宽度
document.querySelector(".SlidingTab-header").scrollTo(headerScrollLength, 0); //默认跳转定位(今天放中间)
})
写在最后
这个组件个人觉得还是有一定的难度,封装完之后对JS的理解更加深刻了一些,后来其实发现滴滴的组件库里有类似的(没有封成我这样把饭最喂嘴里)利用他自己的多个组件也是可以形成类似的效果
如果有同学需要这一块的源码的话可以评论问我要哦
转载自:https://juejin.cn/post/7172113043084541959