大家好,我是南宫。今天写一篇博客来整理下最近刚解决的一个问题,那就是导航栏跟内容联动的问题。
简单说一下我想要的效果:写了一个宽度为屏幕100%的div,居中的部分是一个导航栏,水平排列,默认位于banner下,如果页面滚到了banner下面,要让导航栏固定顶部。如果页面滚到了下方对应的内容,那就高亮对应的tab标记。如果点击了tab,那就要让页面滚到对应的内容,并且让该tab高亮。
(效果是动态的,我随便截取一个场景来展示吧,比如我点击“应用场景”的时候,页面滚动到了对应的内容区域,并且对应的tab高亮了,也能看到导航栏固定顶部)
拆解一下,可以分为这么几个部分:①吸顶、②选择tab,可以让页面滚动到对应内容的位置、③页面滚动到了对应的内容的位置,可以设置tab的选择。
吸顶的实现:
这个是最容易的,我先简单说一下原理:默认导航栏是位于banner下方的,也就是普通的标准流元素,没有浮动没有定位,是正常占位的;而固定顶部的状态下,导航栏被固定定位(position: fixed)到了顶部,这个时候可以设置一下top。我们可以判断当前滚动的位置是否需要固定定位,如果是,那就做一下样式的切换。
显然导航栏有两种状态,所以我们可以写两个class来分别控制。(我这里使用SCSS,可以参考一下我的代码,nav-bar是默认状态,再加上fixed的是固定顶部的状态)
// 导航条
.nav-bar {
height: 61px;
background: url("/img/download/rect_bg.png") repeat-x;
background-size: 5px 61px;
&.fixed {
position: fixed;
top: 56px;
left: 0;
right: 0;
z-index: 10;
}
.nav-bar-item {
position: relative;
margin: 0 47px;
line-height: 55px;
cursor: pointer;
&:hover::after,
&.active::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 4px;
background: $dark-blue;
}
.nav-text {
font-size: 16px;
color: #333;
}
}
}
那么怎么控制导航栏是否固定顶部呢?可以在data里放一个变量(如isFixed)来控制,默认为false。监听一下页面的滚动,如果滚动超过了某个值,就让isFixed改为true,否则改为false。然后在导航栏这边,动态绑定class,“fixed”在isFixed为true才设置上去。
data() {
return {
// 是否固定
isFixed: false,
//...其他data
}
}
// 处理滚动
handleScroll() {
var scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop
// console.log(scrollTop) // 滑动的长度
var offsetTop = document.querySelector('.banner').offsetHeight
// 吸顶效果
if (scrollTop > offsetTop) {
this.isFixed = true
} else {
this.isFixed = false
}
},
<div class="nav-bar" :class="{ fixed: isFixed }">...</div>
Vue项目中如何监听页面的滚动呢?把刚才的handleScroll定义到组件的methods里面,在mounted的时候绑定window的滚动时间,注意是在mounted的时候才可以使用DOM。退出页面的时候,也要记得销毁监听。
mounted() {
window.addEventListener('scroll', this.handleScroll) // 监听滑动事件
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll) // 销毁监听滑动事件
}
点击选择tab,让页面滚动到对应内容的实现:
首先,tab会有普通状态和高亮状态,所以我们也需要用一个变量来控制当前选中的、处于高亮状态的tab是第几个,默认为0,让第一个tab高亮。
data() {
return {
// ...其他data
// 当前选择的tab
currentNav: 0,
}
}
<!-- 这是导航条里的每一个tab,给它动态绑定active类,高亮才显示,并且绑定了点击事件 -->
<div
v-for="(item, index) in obj.contentList"
:key="index"
class="nav-bar-item"
:class="{ active: currentNav == index }"
@click="toBox(index)"
>
<span class="nav-text">{{ item }}</span>
</div>
然后,页面上有好几块内容,每一块对应一个tab,我们给这几块内容的最外层加上一个class,作为标记,便于选择这些锚点元素,比如我给它们加上“j-content”这个类名。这样就实现了导航栏和每块内容的绑定。
<!-- 这是其中的一块内容 -->
<div class="scene j-content">
<h3 class="production-title">应用场景</h3>
<!-- 其他内容 -->
</div>
点击tab的时候,我们可以获取到当前点击的tab的索引(比如第1个的索引是0),在带有j-content的div里,我们找到下标相同的div(如索引为0的时候找到第一个这样的div),获取它顶部的坐标,让页面滚动到这里。
(看下面的代码,从所有的带有j-content的class里找到对应当前索引的div,获取它的offsetTop,然后用window.scrollTo平滑滚动到指定地方,这里用behavior设置了平滑滚动)
// 滚动到哪一块
toBox(index) {
this.currentNav = index
const DOM = document.querySelectorAll('.j-content')[index]
const offsetTop = DOM.offsetTop - 25
// console.log('滚动到哪里', DOM)
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
})
},
页面滚动到对应内容,设置tab高亮的实现:
首页,要判断页面滚到到了哪一块内容,就得监听页面的滚动事件,所以判断的代码需要写到handleScroll方法里。
然后滚动的时候,滚动的位置跟哪些值比较呢?这就需要我们记录每一块内容的offsetTop的位置。根据之前定好的class,找到每一块内容的div,获取一下它们各自的offsetTop,并保存。
// 获取所有锚点元素
const divs = [...document.querySelectorAll('.j-content')]
// 将所有锚点元素offsetTop push到数组内
divs.forEach((item, index) => {
this.contentTopList[index] = item.offsetTop - 25
})
接着,在滚动的过程中,怎么确定当前要高亮的tab是哪个呢?我们可以把当前的scrollTop值与每一个div的offsetTop比较,找到“小于但又最接近”的那个值,把这个下标作为要高亮的tab的下标。
(我补充了一个判断,假如刚开始滚,还没有滚到第一个内容区域的话,navIndex会算出来undefined,为了让这个时候也有tab被高亮,我认为当前高亮的是0.)
let navIndex
// 滚动定位tab高亮的状态
for (let i = 0; i < this.contentTopList.length; i++) {
// 如果当前滚动的top坐标大于第i个的top坐标,就记录下i。
// 记录到最后,i就会是最后一个满足条件的i,也就是刚刚好的那个值
if (scrollTop >= this.contentTopList[i]) {
navIndex = i
}
}
// 把下标赋值给 vue 的 data
this.currentNav = navIndex
if (typeof navIndex !== 'number') {
this.currentNav = 0
}
到这一步,就已经做到了滚动吸顶、点击tab滚动到相应的锚点、滚动高亮对应的tab。但是在调试的时候我又发现了一个问题——点击tab,页面滚动的过程中,经过其他区域的时候,也会点亮对应的tab,这就显得效果有些拖泥带水,不像是被直接定位过去的。
于是我觉得,需要区分这两种情况:“因为点击而直接滚动到这里” 和 “页面自己滚动的时候路过这里”。我在data里加了一个新的变量,叫isClick,默认为false,表示不是点击定位的。在点击tab后,瞬间把isClick赋值为true。然后在“根据滚动位置,判断高亮tab”之前,先判断isClick,确定不是点击定位的才判断。在点击动作完成、滚动完毕后,过一小段时间,把isClick还原成true,以便恢复后续的滚动高亮效果。
完整的handleScroll和toBox代码在这里:
// 处理滚动
handleScroll() {
var scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop
// console.log(scrollTop) // 滑动的长度
var offsetTop = document.querySelector('.banner').offsetHeight
// 判断是否已经记录了每个内容的top,如果不是,就记录一下。如果是,就直接使用
// 获取所有锚点元素
const divs = [...document.querySelectorAll('.j-content')]
// 将所有锚点元素offsetTop push到数组内
divs.forEach((item, index) => {
this.contentTopList[index] = item.offsetTop - 25
})
// if (this.contentTopList.length === 0) {
// }
// 判断当前是否是点击定位的,如果不是,才有滚动定位的效果
if (!this.isClick) {
let navIndex
// 滚动定位tab高亮的状态
for (let i = 0; i < this.contentTopList.length; i++) {
// 如果当前滚动的top坐标大于第i个的top坐标,就记录下i。记录到最后,i就会是最后一个满足条件的i,也就是刚刚好的那个值
if (scrollTop >= this.contentTopList[i]) {
navIndex = i
}
}
// 把下标赋值给 vue 的 data
this.currentNav = navIndex
if (typeof navIndex !== 'number') {
this.currentNav = 0
}
}
// 吸顶效果
if (scrollTop > offsetTop) {
this.isFixed = true
} else {
this.isFixed = false
}
},
// 滚动到哪一块
toBox(index) {
// 点击滚动到指定的位置,要去掉滚动的过程中因为位置变化带来的效果
this.isClick = true
this.currentNav = index
const DOM = document.querySelectorAll('.j-content')[index]
const offsetTop = DOM.offsetTop - 25
// console.log('滚动到哪里', DOM)
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
})
// 过一段时间,把isClick还原
setTimeout(() => {
this.isClick = false
}, 800)
},
还有20分钟就是新年了,我总算赶在2021年写完了这一篇博客,欢迎有类似需要的小伙伴来探讨哦,谢谢大家!