前言🚴♀🚴♀
之前在写项目的时候,用到了element-plus,在这个项目中需要使用tabs组件,于是我便萌生了一个想法:自己封装一个tabs组件,感觉应该也不是很难。
最终效果图
实现的思路和步骤🤔🤔
- 首先一个tabs我认为由两部分组成:头部和内容区域,头部区域也就是表头部分,那么这部分如何渲染呢?相信有些同学就会说,我们可以传递一个数组进去,但是这样下面显示的内容和表头就有些割裂的意思,所以当我查看element-plus官网时他是这么做的:
可见他是首先将el-tab-panel插入,然后在传入两项label和name,label代表这一项的标题,name可以理解为是区别不同项的标识
然后父组件通过useSlot获取到lable和name这个属性来进行渲染.
首先实现tabs这部分🛰🛰
整体上分为两部分,一部分是显示标题,一部分是要预留插槽为了将来插入tabs-panel准备
在样式布局方面我使用了bootstrap
tabs布局部分
<template>
<div class="card text-center "
:style="{maxWidth:650+'px'}">
<div class="card-header">
<ul class="nav card-header-tabs">
<li class="nav-item"
v-for="titleInfo,index in titles"
:key="index"
:class="{'active-style':titleInfo?.name===currentTab}"
@click.prevent="selectTab(titleInfo?.name,index)">
<a class="nav-link "
href="#"
ref="navLink">{{ titleInfo?.title }}</a>
</li>
</ul>
<!--底部下划线部分,宽度会根据当前标题的宽度改变-->
<div class="underline">
<li :style="{left:updateLeft+'px',width:underlineWidth+'px'}"></li>
</div>
</div>
<!--这部分是要插入的tabs-panel部分-->
<div class="card-body">
<slot></slot>
</div>
</div>
</template>
tabs中的ts代码
基本结构
这部分是基本的框架,下面我将具体拆分开来讲解:
<script lang="ts">
import 'bootstrap/dist/css/bootstrap.css'
import { onMounted, provide, ref, useSlots, watchEffect } from 'vue'
import { defineComponent } from 'vue'
import TabCustomPanel from './TabCustom-panel.vue'
export default defineComponent({
//这个是tabs默认显示哪一个内容,默认值为空
components: {
TabCustomPanel,
},
name: 'Tabs',
setup(props, { emit }) {
//具体的代码......
})
return {
titles,
currentTab,
selectTab,
updateLeft,
navLink,
underlineWidth,
}
},
})
</script>
声明/接收变量🍆🍆
- 定义接受的变量的类型,并且设置默认值为空。
- 声明代码中需要的变量
export default defineComponent({
props: {
default: {
type: String,
default: '',
require: false,
},
},
setup(){
//通过useSlots方法获取插槽中的内容
const slots = useSlots()
// 获取underline的DOM元素
let updateLeft = ref(0)
// 获取nav-link的DOM元素(是"nav-item下的a元素")
let navLink = ref(null)
// navLink元素的宽度
let navLinkWidth = ref(0)
// 设置下滑线的宽度
let underlineWidth = ref(0)
// 定义一个currentTab,表示的时当前点击的标题,里面是标签的name属性,默认的值是default
const currentTab = ref(props.default)
}
})
selectTab函数部分(实现切换功能)🍎🍎
// 定义切换显示函数
// 当切换标题的时候触发显示内容更新和下滑线的移动
const selectTab = (name: string, titleIndex: number) => {
// 当点击切换函数之后,重新将新的值赋给currentTab
currentTab.value = name
// 获取当前的a标签的宽度,依次来设置下划线的宽度
navLinkWidth.value = (navLink.value as any)[titleIndex].clientWidth
// 当切换标题的时候,更换下划线的宽度,下划线的宽度随标题的长度改变
underlineWidth.value = navLinkWidth.value * 0.8
//设置下划线的距离,利用了元素的offsetLeft属性,根据当前标题的offsetLeft来决定下划线的偏移量
if (titleIndex === 0) {
updateLeft.value = 0
} else {
updateLeft.value = (navLink.value as any)[titleIndex].offsetLeft
}
// 给自定义事件tab-click发送数据,子向父组件传值,在使用tabs的组件中可以获取当前标题的name属性
emit('tabs-click', currentTab.value)
}
这个函数功能比较全面:
- 首先实现了点击标题实现了标题的切换(currentTab表示的是当前点击的标题的name属性值)
- 实现了下滑线的宽度自适应:下划线的宽度会随着标题的宽度的改变而改变,并且改变下划线的位置
- 并且这个函数可以通过emit将值传递给父组件
使用onMounted在挂载组件时进行首次操作🎃🎃
- 在第一次渲染时,由于父组件传递的值default可能为空,因此要对其进行判断如果为空,则默认选中标题。
- 首次渲染时同样下划线的宽度也要跟选中的标题绑定,因此也要进行处理
onMounted(() => {
// 更新当前选中的tabBar,设置默认的标题栏
const defaultTab = titles.find(
(child) => child?.name === currentTab.value
)
// 获取当前所在标题栏的index
let titleIndex = titles.findIndex(
(item) => item?.name === currentTab.value
)
//进行这一步的判断的主要原因是因为default可能是空,由于currentTab的默认值就是default所以也可能为空
//那么defaultTab就也为空,这个时候就要默认显示第一个标题的内容
if (defaultTab) {
// 设置currentTab的默认值
currentTab.value = defaultTab.name
} else if (titles.length > 0) {
currentTab.value = titles[0]?.name
}
// 获取nav-link元素的宽度,并给underline设置宽度
// 在刚挂载的时候,第一次下划线显示的位置
// 判断当前选择的是哪一个标题,并且获取索引值
const index = titles.findIndex((item) => item?.name === currentTab.value)
//navLink是一个数组,里面是(是"nav-item下的a元素")
navLinkWidth.value = (navLink.value as any)[index].clientWidth
// 设置下划线的宽度
underlineWidth.value = navLinkWidth.value * 0.8
// 在组件挂载之后设置上一次的prevtitleIndex
})
获取slot插槽中的元素
通过使用useSlot方法获取当前页面的默认插槽进而可以获取里面的props进而获取子组件上面的配置项"title"和"name"
// 获取slot插槽中的子组件上面的title和name属性,以便渲染navBar
//解构出item中的props方法,最终返回titles数组里面
const slots =useSlot();
const titles = slots.default!().map(({ props }) => {
if (props) {
const { title, name } = props
return {
title,
name,
}
}
使用provide方法将currentTab传递给子组件tab-panel,子组件由此通过v-if来判断显示那个内容
// 将currentTab传递过去,注意要将其作为ref包裹的值传递过去否则会失去响应式
//使用provide方法
provide('currentTab', currentTab)
下划线样式以及选中标题时的样式
.active-style {
box-shadow: -3px -3px 3px 3px rgba(209, 204, 204, 0.47);
border-radius: 10%;
}
.underline {
position: relative;
li {
transition: all 0.4s ease;
height: 3px;
background-color: rgb(126, 225, 225);
position: absolute;
a {
box-sizing: border-box;
}
}
}
完成tab-panel部分 😙😙
<template>
<!-- 判断插入的tab-panel是不是当前应该显示的 -->
<div v-if="currentTab===name">
<slot></slot>
</div>
</template>
子组件tab-panel主要就是通过inject接收父组件传递过来的currentTab,来通过v-if来判断当前选中的标题。
<script lang="ts">
import { defineComponent, inject, ref, watch } from 'vue'
export default defineComponent({
name: 'tabsCustomList',
props: {
title: {
type: String,
require: true,
},
name: {
type: String,
require: true,
},
},
setup(props) {
//接收tab传递过来的currentTab
let currentTab = inject('currentTab') as string
return {
currentTab,
}
},
})
</script>
在页面中使用⛹️♀️⛹️♀️
<div style="width: 700px;">
<TabsCustom :default="'first'"
ref="tabsCustom"
@tabs-click="handleTabs">
<TabsCustom-Panel title="省级"
name="first">
<div ref="ecarts1"
id="demo"
style="height: 250px;"></div>
</TabsCustom-Panel>
<TabsCustom-Panel title="国际或者国家级"
name="second">
<div ref="ecarts2"
style="height: 250px;"></div>
</TabsCustom-Panel>
</TabsCustom>
</div>
当然我这里面配合了echarts使用,在自己使用的时候我们可以是显示文字或者其他图片等
当然,配合echarts使用会有一点小问题,是由于echart所导致的bug,这个我们留到下次来讲解
总结🗽🗽
完成了tabs组件的封装,可以更加深刻的体会vue中的模块化组件的思想.并且自己也使用了许多新的技术,比如provide和inject,slot的使用等等.并且当完成这个组件之后我们会更加的有成就感.