vue悬浮导航实现内容滚动时,导航跟随选中,点击导航滚动到相应位置,
<template>
<div>
<div class="goods_navBg" ref="navHeader">模拟悬浮导航</div>
<div class="goods_box">box</div>
<!-- 吸顶导航 start -->
<GoodsInfoNavTab class="goods_sticky" :navHeaderHeight="navHeaderHeight" :tabTitle="tabTitle"></GoodsInfoNavTab>
<!-- 吸顶导航 end -->
<ul class="ulMain">
<li class="goodsMBox" v-for="(item, index) in tabTitle" :key="index">{{ item }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'goodsPage',
data() {
return {
tabTitle: ['Tab1', 'Tab2', 'Tab3', 'Tab4'],//子导航
navHeaderHeight: 0,//子导航高度
}
},
mounted() {
//获取悬浮导航的高度
this.navHeaderHeight = this.$refs.navHeader.offsetHeight;
},
methods: {}
}
</script>
<style scoped>
.goods_navBg {
width: 7.5rem;
height: 0.88rem;
background: red;
z-index: 999;
position: fixed;
top: 0;
left: 0;
text-align: center;
font-size: 0.4rem;
line-height: 0.88rem;
color: white;
}
.goods_box {
width: 100%;
height: 5rem;
background: green;
margin-top: 0.88rem;
font-size: 1rem;
text-align: center;
line-height: 5rem;
}
.goods_sticky {
position: sticky;
top: 0.88rem;
z-index: 99;
}
.ulMain {
width: 100%;
}
.goodsMBox {
height: 10rem;
font-size: 0.5rem;
color: white;
}
.ulMain li:nth-child(1) {
background: blue;
}
.ulMain li:nth-child(2) {
background: blueviolet;
}
.ulMain li:nth-child(3) {
background: orange;
}
.ulMain li:nth-child(4) {
background: pink;
}</style>
导航组件:
<template>
<div>
<ul class="goods_ceiling" ref="navTab">
<a v-for="(item, index) in tabTitle" :key="index" @click="scrollToPosition(index)"
:class="{ js_goods_ceilTitle: active == index }">
<li class="goods_ceilTitle">{{ item }}</li>
</a>
</ul>
</div>
</template>
<script>
export default {
name: 'GoodsInfoNavTab',
props: {
tabTitle: {
type: Array,
default: () => []
},
navHeaderHeight: {
type: Number,
default: 0
}
},
data() {
return {
active: 0,// 当前激活的导航索引
}
},
mounted() {
//监听页面滚动条事件
window.addEventListener("scroll", this.onScroll);
},
destroy() {
// 必须移除监听器,不然当该vue组件被销毁了,监听器还在就会出错
window.removeEventListener("scroll", this.onScroll);
},
methods: {
onScroll() {
let navTabHeight = this.$refs.navTab.offsetHeight //获取子导航的高度
let navHeaderHeight = this.navHeaderHeight //获取悬浮导航的高度
// 获取所有锚点元素
const navContents = document.querySelectorAll('.goodsMBox')
// 所有锚点元素的 offsetTop
const offsetTopArr = []
navContents.forEach(item => {
offsetTopArr.push(item.offsetTop)
})
// 获取当前文档流的 scrollTop
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// 定义当前点亮的导航下标
let navIndex = 0
for (let n = 0; n < offsetTopArr.length; n++) {
// 如果 scrollTop 大于等于第 n 个元素的 offsetTop 则说明 n-1 的内容已经完全不可见
// 那么此时导航索引就应该是 n 了
if (scrollTop >= offsetTopArr[n] - navTabHeight - navHeaderHeight) {
navIndex = n
}
}
// 把下标赋值给 vue 的 data
this.active = navIndex
},
scrollToPosition(index) {
let navTabHeight = this.$refs.navTab.offsetHeight //获取子导航的高度
let navHeaderHeight = this.navHeaderHeight //获取悬浮导航的高度
// 获取目标的 offsetTop
// css选择器是从 1 开始计数,我们是从 0 开始,所以要 +1
const targetOffsetTop = document.querySelector(`.goodsMBox:nth-child(${index + 1})`).offsetTop - navTabHeight - navHeaderHeight
// 获取当前 offsetTop
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// 定义一次跳 50 个像素,数字越大跳得越快,但是会有掉帧得感觉,步子迈大了会扯到蛋
const STEP = 50
// 判断是往下滑还是往上滑
if (scrollTop > targetOffsetTop) {
// 往上滑
smoothUp()
} else {
// 往下滑
smoothDown()
}
// 定义往下滑函数
function smoothDown() {
// 如果当前 scrollTop 小于 targetOffsetTop 说明视口还没滑到指定位置
if (scrollTop < targetOffsetTop) {
// 如果和目标相差距离大于等于 STEP 就跳 STEP
// 否则直接跳到目标点,目标是为了防止跳过了。
if (targetOffsetTop - scrollTop >= STEP) {
scrollTop += STEP
} else {
scrollTop = targetOffsetTop
}
document.body.scrollTop = scrollTop
document.documentElement.scrollTop = scrollTop
// 屏幕在绘制下一帧时会回调传给 requestAnimationFrame 的函数
// 关于 requestAnimationFrame 可以自己查一下,在这种场景下,相比 setInterval 性价比更高
requestAnimationFrame(smoothDown)
}
}
// 定义往上滑函数
function smoothUp() {
if (scrollTop > targetOffsetTop) {
if (scrollTop - targetOffsetTop >= STEP) {
scrollTop -= STEP
} else {
scrollTop = targetOffsetTop
}
document.body.scrollTop = scrollTop
document.documentElement.scrollTop = scrollTop
requestAnimationFrame(smoothUp)
}
}
},
}
}
</script>
<style lang="less" scoped>
.goods_ceiling {
width: 7.5rem;
height: 1rem;
background: white;
.flex_item;
.f32;
justify-content: space-around;
}
.goods_ceiling a {
.c;
}
.goods_ceilTitle {
height: 1rem;
line-height: 1rem;
text-align: center;
}
.js_goods_ceilTitle {
font-weight: bold;
position: relative;
}
.js_goods_ceilTitle::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
margin-left: -0.3rem;
width: 0.6rem;
height: 0.08rem;
border-radius: 0.18rem;
background: @c3b8;
}
</style>