uniapp vue2 三端瀑布流

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>
相关推荐
excel5 小时前
Vue 3 编译器源码深度解析:codegen.ts 模块详解
前端
excel5 小时前
Vue 编译器中 walkIdentifiers 源码深度解析
前端
excel5 小时前
一文看懂 Vue 编译器里的插槽处理逻辑(buildSlots.ts)
前端
excel5 小时前
Vue 编译器源码解析:错误系统(errors.ts)
前端
kilito_015 小时前
uniapp主包使用子包的图片 真机会显示不出来
uni-app
余道各努力,千里自同风5 小时前
uni-app 请求封装
前端·uni-app
excel5 小时前
Vue 编译器核心 AST 类型系统与节点工厂函数详解
前端
excel5 小时前
Vue 编译器核心:baseCompile 源码深度解析
前端
fakaifa5 小时前
XYcourse课程预约小程序源码+uniapp前端 全开源+搭建教程
uni-app·php·源码分享·源码下载·xycourse·课程预约小程序