# 滚动跳转左右联动
如果是Vue项目的Hash
模式的路由,则不太建议使用锚元素<a>
来实现。会导致hash
路由丢失状态,导致刷新失败。如果是 history
模式的话,则可以使用。
下面是一个Vue示例, 利用Element.scrollIntoView
这个Api来实现的一种思路。
右侧高亮的思路就是从上到下寻找第一个可见的标题, 判断条件就是top + height / 2 - ScrollContainerTop >= 0
。
表示这个标题的top值加上标题本身一半的高度再减去父元素的top值。取第一个大于等于0的元素为当前高亮的内容。
WARNING
这个方法有一个缺点就是可滚动区域很小时,有时候可见区域内并没有任何一个标题元素。这时候的高亮显示就比较奇怪。
TIP
除了使用标题元素来判断,也可以使用整个内容区的中线来判断。或者判断哪块内容占据的可视区域比例更大,这个都可以根据实际业务来调整。
<template>
<div class="container">
<div class="left"
ref="ScrollContainer"
@scroll="onScroll"
@scrollend="onScrollEnd">
<div v-for="item in list"
:key="item.id">
<h4 :id="item.id"
:data-id="item.id"
class="item-header"
style="background: #eee;">{{item.title}}</h4>
<p :style="{height: item.childHeight + 'px'}">内容</p>
</div>
</div>
<div class="right">
<a v-for="item in list"
:key="item.id"
:class="{'title-item__a': true, 'active': activeId === item.id}"
style="display: block;"
@click="handleJump(item)">{{item.title}}</a>
</div>
</div>
</template>
<script>
/* eslint-disable */
export default {
name: 'ScrollReference',
// mixins: [],
// components: {},
// props: {},
data() {
return {
list: [
{ id: 'A', title: '项目A', childHeight: 200 },
{ id: 'B', title: '项目B', childHeight: 300 },
{ id: 'C', title: '项目C', childHeight: 100 },
{ id: 'D', title: '项目D', childHeight: 500 },
{ id: 'E', title: '项目E', childHeight: 400 },
{ id: 'F', title: '项目F', childHeight: 200 },
{ id: 'G', title: '项目G', childHeight: 300 }
],
activeId: 'A',
// 是否通过点击右侧标题,用scrollIntoView滚动
scrollByClickTitle: false
};
},
// computed: {},
// filters: {},
// watch:{},
// created() {},
// mounted() {},
// beforeRouteEnter(to, from, next) { next(vm => {}) },
// beforeRouteUpdate(to, from, next) {},
// beforeRouteLeave(to, from, next) {},
// beforeDestroy() {},
// destroyed() {},
methods: {
handleJump(item) {
this.activeId = item.id;
const headerEl = document.querySelector(`#${item.id}`);
this.scrollByClickTitle = true;
headerEl.scrollIntoView({
behavior: 'smooth',
// behavior: 'auto',
block: 'start',
inline: 'start'
});
},
onScroll() {
if (this.scrollByClickTitle) {
return;
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.timer = setTimeout(() => {
const ScrollContainer = this.$refs['ScrollContainer'];
const { top: ScrollContainerTop } = ScrollContainer.getBoundingClientRect();
console.log(ScrollContainerTop);
const headerElList = this.$el.querySelectorAll('.item-header');
console.log(headerElList);
for (let i = 0, len = headerElList.length; i < len; i++) {
const headerEl = headerElList[i];
const { top, height } = headerEl.getBoundingClientRect();
// 滚动过一半的高度即认为该模块不可见了
if (top + height / 2 - ScrollContainerTop >= 0) {
this.activeId = headerEl.dataset.id;
break;
}
}
}, 100);
},
onScrollEnd() {
console.log('onScrollEnd');
this.scrollByClickTitle = false;
}
}
};
</script>
<style lang="stylus" scoped>
.container {
display: flex;
height: 100%;
.left {
flex: 0 0 400px;
height: 100%;
overflow-y: auto;
}
.right {
flex: 0 0 200px;
margin-left: 20px;
}
.title-item__a {
display: block;
text-decoration: none;
outline: none;
line-height: 1;
font-size: 14px;
padding: 12px 20px;
box-sizing: border-box;
// border-left: 1px solid #ccc;
cursor: pointer;
position: relative;
transition: 0.2s;
&::after {
content: ' ';
position: absolute;
left: 0px;
top: 0;
bottom: 0;
width: 1px;
background-color: #ccc;
transition: 0.2s;
}
&:link {
color: #666;
}
&:visited {
color: #333;
}
&:hover {
color: #333;
}
&:active {
color: #409eff;
}
&.active {
color: #409eff;
&::after {
width: 2px;
background-color: #409eff;
}
}
}
}
</style>