当前位置:   article > 正文

程序员节?那就赏自己开发一个Tabs选项卡吧_tabs组件怎么设计

tabs组件怎么设计

前面开发Pagination分页组件时提到UI中有很多内容隐藏的交互,可以通过一个个的组件将这些通用的内容隐藏交互封装起来,供形形色色的业务使用,Pagination只是内容隐藏交互的其中一种。

本堂课带大家实现另一种内容隐藏交互的组件:Tabs选项卡。

选项卡也叫标签页,它可能是比分页还常见的交互形式。

比如我们使用Chrome浏览器打开的一个个网页就是一个个的选项卡。

在这里插入图片描述

还有VSCode中打开的一个个文件,都是选项卡。

在这里插入图片描述

分页组件一般是针对列表数据的,列表数据太多,一页一页显示,而选项卡则可以理解为对内容进行分组,如果所有内容显示在一个页面里,会显得杂乱,按照一定的逻辑对它们进行分组显示,让用户每次聚焦在当前的分组中,高效地完成交互任务。

1 组件需求

我们要实现的选项卡效果如下:

在这里插入图片描述

选项卡组件最核心功能就是点击Tab标题显示相应的内容,比如点击上面的Tab1,显示Tab1 Content。

2 模块设计

从效果图来看,Tabs组件主要分成两个部分:

  1. 上面的Tab标题列表区域
  2. 下面的内容区域

因为可以划分成两个子组件:

  • Tabs 标签容器
  • Tab 标签项

3 基础版Tabs组件

实现Tabs组件之前,先从开发者的角度看下Tabs组件怎么使用。

<script setup>
import { ref } from 'vue'

const activeTab = ref('tab1')
</script>
<template>
  <s-tabs v-model="activeTab">
    <s-tab id="tab1" title="Tab1">Tab1 Content</s-tab>
    <s-tab id="tab2" title="Tab2">Tab2 Content</s-tab>
    <s-tab id="tab3" title="Tab3">Tab3 Content</s-tab>
  </s-tabs>
</template>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到Tabs组件里面应该包含一个默认插槽,并且支持双向绑定当前激活的Tab标签项。

import { defineComponent, toRefs } from 'vue'

