custom-waterfalls-flow.vue
cpp
<template>
<view class="waterfalls-flow">
<view
v-for="(col, i) in columns"
:key="i"
class="waterfalls-flow-column"
:style="{
width: getColumnWidth(i),
marginLeft: i === 0 ? 0 : columnGap
}"
>
<view v-for="(item, j) in col" :key="item._uid || j" class="column-item" :class="{ show: item._visible }" :style="{ marginBottom: rowGap }">
<slot name="default" :item="item"></slot>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
// 数据源
value: { type: Array, default: () => [] },
// 列数
column: { type: Number, default: 2 },
// 列间距(左右)
gap: { type: String, default: '2%' },
// 行间距(上下)
rowGap: { type: String, default: '20rpx' },
// 固定列宽(可选,如 '300rpx' / '45%' / '160px')
columnWidthCustom: { type: String, default: '' },
// 子项估算高度(用于初始分配)
itemHeight: { type: Number, default: 260 }
},
data() {
return {
columns: [],
columnHeights: [],
columnGap: '2%',
renderedCount: 0,
lastLength: 0
};
},
watch: {
value: {
immediate: true,
handler(val, oldVal) {
if (!val) return;
if (!oldVal || !oldVal.length || val.length < this.lastLength) {
this.initColumns(val);
this.lastLength = val.length;
return;
}
if (val.length > this.lastLength) {
const newItems = val.slice(this.lastLength);
this.lastLength = val.length;
this.appendItems(newItems);
}
}
}
},
methods: {
/* 计算列宽 */
getColumnWidth(i) {
// 若用户传了固定宽度,优先使用
if (this.columnWidthCustom) return this.columnWidthCustom;
// 若 gap 含非百分比单位(rpx/px),直接均分
if (/[a-zA-Z]/.test(this.gap)) {
const colWidth = 100 / this.column;
return `${colWidth}%`;
}
// 默认按百分比算
const colWidth = (100 - (this.column - 1) * parseFloat(this.gap)) / this.column;
return `${colWidth}%`;
},
/* 初始化 */
initColumns(list) {
this.columns = Array.from({ length: this.column }, () => []);
this.columnHeights = Array.from({ length: this.column }, () => 0);
this.columnGap = this.gap;
this.renderedCount = 0;
this.placeNext([...list]);
},
/* 获取最矮列 */
getShortestColumnIndex() {
let min = 0;
for (let i = 1; i < this.columnHeights.length; i++) {
if (this.columnHeights[i] < this.columnHeights[min]) min = i;
}
return min;
},
/* 按列插入 */
async placeNext(list) {
if (!list.length) {
this.$emit('load', { rendered: this.renderedCount });
return;
}
const item = list.shift();
item._uid = ++this.renderedCount;
item._visible = false;
const col = this.getShortestColumnIndex();
this.columns[col].push(item);
this.$nextTick(() => {
setTimeout(() => {
item._visible = true;
this.$forceUpdate();
this.columnHeights[col] += this.itemHeight;
this.placeNext(list);
}, 30);
});
},
async appendItems(newItems) {
if (!newItems || !newItems.length) return;
await this.placeNext([...newItems]);
}
}
};
</script>
<style scoped lang="scss">
.waterfalls-flow {
display: flex;
align-items: flex-start;
width: 100%;
}
.waterfalls-flow-column {
display: flex;
flex-direction: column;
}
.column-item {
width: 100%;
opacity: 0;
transform: translateY(40rpx);
transition: opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1), transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
&.show {
opacity: 1;
transform: translateY(0);
}
}
</style>
父组件使用
cpp
<template>
<view class="page-container">
<!-- ✅ 瀑布流组件使用 -->
<waterfalls-flow
:value="mainList"
:column="2"
gap="18rpx"
row-gap="20rpx"
@load="onWaterfallLoaded"
>
<!-- ✅ 默认插槽(传给每个 item) -->
<template #default="{ item }">
<goods-card :item="item" />
</template>
</waterfalls-flow>
<!-- 加载更多测试按钮 -->
<view class="load-more" @click="loadMore">加载更多</view>
</view>
</template>
<script>
import WaterfallsFlow from '@/components/WaterfallsFlow.vue';
import GoodsCard from '@/components/GoodsCard.vue'; // 假设商品卡片组件
export default {
components: {
WaterfallsFlow,
GoodsCard
},
data() {
return {
mainList: [] // 瀑布流展示数据
};
},
onLoad() {
this.loadMore();
},
methods: {
// 模拟接口加载数据
loadMore() {
const newData = Array.from({ length: 10 }).map((_, i) => ({
id: Date.now() + i,
title: `商品 ${this.mainList.length + i + 1}`,
price: (Math.random() * 100).toFixed(2),
image: `https://picsum.photos/400/${300 + Math.floor(Math.random() * 150)}?t=${Math.random()}`
}));
this.mainList.push(...newData);
},
onWaterfallLoaded(e) {
console.log('✅ 瀑布流已渲染项:', e.rendered);
}
}
};
</script>
<style scoped lang="scss">
.page-container {
padding: 24rpx;
background: #f6f6f6;
min-height: 100vh;
}
.load-more {
text-align: center;
padding: 24rpx 0;
color: #007aff;
font-size: 30rpx;
}
</style>