携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
前言
最近公司运营部门给提了个H5活动页的需求,需要实现页面滚动固定tab、tab切换和页面滚动两者直接的联动效果,而项目中使用的UI组件库无法满足需求。这里记录一下实现方法以及实现过程中遇到的问题。
思路
拆分需求:
- 根据页面滚动固定tab-list
- 滚动页面时根据当前显示的内容区激活对应的tab
- 点击切换tab时页面滚到相应内容区
解决思路:
- 针对第一点,可以通过监听页面的scroll事件,判断当前页面的scrollTop是否超过tab-list与页面顶部的距离。
// 获取页面已滚动的距离
const scrollTop = document.body.scrollTop || document.documentElement.scrollTop || window.pageYOffset;
// 获取tab-list与页面顶部的距离
const headDom = document.querySelector('XXX');
const tabTop = headDom.getBoundingClientRect().height;
// 比较两者的大小
if (scrollTop > tabTop) {
// 固定tab-list
} else {
// 取消固定
}
- 针对第二点,同样可以在页面滚动的事件中进行处理。通过判断各内容区与页面顶部的距离和tab-list高度来解决。注: 在使用内容区和tab-list高度进行判断当前应该激活哪个tab时,判断顺序要从下往上,因为当底下的某一内容区刚满足条件时,它上面的内容区已经超出可视区了。
// 获取tab-list的高度
const tabHeight = document.querySelector('#tabs').offsetHeight;
// 获取各内容区的实例
const cont2 = // 获取各内容区的实例;
const cont3 = // 获取各内容区的实例;
// 判断各内容区顶部距离和tab-list高度
if(cont3.getBoundingClientRect().top <= tabHeight) {
// 激活对应tab
} else if(cont2.getBoundingClientRect().top <= tabHeight) {
// 激活对应tab
} else {
// 激活对应tab
}
针对第三点
第三点算是实现这个需求时碰到的一个坑点?(应该算是坑吧),一开始的想法是通过点击切换tab时,使用scrollIntoView将对应的内容区元素滚动到顶部。
但是因为tab-list是通过fixed定位的,与内容器不在一个文档流,导致使用scrollIntoView会将内容区直接顶到窗口顶部,部分内容会被tab-list覆盖。
元素的顶端将和其所在滚动区的可视区域的顶端对齐
取决于其它元素的布局情况,此元素可能不会完全滚动到顶端或底端。
---- 摘取自 MDN-ScrollIntoView
最后通过修改页面的scrollTop解决该问题,需要计算每个内容区显示在可视区时需要滚动的距离。
// 获取内容区元素实例
const cont1 = this.$refs['cont-1'];
const cont2 = this.$refs['cont-2'];
// 获取内容区高度
const cont1H = cont1.offsetHeight;
const cont2H = cont2.offsetHeight;
/*
根据要切换的tab设置相应的scrollTop
200 是顶部logo的高度,可以动态去获取
*/
if(tab === '1') {
// cont1.scrollIntoView();
document.documentElement.scrollTop = 200;
} else if(tab === '2') {
// cont2.scrollIntoView();
document.documentElement.scrollTop = 200 + cont1H;
} else {
// cont3.scrollIntoView();
document.documentElement.scrollTop = 200 + cont1H + cont2H;
}
最终效果
代码
结构和样式只是为了演示最终效果。
<template>
<div class="demo-wrap">
<!-- header -->
<div class="head-logo">
这是顶部LOGO展示区
</div>
<!-- tab-list -->
<div
id="tabs"
:class="['tab-list', { 'fix-tab': isFixTab }]"
@click="changeTab"
>
<div
v-for="tab in tabList"
:key="tab.value"
:data-tab="tab.value"
:class="['tab-item', { active: activeTab === tab.value }]"
>
<span :data-tab="tab.value" class="tab-name">{{ tab.label }}</span>
</div>
</div>
<!-- content -->
<div ref="cont-wrap" :class="['content-wrap', {'fix-tab-gap': isFixTab}]">
<div ref="cont-1" class="content content-1">这是1号内容区</div>
<div ref="cont-2" class="content content-2">这是2号内容区</div>
<div ref="cont-3" class="content content-3">这是3号内容区</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
// tab-list
tabList: [
{
value: '1',
label: '1号'
},
{
value: '2',
label: '2号'
},
{
value: '3',
label: '3号'
}
],
// 是否需要固定tab-list
isFixTab: false,
// 当前激活的tab
activeTab: '1'
};
},
mounted() {
window.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
// 点击切换tab
changeTab(e) {
const tab = e.target.dataset.tab;
if (!tab || tab === this.activeTab) {
return;
}
this.activeTab = tab;
this.changeScrollTop(tab);
},
// 切换tab时将对应内容区显示在可视区
changeScrollTop(tab) {
// 获取内容区元素实例
const cont1 = this.$refs['cont-1'];
const cont2 = this.$refs['cont-2'];
// 获取内容区高度
const cont1H = cont1.offsetHeight;
const cont2H = cont2.offsetHeight;
/*
根据要切换的tab设置相应的scrollTop
200 是顶部logo的高度,可以动态去获取
*/
if(tab === '1') {
// cont1.scrollIntoView();
document.documentElement.scrollTop = 200;
} else if(tab === '2') {
// cont2.scrollIntoView();
document.documentElement.scrollTop = 200 + cont1H;
} else {
// cont3.scrollIntoView();
document.documentElement.scrollTop = 200 + cont1H + cont2H;
}
},
// 页面滚动事件
handleScroll() {
this.handleFixTab();
this.handleContentScroll();
},
// 页面滚动时,根据当前在可视区窗口显示的内容区激活对应的tab
handleContentScroll() {
const tabHeight = document.querySelector('#tabs').offsetHeight;
const cont2 = this.$refs['cont-2'];
const cont3 = this.$refs['cont-3'];
if(cont3.getBoundingClientRect().top <= tabHeight) {
this.activeTab = '3'
} else if(cont2.getBoundingClientRect().top <= tabHeight) {
this.activeTab = '2'
} else {
this.activeTab = '1'
}
},
// 判定是否要固定tab-list, 根据页面滚动距离是否大于顶部logo的高度
handleFixTab() {
const scrollTop =
document.body.scrollTop ||
document.documentElement.scrollTop ||
window.pageYOffset;
const headDom = document.querySelector('.head-logo');
const tabTop = headDom.getBoundingClientRect().height;
if (scrollTop > tabTop) {
this.isFixTab = true;
} else {
this.isFixTab = false;
}
}
}
};
</script>
<style lang="less" scoped>
.demo-wrap {
position: relative;
margin-bottom: 300px;
.head-logo {
height: 200px;
line-height: 200px;
text-align: center;
background: black;
color: #fff;
}
.tab-list {
display: flex;
background: #fff;
&.fix-tab {
position: fixed;
top: 0;
left: 0;
right: 0;
}
.tab-item {
width: 33%;
text-align: center;
height: 50px;
line-height: 50px;
font-size: 18px;
font-weight: bold;
&.active {
color: #faab0a;
.tab-name {
border-bottom-color: #faab0a;
}
}
.tab-name {
padding-bottom: 6px;
border-bottom: 4px solid transparent;
border-radius: 4px;
}
}
}
.content-wrap {
&.fix-tab-gap {
margin-top: 50px;
}
.content {
width: 100%;
height: 400px;
color: #fff;
font-size: 24px;
line-height: 400px;
text-align: center;
&-1 {
background: red;
}
&-2 {
background: green;
}
&-3 {
background: blue;
}
}
}
}
</style>