方法一:纯css(column-count 多栏布局)
<template>
<div class="container">
<div class="list">
<div class="item" v-for="(item, index) in list" :key="index">
<img :src="item.url" alt="" />
{{ item.id }}
</div>
</div>
<div class="btn" @click="getList">加载更多</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
list: [],
id: 1
}
},
created() {
this.getList()
},
methods: {
async getList() {
const imgList = []
for (let i = 0; i < 10; i++) {
const { data: res } = await axios.get(
'https://dog.ceo/api/breed/pembroke/images/random'
)
imgList.push({
id: this.id++,
url: res.message
})
}
this.list = [...this.list, ...imgList]
}
}
}
</script>
<style lang="scss" scoped>
.list {
width: 800px;
border: 1px solid #ccc;
margin: 0 auto;
column-count: 4; // 列数
column-gap: 10px; // 列间距
.item {
background-color: pink;
margin-bottom: 10px;
text-align: center;
break-inside: avoid; // 避免在列表项内部进行分页,以保持内容的完整性
img {
width: 100%;
display: block;
}
}
}
.btn {
width: 200px;
height: 40px;
line-height: 40px;
border: 1px solid #ccc;
margin: 20px auto;
text-align: center;
cursor: pointer;
&:hover {
background-color: #ccc;
}
}
</style>
缺点
首先布局是从上到下、从左到右的,并不符合传统的从左到右排列。并且是只能数据固定的时候使用,因为如果需要动态加载数据的话,所有数据就会重新排列,导致和上一次渲染的页面是不一样的,如上图所示
注意点
页面可能出现某一列的最后一个元素的内容被自动断开,一部分在当前列尾,一部分在下一列的列头。这时候子元素可以用 break-inside设置为不被截断 avoid来控制,默认值是auto,会被截断
方法二:绝对定位布局
<template>
<div class="waterfall-container">
<div class="list">
<div class="item" v-for="(item, index) in list" :key="index">
<img :src="item.url" alt="" @load="imgLoad" @error="imgLoad"/>
{{ item.id }}
</div>
</div>
<div class="loading" v-if="loader">
<div class="loader"></div>
</div>
<div class="btn" v-else @click="getList">加载更多</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
list: [],
id: 1,
loader: true,
imgLoadNum: 0
}
},
created() {
this.getList()
},
methods: {
setDomPosition() {
// 获取瀑布流容器
const listDom = document.querySelector('.waterfall-container .list')
// 获取所有瀑布流子元素
const itemDoms = document.querySelectorAll('.waterfall-container .item')
// 创建列数和间距
const columns = 4
const margin = 15
// 创建一个数组,用来存储每一列的高度
const hrr = []
// 遍历每个项目,计算并设置其在瀑布流布局中的位置
Array.from(itemDoms).forEach((item, index) => {
// 获取当前项目的宽度和高度
const { offsetWidth, offsetHeight } = item
// // 如果有图片宽高,则计算出图片的实际高度并赋值,就可以避免图片加载导致的高度变化,也可以直接在行内样式中通过计算属性设置,避免多次计算
// const img = item.querySelector('img')
// if (img && this.list[index].height && this.list[index].width) {
// img.style.height = (this.list[index].height * offsetWidth) / this.list[index].width
// }
// 判断当前项目是否属于第一行
if (index < columns) {
// 计算第一行项目的x轴和y轴位置
const x = (offsetWidth + margin) * index + 'px'
const y = 0
// 设置项目的位置
item.style.transform = `translate(${x}, ${y})`
// 将当前项目的高度加上边距,添加到高度记录数组中
hrr.push(offsetHeight + margin)
} else {
// 找到当前高度最小的列
const minH = Math.min(...hrr)
const i = hrr.indexOf(minH)
// 计算非第一行项目的x轴和y轴位置
const x = (offsetWidth + margin) * i + 'px'
const y = minH + 'px'
// 设置项目的位置
item.style.transform = `translate(${x}, ${y})`
// 更新高度记录数组中对应列的高度
hrr[i] += offsetHeight + margin
}
// 设置项目的不透明度,使其可见
item.style.opacity = 1
})
// 根据最大列高设置列表的高度
listDom.style.height = Math.max(...hrr) - margin + 'px'
},
imgLoad() {
this.imgLoadNum += 1
if(this.imgLoadNum === this.list.length) {
this.loader = false
this.setDomPosition()
}
},
async getList() {
this.loader = true
const imgList = []
for (let i = 0; i < 10; i++) {
const { data: res } = await axios.get(
'https://dog.ceo/api/breed/pembroke/images/random'
)
imgList.push({
id: this.id++,
url: res.message
})
}
this.list = [...this.list, ...imgList]
}
}
}
</script>
<style lang="scss" scoped>
.list {
width: 800px;
margin: 0 auto;
position: relative;
padding: 10px;
.item {
position: absolute;
width: calc((100% - 45px) / 4); // 父元素宽度减去列与列的间距再除于4得到每一列的宽度
transform: translate(0, 0);
opacity: 0;
transition: all 0.5s; // 添加动画效果
background-color: pink;
text-align: center;
border-radius: 5px;
img {
width: 100%;
display: block;
}
}
}
.loading {
margin-top: 60px;
margin-bottom: 60px;
.loader {
--d: 22px;
width: 4px;
height: 4px;
border-radius: 50%;
color: $brandColor;
box-shadow: calc(1 * var(--d)) calc(0 * var(--d)) 0 0,
calc(0.707 * var(--d)) calc(0.707 * var(--d)) 0 1px,
calc(0 * var(--d)) calc(1 * var(--d)) 0 2px,
calc(-0.707 * var(--d)) calc(0.707 * var(--d)) 0 3px,
calc(-1 * var(--d)) calc(0 * var(--d)) 0 4px,
calc(-0.707 * var(--d)) calc(-0.707 * var(--d)) 0 5px,
calc(0 * var(--d)) calc(-1 * var(--d)) 0 6px;
animation: l27 1s infinite steps(8);
margin: 0 auto;
}
@keyframes l27 {
100% {
transform: rotate(1turn);
}
}
}
.btn {
width: 200px;
height: 40px;
line-height: 40px;
border: 1px solid #ccc;
margin: 60px auto 20px;
text-align: center;
cursor: pointer;
&:hover {
background-color: #ccc;
}
}
</style>
注释:如果知道图片的宽高,就不需要根据图片加载的事件来判断图片是否渲染完成,可以获取到数据后直接执行
setDomPosition方法,否则就需要使用图片加载事件来判断图片是否渲染完成,完成后在进行计算位置,否则可能出现位置不准确的问题,导致布局错乱
以上两种方式通过动图可以很明显的看出区别,纯css 每次加载都会重新布局,导致顺序每一次都会不一样,而通过 js绝对定位 的方式就可以达到我们想要的效果