vue3造轮子实现tab标签页,代码极其简洁易懂

UI库已上传至npm,可安装体验。文档地址:https://chenxuba.github.io/bibi-ui/#/

先上图:


image.png
/**
 * 第一步,先判断传入的子标签是否是bb-Tab
 * 通过context.slots.default()拿到所有的子标签,然后循环遍历
 * 取每个子标签的type和Tab做对比,这里可以log看下打印
 * console.log(context.slots.default())
 * console.log(Tab)
 * 如果传入的子标签是bb-tab,子标签的type和Tab是完全相等的,由此可判断
 * 传入的子标签是否是bb-Tab,不是的话就抛出错误,让其开发者修改子标签!!!
 */
 setup(props, context){
    const defaults = context.slots.default()
     defaults.forEach(tag => {
      if (tag.type !== Tab) {
        throw new Error("Tabs 子标签必须是bb-Tab")
      }
    })
  }
/**
 * 第二步,获取传入的title(标签名)
 * 打印console.log(defaults)可以拿到props中的title
 * 使用map循环遍历return成一个数组
 */
const titles = defaults.map(tag => {
      return tag.props.title
 })
return { defaults, titles}
/**
 * 第三步,实现基本布局样式
 */
<div class="bb-tabs">
    <div class="bb-tabs__wrap">
      <div class="bb-tabs__nav bb-tabs__nav--line">
        <div class="bb-tab" v-for="(item,i) in titles" :key="i" >
          <span class="bb-tab__text bb-tab__text--ellipsis">{{item}}</span>
        </div>
        <div class="bb-tabs__line">
        </div>
      </div>
    </div>
  </div>

<style lang="scss" scoped>
.bb-tabs {
  width: 100%;
  position: relative;
  .bb-tabs__wrap {
    height: 44px;
    overflow: hidden;
    .bb-tabs__nav {
      position: relative;
      display: flex;
      background-color: #fff;
      user-select: none;
    }
    .bb-tabs__nav--line {
      box-sizing: content-box;
      height: 100%;
      padding-bottom: 15px;
    }

    .bb-tab {
      position: relative;
      display: flex;
      flex: 1;
      align-items: center;
      justify-content: center;
      box-sizing: border-box;
      padding: 0 4px;
      color: #646566;
      font-size: 14px;
      line-height: 20px;
      cursor: pointer;
      .bb-tab__text--ellipsis {
        display: -webkit-box;
        overflow: hidden;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
      }
    }
    .bb-tab--active {
      color: #ee0a24;
      font-weight: 500;
    }
    .bb-tabs__line {
      position: absolute;
      bottom: 15px;
      left: 0;
      z-index: 1;
      width: 30px;
      height: 3px;
      background-color: #ee0a24;
      border-radius: 3px;
    }
  }
}
</style>

实现的样子


image.png
/**
 * 第四步,动态绑定class
 * :class="active==i?'bb-tab--active':''"
 * tab选中状态
 */
<div class="bb-tabs__wrap">
      <div class="bb-tabs__nav bb-tabs__nav--line">
        <div class="bb-tab" v-for="(item,i) in titles" :key="i" :class="active==i?'bb-tab--active':''" >
          <span class="bb-tab__text bb-tab__text--ellipsis">{{item}}</span>
        </div>
        <div class="bb-tabs__line">
        </div>
      </div>
    </div>
/**
     * 第五步,实现点击切换tab选中,暂时先实现颜色切换
     * 给tab绑定一个方法,定义emit传索引匹配
     * 父组件通过 v-model:active='active'双向绑定,实现颜色切换
     * const change = index => {
     * context.emit("update:active", index)
     * }
     */
<div class="bb-tab" v-for="(item,i) in titles" :key="i" :class="active==i?'bb-tab--active':''" @click="change(i)">
          <span class="bb-tab__text bb-tab__text--ellipsis">{{item}}</span>
</div>
const change = index => {
      context.emit("update:active", index)    
}
/**
     * 第六步,动态绑定style,动态改变 小横条 的 translateX
     * 实现点击切换tab动态匀速运动
     * :style="styleObject"
     * const styleObject = reactive({
     * transform: "translateX(35px) translateX(-50%)"
     * })
     * 先模拟一下,在change方法内加入这一行代码
     * styleObject.transform = `translateX(105px) translateX(-50%)`
     * 经测试,点击标签二可实现小横条匀速运动
     */
