Vue实现电梯样式锚点导航效果流程详解
作者:码上编程
1、目标效果
最近喝了不少的咖啡、奶茶,有一个效果我倒是挺好奇怎么实现的:
(1)点击左侧分类菜单,右侧滚动到该分类区域
(2)右侧滑动屏幕,左侧显示当前所处的分类区域
这种功能会出现在商城项目中或者分类数量较多的项目中,专业名称称电梯导航
目标效果:
(1)点击左侧的分类,右侧滑动到指定区域
(2)滚动右侧区域,左边分类显示当前所处的分类区域
2、原理
(1)这要用到原生js关于偏移量和位置相关的api,这些api建立在你的布局是定位的基础上,父亲相对定位,左边分类和右边商品都是绝对定位
(2)左边分类要与右侧商品模块数量一一相等(数量和位置都要对应相等),否则实现不了电梯导航效果
(3)点击左侧分类,右侧跳转到对应模块;这用到了window.scrollTo(水平方向距离,竖直方向距离)
(4)右侧滑动,左侧发生相应的变化,这要用到滚动事件,vue中使用滚动事件需要再onMounted()生命周期注册一下滚动事件
onMounted(() => { window.addEventListener('scroll', handleScroll); })
(5)如何判断滚动到什么程度左侧才显示对应的模块?
- dom.offsetTop:每个dom元素有该属性,表示距离顶部窗口的距离
- document.documentElement.scrollTop:表示页面滚动的距离
- document.documentElement.scrollTop >=dom.offsetTop:显示对应的模块,可以通过遍历商品模块数组,拿到对应的索引,然后设置左边分类对应的dom为激活状态
(6) 出现一个问题:window.scrollTo()将模块滚动至某一位置 与页面滚动事件 发生了冲突,此时可以添加一个互斥变量isLock,等window.scrollTo()滚动结束之后,再放开锁
// 获取选中的dom元素 const typeItemDom = shop.value[val] // 开锁 isLock.value = true // 第一个参数为水平方向,第二个参数为纵轴方向 window.scrollTo(0, typeItemDom.offsetTop) setTimeout(() => { //关锁 isLock.value = false }, 0)
(7)为什么放开锁要在setTimeout里面?根据js事件循环机制,同步任务(主线程代码、new Promise里面的代码)执行速度快于异步任务(setTimeout、setInterval、ajax、promise.then里面的任务),这样才能确保锁是在window.scrollTo() 执行完毕之后才打开的
3、源代码
App.vue
<template> <div> <ClassifyByVue></ClassifyByVue> </div> </template> <script setup> // import ClassifyByJs from './components/ClassifyByJS.vue'; import ClassifyByVue from './components/ClassifyByVue.vue'; </script> <style> * { padding: 0; margin: 0; } </style>
ClassifyByVue.vue
<template> <div class="classify"> <div class="left"> <div class="item" :class="{ active: currentIndex == index }" v-for="(item, index) in types" @click="changeType(index)">{{ item }} </div> </div> <div class="right" @scroll="handleScroll"> <div v-for="(item, index) in shops" :key="index" ref="shop"> <div class="title">{{ item.category }}</div> <div class="item" v-for="(i, j) in item.data" :key="j"> <div class="photo"> <img src="/vite.svg" class="logo" alt="Vite logo" /> </div> <div class="info"> <div class="name">{{ i.name }}</div> <div class="type">{{ i.type }}</div> <div class="desc">{{ i.desc }}</div> <div class="buy">购买</div> </div> </div> </div> </div> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue' let isLock = ref(false) // 分类 let types = ref([ '人气Top', '爆款套餐', '大师咖啡', '小黑杯', '中国茶咖', '生椰家族', '厚乳拿铁', '丝绒拿铁', '生酪拿铁', '经典拿铁', ]) // 商品 let shops = ref([ { category: '人气Top', data: [ { name: '冰吸生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '摸鱼生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '茉莉花香拿铁', type: '咖啡', desc: '咖啡' }, { name: '丝绒拿铁', type: '咖啡', desc: '咖啡' }, { name: '小甘橘美式', type: '咖啡', desc: '咖啡' }, ] }, { category: '爆款套餐', data: [ { name: '2杯套餐', type: '咖啡', desc: '咖啡' }, { name: '3杯套餐', type: '咖啡', desc: '咖啡' }, { name: '4杯套餐', type: '咖啡', desc: '咖啡' }, { name: '5杯套餐', type: '咖啡', desc: '咖啡' }, { name: '不喝咖啡套餐', type: '咖啡', desc: '咖啡' }, { name: '必喝套餐', type: '咖啡', desc: '咖啡' }, ] }, { category: '大师咖啡', data: [ { name: '美式', type: '咖啡', desc: '咖啡' }, { name: '加浓美式', type: '咖啡', desc: '咖啡' }, { name: '橙C美式', type: '咖啡', desc: '咖啡' }, { name: '澳瑞白', type: '咖啡', desc: '咖啡' }, { name: '卡布奇诺', type: '咖啡', desc: '咖啡' }, { name: '玛奇朵', type: '咖啡', desc: '咖啡' }, ] }, { category: '小黑杯', data: [ { name: '云南小柑橘', type: '咖啡', desc: '咖啡' }, { name: '广东小柑橘', type: '咖啡', desc: '咖啡' }, { name: '广西小柑橘', type: '咖啡', desc: '咖啡' }, { name: '福建小柑橘', type: '咖啡', desc: '咖啡' }, { name: '湖南小柑橘', type: '咖啡', desc: '咖啡' }, { name: '江西小柑橘', type: '咖啡', desc: '咖啡' }, ] }, { category: '中国茶咖', data: [ { name: '碧螺知春拿铁', type: '咖啡', desc: '咖啡' }, { name: '茉莉花香拿铁', type: '咖啡', desc: '咖啡' }, { name: '菊花香拿铁', type: '咖啡', desc: '咖啡' }, { name: '梅花香拿铁', type: '咖啡', desc: '咖啡' }, { name: '兰花香拿铁', type: '咖啡', desc: '咖啡' }, { name: '玫瑰花香拿铁', type: '咖啡', desc: '咖啡' }, ] }, { category: '生椰家族', data: [ { name: '冰吸生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '摸鱼生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '椰云拿铁', type: '咖啡', desc: '咖啡' }, { name: '丝绒拿铁', type: '咖啡', desc: '咖啡' }, { name: '陨石拿铁', type: '咖啡', desc: '咖啡' }, ] }, { category: '厚乳拿铁', data: [ { name: '厚乳拿铁', type: '咖啡', desc: '咖啡' }, { name: '生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '茉莉花香拿铁', type: '咖啡', desc: '咖啡' }, { name: '椰云拿铁', type: '咖啡', desc: '咖啡' }, { name: '丝绒拿铁', type: '咖啡', desc: '咖啡' }, { name: '海盐拿铁', type: '咖啡', desc: '咖啡' }, ] }, { category: '丝绒拿铁', data: [ { name: '丝绒拿铁', type: '咖啡', desc: '咖啡' }, { name: '生椰丝绒拿铁', type: '咖啡', desc: '咖啡' }, { name: '黑糖丝绒拿铁', type: '咖啡', desc: '咖啡' }, { name: '椰云丝绒拿铁', type: '咖啡', desc: '咖啡' }, { name: '香草丝绒拿铁', type: '咖啡', desc: '咖啡' } ] }, { category: '生酪拿铁', data: [ { name: '生酪拿铁', type: '咖啡', desc: '咖啡' }, { name: '绿豆拿铁', type: '咖啡', desc: '咖啡' }, { name: '红豆拿铁', type: '咖啡', desc: '咖啡' }, { name: '黑豆拿铁', type: '咖啡', desc: '咖啡' }, { name: '黄豆拿铁', type: '咖啡', desc: '咖啡' } ] }, { category: '经典拿铁', data: [ { name: '拿铁', type: '咖啡', desc: '咖啡' }, { name: '陨石拿铁', type: '咖啡', desc: '咖啡' }, { name: '焦糖拿铁', type: '咖啡', desc: '咖啡' }, { name: '生椰拿铁', type: '咖啡', desc: '咖啡' }, { name: '美式', type: '咖啡', desc: '咖啡' } ] }, ]) // 获取右侧商品的ref实例 let shop = ref(null) // 用来表示当前选中处于激活状态的分类的索引 let currentIndex = ref(0) // 切换类型 const changeType = val => { currentIndex.value = val // 获取选中的dom元素 const typeItemDom = shop.value[val] // 开锁 isLock.value = true // 第一个参数为水平方向,第二个参数为纵轴方向 window.scrollTo(0, typeItemDom.offsetTop) setTimeout(() => { //关锁 isLock.value = false }, 0) } // 监听页面滚动 const handleScroll = () => { // 锁关了滚动事件才有效 if (!isLock.value) { types.value.forEach((item, index) => { // console.dir(shop.value[index]); const shopItemDom = shop.value[index] // 每个模块距离顶部的距离 const offsetTop = shopItemDom.offsetTop // 页面滚动的距离 const scrollTop = document.documentElement.scrollTop if (scrollTop >= offsetTop) { // 给左边分类设置激活的效果 currentIndex.value = index } }) } } onMounted(() => { window.addEventListener('scroll', handleScroll); }) onBeforeUnmount(() => { window.removeEventListener('scroll', handleScroll); }) </script> <style scoped lang="less"> .classify { display: flex; position: relative; .left { display: flex; flex-direction: column; align-items: center; position: fixed; left: 0; top: 0; bottom: 0; width: 92px; overflow-y: scroll; border-right: 1px solid #C1C2C4; .item { display: flex; justify-content: center; align-items: center; width: 67px; height: 29px; font-size: 15px; font-family: PingFang SC-Semibold, PingFang SC; font-weight: 600; color: #333333; } .active { color: #00B1FF; } .item:not(:last-child) { margin-bottom: 25px; } } .right { flex: 1; position: absolute; top: 0; right: 17px; overflow-y: scroll; .title { font-size: 18px; margin-bottom: 5px; } .item { display: flex; justify-content: space-between; margin-bottom: 17px; width: 246px; height: 73px; .photo { width: 58px; height: 58px; img { width: 100%; height: 100%; border-radius: 12px; border: 1px solid gray; } } .info { display: flex; flex-direction: column; position: relative; width: 171px; height: 73px; box-shadow: 0px 1px 0px 0px rgba(221, 221, 221, 1); .name { padding-left: 0; font-size: 17px; font-weight: 600; color: #333333; } .type, .desc { font-size: 14px; font-weight: 400; color: #999999; } .buy { display: flex; align-items: center; justify-content: center; position: absolute; right: 0; top: 17px; width: 67px; height: 29px; background: #E7E8EA; border-radius: 21px; font-size: 15px; font-family: PingFang SC-Semibold, PingFang SC; font-weight: 600; color: #05AFFA; } } } } } </style>
到此这篇关于Vue实现电梯样式锚点导航效果流程详解的文章就介绍到这了,更多相关Vue电梯锚点导航内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!