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>
网友评论