Vue實(shí)現(xiàn)電梯樣式錨點(diǎn)導(dǎo)航效果流程詳解
1、目標(biāo)效果
最近喝了不少的咖啡、奶茶,有一個(gè)效果我倒是挺好奇怎么實(shí)現(xiàn)的:
(1)點(diǎn)擊左側(cè)分類菜單,右側(cè)滾動到該分類區(qū)域
(2)右側(cè)滑動屏幕,左側(cè)顯示當(dāng)前所處的分類區(qū)域
這種功能會出現(xiàn)在商城項(xiàng)目中或者分類數(shù)量較多的項(xiàng)目中,專業(yè)名稱稱電梯導(dǎo)航
目標(biāo)效果:
(1)點(diǎn)擊左側(cè)的分類,右側(cè)滑動到指定區(qū)域
(2)滾動右側(cè)區(qū)域,左邊分類顯示當(dāng)前所處的分類區(qū)域
2、原理
(1)這要用到原生js關(guān)于偏移量和位置相關(guān)的api,這些api建立在你的布局是定位的基礎(chǔ)上,父親相對定位,左邊分類和右邊商品都是絕對定位
(2)左邊分類要與右側(cè)商品模塊數(shù)量一一相等(數(shù)量和位置都要對應(yīng)相等),否則實(shí)現(xiàn)不了電梯導(dǎo)航效果
(3)點(diǎn)擊左側(cè)分類,右側(cè)跳轉(zhuǎn)到對應(yīng)模塊;這用到了window.scrollTo(水平方向距離,豎直方向距離)
(4)右側(cè)滑動,左側(cè)發(fā)生相應(yīng)的變化,這要用到滾動事件,vue中使用滾動事件需要再onMounted()生命周期注冊一下滾動事件
onMounted(() => { window.addEventListener('scroll', handleScroll); })
(5)如何判斷滾動到什么程度左側(cè)才顯示對應(yīng)的模塊?
- dom.offsetTop:每個(gè)dom元素有該屬性,表示距離頂部窗口的距離
- document.documentElement.scrollTop:表示頁面滾動的距離
- document.documentElement.scrollTop >=dom.offsetTop:顯示對應(yīng)的模塊,可以通過遍歷商品模塊數(shù)組,拿到對應(yīng)的索引,然后設(shè)置左邊分類對應(yīng)的dom為激活狀態(tài)
(6) 出現(xiàn)一個(gè)問題:window.scrollTo()將模塊滾動至某一位置 與頁面滾動事件 發(fā)生了沖突,此時(shí)可以添加一個(gè)互斥變量isLock,等window.scrollTo()滾動結(jié)束之后,再放開鎖
// 獲取選中的dom元素 const typeItemDom = shop.value[val] // 開鎖 isLock.value = true // 第一個(gè)參數(shù)為水平方向,第二個(gè)參數(shù)為縱軸方向 window.scrollTo(0, typeItemDom.offsetTop) setTimeout(() => { //關(guān)鎖 isLock.value = false }, 0)
(7)為什么放開鎖要在setTimeout里面?根據(jù)js事件循環(huán)機(jī)制,同步任務(wù)(主線程代碼、new Promise里面的代碼)執(zhí)行速度快于異步任務(wù)(setTimeout、setInterval、ajax、promise.then里面的任務(wù)),這樣才能確保鎖是在window.scrollTo() 執(zhí)行完畢之后才打開的
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', '爆款套餐', '大師咖啡', '小黑杯', '中國茶咖', '生椰家族', '厚乳拿鐵', '絲絨拿鐵', '生酪拿鐵', '經(jīng)典拿鐵', ]) // 商品 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: '經(jīng)典拿鐵', data: [ { name: '拿鐵', type: '咖啡', desc: '咖啡' }, { name: '隕石拿鐵', type: '咖啡', desc: '咖啡' }, { name: '焦糖拿鐵', type: '咖啡', desc: '咖啡' }, { name: '生椰拿鐵', type: '咖啡', desc: '咖啡' }, { name: '美式', type: '咖啡', desc: '咖啡' } ] }, ]) // 獲取右側(cè)商品的ref實(shí)例 let shop = ref(null) // 用來表示當(dāng)前選中處于激活狀態(tài)的分類的索引 let currentIndex = ref(0) // 切換類型 const changeType = val => { currentIndex.value = val // 獲取選中的dom元素 const typeItemDom = shop.value[val] // 開鎖 isLock.value = true // 第一個(gè)參數(shù)為水平方向,第二個(gè)參數(shù)為縱軸方向 window.scrollTo(0, typeItemDom.offsetTop) setTimeout(() => { //關(guān)鎖 isLock.value = false }, 0) } // 監(jiān)聽頁面滾動 const handleScroll = () => { // 鎖關(guān)了滾動事件才有效 if (!isLock.value) { types.value.forEach((item, index) => { // console.dir(shop.value[index]); const shopItemDom = shop.value[index] // 每個(gè)模塊距離頂部的距離 const offsetTop = shopItemDom.offsetTop // 頁面滾動的距離 const scrollTop = document.documentElement.scrollTop if (scrollTop >= offsetTop) { // 給左邊分類設(shè)置激活的效果 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>
到此這篇關(guān)于Vue實(shí)現(xiàn)電梯樣式錨點(diǎn)導(dǎo)航效果流程詳解的文章就介紹到這了,更多相關(guān)Vue電梯錨點(diǎn)導(dǎo)航內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
unplugin-auto-import的配置以及eslint報(bào)錯(cuò)解決詳解
unplugin-auto-import?解決了vue3-hook、vue-router、useVue等多個(gè)插件的自動導(dǎo)入,也支持自定義插件的自動導(dǎo)入,是一個(gè)功能強(qiáng)大的typescript支持工具,這篇文章主要給大家介紹了關(guān)于unplugin-auto-import的配置以及eslint報(bào)錯(cuò)解決的相關(guān)資料,需要的朋友可以參考下2022-08-08關(guān)于vue的語法規(guī)則檢測報(bào)錯(cuò)問題的解決
在配置路有的時(shí)候,陸續(xù)出現(xiàn)了各種報(bào)錯(cuò)其中最多的是一些寫法,例如空格,縮進(jìn),各種括號,這篇文章主要介紹了關(guān)于vue的語法規(guī)則檢測報(bào)錯(cuò)問題的解決,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-05-05vue自定義指令的創(chuàng)建和使用方法實(shí)例分析
這篇文章主要介紹了vue自定義指令的創(chuàng)建和使用方法,結(jié)合完整實(shí)例形式分析了vue.js創(chuàng)建及使用自定義指令的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2018-12-12Vue開發(fā)過程中遇到的疑惑知識點(diǎn)總結(jié)
vue是法語中視圖的意思,Vue.js是一個(gè)輕巧、高性能、可組件化的MVVM庫,同時(shí)擁有非常容易上手的API。下面這篇文章主要給大家總結(jié)了Vue在開發(fā)過程中遇到的疑惑知識點(diǎn),有需要的朋友可以參考借鑒,下面來一起看看吧。2017-01-01