vue3+TypeScript封装tabs组件

前言🚴‍♀🚴‍♀

之前在写项目的时候,用到了element-plus,在这个项目中需要使用tabs组件,于是我便萌生了一个想法:自己封装一个tabs组件,感觉应该也不是很难。

最终效果图

在这里插入图片描述

实现的思路和步骤🤔🤔

  1. 首先一个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>
声明/接收变量🍆🍆
  1. 定义接受的变量的类型,并且设置默认值为空。
  2. 声明代码中需要的变量
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)
    }

这个函数功能比较全面:

  1. 首先实现了点击标题实现了标题的切换(currentTab表示的是当前点击的标题的name属性值)
  2. 实现了下滑线的宽度自适应:下划线的宽度会随着标题的宽度的改变而改变,并且改变下划线的位置
  3. 并且这个函数可以通过emit将值传递给父组件
使用onMounted在挂载组件时进行首次操作🎃🎃
  1. 在第一次渲染时,由于父组件传递的值default可能为空,因此要对其进行判断如果为空,则默认选中标题。
  2. 首次渲染时同样下划线的宽度也要跟选中的标题绑定,因此也要进行处理
 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的使用等等.并且当完成这个组件之后我们会更加的有成就感.

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

古风残影

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值