先看效果:
代码:
<template>
<div class="container">
<!-- 左侧fixed导航区域 -->
<div class="left">
<div
v-for="item in leftList"
:key="item.id"
class="left_item"
:class="item.id == selectedId ? 'selected' : ''"
@click="leftItemClick(item.id)"
>
{{ item.title }}
</div>
</div>
<!-- 右侧内容区域 -->
<div class="right">
<div v-for="item in rightList" :key="item.id" class="right_item">
{{ item.content }}
</div>
</div>
</div>
</template>
<script>
import scrollUtil from "@/common/js/scrollUtil.js";
export default {
data() {
return {
leftList: [],
rightList: [],
selectedId: 1,
offsetTopArr: [],
};
},
created() {
this.initData();
},
mounted() {
// 监听滚动事件
window.addEventListener("scroll", this.onScroll);
this.getOffsetTopArr();
},
destroyed() {
// 移除监听器
window.removeEventListener("scroll", this.onScroll);
},
methods: {
//获取右侧所有item的offsetTop
getOffsetTopArr() {
this.$nextTick(() => {
let rightListItems = document.querySelectorAll(".right .right_item");
if (rightListItems && rightListItems.length > 0) {
rightListItems.forEach((item) => {
this.offsetTopArr.push(item.offsetTop);
});
console.log(this.offsetTopArr);
}
});
},
//页面滚动监听
onScroll() {
// 获取当前文档流的 scrollTop
const scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
console.log("scrollTop=" + scrollTop);
for (let i = 0; i < this.offsetTopArr.length; i++) {
if (scrollTop >= this.offsetTopArr[i]) {
this.selectedId = this.rightList[i].parentId;
}
// todo 优化 => 改成小于等于,加break 少循环
}
},
//左侧item点击事件
leftItemClick(id) {
this.selectedId = id;
let index = 0;
for (let i = 0; i < this.rightList.length; i++) {
if (this.rightList[i].parentId == id) {
index = i;
break;
}
}
const targetOffsetTop = this.offsetTopArr[index];
scrollUtil.scrollTo(targetOffsetTop);
},
//初始化数据源
initData() {
for (let index = 1; index < 10; index++) {
for (let i = 1; i < 5; i++) {
this.rightList.push({
id: index + "-" + i,
parentId: index,
content: "content-" + index,
});
}
this.leftList.push({
id: index,
title: "title-" + index,
});
}
},
},
};
</script>
<style lang="less" scoped>
.container {
position: relative;
min-height: 100vh;
background: #fff;
.left {
position: fixed;
width: 120px;
height: 100%;
min-height: 100vh;
overflow: auto;
float: left;
background: #f2f2f2;
.left_item {
width: 100%;
height: 60px;
text-align: center;
line-height: 60px;
}
.selected {
background: #fff;
font-weight: bold;
color: #07c160;
}
}
.right {
margin-left: 120px;
width: calc(100vw - 120px);
overflow: auto;
.right_item {
width: 100%;
height: 200px;
text-align: center;
line-height: 200px;
font-size: 24px;
border-bottom: 1px solid #ccc;
}
}
}
</style>
其中scrollUtil.js:
const scrollUtil = {
/**
* 滚动到目标offsetTop
* @param {*} targetOffsetTop 目标offsetTop
*/
scrollTo: function (targetOffsetTop) {
// 当前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);
}
}
},
};
export default scrollUtil;
注意:样式要加box-sizing配置,消除border的影响,否则左边index根据页面滚动定位会不准确
没加box-sizing,offsetTopArr的打印结果:
加了box-sizing,offsetTopArr的打印结果: