没有组件库支持可滑动切换Tabs?自己封装一个高级组件SlidingTab

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天

前言

前段时间我在写个人项目的时候需要一个组件,就是在移动端非常常见的功能—-可滑动tabs栏,但是我找了一下Vue3的组件库,主要是我常用的 京东NutUI 和 有赞Vant 都没有类似组件,但是又非常想实现那样的效果,在这个万般无奈的背景下,开始了自己的挖坑之路。

预览最终情况

1_main.gif

开始构思

首先,要确定的是,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即超过容器时滚动,以此来实现能够左右滑动

2_tab.gif

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

思路:

  1. 首先也是需要采用超出容器的属性但是超出后我们选择隐藏即overflow-x:hidden以此来只显示一个tab下的content
  2. 左滑右滑,滑动一下后马上滑动到下一个,并且点击tab也要马上到滑动到对应的content中
  3. 滑动效果类似于轮播图,直接滑动到下一块,并非如Tabs可以只滑动一半
  4. 区分滑动事件和点击事件
  5. 降低耦合性,让各种不同的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'});
}

做了一些更加让用户舒服且符合业务的优化

  1. 上一代码块中的 最后一块内容

    //去计算调整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'});
    
  2. 因为tabs是星期,这边挂载时默认选择了当天,还有各种默认滚动的距离等等

  3. 因为有 slot 插槽存在 所以不能把类写死,我们是querySelector(类名)来滚的

  4. 内容长度以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的理解更加深刻了一些,后来其实发现滴滴的组件库里有类似的(没有封成我这样把饭最喂嘴里)利用他自己的多个组件也是可以形成类似的效果

如果有同学需要这一块的源码的话可以评论问我要哦

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容