<div class="bb-tabs__line" style="transition-duration: 0.3s;" :style="styleObject"> </div>
const styleObject = reactive({
      transform: "translateX(35px) translateX(-50%)"
      //styleObject.transform = `translateX(105px) translateX(-50%)`
})
return { defaults, titles, change, styleObject }
/**
     * 第七步,因为tab的title字数不固定,所以宽度也不固定,要动态获取选中tab的宽度
     * 要用到ref,给tab动态绑定ref,怎么绑定呢?
     * :ref="el =>{if (el) navItems[index] = el}"
     * const navItems = ref([])
     * console.log({ ...navItems.value })
     * 记得要在onMounted函数内打印才能获取到dom
     */
 <div class="bb-tab" v-for="(item,i) in titles" :key="i" :class="active==i?'bb-tab--active':''" @click="change(i)"
          :ref="el =>{if (el) navItems[i] = el}">
          <span class="bb-tab__text bb-tab__text--ellipsis">{{item}}</span>
 </div>
const navItems = ref([])
onMounted(() => {
      // console.log({ ...navItems.value })
      /**
       * 第八步,拿到选中的tab的Dom
       * const doms = navItems.value
       * const activeDom = doms.filter(div =>
       * div.classList.contains("bb-tab--active"))[0]
       */
      const doms = navItems.value
      const activeDom = doms.filter(div => div.classList.contains("bb-tab--active"))[0]
      // console.log(activeDom)
      const { width } = activeDom.getBoundingClientRect()
      // console.log(width)
      /**
       *  动态改变styleObject中的transform,先把 styleObject.transform = ""
       *  styleObject.transform =
       * `translateX(${(width * (props.active + (props.active + 1))) / 2}px)
       *  translateX(-50%)`
       *  解释一下:为什么是 ${(width * (props.active + (props.active + 1))) / 2}px)
       *  通过拿到的选中dom的宽度计算,得出规律公式,width * (索引 + (索引+1)) / 2
       */
      styleObject.transform = `translateX(${(width * (props.active + (props.active + 1))) / 2}px) translateX(-50%)`
    })

return { defaults, titles, change, styleObject, navItems }
const change = index => {
      context.emit("update:active", index)
      // styleObject.transform = `translateX(105px) translateX(-50%)`
      /**
       * 第九步,点击改变 styleObject.transform ,这里的获取width代码有点重复,大家有想法的可以
       * 自行优化。
       */
      const doms = navItems.value
      const activeDom = doms.filter(div => div.classList.contains("bb-tab--active"))[0]
      const { width } = activeDom.getBoundingClientRect()
      styleObject.transform = `translateX(${(width * (index + (index + 1))) / 2}px) translateX(-50%)`
    }
/**
 * 第十步,完成 content 布局样式html、css
 */
<div class="bb-tabs__content">
      <component class="bb-tabs-content-item" v-for="(item,index) in defaults" :key="index" :is="item"
        :class="active===index?'bb-tabs-content--active':''" />
    </div>
 /**
     * 最后一步:
     * <component class="bb-tabs-content-item" v-for="(item,index) in defaults"
     * :key="index" :is="item"
     * :class="active===index?'bb-tabs-content--active':''" />
     * 完事在Tab组件内 写样式:
     * .bb-tabs-content-item {
        display: none;
        padding: 24px 20px;
        background-color: #fff;
        &.bb-tabs-content--active {
          display: block;
          color: #323233;
          font-size: 16px;
        }
      }
     */

最终代码

<template>
  <!-- tabs - nav -->
  <div class="bb-tabs">
    <div class="bb-tabs__wrap">
      <div class="bb-tabs__nav bb-tabs__nav--line" :style="tabNavStyle">
        <div class="bb-tab" v-for="(item,i) in titles" :key="i" :class="active==i?'bb-tab--active':''" @click="change(i)"
          :ref="el =>{if (el) navItems[i] = el}" :style="active==i?activeObj:inactiveObj">
          <span class="bb-tab__text bb-tab__text--ellipsis">{{item}}</span>
        </div>
        <!-- bb-tabs__line -->
        <div class="bb-tabs__line" style="transition-duration: 0.3s;" :style="styleObject">
        </div>
      </div>
    </div>
    <!-- bb-tabs__content -->
    <div class="bb-tabs__content">
      <component class="bb-tabs-content-item" v-for="(item,index) in defaults" :key="index" :is="item"
        :class="active===index?'bb-tabs-content--active':''" />
    </div>
  </div>