export default defineComponent({
  name: 'STabs',
  props: {
    modelValue: {
      type: String,
    },
  },
  emits: ['update:modelValue'],
  setup(props, { slots, emit }) {
    const { modelValue } = toRefs(props)

    return () => {
      return <div class="s-tabs">
        { slots.default?.() }
      </div>
    }
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

Tab组件也有一个默认插槽,并且有两个参数:id和title。

import { defineComponent, toRefs } from 'vue'

export default defineComponent({
  name: 'STab',
  props: {
    id: {
      type: String,
      required: true,
    },
    title: {
      type: String,
      required: true,
    }
  },
  setup(props, { slots }) {
    const { id, title } = toRefs(props)

    return () => {
      return <div class="s-tab">
        { slots.default?.() }
      </div>
    }
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

我们使用下试试看:

  <s-tabs v-model="activeTab">
    <s-tab id="tab1" title="Tab1">Tab1 Content</s-tab>
    <s-tab id="tab2" title="Tab2">Tab2 Content</s-tab>
    <s-tab id="tab3" title="Tab3">Tab3 Content</s-tab>
  </s-tabs>
  • 1
  • 2
  • 3
  • 4
  • 5

发现没有显示Tab标签列表,内容区域显示的也不正确。
在这里插入图片描述

接下来我们就来解决这两个问题,解决完,基础版本的Tabs组件就基本成型了。

在Tabs组件中增加Tab标签列表。

  setup(props, { slots, emit }) {
    const { modelValue } = toRefs(props)
    const tabsData = [
      { id: 'tab1', title: 'Tab1' },
      { id: 'tab2', title: 'Tab2' },
      { id: 'tab3', title: 'Tab3' },
    ]

    const activeTab = ref(modelValue.value)

    const changeTab = (tabId) => {
      activeTab.value = tabId
    }

    return () => {
      return <div class="s-tabs">
        {/* Tab标签列表 */}
        <ul class="s-tabs__nav">
          {
            tabsData.value.map(tab => <li
              class={tab.id === modelValue.value ? 'active' : ''}
              onClick={() => changeTab(tab.id)}
            >
              { tab.title }
            </li>)
          }
        </ul>

        {/* 内容区域 */}
        { slots.default?.() }
      </div>
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

加上适当的样式,就能显示如下效果。

在这里插入图片描述

Tab标签页也能正常切换,只是内容区域显示还不对。

Tab子组件是用来承载每个标签项的标题和内容的,但它并不知道自己有没有被选中,需要从Tabs组件传递过去,我们可以使用provide/inject实现父子组件的数据传递。

现在Tabs中provide当前激活的标签项

  setup(props, { slots, emit }) {
    const { modelValue } = toRefs(props)
    const tabsData = [
      { id: 'tab1', title: 'Tab1' },
      { id: 'tab2', title: 'Tab2' },
      { id: 'tab3', title: 'Tab3' },
    ]
    const activeTab = ref(modelValue.value)
    
    // 父 -> 子 传递 activeTab 激活标签
    provide('active-tab', activeTab)

    const changeTab = (tabId) => {
      activeTab.value = tabId
    }

    return () => {
      return <div class="s-tabs">
        {/* Tab标签列表 */}
        <ul class="s-tabs__nav">
          {
            tabsData.value.map(tab => <li
              class={tab.id === modelValue.value ? 'active' : ''}
              onClick={() => changeTab(tab.id)}
            >
              { tab.title }
            </li>)
          }
        </ul>

        {/* 内容区域 */}
        { slots.default?.() }
      </div>
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

再在Tab中接收activeTab,并判断是否与当前tab的id相等,相等才显示相应的标签页内容

  setup(props, { slots }) {
    const { id, title } = toRefs(props)

    // 接收父传递过来的activeTab
    const activeTab = inject('active-tab')

    return () => {
      return <>
        {
          id.value === activeTab.value &&
          <div class="s-tab">{ slots.default?.() }</div>
        }
      </>
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

效果如下:

在这里插入图片描述

看着效果是有了,不过还有一个问题,就是Tabs中的tabsData是写死的,需要从Tab子组件中获取。

从子组件获取数据

由于选项卡的数据都是从tab子组件中传入的,因此要想办法将子组件中的数据传递到tabs父组件,替换掉之前写死的tabsData数据。

先将tabsData定义为一个空数组,并通过provide传递给tab子组件:

const tabsData = ref([])
provide('tabs-data', tabsData)
  • 1
  • 2

然后在tab组件中通过inject获取到tabsData,并将其中的id和title数据push到tabsData数组中:

  const tabsData = inject('tabs-data')
  tabsData.value.push({
    id: id.value,
    title: title.value,
  })
  • 1
  • 2
  • 3
  • 4
  • 5

这样数据就从tab子组件传递到tabs组件啦,效果和之前是一样的。

4 打开和关闭标签

打开和关闭标签其实就是在操作tabsData数据。

关闭标签

我们先实现关闭标签的功能。

主要分成三个步骤:

  • 增加api:closable
  • 增加关闭标签的图标
  • 增加closeTab方法

第一步:增加API:closable

props: {
  modelValue: {
    type: String,
  },
  
  // 是否开启关闭标签的功能
  closable: {
    type: Boolean,
    default: false,
  },
},

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

第二步:增加关闭标签的图标

const { modelValue, closable } = toRefs(props)

const closeTab = (tab) => {
  // 关闭标签的逻辑
}

{/* Tab标签列表 */}
<ul class="s-tabs__nav">
  {
    tabsData.value.map(tab => <li
      class={tab.id === modelValue.value ? 'active' : ''}
      onClick={() => changeTab(tab.id)}
    >
      { tab.title }

      {/* 关闭标签 */}
      {
        closable.value &&
        <svg onClick={() => closeTab(tab)} style="margin-left: 8px;" viewBox="0 0 1024 1024" width="12" height="12">
          <path d="M610.461538 500.184615l256-257.96923c11.815385-11.815385 11.815385-29.538462 0-41.353847l-39.384615-41.353846c-11.815385-11.815385-29.538462-11.815385-41.353846 0L527.753846 417.476923c-7.876923 7.876923-19.692308 7.876923-27.569231 0L242.215385 157.538462c-11.815385-11.815385-29.538462-11.815385-41.353847 0l-41.353846 41.353846c-11.815385 11.815385-11.815385 29.538462 0 41.353846l257.969231 257.969231c7.876923 7.876923 7.876923 19.692308 0 27.56923L157.538462 785.723077c-11.815385 11.815385-11.815385 29.538462 0 41.353846l41.353846 41.353846c11.815385 11.815385 29.538462 11.815385 41.353846 0L498.215385 610.461538c7.876923-7.876923 19.692308-7.876923 27.56923 0l257.969231 257.969231c11.815385 11.815385 29.538462 11.815385 41.353846 0L866.461538 827.076923c11.815385-11.815385 11.815385-29.538462 0-41.353846L610.461538 527.753846c-7.876923-7.876923-7.876923-19.692308 0-27.569231z"></path>
        </svg>
      }
    </li>)
  }
</ul>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

效果如下:

在这里插入图片描述

第三步:实现关闭标签的逻辑

const closeTab = (tab) => {
  // splice方法用于修改数组(会改变原数组)
  // 第一个参与表示从哪个位置index开始删除,第二个参数表示删除多少个元素
  const tabIndex = tabsData.value.findIndex(item => item.id === tab.id);
  tabsData.value.splice(tabIndex, 1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

效果正常:

在这里插入图片描述

小作业:

  • 只剩最后一个标签时,应该不可以删除
  • 删除当前激活的标签时,应该自动激活下一个标签,如果是关闭最后一个标签,则应该激活前一个标签

增加标签

增加标签和关闭标签的实现方式差不多。

第一步:增加api:addable

props: {
  modelValue: {
    type: String,
  },
  closable: {
    type: Boolean,
    default: false,
  },
  
  // 是否开启添加标签功能
  addable: {
    type: Boolean,
    default: false,
  },
},
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

第二步:增加新增标签的图标

const { modelValue, closable, addable } = toRefs(props)

const addTab = () => {
  // 添加标签的逻辑
}

<ul class="s-tabs__nav">
  {
    tabsData.value.map(tab => <li
      class={tab.id === modelValue.value ? 'active' : ''}
      onClick={() => changeTab(tab.id)}
    >
      { tab.title }

      {/* 关闭标签 */}
      {
        closable.value &&
        <svg onClick={() => closeTab(tab)} style="margin-left: 8px;" viewBox="0 0 1024 1024" width="12" height="12">
          <path d="M610.461538 500.184615l256-257.96923c11.815385-11.815385 11.815385-29.538462 0-41.353847l-39.384615-41.353846c-11.815385-11.815385-29.538462-11.815385-41.353846 0L527.753846 417.476923c-7.876923 7.876923-19.692308 7.876923-27.569231 0L242.215385 157.538462c-11.815385-11.815385-29.538462-11.815385-41.353847 0l-41.353846 41.353846c-11.815385 11.815385-11.815385 29.538462 0 41.353846l257.969231 257.969231c7.876923 7.876923 7.876923 19.692308 0 27.56923L157.538462 785.723077c-11.815385 11.815385-11.815385 29.538462 0 41.353846l41.353846 41.353846c11.815385 11.815385 29.538462 11.815385 41.353846 0L498.215385 610.461538c7.876923-7.876923 19.692308-7.876923 27.56923 0l257.969231 257.969231c11.815385 11.815385 29.538462 11.815385 41.353846 0L866.461538 827.076923c11.815385-11.815385 11.815385-29.538462 0-41.353846L610.461538 527.753846c-7.876923-7.876923-7.876923-19.692308 0-27.569231z"></path>
        </svg>
      }
    </li>)
  }

  {/* 添加标签 */}
  {
    addable.value &&
    <li>
      <svg onClick={addTab} viewBox="0 0 1024 1024" width="14" height="14">
        <path d="M590.769231 571.076923h324.923077c15.753846 0 29.538462-13.784615 29.538461-29.538461v-59.076924c0-15.753846-13.784615-29.538462-29.538461-29.538461H590.769231c-11.815385 0-19.692308-7.876923-19.692308-19.692308V108.307692c0-15.753846-13.784615-29.538462-29.538461-29.538461h-59.076924c-15.753846 0-29.538462 13.784615-29.538461 29.538461V433.230769c0 11.815385-7.876923 19.692308-19.692308 19.692308H108.307692c-15.753846 0-29.538462 13.784615-29.538461 29.538461v59.076924c0 15.753846 13.784615 29.538462 29.538461 29.538461H433.230769c11.815385 0 19.692308 7.876923 19.692308 19.692308v324.923077c0 15.753846 13.784615 29.538462 29.538461 29.538461h59.076924c15.753846 0 29.538462-13.784615 29.538461-29.538461V590.769231c0-11.815385 7.876923-19.692308 19.692308-19.692308z"></path>
      </svg>
    </li>
  }
</ul>

{/* 通过默认插槽 d-tab 添加的标签 */}
{ slots.default?.() }

{/* 通过添加标签按钮添加的标签 */}
{
  tabsData.value.find(tab => tab.id === activeTab.value)?.idType === 'random' &&
  tabsData.value.map(tab => {
    if (tab.idType === 'random' && tab.id === activeTab.value) {
      return <div class="s-tab">{tab.content}</div>
    }
  })
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

效果如下:

在这里插入图片描述

第三步:实现添加标签的逻辑

const addTab = () => {
  tabsData.value.push({
    id: randomId(),
    idType: 'random',
    title: `New Tab`,
    content: `New Tab Content`,
  })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

效果正常!

在这里插入图片描述

小作业:

  • 实现生成随机id的方法 randomId
export function randomId(n = 8): string {
  const str = 'abcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < n; i++) {
    result += str[parseInt((Math.random() * str.length).toString())];
  }
  return result;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

完成!

基本用法:

<template>
  <s-tabs v-model="activeTab">
    <s-tab id="tab1" title="Tab1">Tab1 Content</s-tab>
    <s-tab id="tab2" title="Tab2">Tab2 Content</s-tab>
    <s-tab id="tab3" title="Tab3">Tab3 Content</s-tab>
  </s-tabs>
</template>
<script>
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const activeTab = ref('tab1')

    return {
      activeTab,
    }
  }
})
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述

打开和关闭标签:

<template>
  <s-tabs v-model="activeTab" closable addable>
    <s-tab id="tab1" title="Tab1">Tab1 Content</s-tab>
    <s-tab id="tab2" title="Tab2">Tab2 Content</s-tab>
    <s-tab id="tab3" title="Tab3">Tab3 Content</s-tab>
  </s-tabs>
</template>
<script>
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const activeTab = ref('tab2')

    return {
      activeTab,
    }
  }
})
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号