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 = sumsum += 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 = sumsum += 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>