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 を追加し、ウィンドウのスクロール後に解放することができます。 scrollTo() はロックを終了します

    // 获取选中的dom元素
    const typeItemDom = shop.value[val]

    // 开锁
    isLock.value = true

    // 第一个参数为水平方向,第二个参数为纵轴方向
    window.scrollTo(0, typeItemDom.offsetTop)

    setTimeout(() => {
        //关锁
        isLock.value = false
    }, 0)

(7) setTimeout でロックを解放する必要があるのはなぜですか? js イベント ループ メカニズムに従って、同期タスク (メイン スレッド コード、新しい Promise のコード) は、ロックが確実に ウィンドウ内にあるように非同期タスク (setTimeout、setInterval、ajax、promise.then のタスク)よりも高速に実行されます。実行後に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>

おすすめ

転載: blog.csdn.net/weixin_42375707/article/details/130669872