前端业务场景-tab栏与内容区的联动
在日常的工作的业务场景中,我们可能会遇到这么一些场景;有一些页面会因为内容或者主题的不同划分为许多的不同的区域,这样的场景页面内容一般都比较多比较长,这时候为了用户的体验往往会在旁边加一个tab栏;但是如果只是单纯的tab栏没有任何交互,那就会给用户的体验很不好;因此我们需要给tab栏与内容区加上联动效果让客户的体验得到提升。
基本信息
我是使用Vue和element-plus演示这一业务场景。
具体的业务场景页面如下:
容器组件实现
创建内容区域与tab栏的组件,导入到容器组件TabInteract,代码:
<script setup lang="ts">
import MainArea from './components/MainArea.vue'
import TabBar from './components/TabBar.vue'
</script>
<template>
<div class="TabInteract">
<MainArea ref="mainAreaRef" />
<TabBar ref="tabBarRef" />
</div>
</template>
tab栏代码实现
tab栏是用的element-plus的tabs组件,只是用css改变样式,有时间的也可以自己写一个。组件TabBar代码:
<script setup lang="ts">
import { tabListData } from '../js/util'
import { ref } from 'vue'
let active = ref('ContentARef')
</script>
<template>
<div class="TabBar">
<el-tabs v-model="active" tab-position="right" class="demo-tabs">
<el-tab-pane v-for="item in tabListData" :key="item.id" :label="item.label" :name="item.label">
</el-tab-pane>
</el-tabs>
</div>
</template>
tab的内容数据是使用一个数组循环出来的,定义一个属性active初始值为第一个tab元素的ref,点击切换就是修改对应的标签名即可完成切换tab。
tab的内容数据数组代码:
export const tabListData = [
{
id: 1,
label: 'ContentA',
offsetTop: null
},
{
id: 2,
label: 'ContentB',
offsetTop: null
},
{
id: 3,
label: 'ContentC',
offsetTop: null
},
{
id: 4,
label: 'ContentD',
offsetTop: null
},
]
内容区域实现
内容区域MainArea代码:
<script setup lang="ts">
import ContentA from './ContentA.vue'
import ContentD from './ContentD.vue'
import ContentC from './ContentC.vue'
import ContentB from './ContentB.vue'
</script>
<template>
<div class="MainArea">
<ContentA ref="ContentARef" />
<ContentB ref="ContentBRef" />
<ContentC ref="ContentCRef" />
<ContentD ref="ContentDRef" />
</div>
</template>
内容区域我是将每个区域都定义成一个组件,然后给每个组件定义了ref,将ref记录对应的tab数据当,方便后面使用。
点击tab页切换到相应的内容区域
tab栏区域中实现点击tab页切换到相应的内容区域主要用到了scrollIntoView() 这个api,这个方法会滚动元素的父容器,使被调用scrollIntoView() 的元素对用户可见。
tab栏组件TabBar
在tab栏组件TabBar中<el-tabs>中添加点击监听事件tabChange ,然后使用emit 抛出子组件的监听事件,代码:
<script setup lang="ts">
import { tabListData } from '../js/util'
import { ref } from 'vue'
let active = ref('ContentARef')
const emit = defineEmits<{(e: 'emitTabChange', val: string): void}>()
const tabChange = (val) => {
emit('emitTabChange', item.paneName)
}
defineExpose({
active
})
</script>
<template>
<div class="TabBar">
<el-tabs v-model="active" @tab-click="tabChange" tab-position="right" class="demo-tabs">
<el-tab-pane v-for="item in tabListData" :key="item.id" :label="item.label" :name="item.domRef">
</el-tab-pane>
</el-tabs>
</div>
</template>
内容区域组件MainArea
在内容区域组件MainArea中首先从在组件TabBar点击事件中获取到的ref名,拿到ref名可以动态的获取到点击tab栏对应的内容区域的dom,最后再调用scrollIntoView() ,使用vue3的defineExpose抛出该函数,方便父组件调用。代码:
<script setup lang="ts">
import ContentA from './ContentA.vue'
import ContentD from './ContentD.vue'
import ContentC from './ContentC.vue'
import ContentB from './ContentB.vue'
import { ref } from 'vue'
const mainAreaRef = ref(null)
const setTabSwitch = (tabName) => {
const nodeList = Array.from(mainAreaRef.value.childNodes) // 将存子实例对象的类数组转化为数组
const currerObj = nodeList.find(el => el.className === tabName) // 拿到当前激活的实例对象
currerObj.scrollIntoView()
}
defineExpose({
setTabSwitch,
});
</script>
<template>
<div class="MainArea">
<ContentA ref="ContentARef" />
<ContentB ref="ContentBRef" />
<ContentC ref="ContentCRef" />
<ContentD ref="ContentDRef" />
</div>
</template>
容器组件TabInteract
在容器组件中监听子组件TabBar的自定义事件tabChange,根据MainArea组件的ref名拿到组件的对象,再调用子组件MainArea抛出的函数setTabSwitch。代码:
<script setup lang="ts">
import MainArea from './components/MainArea.vue'
import TabBar from './components/TabBar.vue'
import { ref } from 'vue'
const mainAreaRef = ref(null)
const tabChange = (val) =>{
mainAreaRef.value.setTabSwitch(val)
}
</script>
<template>
<div class="TabInteract">
<MainArea ref="mainAreaRef" />
<TabBar ref="tabBarRef" @emitTabChange="tabChange"/>
</div>
</template>
以上就可以实现点击tab页切换到相应的内容区域了。
滚动内容区域切换tab栏选中
内容区域组件MainArea
监听组件滚动事件scroll,再实现两个方法,计算个个内容区域距离顶部的距离,以及当前滚动条处在的位置,经过计算后会要切换的tab的ref名,拿到后再用emit抛出。代码:
<script setup lang="ts">
import ContentA from './ContentA.vue'
import ContentD from './ContentD.vue'
import ContentC from './ContentC.vue'
import ContentB from './ContentB.vue'
import { ref } from 'vue'
import { getCurrentScrollDom, tabListData } from '../js/util'
const mainAreaRef = ref(null)
const emit = defineEmits<{(e: 'emitHandleScroll', val: string): void}>()
const setTabSwitch = (tabName) => {
const nodeList = Array.from(mainAreaRef.value.childNodes) // 将存子实例对象的类数组转化为数组
const currerObj = nodeList.find(el => el.className === tabName) // 拿到当前激活的实例对象
currerObj.scrollIntoView()
}
const handleScroll = () => {
emit('emitHandleScroll', getCurrentScrollDom(tabListData, mainAreaRef))
}
defineExpose({
setTabSwitch,
handleScroll,
});
</script>
<template>
<div ref="mainAreaRef" class="MainArea" @scroll="handleScroll">
<ContentA ref="ContentARef" />
<ContentB ref="ContentBRef" />
<ContentC ref="ContentCRef" />
<ContentD ref="ContentDRef" />
</div>
</template>
计算函数代码:
//获取dom的offsetTop
const getOffsetTop = (tabInfo, _self) => {
tabInfo.forEach((el, i) => {
tabInfo[i].offsetTop = _self.value.childNodes[i]?.offsetTop
})
return tabInfo
}
export const getCurrentScrollDom = (tabInfo, _self) => {
const current = _self.value.scrollTop + 20 // 获取当前滚动条离顶部高度
getOffsetTop(tabInfo, _self) // 获取各个区域高度
let currentTab = ''
tabInfo.forEach((el) => {
if(current + 100 > el.offsetTop) { //增加100,可以让内容区域没有到达顶部就切换tab
currentTab = el.label
}
})
return currentTab
}
容器组件TabInteract
在容器中监听子组件MainArea抛出的自定义事件,在监听函数中使用ref拿到TabBar组件的对象修改它的active字段,改变当前选中的tab。代码:
<script setup lang="ts">
import MainArea from './components/MainArea.vue'
import TabBar from './components/TabBar.vue'
import { ref } from 'vue'
const mainAreaRef = ref(null)
const tabBarRef = ref(null)
const tabChange = (val) =>{
mainAreaRef.value.setTabSwitch(val)
}
const emitHandleScroll = (val) => {
tabBarRef.value.active = val
}
</script>
<template>
<div class="TabInteract">
<MainArea ref="mainAreaRef" @emitHandleScroll="emitHandleScroll"/>
<TabBar ref="tabBarRef" @emitTabChange="tabChange"/>
</div>
</template>
这样就可以完成内容区域滚动与tab的联动效果。
总结
点击tab页切换到相应的内容区域:主要使用的是 scrollIntoView() ;滚动内容区域切换tab栏选中:监听滚动事件计算出当前滚动条的offsetTop和每个内容区域的offsetTop,再根据实际情况进行判断就可以得到当前所处于的区域,拿到tab标识再更换选中状态。
注意:本来是使用getCurrentInstance()获得组件ref实例来实现的,但是貌似这个api没有在官方文档里(感谢评论区jy的提醒), Vue官方并不推荐使用,在打包项目的时候可能会有问题,因此修改了实现方式,大家在项目中使用时也可以注意下,考虑好要不要在项目中使用该api;不过实现方式有很多种希望大家可以学习到的是实现效果的方法而不是方式。
最后
本人是菜鸟一枚,平时都是使用vue2开发,第一次用vue3,例子当中肯定是有很多写得不好的地方,以及需要优化的代码,这也是第一次分享文章,写的肯定不太好,不好的地方希望大家谅解,同时也可以指出;这样我也能改正,大家都能得到成长,毕竟大家好才是真的好。