先看效果图
直接上代码
utils.js
// 用于模拟接口请求
export const getRemoteData = (data = '获取数据', time = 2000) => {return new Promise((resolve) => {setTimeout(() => {console.log(`模拟获取接口数据`, data)resolve(data)}, time)})
}// 获取数组随机项
export const getRandomElement = (arr) => {var randomIndex = Math.floor(Math.random() * arr.length);return arr[randomIndex];
}// 指定范围随机数
export const getRandomNumber = (min, max) => {return Math.floor(Math.random() * (max - min + 1) + min);
}// 节流
export const throttle = (fn, time) => {let timer = nullreturn (...args) => {if (!timer) {timer = setTimeout(() => {timer = nullfn.apply(this, args)}, time)}}
}
// 防抖
export const debounce = (fn, time) => {let timer = nullreturn (...args) => {clearTimeout(timer)timer = setTimeout(() => {fn.apply(this, args)}, time)}
}
data.js
模拟后台返回的数据
import { getRandomElement, getRandomNumber } from "./utils.js"const colorList = ['red', 'blue', 'green', 'pink', 'yellow', 'orange', 'purple', 'brown', 'gray', 'skyblue']export const createList = (pageSize) => {let list = Array.from({ length: pageSize }, (v, i) => i)return list.map(x => {return {background: getRandomElement(colorList),width: getRandomNumber(200, 600),height: getRandomNumber(400, 700),x: 0,y: 0}})
}
瀑布流布局组件waterfall.vue
<template><div class="waterfall-container" ref="containerRef" @scroll="handleScroll"><div class="waterfall-list"><divclass="waterfall-item"v-for="(item, index) in resultList":key="index":style="{width: `${item.width}px`,height: `${item.height}px`,transform: `translate3d(${item.x}px, ${item.y}px, 0)`,}"><slot name="item" v-bind="item"></slot></div></div></div>
</template>
<script setup>
import { ref, onMounted, computed, nextTick, onUnmounted } from "vue";
import { createList } from "@/common/data.js";
import { getRemoteData, throttle, debounce } from "@/common/utils.js";
const props = defineProps({// 间距gap: {type: Number,default: 10,},// 列数columns: {type: Number,default: 3,},// 距离底部bottom: {type: Number,default: 0,},// 分页大小pageSize: {type: Number,default: 10,},
});// 容器ref
const containerRef = ref(null);// 卡片宽度
const cardWidth = ref(0);// 列高度
const columnHeight = ref(new Array(props.columns).fill(0));// 数据list
const resultList = ref([]);// 当前页码
const pageNum = ref(1);// 加载状态
const loading = ref(false);// 计算最小列高度及其下标
const minColumn = computed(() => {let minIndex = -1,minHeight = Infinity;columnHeight.value.forEach((item, index) => {if (item < minHeight) {minHeight = item;minIndex = index;}});return {minIndex,minHeight,};
});// 获取接口数据
const getData = async () => {loading.value = true;const list = createList(props.pageSize);const resList = await getRemoteData(list, 300).finally(() => (loading.value = false));pageNum.value++;resultList.value = [...resultList.value, ...getList(resList)];
};// 滚动到底部获取新一页数据-节流
const handleScroll = throttle(() => {const { scrollTop, clientHeight, scrollHeight } = containerRef.value;const bottom = scrollHeight - clientHeight - scrollTop;if (bottom <= props.bottom) {!loading.value && getData();}
});// 拼装数据结构
const getList = (list) => {return list.map((x, index) => {const cardHeight = Math.floor((x.height * cardWidth.value) / x.width);const { minIndex, minHeight } = minColumn.value;const isInit = index < props.columns && resultList.length <= props.pageSize;if (isInit) {columnHeight.value[index] = cardHeight + props.gap;} else {columnHeight.value[minIndex] += cardHeight + props.gap;}return {width: cardWidth.value,height: cardHeight,x: isInit? index % props.columns !== 0? index * (cardWidth.value + props.gap): 0: minIndex % props.columns !== 0? minIndex * (cardWidth.value + props.gap): 0,y: isInit ? 0 : minHeight,background: x.background,};});
};// 监听元素
const resizeObserver = new ResizeObserver(() => {handleResize();
});// 重置计算宽度以及位置
const handleResize = debounce(() => {const containerWidth = containerRef.value.clientWidth;cardWidth.value =(containerWidth - props.gap * (props.columns - 1)) / props.columns;columnHeight.value = new Array(props.columns).fill(0);resultList.value = getList(resultList.value);
});const init = () => {if (containerRef.value) {const containerWidth = containerRef.value.clientWidth;cardWidth.value =(containerWidth - props.gap * (props.columns - 1)) / props.columns;getData();resizeObserver.observe(containerRef.value);}
};onMounted(() => {init();
});
// 取消监听
onUnmounted(() => {containerRef.value && resizeObserver.unobserve(containerRef.value);
});
</script><style lang="scss">
.waterfall {&-container {width: 100%;height: 100%;overflow-y: scroll;overflow-x: hidden;}&-list {width: 100%;position: relative;}&-item {position: absolute;left: 0;top: 0;box-sizing: border-box;transition: all 0.3s;}
}
</style>
使用该组件(这里columns
写死了3列)
<template><div class="container"><WaterFall :columns="3" :gap="10"><template #item="{ background }"><div class="card-box" :style="{ background }"></div></template></WaterFall></div>
</template><script setup>
import WaterFall from "@/components/waterfall.vue";
</script><style scoped lang="scss">
.container {width: 700px; /* 一般业务场景不是固定宽度 */height: 800px;border: 2px solid #000;margin-top: 10px;margin-left: auto;
}
.card-box {position: relative;width: 100%;height: 100%;border-radius: 4px;
}
</style>
若要响应式调整列数,可参考以下代码
const fContainerRef = ref(null);
const columns = ref(3);
const fContainerObserver = new ResizeObserver((entries) => {changeColumn(entries[0].target.clientWidth);
});// 根据宽度,改变columns列数
const changeColumn = (width) => {if (width > 1200) {columns.value = 5;} else if (width >= 768 && width < 1200) {columns.value = 4;} else if (width >= 520 && width < 768) {columns.value = 3;} else {columns.value = 2;}
};onMounted(() => {fContainerRef.value && fContainerObserver.observe(fContainerRef.value);
});onUnmounted(() => {fContainerRef.value && fContainerObserver.unobserve(fContainerRef.value);
});
瀑布流布局组件监听columns
变化
watch(() => props.columns,() => {handleResize();}
);