前言🚴♀🚴♀
之前在写项目的时候,用到了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的使用等等.并且当完成这个组件之后我们会更加的有成就感.