</template>

<script lang='ts'>
import { computed, nextTick, onMounted, reactive, ref } from "vue"
import Tab from "./Tab.vue"
export default {
  props: {
    active: {
      type: Number,
      default: 0
    },
    background: {
      type: String
    },
    color: {
      type: String
    },
    lineWidth: {
      type: String
    },
    lineHeight: {
      type: String
    },
    titleActiveColor: {
      type: String
    },
    titleInactiveColor: {
      type: String
    }
  },
  setup(props, context) {
    // 获取选中tab宽度的公用方法
    function getActiveTabWidth() {
      const doms = navItems.value
      const activeDom = doms.filter(div => div.classList.contains("bb-tab--active"))[0]
      const { offsetLeft, offsetWidth } = activeDom
      const left = offsetLeft + offsetWidth / 2
      return left
    }
    /**
     * 动态绑定ref
     * :ref="el =>{if (el) navItems[i] = el}"
     */
    const navItems = ref([])
    /**
     * 验证子标签合法性
     */
    const defaults = context.slots.default()
    defaults.forEach(tag => {
      if (tag.type !== Tab) {
        throw new Error("Tabs 子标签必须是bb-Tab")
      }
    })
    /**
     * 获取tab标签名
     */
    const titles = defaults.map(tag => {
      return tag.props.title
    })
    /**
     * 动态绑定style,改变小横条的样式
     */
    const styleObject = reactive({
      transform: "",
      background: props.color,
      width: props.lineWidth,
      height: props.lineHeight
    })

    onMounted(() => {
      /**
       * 在Dom渲染完成后赋初始值,改变小横条的位置
       */
      styleObject.transform = `translateX(${getActiveTabWidth()}px) translateX(-50%)`
    })
    /**
     * 点击切换方法
     */
    const change = index => {
      context.emit("update:active", index)
      nextTick(() => {
        styleObject.transform = `translateX(${getActiveTabWidth()}px) translateX(-50%)`
      })
    }

    /**
     * 扩展:
     * 动态绑定nav的style,背景颜色
     */
    const tabNavStyle = reactive({
      background: props.background
    })
    /**
     * 扩展:
     * 动态设置选中的标签字体颜色
     */
    const activeObj = reactive({
      color: props.titleActiveColor
    })
    /**
     * 扩展:
     * 动态设置未选中的标签字体颜色
     */
    const inactiveObj = reactive({
      color: props.titleInactiveColor
    })
    return { defaults, titles, change, styleObject, navItems, tabNavStyle, activeObj, inactiveObj }
  }
}
</script>

<style lang="scss" scoped>
.bb-tabs {
  width: 100%;
  position: relative;
  .bb-tabs__wrap {
    height: 44px;
    overflow: hidden;
    .bb-tabs__nav {
      position: relative;
      display: flex;
      background-color: white;
      user-select: none;
      overflow-x: scroll;
      // overflow-x: hidden;
      -webkit-overflow-scrolling: touch;
      overflow-y: hidden;
    }
    .bb-tabs__nav--line {
      box-sizing: content-box;
      height: 100%;
      padding-bottom: 15px;
    }

    .bb-tab {
      position: relative;
      display: flex;
      flex: 1 0 auto;
      align-items: center;
      justify-content: center;
      box-sizing: border-box;
      padding: 0 4px;
      padding: 0 12px;
      color: #646566;
      font-size: 14px;
      line-height: 20px;
      cursor: pointer;
      .bb-tab__text--ellipsis {
        display: -webkit-box;
        overflow: hidden;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
      }
    }
    .bb-tab--active {
      font-weight: 500;
    }
    .bb-tabs__line {
      position: absolute;
      bottom: 15px;
      left: 0;
      z-index: 1;
      width: 30px;
      height: 3px;
      background-color: #ee0a24;
      border-radius: 3px;
    }
  }
}
</style>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容