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;
}, 300 * Math.abs(currentIndex.value - index) > 1000
? 1000
: 300 * 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;
}, 300 * Math.abs(currentIndex.value - index) > 1000
? 1000
: 300 * 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;
}, 300 * Math.abs(currentIndex.value - index) > 1000
? 1000
: 300 * 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>
5. 如果只让list内容区域滚动
<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[0] }}</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;
}, 300 * Math.abs(currentIndex.value - index) > 1000
? 1000
: 300 * 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) {
// 因为只有list内容部分在滚动, 根据scrollTop判断滚动到哪个区域, 进而判断anchorlink的高亮
const gap = scrollTop;
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(() => {
wheelRef.value.addEventListener('scroll', handleScroll, false);
});
})
onBeforeUnmount(() => { // 页面即将销毁取消事件监听
wheelRef.value.removeEventListener('scroll', handleScroll);
})
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.fixed-top-container {
display: flex;
flex-direction: column;
height: 100vh;
& .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 {
flex: 1;
list-style: none;
font-size: 16px;
line-height: 40px;
overflow: auto;
&>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>
优化scroll判断
之前在anchorClick事件中使用延时函数, 延迟300-1000毫秒再点亮anchorlink, 主要是因为scrollIntoView触发了scroll事件, 而scroll函数中已经有一套判断anchorClick高亮的方法, 但那套方法有些anchorlink因为滚动距离的限制永远不可能点亮, 与anchorlink点击必须高亮的原则相悖,
现在的需求就是要让scroll事件函数中要照顾到每个anchorlink都可以根据滚动距离判断被点亮的情况!!!
原理:
1) 当列表内容大于其容器高度时,在list内容其后添加一个div标签, 高度就是容器高度(但注意要保留一个list-item, 否则显得很假)
2) 即html高度 - header高度 - nav高度 - 1个list-item的高度
3) 这样在有scroll事件触发时anchorlink就能够保证每个anchorlink都可能被点亮(之前的判断条件下有些anchorlink是永远不可能点亮的, 比如'M');
<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) in item.list">{{ item.text }}</li>
</ul>
</li>
<!-- 列表内容后面拼接上一段空白的文档, 让list内容可以scroll到更底部去!!! -->
<div class="rest-list-blank" v-show="restListBlankShow"></div>
</ul>
<ul class="anchor-conatiner">
<li v-for="(item, index) in listData1" :class="currentIndex === index ? 'current' : ''"
@click="anchorClick(index)">
{{ item.name[0] }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, Ref } from 'vue';
import _ from 'lodash';
interface IProps {
headerHeight: string;
navHeight: string;
itemHeight: string;
}
const props = defineProps<IProps>();
// 从父页面传过来的数据, 可以在script标签中使用, 也可以在style标签内使用----start----
const headerHeight = props.headerHeight;
const navHeight = props.navHeight
const itemHeight = props.itemHeight;
// 从父页面传过来的数据, 可以在script标签中使用, 也可以在style标签内使用----end----
const headerRef = ref('headerRef') as Ref;
const navRef = ref('navRef') as Ref;
const wheelRef = ref('wheelRef') as Ref;
const currentIndex = ref(0);
const scrollWrapperRef = ref('scrollWrapperRef') as Ref;
const restListBlankShow = ref(false);
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 sumHeight = 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 = sumHeight;
const listItemHeight = Number(itemHeight.replace('px', ''));
sumHeight += listItemHeight * (list.length + 1);
return {
listItemsHeightArrs: [sum1, sumHeight], // 前一个标题内的list内容累计高度, 当前标题内的内容累计高度
name: arr[index] + '-累计高度为:' + JSON.stringify([sum1, sumHeight]),
list,
}
}));
// mock数据-----------------------end--------------
function anchorClick(index) {
time = Date.now();
wheelRef.value.children[index].scrollIntoView({
block: 'start',
behavior: 'smooth'
});
// 给一些延时, 再点亮anchor, 同时不再限制scroll事件函数里面滚动高亮的判断
nextTick(() => {
time = null;
currentIndex.value = index;
})
}
//滚动的函数
function handleScroll(e) {
time && console.log((Date.now() - time) / 1000, '滚动间隔时间')
const scrollingElement = e.target;
const scrollTop = scrollingElement.scrollTop;
const headerOffsetHeight = headerRef.value.offsetHeight;
const navOffsetHeight = navRef.value.offsetHeight;
const windowClientHeight = scrollingElement.clientHeight;
const windowScrollHeight = scrollingElement.scrollHeight;
const lastIdx = listData1.length - 1
if (scrollTop >= headerOffsetHeight + navOffsetHeight) {
// 因为只有list内容部分在滚动, 根据scrollTop判断滚动到哪个区域, 进而判断anchorlink的高亮
const gap = scrollTop;
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 = lastIdx;
}
}
onMounted(() => {
nextTick(() => {
console.log(sumHeight, wheelRef.value.offsetHeight, 'sumHeight - wheelRef.value.offsetHeight')
// 当list内容总高度大于容器高度的时候, 让list内容再拼接一个标签, 让滚动事件判断的anchor高亮可以每个点都可能被点亮!!!
restListBlankShow.value = sumHeight - wheelRef.value.offsetHeight > 0;
wheelRef.value.addEventListener('scroll', handleScroll, false);
});
})
onBeforeUnmount(() => { // 页面即将销毁取消事件监听
wheelRef.value.removeEventListener('scroll', handleScroll);
})
</script>
<style scoped lang="scss">
// 使用v-bind可以获取从props中获取的???px的值
$header-height: v-bind(headerHeight);
$nav-height: v-bind(navHeight);
$list-item-height: v-bind(itemHeight);
// 列表内容后面拼接上一段空白的文档, 让list内容可以scroll到更底部去!!!
@mixin listItemHeight() {
height: calc(100vh - v-bind(headerHeight) - v-bind(navHeight) - v-bind(itemHeight));
}
* {
margin: 0;
padding: 0;
}
.fixed-top-container {
display: flex;
flex-direction: column;
height: 100vh;
& .header {
height: $header-height;
width: 100%;
background-color: #ff5555;
}
& .fixed-top-nav {
display: flex;
width: 100%;
background-color: #f90;
& .box {
font-size: 14px;
height: $nav-height;
line-height: $nav-height;
color: #333;
flex: 1 1 0%;
}
}
& .fixed-top-list {
flex: 1;
list-style: none;
font-size: 16px;
line-height: $list-item-height;
overflow: auto;
&>li {
background-color: green;
}
& li {
box-sizing: border-box;
}
& .list-item {
width: 100%;
height: $list-item-height;
line-height: $list-item-height;
font-size: 16px;
border-bottom: 1px solid #333;
background-color: #fff;
}
& .rest-list-blank {
@include listItemHeight();
}
}
.anchor-conatiner {
position: fixed;
top: 10%;
right: 10px;
& li {
font-size: 14px;
&.current {
color: red;
}
&.light {
color: green;
}
}
}
}
</style>
父页面使用:
<PhoneListScroll :header-height="'200px'" :nav-height="'30px'" :item-height="'40px'"/>
注意:其实这种高度什么的在子页面写就行了, 这么写不过是增加父子通信的示例罢了
6. 使用css3的transform的平移提现滑动效果
6-1 做外层的标签定位必须是fixed(为了滑动时页面不抖动)
.fixed-top-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
6-2 点击achorlink时
相关anchorlink高亮, 页面滑动到指定位置(类似a标签的锚点效果)
function anchorClick(index) {
const topHeight = headerOffsetHeight.value + navOffsetHeight.value;
const sumListLength0 = index > 0 ? listData1.value[index - 1].sumListLength : 0
wheelTransform.value = `translateY(${0 - listLineHeight.value * (index + sumListLength0) + topHeight}px)`
wheelTransition.value = "transform 700ms cubic-bezier(0.19, 1, 0.22, 1)"
nextTick(() => {
currentIndex.value = index;
});
}
6-3 滑动事件
listenerTouchStart
1) 在此事件中使用finger记录下滑动开始手指的数据
// 开始滑动
function listenerTouchStart(ev) {
ev.stopPropagation();
isInertial.value = false; // 初始状态没有惯性滚动
finger.startY = ev.targetTouches[0].pageY; // 获取手指开始点击的位置
finger.prevMove = finger.currentMove; // 保存手指上一次的滑动距离
finger.startTime = Date.now(); // 保存手指开始滑动的时间
}
listenerTouchMove
1) 使用finger记录现在的手指数据, 并计算现在和开始滑动时2次手指位置的纵向直线距离;
2) 使用transform: translateY(xxx), 让页面有类似滚动的效果
// 滑动过程中
function listenerTouchMove(ev) {
ev.stopPropagation();
// startY: 开始滑动的touch目标的pageY: ev.targetTouches[0].pageY减去
const nowStartY = ev.targetTouches[0].pageY; // 获取当前手指的位置
// finger.startY - nowStart为此次滑动的距离, 再加上上一次滑动的距离finger.prevMove, 路程总长: (finger.startY - nowStartY) + finger.prevMove
finger.currentMove = finger.startY - nowStartY + finger.prevMove;
let wheelDom = _.get(wheelRef, "value");
if (wheelDom) {
wheelTransform.value = `translateY(${finger.currentMove}px)`;
}
}
listenerTouchEnd
1) 使用finger记录结束的手指数据, 并计算结束时和上次的2次手指位置的纵向直线距离;
2) 让页面再惯性滑动一段时间, 让动画看起来不要那么死板, 因为显示器普遍都是60帧, 所以浏览器大概的刷新频率是1000/60秒
3) 使用惯性滑动函数inertia, 并整理inertia函数需要的参数传入, 再使用animate这个防抖函数优化一下
// 滑动结束
function listenerTouchEnd(ev) {
ev.stopPropagation();
const _endY = ev.changedTouches[0].pageY; // 获取结束时手指的位置
const _entTime = Date.now(); // 获取结束时间
const v = (finger.startY - _endY) / (_entTime - finger.startTime); // 滚动完毕求移动速度 v = (s初始-s结束) / t
const absV = Math.abs(v);
isInertial.value = true; // 最好惯性滚动,才不会死板
animate.start(() => inertia({ start: absV, position: Math.round(absV / v), target: 0 })); // Math.round(absV / v)=>+/-1
}
6-4 惯性滑动函数inertia
// 惯性滑动函数
function inertia({ start, position, target }) {
if (start <= target || !isInertial.value) {
animate.stop();
finger.prevMove = finger.currentMove;
return;
}
// 这段时间走的位移 S = (+/-)vt + 1/2at^2 + s1;
const move =
position * start * FRESH_TIME +
0.5 * a * Math.pow(FRESH_TIME, 2) +
finger.currentMove;
const newStart = position * start + a * FRESH_TIME; // 根据求末速度公式: v末 = (+/-)v初 + at
let actualMove = move; // 最后的滚动距离
let wheelDom = _.get(wheelRef, "value");
// 已经到达目标
// 当滑到第一个或者最后一个picker数据的时候, 不要滑出边界
const minIdx = 0;
const maxIdx = sumIndex;
const topHeight = headerOffsetHeight.value + navOffsetHeight.value;
const lineHeight = listLineHeight.value;
if (Math.abs(newStart) >= Math.abs(target)) {
if (move > topHeight) {
// 让滚动在文字区域内,超出区域的滚回到边缘的第一个文本处
actualMove = topHeight + minIdx * lineHeight;
} else if (Math.round((Math.abs(move) + topHeight) / lineHeight) >= maxIdx) {
// 让滚动在文字区域内,超出区域的滚回到边缘的最后一个文本处
actualMove = position * (maxIdx - Math.ceil(topHeight / lineHeight)) * lineHeight;
}
if (wheelDom) wheelTransition.value =
"transform 700ms cubic-bezier(0.19, 1, 0.22, 1)";
}
// finger.currentMove赋值是为了判断anchorlink的高亮
finger.currentMove = actualMove;
handleScroll({ scrollTop: Math.abs(finger.currentMove - topHeight) })
if (wheelDom) wheelTransform.value = `translateY(${finger.currentMove}px)`;
// animate.stop();
// animate.start(() => inertia.bind({ start: newStart, position, target }));
}
animate.js 主要是防抖, 优化性能
export default class Animate {
constructor() {
this.timer = null;
}
start = (fn) => {
if (!fn) {
throw new Error('需要执行函数');
}
if (this.timer) {
this.stop();
}
this.timer = requestAnimationFrame(fn);
};
stop = () => {
if (!this.timer) {
return;
}
cancelAnimationFrame(this.timer);
this.timer = null;
};
}
// 或者
// function Animate () {
// return this.timer;
// }
// Animate.prototype.start = function (fn) {
// if (!fn) {
// throw new Error('需要执行函数');
// }
// if (this.timer) {
// this.stop();
// }
// this.timer = requestAnimationFrame(fn);
// }
// Animate.prototype.stop = function () {
// if (!this.timer) {
// return;
// }
// cancelAnimationFrame(this.timer);
// this.timer = null;
// }
// export default Animate;
6-5 滑动真正结束(包括惯性滑动), 判断anchorlink高亮
function handleScroll({ scrollTop }) {
const windowClientHeight = scrollWrapperRef.value.clientHeight;
const windowScrollHeight = scrollWrapperRef.value.scrollHeight;
// 如果滚动元素的scrollTop比header元素的高度+offsetTop还大, 就让nav部分悬停在顶部!!!
if (scrollTop === 0) currentIndex.value = 0;
else {
// 因为只有list内容部分在滚动, 根据scrollTop判断滚动到哪个区域, 进而判断anchorlink的高亮
const gap = scrollTop;
const idx = _.findIndex(listData1.value, ee => {
const a = _.get(ee, 'listItemsHeightArrs');
if (gap >= a[0] && gap < a[1]) {
return ee
}
})
currentIndex.value = idx;
}
// 滑到底部
if (windowClientHeight + scrollTop === windowScrollHeight) {
currentIndex.value = listData1.value.length - 1;
}
}
6-6 使用css3动画模拟scroll事件的核心代码:
vue文件:
<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" :style="{ transition: wheelTransition, transform: wheelTransform }">
<li v-for="(item) 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[0] }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, Ref } from 'vue';
import _ from 'lodash';
import Animate from '../../utils/animate';
const a = -0.03; // 加速度
const REM_UNIT = 37.5; // 默认375px屏幕宽度下的, html的字体大小为37.5px;
const LINE_HEIGHT = 40; // list内容文字行高(默认375px屏幕宽度下!);
const listLineHeight = ref(LINE_HEIGHT);
const FRESH_TIME = 1000 / 60; // 动画帧刷新的频率大概是1000 / 60
const isInertial = ref(false);
const headerRef = ref('headerRef') as Ref;
const navRef = ref('navRef') as Ref;
const headerOffsetHeight = ref(0);
const navOffsetHeight = ref(0);
const wheelRef = ref('wheelRef') as Ref;
const wheelTransition = ref('');
const wheelTransform = ref('');
const scrollWrapperRef = ref('scrollWrapperRef') as Ref;
// 存储手指滑动的数据
const finger = reactive({
startY: 0,
startTime: 0, // 开始滑动时间(单位:毫秒)
currentMove: 0,
prevMove: 0,
});
const currentIndex = ref(0);
const animate = new Animate();
// 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 sumHeight = 0;
let sumIndex = 0;
const listData1 = ref<any[]>([]);
const getData1 = (listLineHeight) => {
sumHeight = 0;
sumIndex = 0;
let sumIndex1 = 0
return 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 sumHeight1 = sumHeight
sumHeight += listLineHeight.value + listLineHeight.value * list.length;
sumIndex += list.length + 1;
sumIndex1 += list.length
if (index === arr.length - 1) sumIndex -= 1;
return {
listItemsHeightArrs: [sumHeight1, sumHeight], // 前一个标题内的list内容累计高度, 当前标题内的内容累计高度
name: arr[index] + '-累计高度为:' + JSON.stringify([sumHeight1, sumHeight]),
list,
sumListLength: sumIndex1,
}
})
};
function initPage() {
const currentHtmlFontSize: any = document.documentElement.style.fontSize.replace('px', '');
// 列表li标签是根据css计算公式判断的, 也可以直接通过dom得到相关的cliengtHeight数据!!!
listLineHeight.value = Number(currentHtmlFontSize * LINE_HEIGHT/ REM_UNIT);
listData1.value = getData1(listLineHeight);
headerOffsetHeight.value = headerRef.value.offsetHeight;
navOffsetHeight.value = navRef.value.offsetHeight;
}
// mock数据-----------------------end--------------
function anchorClick(index) {
const topHeight = headerOffsetHeight.value + navOffsetHeight.value;
const sumListLength0 = index > 0 ? listData1.value[index - 1].sumListLength : 0
wheelTransform.value = `translateY(${0 - listLineHeight.value * (index + sumListLength0) + topHeight}px)`
wheelTransition.value = "transform 700ms cubic-bezier(0.19, 1, 0.22, 1)"
nextTick(() => {
currentIndex.value = index;
});
}
//滚动的函数
function handleScroll({ scrollTop }) {
const windowClientHeight = scrollWrapperRef.value.clientHeight;
const windowScrollHeight = scrollWrapperRef.value.scrollHeight;
// 如果滚动元素的scrollTop比header元素的高度+offsetTop还大, 就让nav部分悬停在顶部!!!
if (scrollTop === 0) currentIndex.value = 0;
else {
// 因为只有list内容部分在滚动, 根据scrollTop判断滚动到哪个区域, 进而判断anchorlink的高亮
const gap = scrollTop;
const idx = _.findIndex(listData1.value, ee => {
const a = _.get(ee, 'listItemsHeightArrs');
if (gap >= a[0] && gap < a[1]) {
return ee
}
})
currentIndex.value = idx;
}
// 滑到底部
if (windowClientHeight + scrollTop === windowScrollHeight) {
currentIndex.value = listData1.value.length - 1;
}
}
// 开始滑动
function listenerTouchStart(ev) {
ev.stopPropagation();
isInertial.value = false; // 初始状态没有惯性滚动
finger.startY = ev.targetTouches[0].pageY; // 获取手指开始点击的位置
finger.prevMove = finger.currentMove; // 保存手指上一次的滑动距离
finger.startTime = Date.now(); // 保存手指开始滑动的时间
}
// 滑动过程中
function listenerTouchMove(ev) {
ev.stopPropagation();
// startY: 开始滑动的touch目标的pageY: ev.targetTouches[0].pageY减去
const nowStartY = ev.targetTouches[0].pageY; // 获取当前手指的位置
// finger.startY - nowStart为此次滑动的距离, 再加上上一次滑动的距离finger.prevMove, 路程总长: (finger.startY - nowStartY) + finger.prevMove
finger.currentMove = finger.startY - nowStartY + finger.prevMove;
let wheelDom = _.get(wheelRef, "value");
if (wheelDom) {
wheelTransform.value = `translateY(${finger.currentMove}px)`;
}
}
// 滑动结束
function listenerTouchEnd(ev) {
ev.stopPropagation();
const _endY = ev.changedTouches[0].pageY; // 获取结束时手指的位置
const _entTime = Date.now(); // 获取结束时间
const v = (finger.startY - _endY) / (_entTime - finger.startTime); // 滚动完毕求移动速度 v = (s初始-s结束) / t
const absV = Math.abs(v);
isInertial.value = true; // 最好惯性滚动,才不会死板
animate.start(() => inertia({ start: absV, position: Math.round(absV / v), target: 0 })); // Math.round(absV / v)=>+/-1
}
function inertia({ start, position, target }) {
if (start <= target || !isInertial.value) {
animate.stop();
finger.prevMove = finger.currentMove;
return;
}
// 这段时间走的位移 S = (+/-)vt + 1/2at^2 + s1;
const move =
position * start * FRESH_TIME +
0.5 * a * Math.pow(FRESH_TIME, 2) +
finger.currentMove;
const newStart = position * start + a * FRESH_TIME; // 根据求末速度公式: v末 = (+/-)v初 + at
let actualMove = move; // 最后的滚动距离
let wheelDom = _.get(wheelRef, "value");
// 已经到达目标
// 当滑到第一个或者最后一个picker数据的时候, 不要滑出边界
const minIdx = 0;
const maxIdx = sumIndex;
const topHeight = headerOffsetHeight.value + navOffsetHeight.value;
const lineHeight = listLineHeight.value;
if (Math.abs(newStart) >= Math.abs(target)) {
if (move > topHeight) {
// 让滚动在文字区域内,超出区域的滚回到边缘的第一个文本处
actualMove = topHeight + minIdx * lineHeight;
} else if (Math.round((Math.abs(move) + topHeight) / lineHeight) >= maxIdx) {
// 让滚动在文字区域内,超出区域的滚回到边缘的最后一个文本处
actualMove = position * (maxIdx - Math.ceil(topHeight / lineHeight)) * lineHeight;
}
if (wheelDom) wheelTransition.value =
"transform 700ms cubic-bezier(0.19, 1, 0.22, 1)";
}
// finger.currentMove赋值是为了判断anchorlink的高亮
finger.currentMove = actualMove;
handleScroll({ scrollTop: Math.abs(finger.currentMove - topHeight) })
if (wheelDom) wheelTransform.value = `translateY(${finger.currentMove}px)`;
// animate.stop();
// animate.start(() => inertia.bind({ start: newStart, position, target }));
}
onMounted(() => {
nextTick(() => {
initPage();
// 绑定相关事件
const dom = wheelRef.value
dom.addEventListener("touchstart", listenerTouchStart, false);
dom.addEventListener("touchmove", listenerTouchMove, false);
dom.addEventListener("touchend", listenerTouchEnd, false);
});
})
onBeforeUnmount(() => { // 页面即将销毁取消事件监听
const dom = wheelRef.value
dom.removeEventListener("touchstart", listenerTouchStart, false);
dom.removeEventListener("touchmove", listenerTouchMove, false);
dom.removeEventListener("touchend", listenerTouchEnd, false);
})
</script>
<style scoped lang="scss">
$pxToRemItem: 37.5px;
@function pxToRem($px) {
$item: $pxToRemItem;
@return $px/$item+rem;
}
* {
margin: 0;
padding: 0;
}
html,
body {
overflow: hidden;
}
.fixed-top-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
& .header {
position: relative;
height: 200px;
width: 100%;
background-color: #ff5555;
z-index: 9;
}
& .fixed-top-nav {
position: relative;
display: flex;
width: 100%;
background-color: #f90;
z-index: 9;
& .box {
font-size: 14px;
height: 30px;
line-height: 30px;
color: #333;
flex: 1 1 0%;
}
}
& .fixed-top-list {
position: absolute;
top: 0;
left: 0;
width: 100%;
list-style: none;
font-size: 16px;
line-height: pxToRem(40px);
z-index: 0;
&>li {
background-color: green;
}
& li {
box-sizing: border-box;
}
& .list-item {
width: 100%;
height: pxToRem(40px);
line-height: pxToRem(40px);
font-size: 16px;
border-bottom: 1px solid #333;
background-color: #fff;
}
}
.anchor-conatiner {
position: fixed;
top: 10%;
right: 10px;
z-index: 10;
& li {
font-size: 14px;
&.current {
color: red;
}
&.light {
color: green;
}
}
}
}
</style>
rem.js
// 设置 rem 函数
const defaultHtmlFontSize = 37.5;
export const setRem = () => {
// 375 默认大小37.5px; 375px = 120rem ;每个元素px基础上/37.5
const designScreenWidth = 375;
const scale = designScreenWidth / defaultHtmlFontSize;
const htmlWidth =
document.documentElement.clientWidth || document.body.clientWidth;
// 得到html的Dom元素
const htmlDom = document.getElementsByTagName("html")[0];
// 设置根元素字体大小
htmlDom.style.fontSize = htmlWidth / scale + "px";
};
export const initRem = () => {
// 初始化
setRem();
// 改变窗口大小时重新设置 rem
window.onresize = function () {
setRem();
};
};
main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { initRem } from "./utils/rem";
import * as Vue from "vue";
import store from './store';
const app = createApp(App);
app.use(store);
app.mount("#app");
initRem()
关于css3模拟scroll效果, 还有模仿3d滚轮的文章有提到, 有兴趣可以看一下:
react例子:
react写一个简单的3d滚轮picker组件-CSDN博客
vue例子:文章来源:https://www.toymoban.com/news/detail-738039.html
【精选】使用vue写一个picker插件,使用3d滚轮的原理_vue picker-CSDN博客文章来源地址https://www.toymoban.com/news/detail-738039.html
到了这里,关于vue3简单写导航anchor示例(支持点击高亮和滚动判断高亮)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!