Vue实现电梯样式锚点导航效果流程详解

 更新时间:2023年05月16日 09:08:07   作者:码上编程  
这篇文章主要介绍了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电梯锚点导航内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Vue中的计算属性介绍

    Vue中的计算属性介绍

    这篇文章主要介绍了Vue中的计算属性,模板内的表达式,用于简单运算,但是模板中放入太多的逻辑会让模板过重且难以维护,更多具体内容一起进入下面文章学习吧,需要的朋友也可以参考一下
    2021-12-12
  • Vue中关于this指向的问题示例详解

    Vue中关于this指向的问题示例详解

    在Vue中,方法体里用this调用vue实例的数据,有时会指向window,导致调用失败报错,这篇文章主要介绍了Vue中关于this指向的问题 ,需要的朋友可以参考下
    2022-07-07
  • 关于Vue中使用alibaba的iconfont矢量图标的问题

    关于Vue中使用alibaba的iconfont矢量图标的问题

    这篇文章主要介绍了Vue使用alibaba的iconfont矢量图标的问题,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • webstorm添加*.vue文件支持

    webstorm添加*.vue文件支持

    这篇文章主要介绍了webstorm添加*.vue文件支持,webstorm很多的插件内置,不用安装插件,下面尝试用vue和es6做项目,有兴趣的可以了解一下
    2018-05-05
  • Vuejs第十篇之vuejs父子组件通信

    Vuejs第十篇之vuejs父子组件通信

    这篇文章主要介绍了Vuejs第十篇之vuejs父子组件通信的相关资料,本文介绍的非常详细,具有参考借鉴价值,需要的朋友可以参考下
    2016-09-09
  • Vuex简单入门

    Vuex简单入门

    本篇文章主要介绍了初步认识理解Vuex,Vuex就是在一个项目中,提供唯一的管理数据源的仓库,有兴趣的可以了解一下
    2017-04-04
  • Vue中使用webpack别名的方法实例详解

    Vue中使用webpack别名的方法实例详解

    本文通过实例给大家介绍了Vue中使用webpack别名的方法,非常不错,具体一定的参考借鉴价值,需要的朋友可以参考下
    2018-06-06
  • VUE使用echarts 5.0以上版本渲染器未导入错误问题

    VUE使用echarts 5.0以上版本渲染器未导入错误问题

    这篇文章主要介绍了VUE使用echarts 5.0以上版本渲染器未导入错误问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • vue3自定义dialog、modal组件的方法

    vue3自定义dialog、modal组件的方法

    这篇文章主要介绍了vue3自定义dialog、modal组件的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • vue中对时间戳的处理方式

    vue中对时间戳的处理方式

    这篇文章主要介绍了vue中对时间戳的处理方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06

最新评论