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>

