1. 点击anchor, 相应的anchorlink高亮
function anchorClick(index) {
forceStop.value = true;
time = Date.now();
wheelRef.value.children[index].scrollIntoView({
block: 'start',
behavior: 'smooth'
});
// 给一些延时, 再点亮anchor, 同时不再限制scroll事件函数里面滚动高亮的判断
setTimeout(() => {
forceStop.value = false;
time = null;
currentIndex.value = index;
}, 100 * Math.abs(currentIndex.value - index) > 1000
? 1000
: 100 * Math.abs(currentIndex.value - index));
}
2. scroll页面, 根据列表的容器高度和滚动块之间的数值关系判断anchor高亮:
//滚动的函数
function handleScroll(e) {
time && console.log((Date.now() - time) / 1000, '滚动间隔时间', forceStop.value)
if (forceStop.value) {
return;
}
const scrollingElement = e.target;
const scrollTop = scrollingElement.scrollTop;
const headerOffsetTop = headerRef.value.offsetTop;
const headerOffsetHeight = headerRef.value.offsetHeight;
const navOffsetTop = navRef.value.offsetTop;
const navOffsetHeight = navRef.value.offsetHeight;
const windowClientHeight = scrollingElement.clientHeight;
const windowScrollHeight = scrollingElement.scrollHeight;
// 如果滚动元素的scrollTop比header元素的高度+offsetTop还大, 就让nav部分悬停在顶部!!!
if (scrollTop >= headerOffsetHeight + headerOffsetTop) {
// 因为nav悬停了, 所以scrollTop - header的高度就是判断靠近顶部窗口的可见的list内容了, 从而和anchorlink的高亮产生联系
const gap = scrollTop - headerOffsetHeight;
const idx = _.findIndex(listData1, ee => {
const a = _.get(ee, 'listItemsHeightArrs');
if (gap >= a[0] && gap < a[1]) {
return ee
}
})
currentIndex.value = idx;
isFixed.value = true;
} else {
isFixed.value = false;
currentIndex.value = 0;
}
// 滑到底部
if (windowClientHeight + scrollTop === windowScrollHeight) {
currentIndex.value = listData1.length - 1;
}
}
3. 完整示例代码:
<template>
<div class="fixed-top-container" :ref="scrollWrapperRef">
<header class="header" :ref="headerRef">头部</header>
<nav class="fixed-top-nav" :ref="navRef" :class="{ isFixed }">
<div class="box" v-for="(item, index) in navData" :key="index">
{{ item.title }}
</div>
</nav>
<ul class="fixed-top-list" :ref="wheelRef">
<li v-for="(item, index) in listData1">
{{ item.name }}
<ul>
<li class="list-item" v-for="(item, index) in item.list">{{ item.text }}</li>
</ul>
</li>
</ul>
<ul class="anchor-conatiner">
<li v-for="(item, index) in listData1" :class="currentIndex === index ? 'current' : ''" @click="anchorClick(index)">
{{ item.name }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, Ref } from 'vue';
import _ from 'lodash';
const isFixed = ref(false); //是否固定的
const headerRef = ref('headerRef') as Ref;
const navRef = ref('navRef') as Ref;
const wheelRef = ref('wheelRef') as Ref;
const currentIndex = ref(0);
const forceStop = ref(false);
const scrollWrapperRef = ref('scrollWrapperRef') as Ref;
let time: any = null
// mock数据-----------------------start--------------
const navData = reactive([
{ title: 'navRef', id: 1 },
{ title: 'nav2', id: 2 },
{ title: 'nav3', id: 3 },
{ title: 'nav4', id: 4 },
]);
const arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'];
let sum = 0;
const listData1 = reactive(Array.from({ length: arr.length }, (item, index) => {
const list = Array.from({ length: 5 }, (item, i) => ({
id: 'list-item-' + i + 1,
text: 'list-item-text-' + i,
name: 'list-name-' + i,
}));
const sum1 = sum
sum += 40 * (list.length + 1)
return {
listItemsHeightArrs: [sum1, sum], // 前一个标题内的list内容累计高度, 当前标题内的内容累计高度
name: arr[index] + '-累计高度为:' + JSON.stringify([sum1, sum]),
list,
}
}));
// mock数据-----------------------end--------------
function anchorClick(index) {
forceStop.value = true;
time = Date.now();
wheelRef.value.children[index].scrollIntoView({
block: 'start',
behavior: 'smooth'
});
// 给一些延时, 再点亮anchor, 同时不再限制scroll事件函数里面滚动高亮的判断
setTimeout(() => {
forceStop.value = false;
time = null;
currentIndex.value = index;
}, 100 * Math.abs(currentIndex.value - index) > 1000
? 1000
: 100 * Math.abs(currentIndex.value - index));
}
//滚动的函数
function handleScroll(e) {
time && console.log((Date.now() - time) / 1000, '滚动间隔时间', forceStop.value)
if (forceStop.value) {
return;
}
const scrollingElement = e.target;
const scrollTop = scrollingElement.scrollTop;
const headerOffsetTop = headerRef.value.offsetTop;
const headerOffsetHeight = headerRef.value.offsetHeight;
const navOffsetHeight = navRef.value.offsetHeight;
const windowClientHeight = scrollingElement.clientHeight;
const windowScrollHeight = scrollingElement.scrollHeight;
// 如果滚动元素的scrollTop比header元素的高度+offsetTop还大, 就让nav部分悬停在顶部!!!
if (scrollTop >= headerOffsetHeight + headerOffsetTop) {
// 因为nav悬停了, 所以scrollTop - header的高度就是判断靠近顶部窗口的可见的list内容了, 从而和anchorlink的高亮产生联系
const gap = scrollTop - headerOffsetHeight;
const idx = _.findIndex(listData1, ee => {
const a = _.get(ee, 'listItemsHeightArrs');
if (gap >= a[0] && gap < a[1]) {
return ee
}
})
currentIndex.value = idx;
isFixed.value = true;
} else {
isFixed.value = false;
currentIndex.value = 0;
}
// 滑到底部
if (windowClientHeight + scrollTop === windowScrollHeight) {
currentIndex.value = listData1.length - 1;
}
}
onMounted(() => {
nextTick(() => {
scrollWrapperRef.value.addEventListener('scroll', handleScroll, false);
});
})
onBeforeUnmount(() => { // 页面即将销毁取消事件监听
scrollWrapperRef.value.removeEventListener('scroll', handleScroll);
})
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.fixed-top-container {
height: 100vh;
overflow: auto;
& .header {
height: 200px;
width: 100%;
background-color: #ff5555;
}
& .fixed-top-nav {
display: flex;
width: 100%;
background-color: #f90;
&.isFixed {
position: fixed;
left: 0;
top: 0;
z-index: 999;
}
& .box {
font-size: 14px;
height: 30px;
line-height: 30px;
color: #333;
flex: 1 1 0%;
}
}
& .fixed-top-list {
list-style: none;
font-size: 16px;
line-height: 40px;
&>li {
background-color: green;
}
& li {
box-sizing: border-box;
}
& .list-item {
width: 100%;
height: 40px;
line-height: 40px;
font-size: 16px;
border-bottom: 1px solid #333;
background-color: #fff;
}
}
.anchor-conatiner {
position: fixed;
top: 10%;
right: 10px;
& li {
font-size: 14px;
&.current {
color: red;
}
&.light {
color: green;
}
}
}
}
</style>
4. 如果不让nav部分悬停:
<template>
<div class="fixed-top-container" :ref="scrollWrapperRef">
<header class="header" :ref="headerRef">头部</header>
<nav class="fixed-top-nav" :ref="navRef">
<div class="box" v-for="(item, index) in navData" :key="index">
{{ item.title }}
</div>
</nav>
<ul class="fixed-top-list" :ref="wheelRef">
<li v-for="(item, index) in listData1">
{{ item.name }}
<ul>
<li class="list-item" v-for="(item, index) in item.list">{{ item.text }}</li>
</ul>
</li>
</ul>
<ul class="anchor-conatiner">
<li v-for="(item, index) in listData1" :class="currentIndex === index ? 'current' : ''" @click="anchorClick(index)">
{{ item.name }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, Ref } from 'vue';
import _ from 'lodash';
const headerRef = ref('headerRef') as Ref;
const navRef = ref('navRef') as Ref;
const wheelRef = ref('wheelRef') as Ref;
const currentIndex = ref(0);
const forceStop = ref(false);
const scrollWrapperRef = ref('scrollWrapperRef') as Ref;
let time: any = null
// mock数据-----------------------start--------------
const navData = reactive([
{ title: 'navRef', id: 1 },
{ title: 'nav2', id: 2 },
{ title: 'nav3', id: 3 },
{ title: 'nav4', id: 4 },
]);
const arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'];
let sum = 0;
const listData1 = reactive(Array.from({ length: arr.length }, (item, index) => {
const list = Array.from({ length: 5 }, (item, i) => ({
id: 'list-item-' + i + 1,
text: 'list-item-text-' + i,
name: 'list-name-' + i,
}));
const sum1 = sum
sum += 40 * (list.length + 1)
return {
listItemsHeightArrs: [sum1, sum], // 前一个标题内的list内容累计高度, 当前标题内的内容累计高度
name: arr[index] + '-累计高度为:' + JSON.stringify([sum1, sum]),
list,
}
}));
// mock数据-----------------------end--------------
function anchorClick(index) {
forceStop.value = true;
time = Date.now();
wheelRef.value.children[index].scrollIntoView({
block: 'start',
behavior: 'smooth'
});
// 给一些延时, 再点亮anchor, 同时不再限制scroll事件函数里面滚动高亮的判断
setTimeout(() => {
forceStop.value = false;
time = null;
currentIndex.value = index;
}, 100 * Math.abs(currentIndex.value - index) > 1000
? 1000
: 100 * Math.abs(currentIndex.value - index));
}
//滚动的函数
function handleScroll(e) {
time && console.log((Date.now() - time) / 1000, '滚动间隔时间', forceStop.value)
if (forceStop.value) {
return;
}
const scrollingElement = e.target;
const scrollTop = scrollingElement.scrollTop;
const headerOffsetTop = headerRef.value.offsetTop;
const headerOffsetHeight = headerRef.value.offsetHeight;
const navOffsetTop = navRef.value.offsetTop;
const navOffsetHeight = navRef.value.offsetHeight;
const windowClientHeight = scrollingElement.clientHeight;
const windowScrollHeight = scrollingElement.scrollHeight;
// navOffsetTop-headerOffsetTop就是header的高度, 还需要加上nav的高度才是list内容上面的块的高度
const gap = scrollTop - (navOffsetTop-headerOffsetTop+navOffsetHeight);
if (gap >= 0) {
const idx = _.findIndex(listData1, ee => {
const a = _.get(ee, 'listItemsHeightArrs');
if (gap >= a[0] && gap < a[1]) {
return ee
}
})
currentIndex.value = idx;
}
else {
currentIndex.value = 0;
}
// 滑到底部
if (windowClientHeight + scrollTop === windowScrollHeight) {
currentIndex.value = listData1.length - 1;
}
}
onMounted(() => {
nextTick(() => {
scrollWrapperRef.value.addEventListener('scroll', handleScroll, false);
});
})
onBeforeUnmount(() => { // 页面即将销毁取消事件监听
scrollWrapperRef.value.removeEventListener('scroll', handleScroll);
})
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.fixed-top-container {
height: 100vh;
overflow: auto;
& .header {
height: 200px;
width: 100%;
background-color: #ff5555;
}
& .fixed-top-nav {
display: flex;
width: 100%;
background-color: #f90;
& .box {
font-size: 14px;
height: 30px;
line-height: 30px;
color: #333;
flex: 1 1 0%;
}
}
& .fixed-top-list {
list-style: none;
font-size: 16px;
line-height: 40px;
&>li {
background-color: green;
}
& li {
box-sizing: border-box;
}
& .list-item {
width: 100%;
height: 40px;
line-height: 40px;
font-size: 16px;
border-bottom: 1px solid #333;
background-color: #fff;
}
}
.anchor-conatiner {
position: fixed;
top: 10%;
right: 10px;
& li {
font-size: 14px;
&.current {
color: red;
}
&.light {
color: green;
}
}
}
}
</style>