# Nuxt3 瀑布流由左至右排列+無限加載(含搜尋功能) ## HTML結構 ```= <template> <div> <section class="section_container"> <template v-if="Loading"> Loading... </template> <template v-else> <div class="top_block"> <div class="search_box"> <input type="text" aria-label="圖片搜尋" placeholder="圖片搜尋" v-model.trim="keyword" aria-labelledby="圖片搜尋"> <div class="search_icon"> <svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10.3112 17.2809C14.2386 17.2809 17.4223 13.9497 17.4223 9.84044C17.4223 5.73121 14.2386 2.40001 10.3112 2.40001C6.38392 2.40001 3.2002 5.73121 3.2002 9.84044C3.2002 13.9497 6.38392 17.2809 10.3112 17.2809Z" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20.8002 20.8L15.2002 15.2" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </div> </div> </div> <template v-if="imgData.length > 0"> <ul class="columns_list_block"> <template v-for="(item,index) in imgData"> <li class="item"> <img :src="itme.url" alt="item.title"> </li> </template> </ul> <div ref="lastItemRef"></div> </template> <template v-else> <div> <p>當前無資料,詳請請洽相關人員</p> </div> </template> </template> </section> </div> </template> ``` ## JS部分 ```= <script setup> import { ref, onMounted, watch, onBeforeUnmount, nextTick} from 'vue' // 變更meta的title const changeMeta = () =>{ useHead({ title: '相片集', }) } changeMeta(); const Loading = ref(true) // route const route = await useRoute() const currentSearch = ref(route.query.search) // 搜尋 const keyword = ref('') if(currentSearch.value != undefined){ keyword.value = currentSearch.value } else { keyword.value = '' } const router = useRouter() watch(keyword, () =>{ currentPage.value = 1 router.push({query: { search: keyword.value }}) }) // data變數設置 const imgList = ref() //後端response的資料 const currentPage = ref(1) //當前筆數 const isNext = ref(false) //是否還有下一筆數 const lastItemRef = ref(null) //判斷是否為最後一筆的div const newData = ref([]) //新取得response的資料 const imgData = ref([]) const limit = ref(8) // 每次發送所要請求的筆數 // api串接 import Image_Api from '@/assets/js/api/get_img.js' const imgAPI = async(page) =>{ try { if(keyword.value == undefined || keyword.value == ''){ imgList.value = await Image_Api.getImgs(limit.value, page) } else { imgList.value = await Image_Api.getSearch(limit.value, page, currentSearch.value) } imgData.value = imgList.value.data if(imgData.value.length == limit.value){ isNext.value = true } } catch (error) { console.log('response失誤', error); } } // 滾輪事件 const handleScroll = () => { // 取得當前捲軸距離最上方的距離 const scrollY = window.scrollY; // 取得取視窗的高度 const windowHeight = window.innerHeight; // 依靠 API 所得到的response判斷當前資料庫是否有資料存在 if(articleData.value.length !== 0){ // 判斷lastItemRef這個元素是否存在 if (lastItemRef.value) { //使用getBoundingClientRect()方法取得lastItemRef元素的大小及相對於視口的位置的信息 const lastItemRect = lastItemRef.value.getBoundingClientRect(); // 當 scrollY(當前捲軸距離最上方的距離)+ windowHeight(視窗的高度) 大於等於 lastItemRect.top(lastItemRect距離視窗上方位置) // 判定為滾至最後一筆 並且 isNext 需為 true 才重新發送請求,取得下一筆資料更新至當前data陣列中 if (scrollY + windowHeight >= lastItemRect.top && isNext.value === true) { isNext.value = false loadMoreData(); } } } }; const loadMoreData = async () => { currentPage.value = currentPage.value + 1; try { if(keyword.value == undefined || keyword.value == ''){ const response = await Image_Api.getArticles(limit.value, currentPage.value); newData.value = response.data; } else { const response = await Image_Api.getSearch(limit.value, currentPage.value, keyword.value) newData.value = response.data; } // 判定response是否有得到新的資料,有才會將新得到的資料新增進入現有的data陣列中 if(newData.value.length > 0){ articleData.value = [...articleData.value, ...newData.value]; // 使用nextTick等待Vue進行DOM更新然後執行回調函數 // 因为 cascadeDisplay 函數需要更多時間来準備或處理,將回調傳遞給 requestAnimationFrame使回調將在瀏覽器的下一個繪製週期才做執行 nextTick(() => { requestAnimationFrame(() => { cascadeDisplay(); }); }); //判斷新的Data是否與請求數量相同,相同代表可能會有下一筆資料 if(newData.value.length == limit.value){ isNext.value = true; } } else { // 若新的Data小於請求數量判定為無資料則isNext設定為false,關閉下次滾至最下方時發送請求 isNext.value = false; } } catch (error) { console.log('response失誤', error) } }; // JS瀑布流排版 const cascadeDisplay = async() =>{ try { // 間距設定 let gap = 20 // 瀑布流所要排版的容器 let articleContainer = document.querySelector('.columns_list_block') // 所要排版的data let items = document.querySelectorAll('.columns_list_block .item') // 取得當前所要排版的容器寬度 let articleContainerWidth = articleContainer.offsetWidth // 取得data的寬度 let itemsWidth = items[0].offsetWidth // 計算當前每一排所能擺放的數量 let columnsCount = parseInt(articleContainerWidth / (itemsWidth + 20)) // 計算置中所需要的兩側距離 // (容器寬度 - 總item寬度 - 總間距)/2 let leftPadding = (articleContainerWidth - (itemsWidth * columnsCount) - (gap * (columnsCount - 1))) / 2 // 用來紀錄第一列item的高度 let fistRowPhotosHeightArray = [] for (let i = 0; i < items.length; i++) { // 放上第一列的data if (i < columnsCount) { items[i].style.top = 0 items[i].style.left = ((itemsWidth + gap) * i + leftPadding) + 'px' // 紀錄第一列的data高 fistRowPhotosHeightArray.push(items[i].offsetHeight) } else { // 放上第二列開始的data // 找出第一列的最小高度 let minHeight = Math.min(...fistRowPhotosHeightArray) // 紀錄最小高度的index,以取得對應到第一列的位置,來決定left要移動多少 let index = fistRowPhotosHeightArray.indexOf(minHeight) // 調整接續的photo位置,放到目前最小高度的地方 items[i].style.top = minHeight + gap + 'px' // 取得對應到第一列photo的left位置 items[i].style.left = items[index].offsetLeft + 'px' // 最後!!再把原本儲存在陣列裡面為最小高度的值,更新上最新的高度(原本的高度+新的高度+間隔) fistRowPhotosHeightArray[index] = fistRowPhotosHeightArray[index] + items[i].offsetHeight + gap // 將當前容器高度高行為高度最高的那一列高度 articleContainer.style.height = fistRowPhotosHeightArray[index] + 'px' } } } catch (error) { // 當執行失敗代表當前data為空值 console.log('nodata'); } } onMounted(() => { // 當捲軸捲動執行handleScroll函數,判斷捲軸是否已至瀑布流最下方 window.addEventListener('scroll', handleScroll); // 當螢幕寬度變更則重新呼叫瀑布流排版函式 window.addEventListener('resize', cascadeDisplay); cascadeDisplay() }) onBeforeUnmount(() => { window.removeEventListener('resize', cascadeDisplay); }); // 當route變更則更新當前變數資料將page變數重置,並重新執行api請求及瀑布流排版 watch(route, async(newValue) =>{ currentPage.value = 1 currentSearch.value = newValue.query.search await articleAPI(currentPage.value) nextTick(() => { requestAnimationFrame(() => { cascadeDisplay(); }); }); }) </script> ``` ## 必要的CSS屬性 其它樣式請依照網站設計去做更改 ``` <style lang="scss"> .columns_list_block{ margin-top: 40px; position: relative; padding-bottom: 40px; .item{ width: 275px; height: auto; position: absolute; } } </style> ``` [參考網址:文科少女學程式](https://pink-learn-frontend.medium.com/%E5%89%8D%E7%AB%AF%E6%96%B0%E6%89%8B%E6%97%A5%E8%A8%98-%E7%80%91%E5%B8%83%E6%B5%81%E5%88%87%E7%89%88%E7%B7%B4%E7%BF%92-javascript-38adb80f443b) [無限載入、惰性載入(Nuxt 3)筆記](https://hackmd.io/QdVAMwWaR3y9_p7p0KyHTg?view) ###### 持續更新中! ###### 以上為自學中的筆記如有錯誤,懇請留言指正,感謝!