赞
踩
前面开发Pagination分页组件时提到UI中有很多内容隐藏的交互,可以通过一个个的组件将这些通用的内容隐藏交互封装起来,供形形色色的业务使用,Pagination只是内容隐藏交互的其中一种。
本堂课带大家实现另一种内容隐藏交互的组件:Tabs选项卡。
选项卡也叫标签页,它可能是比分页还常见的交互形式。
比如我们使用Chrome浏览器打开的一个个网页就是一个个的选项卡。
还有VSCode中打开的一个个文件,都是选项卡。
分页组件一般是针对列表数据的,列表数据太多,一页一页显示,而选项卡则可以理解为对内容进行分组,如果所有内容显示在一个页面里,会显得杂乱,按照一定的逻辑对它们进行分组显示,让用户每次聚焦在当前的分组中,高效地完成交互任务。
我们要实现的选项卡效果如下:
选项卡组件最核心功能就是点击Tab标题显示相应的内容,比如点击上面的Tab1,显示Tab1 Content。
从效果图来看,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>
可以看到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> } } })
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> } } })
我们使用下试试看:
<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>
发现没有显示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> } }
加上适当的样式,就能显示如下效果。
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> } }
再在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>
}
</>
}
}
效果如下:
看着效果是有了,不过还有一个问题,就是Tabs中的tabsData是写死的,需要从Tab子组件中获取。
由于选项卡的数据都是从tab子组件中传入的,因此要想办法将子组件中的数据传递到tabs父组件,替换掉之前写死的tabsData数据。
先将tabsData定义为一个空数组,并通过provide传递给tab子组件:
const tabsData = ref([])
provide('tabs-data', tabsData)
然后在tab组件中通过inject获取到tabsData,并将其中的id和title数据push到tabsData数组中:
const tabsData = inject('tabs-data')
tabsData.value.push({
id: id.value,
title: title.value,
})
这样数据就从tab子组件传递到tabs组件啦,效果和之前是一样的。
打开和关闭标签其实就是在操作tabsData数据。
我们先实现关闭标签的功能。
主要分成三个步骤:
第一步:增加API:closable
props: {
modelValue: {
type: String,
},
// 是否开启关闭标签的功能
closable: {
type: Boolean,
default: false,
},
},
第二步:增加关闭标签的图标
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>
效果如下:
第三步:实现关闭标签的逻辑
const closeTab = (tab) => {
// splice方法用于修改数组(会改变原数组)
// 第一个参与表示从哪个位置index开始删除,第二个参数表示删除多少个元素
const tabIndex = tabsData.value.findIndex(item => item.id === tab.id);
tabsData.value.splice(tabIndex, 1);
}
效果正常:
小作业:
增加标签和关闭标签的实现方式差不多。
第一步:增加api:addable
props: {
modelValue: {
type: String,
},
closable: {
type: Boolean,
default: false,
},
// 是否开启添加标签功能
addable: {
type: Boolean,
default: false,
},
},
第二步:增加新增标签的图标
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> } }) }
效果如下:
第三步:实现添加标签的逻辑
const addTab = () => {
tabsData.value.push({
id: randomId(),
idType: 'random',
title: `New Tab`,
content: `New Tab Content`,
})
}
效果正常!
小作业:
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;
}
完成!
基本用法:
<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>
打开和关闭标签:
<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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。