vue3简单写导航anchor示例(支持点击高亮和滚动判断高亮)

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 = 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;}, 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 = 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;}, 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 = 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;}, 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>

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 : 0wheelTransform.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.prevMovefinger.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结束) / tconst 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初 + atlet 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 = 0return 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 = sumHeightsumHeight += listLineHeight.value + listLineHeight.value * list.length;sumIndex += list.length + 1;sumIndex1 += list.lengthif (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 : 0wheelTransform.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.prevMovefinger.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结束) / tconst 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初 + atlet 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.valuedom.addEventListener("touchstart", listenerTouchStart, false);dom.addEventListener("touchmove", listenerTouchMove, false);dom.addEventListener("touchend", listenerTouchEnd, false);});
})onBeforeUnmount(() => { // 页面即将销毁取消事件监听const dom = wheelRef.valuedom.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.5const 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();// 改变窗口大小时重新设置 remwindow.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例子:

【精选】使用vue写一个picker插件,使用3d滚轮的原理_vue picker-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/128175.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序 人工智能志愿者服务活动报名系统uniAPP+vue

基于java语言设计并实现了人工智能志愿者服务APP。该APP基于B/S即所谓浏览器/服务器模式&#xff0c;应用SpringBoot框架与HBuilder X技术&#xff0c;选择MySQL作为后台数据库。系统主要包括用户、志愿活动、活动报名、活动签到、服务职责、服务排行等功能模块。 本文首先介绍…

Zynq-Linux移植学习笔记之64- 国产ZYNQ在linux下配置国产5396芯片

1、背景介绍 复旦微ZYNQ通过SPI配置国产JEM5396&#xff0c;框图如下&#xff1a; 现在需要在linux下的应用程序内配置JEM5396的寄存器。其中FMQL和进口的XILINX ZYNQ类似&#xff0c;JEM5396和进口的BCM5396兼容。因此可以参考进口ZYNQ在linux下配置BCM5396过程。Zynq-Linux移…

云服务器 centos 部署 code-server 并配置 c/c++ 环境

将你的云服务器改为 centos 8 为什么要将云服务器的操作系统改成 centos 8 呢&#xff1f;原因就是 centos 7 里面的配置满足不了 code-server 的需求。如果你使用的是 centos 7 那么就需要你升级一些东西&#xff0c;这个过程比较麻烦。我在 centos 7 上面运行 code-server 的…

k8s、pod

Pod k8s中的port【端口&#xff1a;30000-32767】 port &#xff1a;为Service 在 cluster IP 上暴露的端口 targetPort&#xff1a;对应容器映射在 pod 端口上 nodePort&#xff1a;可以通过k8s 集群外部使用 node IP node port 访问Service containerPort&#xff1a;容…

velero备份k8s集群

流程图 velero备份原理 本地 Velero 客户端发送备份指令。Kubernetes 集群内就会创建一个 Backup 对象。BackupController 监测 Backup 对象并开始备份过程。BackupController 会向 API Server 查询相关数据。BackupController 将查询到的数据备份到远端的对象存储。 velero的…

osg三角带

案例1 #include <osg/Geode> #include <osg/Geometry> #include <osgDB/Registry> #include <osgDB/WriteFile> #include <osg/Notify> #include <osg/PrimitiveSet> #include <osgViewer/Viewer> #include <osgUtil/Optimizer&g…

【数据结构】希尔排序

文章目录 前言一、希尔排序的演示图例二、希尔排序&#xff1a;插入排序的优化版本☆三、核心算法思路四、算法思路步骤&#xff08;一&#xff09;预排序 gap>1&#xff08;二&#xff09;gap1 插入排序 完成排序收尾 五、码源详解&#xff08;1&#xff09;ShellSort1 ——…

Linux CentOS7.9安装OpenJDK17

Linux CentOS7.9安装OpenJDK17 一、OpenJDK下载 清华大学开源软件镜像站 国内的站点&#xff0c;下载速度贼快 二、上传解压 文件上传到服务器后&#xff0c;解压命令&#xff1a; tar -zxvf jdk-xxxx-linux-x64.tar.gz三、配置环境 export JAVA_HOME/home/local/java/j…

idea 配置checkstyle全过程

checkstyle是提高代码质量,检查代码规范的很好用的一款工具&#xff0c;本文简单介绍一下集成的步骤&#xff0c;并提供一份完整的checkstyle的代码规范格式文件&#xff0c;以及常见的格式问题的解决方法。 一&#xff0c;安装 打开idea的文件选项&#xff0c;选择设置&…

搜维尔科技:scalefit生物力学人体工学软件分析!

人体工程学分析 21加载参数和头像显示 识别(隐藏的)健康风险 根据DGUV交通灯进行生物反馈(DIN/ISO) 实时应力分析 三维空间可视化 静态/动态肩载 用左/右赋值加载输入 腰椎间盘压缩计算 距离和定时器显示 带有运动跟踪的化身/视频叠加 外骨骼与CAD工作站仿真 CSV原始…

【机器学习合集】模型设计之残差网络 ->(个人学习记录笔记)

文章目录 模型设计之残差网络1. 什么是残差结构1.1 网络加深遇到的优化问题1.2 short connect技术 2. 残差网络及有效性理解2.1 残差网络 3. 残差网络的发展3.1 密集残差网络3.2 更宽的残差网络(wide resnet)3.3 分组残差网络3.4 Dual Path Network3.5 加权残差网络3.6 预激活残…

WIN11如何固定文件夹查看方式

找一个文件夹&#xff0c;设置成自己需要的视图方式 文件夹选项>查看>应用到文件夹 缺点&#xff1a;所有相同类型文件夹都会使用此视图

数据结构——线性表①(顺序表)

一、线性表定义 线性表是一种数据结构&#xff0c;它是由n个具有相同数据类型的数据元素a1,a2,…,an组成的有限序列。 其中&#xff0c;除第一个元素a1外&#xff0c;每一个元素有且只有一个直接前驱元素&#xff0c;除了最后一个元素an外&#xff0c;每一个元素有且只有一个…

Redis(11)| 持久化AOF和RDB

一、AOF&#xff08;Append Only File&#xff09; Redis 每执行一条写操作命令&#xff0c;就把该命令以追加的方式写入到一个文件里&#xff0c;然后重启 Redis 的时候&#xff0c;先去读取这个文件里的命令&#xff0c;并且执行它。 注意&#xff1a;只会记录写操作命令&am…

微信小程序:两层循环的练习,两层循环显示循环图片大图(大图显示、多层循环)

效果 代码分析 外层循环 外层循环的框架 <view wx:for"{{info}}" wx:key"index"></view> wx:for"{{info}}"&#xff1a;这里wx:for指令用于指定要遍历的数据源&#xff0c;即info数组。当遍历开始时&#xff0c;会依次将数组中的每…

stable-diffusion 电商领域prompt测评集合

和GhostReivew一个思路&#xff0c;还是从比较好的图片或者是civitai上找一些热门的prompt&#xff0c;从小红书上也找到了不少的prompt&#xff0c;lexica.art上也有不少&#xff0c;主要是为了电商场景的一些测评&#xff1a; 小红书、civitai、Lexica、Liblib.ai、 depth o…

Leetcode—707.设计链表【中等】双链表的设计明天再写

2023每日刷题&#xff08;十七&#xff09; Leetcode—707.设计链表 设计单链表实现代码 typedef struct Node {int val;struct Node* next; } MyLinkedList;MyLinkedList* myLinkedListCreate() {MyLinkedList* mList (MyLinkedList *)malloc(sizeof(MyLinkedList));mList-&…

Wpf 使用 Prism 实战开发Day02

一.设计首页导航条 导航条的样式&#xff0c;主要是从Material DesignThemes UI 拷贝过来修改的,项目用了这个UI组件库&#xff0c;就看自己需要什么&#xff0c;就去拷过来使用&#xff0c;界面布局或其他组件使用&#xff0c;不做介绍。 直接下载源码&#xff0c;编译运行就可…

【进程控制⑥】:进程替换/exec*()系列接口

【进程控制⑥】&#xff1a;进程替换/ exec*(&#xff09;系列接口 一.进程替换原理二.替换特点1.独立性2.唯一性3.不变性4.不返回 三.程序替换应用【exec*系列系统调用】①execl&#xff1a;②execlp&#xff1a;③execv&#xff1a;④execle&#xff1a; 一.进程替换原理 我…

视频编码转换技巧:视频批量转码H264转H265,高效且顺畅

随着数字媒体的广泛应用&#xff0c;视频编码转换已成为一种普遍的需求。不同的视频格式和编码标准使得在不同设备上播放视频成为可能&#xff0c;同时也带来了兼容性和传输效率的问题。本文讲解引用云炫AI智剪使视频编码转换技巧&#xff0c;即批量将H264编码转换为H265编码&a…