瀑布流
瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数清新站基本为这类风格。这是百度百科对其的描述
简而言之:就是一种自适应布局,根据每一项的高度以及每一列的高度,决定这一项放在哪一列中展示,随着加载数据的增多,使得每一列的高度不会相差太多,不能出现一列过长或过短的情况
展示效果:
实现代码
vue3实现过程:
有几项就初始化几项数据,_allHeight:记录当前列的高度,将下一项放在目前最短的一列中,即:_allHeight最小的一列中
<div v-for="(col, colIndex) in state.xRenderList" :key="colIndex" class="col">
<div
v-for="item in col.cardArr"
:key="item.id"
class="col-card"
>
<img :src="item.link" class="card-img" />
<div class="card-des">
<div class="card-title ellipsis">{{ item.title }}</div>
<div class="card-con">{{ item.overview }}</div>
</div>
</div>
</div>
<script setup>
const state = reactive({
// 分页信息
pagination: {
pageIndex: 1, // 页码
pageSize: 20, // 每页数据条数
},
colList: [], //每一次从远程获取的列表
//渲染列表:想要展示几列就初始化几项
xRenderList: [
{
_allHeight: 0, //用于计算一列的总高度
cardArr: [],
},
{
_allHeight: 0,
cardArr: [],
},
{
_allHeight: 0,
cardArr: [],
},
{
_allHeight: 0,
cardArr: [],
},
],
isLoading: false, //是否正在加载计算高度
})
</script>
当获取到数据之后,开始加载每一项的数据:
// 获取列表
// calc:true:标识需要计算高度
async function getCaseList(calc) {
state.isLoading = true
const params = {
pageIndex: state.pagination.pageIndex,
pageSize: state.pagination.pageSize,
}
const res = await getList(params).catch((e) => {})
if (res?.code === 200) {
const { records } = res.data.data
//先置空
state.colList = []
state.colList = records || []
for (let i = 0; i < state.colList.length; i++) {
const item = state.colList[i]
//根据每一项的高度,加载方法,
await addItem(item,calc)
}
}
state.isLoading = false
}
//加载初始内容
await getCaseList()
预加载每一项图片的高度,文本高度先忽略。
预加载图片方法:(大致计算每一项的高度)
// 假设图片、概述初始宽度
const _imgWidth = 285
const _txtWidth = 0
const _marginHight = 30
// 预加载图片高度,计算单个卡片高度
function preload(item) {
return new Promise(async (resolve, reject) => {
let currentHeight = 0
// 无图时
if (!item.link) {
currentHeight = _txtWidth + _marginHight
resolve(currentHeight)
} else {
const oImg = new Image()
oImg.src = item.link
oImg.onload = oImg.onerror = (e) => {
// 预加载图片,计算图片容器的高
const imgHeight = e.type === 'load' ? Math.round(_imgWidth * (oImg.height / oImg.width)) : _imgWidth
currentHeight = imgHeight + _txtWidth + _marginHight
resolve(currentHeight)
}
}
})
}
根据预加载的高度加载每一项的方法
async function addItem(item, calc) {
// 初始时,不计算高度
const itemHeight = calc ? await preload(item) : 1000
// 放入最小高度的一列中
let minHight = state.xRenderList[0]._allHeight
let minIndex = 0
for (let i = 0; i < state.xRenderList.length; i++) {
const col = state.xRenderList[i]
if (minHight > col._allHeight) {
minHight = col._allHeight
minIndex = i
}
}
//在列里添加这一项
state.xRenderList[minIndex].cardArr.push(item)
state.xRenderList[minIndex]._allHeight = state.xRenderList[minIndex]._allHeight + itemHeight
}
因为初始时不计算高度,所以到客户端时,再重新计算图片高度加载每一个card
onMounted(async () => {
state.xRenderList = [
{
_allHeight: 0, //用于服务端渲染计算一列的总高度
cardArr: [],
},
{
_allHeight: 0,
cardArr: [],
},
{
_allHeight: 0,
cardArr: [],
},
{
_allHeight: 0,
cardArr: [],
},
]
for (const v of state.caseList) {
await addItem(v, true)
}
})
向下滚动时,继续加载数据,改变pageindex,向后端请求下一页的数据
优化:当上一次请求还没结束或上一次请求为空,滚动时就不再向后端发请求了
onMounted(async () => {
document.addEventListener('scroll', async function () {
/*判断是否触底*/
//如果正在预加载
if (state.isLoading) {
return
}
// 如果上次请求的数据为空表明没有下一页数据了,就阻止请求
if (state.colList.length === 0) {
return
}
//获取所有列元素,重新计算高度(防止有的文本过长,导致有的列加载不平均 )
const cols = document.getElementsByClassName('col')
for (let i = 0; i < cols.length; i++) {
state.xRenderList[i]._allHeight = cols[i].clientHeight
}
//滚动条距离顶部距离
const st = document.documentElement.scrollTop
//页面可视区域高度
const ch = document.documentElement.clientHeight
//页面总高度
const sh = document.documentElement.scrollHeight
//页面可视区域高度 + 滚动条距离顶部距离,大于页面总高度的99%则表示触底
//之后执行加载数据方法即可
if (ch + st > sh * 0.99) {
//触底,向后端请求下一页的数据
state.pagination.pageIndex++
// 滚动时,需要计算高度加载样式
await getList(true)
}
})
})
补充:
样式:
.waterfall-wrap::after {
display: block;
width: 100%;
height: 1px;
clear: both;
content: '';
}
.waterfall-wrap {
padding-top: 30px;
.col {
float: left;
width: calc(25% - 15px);
margin-right: 20px;
.col-card {
width: 100%;
margin-bottom: 20px;
.card-img {
width: 100%;
border-radius: 8px 8px 0 0;
// object-fit: scale-down;
}
.card-des {
padding: 5px 22px 18px;
background: rgba(#fff, 0.85);
border-radius: 0 0 8px 8px;
.card-title {
font-size: 18px;
font-weight: bold;
line-height: 36px;
color: #4d4d4d;
}
.card-con {
font-size: 14px;
line-height: 20px;
color: #7a8ba6;
}
}
}
}
}
.waterfall-wrap > div:nth-child(4n) {
margin-right: 0;
}
.case-img-link {
cursor: pointer;
}