# 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)
###### 持續更新中!
###### 以上為自學中的筆記如有錯誤,懇請留言指正,感謝!