携手创作,共同成长!这是我参与「掘金日新计划 · 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>