vue3造輪子實現(xiàn)tab標(biāo)簽頁,代碼極其簡潔易懂

UI庫已上傳至npm,可安裝體驗。文檔地址:https://chenxuba.github.io/bibi-ui/#/

先上圖:


image.png
/**
 * 第一步,先判斷傳入的子標(biāo)簽是否是bb-Tab
 * 通過context.slots.default()拿到所有的子標(biāo)簽,然后循環(huán)遍歷
 * 取每個子標(biāo)簽的type和Tab做對比,這里可以log看下打印
 * console.log(context.slots.default())
 * console.log(Tab)
 * 如果傳入的子標(biāo)簽是bb-tab,子標(biāo)簽的type和Tab是完全相等的,由此可判斷
 * 傳入的子標(biāo)簽是否是bb-Tab,不是的話就拋出錯誤,讓其開發(fā)者修改子標(biāo)簽?。?!
 */
 setup(props, context){
    const defaults = context.slots.default()
     defaults.forEach(tag => {
      if (tag.type !== Tab) {
        throw new Error("Tabs 子標(biāo)簽必須是bb-Tab")
      }
    })
  }
/**
 * 第二步,獲取傳入的title(標(biāo)簽名)
 * 打印console.log(defaults)可以拿到props中的title
 * 使用map循環(huán)遍歷return成一個數(shù)組
 */
const titles = defaults.map(tag => {
      return tag.props.title
 })
return { defaults, titles}
/**
 * 第三步,實現(xiàn)基本布局樣式
 */
<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>

實現(xiàn)的樣子


image.png
/**
 * 第四步,動態(tài)綁定class
 * :class="active==i?'bb-tab--active':''"
 * tab選中狀態(tài)
 */
<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>
/**
     * 第五步,實現(xiàn)點擊切換tab選中,暫時先實現(xiàn)顏色切換
     * 給tab綁定一個方法,定義emit傳索引匹配
     * 父組件通過 v-model:active='active'雙向綁定,實現(xiàn)顏色切換
     * 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)    
}
/**
     * 第六步,動態(tài)綁定style,動態(tài)改變 小橫條 的 translateX
     * 實現(xiàn)點擊切換tab動態(tài)勻速運動
     * :style="styleObject"
     * const styleObject = reactive({
     * transform: "translateX(35px) translateX(-50%)"
     * })
     * 先模擬一下,在change方法內(nèi)加入這一行代碼
     * styleObject.transform = `translateX(105px) translateX(-50%)`
     * 經(jīng)測試,點擊標(biāo)簽二可實現(xiàn)小橫條勻速運動
     */
<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字數(shù)不固定,所以寬度也不固定,要動態(tài)獲取選中tab的寬度
     * 要用到ref,給tab動態(tài)綁定ref,怎么綁定呢?
     * :ref="el =>{if (el) navItems[index] = el}"
     * const navItems = ref([])
     * console.log({ ...navItems.value })
     * 記得要在onMounted函數(shù)內(nèi)打印才能獲取到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)
      /**
       *  動態(tài)改變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的寬度計算,得出規(guī)律公式,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代碼有點重復(fù),大家有想法的可以
       * 自行優(yōu)化。
       */
      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組件內(nèi) 寫樣式:
     * .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
    }
    /**
     * 動態(tài)綁定ref
     * :ref="el =>{if (el) navItems[i] = el}"
     */
    const navItems = ref([])
    /**
     * 驗證子標(biāo)簽合法性
     */
    const defaults = context.slots.default()
    defaults.forEach(tag => {
      if (tag.type !== Tab) {
        throw new Error("Tabs 子標(biāo)簽必須是bb-Tab")
      }
    })
    /**
     * 獲取tab標(biāo)簽名
     */
    const titles = defaults.map(tag => {
      return tag.props.title
    })
    /**
     * 動態(tài)綁定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%)`
      })
    }

    /**
     * 擴展:
     * 動態(tài)綁定nav的style,背景顏色
     */
    const tabNavStyle = reactive({
      background: props.background
    })
    /**
     * 擴展:
     * 動態(tài)設(shè)置選中的標(biāo)簽字體顏色
     */
    const activeObj = reactive({
      color: props.titleActiveColor
    })
    /**
     * 擴展:
     * 動態(tài)設(shè)置未選中的標(biāo)簽字體顏色
     */
    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>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容