前面开发Pagination分页组件时提到UI中有很多内容隐藏的交互,可以通过一个个的组件将这些通用的内容隐藏交互封装起来,供形形色色的业务使用,Pagination只是内容隐藏交互的其中一种。
本堂课带大家实现另一种内容隐藏交互的组件:Tabs选项卡。
选项卡也叫标签页,它可能是比分页还常见的交互形式。
比如我们使用Chrome浏览器打开的一个个网页就是一个个的选项卡。
还有VSCode中打开的一个个文件,都是选项卡。
分页组件一般是针对列表数据的,列表数据太多,一页一页显示,而选项卡则可以理解为对内容进行分组,如果所有内容显示在一个页面里,会显得杂乱,按照一定的逻辑对它们进行分组显示,让用户每次聚焦在当前的分组中,高效地完成交互任务。
1 组件需求
我们要实现的选项卡效果如下:
选项卡组件最核心功能就是点击Tab标题显示相应的内容,比如点击上面的Tab1,显示Tab1 Content。
2 模块设计
从效果图来看,Tabs组件主要分成两个部分:
- 上面的Tab标题列表区域
- 下面的内容区域
因为可以划分成两个子组件:
- 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>
可以看到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组件啦,效果和之前是一样的。
4 打开和关闭标签
打开和关闭标签其实就是在操作tabsData数据。
关闭标签
我们先实现关闭标签的功能。
主要分成三个步骤:
- 增加api:closable
- 增加关闭标签的图标
- 增加closeTab方法
第一步:增加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`,
})
}
效果正常!
小作业:
- 实现生成随机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;
}
完成!
基本用法:
<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>
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/79696.html