# Waterfall 瀑布流

组件名:uv-waterfall

点击下载&安装 (opens new window)

该组件主要用于瀑布流式布局显示,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。常用于一些电商商品展示等,如某宝首页。

# 平台兼容性

App(vue) App(nvue) H5 小程序 VUE2 VUE3

注意

  1. 瀑布流集成了uni-app官方提供的nvue组件waterfall (opens new window),在nvue中性能更佳,nvuevue中使用有些区别,下面会在示例中说明。
  2. APP-NVUE中,该组件中不能使用uv-image等组件,具体原因请查看:android端list中为什么不能使用uv-image等组件 (opens new window)

# 基本使用 (vue生效)

  • 这是vue页面的使用法,nvue的写法在下面单独说明nvue中使用
  • 通过column-count设置瀑布流列数,默认2,范围1-5。
  • 如果列数为2,则需要定义两个插槽值<template v-slot:list1>...</template><template v-slot:list2">...</template>
  • 如果列数为3,则需要定义三个插槽值<template v-slot:list1>...</template><template v-slot:list2">...</template><template v-slot:list3">...</template>,更多列数以此类推。
<template>
	<view class="waterfall">
		<uv-waterfall ref="waterfall"
			v-model="list"
			:add-time="10"
			:left-gap="leftGap"
			:right-gap="rightGap"
			:column-gap="columnGap"
			@changeList="changeList">
			<!-- 第一列数据 -->
			<template v-slot:list1>
				<!-- 为了磨平部分平台的BUG,必须套一层view -->
				<view>
					<view v-for="(item, index) in list1"
						:key="item.id"
						class="waterfall-item">
						<view class="waterfall-item__image" :style="[imageStyle(item)]">
							<image :src="item.image" mode="widthFix" :style="{width:item.width+'px'}"></image>
						</view>
						<view class="waterfall-item__ft">
							<view class="waterfall-item__ft__title">
								<text class="value">{{item.title}}</text>
							</view>
							<view class="waterfall-item__ft__desc uv-line-2">
								<text class="value">{{item.desc}}</text>
							</view>
						</view>
					</view>
				</view>
			</template>
			<!-- 第二列数据 -->
			<template v-slot:list2>
				<!-- 为了磨平部分平台的BUG,必须套一层view -->
				<view>
					<view v-for="(item, index) in list2"
						:key="item.id"
						class="waterfall-item">
						<view class="waterfall-item__image" :style="[imageStyle(item)]">
							<image :src="item.image" mode="widthFix" :style="{width:item.width+'px'}"></image>
						</view>
						<view class="waterfall-item__ft">
							<view class="waterfall-item__ft__title">
								<text class="value">{{item.title}}</text>
							</view>
							<view class="waterfall-item__ft__desc uv-line-2">
								<text class="value">{{item.desc}}</text>
							</view>
						</view>
					</view>
				</view>
			</template>
		</uv-waterfall>
	</view>
</template>
<script>
	import { guid } from '@/uni_modules/uv-ui-tools/libs/function/index.js'
	export default {
		data() {
			return {
				list: [],// 瀑布流全部数据
				list1: [],// 瀑布流第一列数据
				list2: [],// 瀑布流第二列数据
				leftGap: 10,
				rightGap: 10,
				columnGap: 10
			}
		},
		computed: {
			imageStyle(item) {
				return item=>{
					const v = uni.upx2px(750) - this.leftGap - this.rightGap - this.columnGap;
					const w = v/2;
					const rate = w / item.w;
					const h = rate* item.h;
					return {
						width: w + 'px',
						height: h + 'px'
					}
				}
			}
		},
		async onLoad() {
			const { data } = await this.getData();
			this.list = data;
		},
		methods: {
			// 这点非常重要:e.name在这里返回是list1或list2,要手动将数据追加到相应列
			changeList(e){
				this[e.name].push(e.value);
			},
			// 模拟的后端数据
			getData() {
				return new Promise((resolve)=>{
					const imgs = [
						{url: 'https://via.placeholder.com/100x110.png/3c9cff/fff',width: 100, height: 110},
						{url: 'https://via.placeholder.com/200x220.png/f9ae3d/fff',width: 200, height: 220},
						{url: 'https://via.placeholder.com/300x340.png/5ac725/fff',width: 300, height: 340},
						{url: 'https://via.placeholder.com/400x400.png/f56c6c/fff',width: 400, height: 400},
						{url: 'https://via.placeholder.com/500x510.png/909399/fff',width: 500, height: 510},
						{url: 'https://via.placeholder.com/600x606.png/3c9cff/fff',width: 600, height: 606},
						{url: 'https://via.placeholder.com/310x422.png/f1a532/fff',width: 310, height: 422},
						{url: 'https://via.placeholder.com/320x430.png/3c9cff/fff',width: 320, height: 430},
						{url: 'https://via.placeholder.com/330x424.png/f9ae3d/fff',width: 330, height: 424},
						{url: 'https://via.placeholder.com/340x435.png/5ac725/fff',width: 340, height: 435},
						{url: 'https://via.placeholder.com/350x440.png/f56c6c/fff',width: 350, height: 440},
						{url: 'https://via.placeholder.com/380x470.png/909399/fff',width: 380, height: 470}
					];
					let list = [];
					const doFn = (i)=>{
						const randomIndex = Math.floor(Math.random() * 10);
						return {
							id: guid(),
							allowEdit: i==0,
							image: imgs[randomIndex].url,
							w: imgs[randomIndex].width,
							h: imgs[randomIndex].height,
							title: i % 2 == 0 ? `(${this.list.length + i + 1})体验uv-ui框架`: `(${this.list.length + i +1})uv-ui支持多平台`,
							desc: i % 2 == 0 ? `(${this.list.length + i + 1})欢迎使用uv-ui,uni-app生态专用的UI框架` : 
							`(${this.list.length + i})开发者编写一套代码, 可发布到iOS、Android、H5、以及各种小程序`
						}
					};
					// 模拟异步
					setTimeout(() => {
						for (let i = 0; i < 20; i++) {
							list.push(doFn(i));
						}
						resolve({data:list});
					}, 200)
				})
			}
		}
	}
</script>
<style>
	page {
		background: #f1f1f1;
	}
</style>
<style scoped lang="scss">
	$show-lines: 1;
	@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
	.waterfall-item {
		overflow: hidden;
		margin-top: 10px;
		border-radius: 6px;
	}
	.waterfall-item__ft {
		padding: 20rpx;
		background: #fff;
		&__title {
			margin-bottom: 10rpx;
			line-height: 48rpx;
			font-weight: 700;
			.value {
				font-size: 32rpx;
				color: #303133;
			}
		}
		&__desc .value {
			font-size: 28rpx;
			color: #606266;
		}
		&__btn {
			padding: 10px 0;
		}
	}
</style>

# 删除某项数据 (vue生效)

  • 该用法是在基本使用的代码基础上进行扩展
  • 使用ref调用组件内置方法remove(id),注意需要带上数据中id参数。执行之后,必须在@remove回调中处理列表数据,如下:
<template>
	<view class="waterfall">
		<uv-waterfall ref="waterfall" @remove="remove">...</uv-waterfall>
	</view>
</template>
<script>
	export default {
		methods: {
			// eg:长按某项执行删除操作
			longHandle(item) {
				let that = this;
				uni.showModal({
					title: '提示',
					content: '你确定删除该项?',
					success(res) {
						if (res.confirm) {
							that.$refs.waterfall.remove(item.id);
						}
					}
				})
			},
			// 删除某项后返回对应id,根据id标识在列数据中手动删除该项数据
			remove(id){
				this.list1.forEach((item,index)=>{
					if(item.id == id){
						this.list1.splice(index,1);
					}
				})
				this.list2.forEach((item,index)=>{
					if(item.id == id){
						this.list2.splice(index,1);
					}
				})
			}
		}
	}
</script>

# 清除所有数据 (vue生效)

  • 该用法是在基本使用的代码基础上进行扩展
  • 使用ref调用组件内置方法clear(),该方法执行后触发@clear回调
  • 使用场景:下拉刷新数据或tab切换时更改数据,需要先清空数据,再去赋值新的数据等场景。以下是以下拉刷新数据为例:
<template>
	<view class="waterfall">
		<uv-waterfall ref="waterfall" @clear="clear">...</uv-waterfall>
	</view>
</template>
<script>
	export default {
		// 下拉刷新数据
		async onPullDownRefresh() {
			this.list = [];
			this.$refs.waterfall.clear();
			this.list1 = [];
			this.list2 = [];
			const { data } = await this.getData();
			this.list = data;
			uni.showToast({
				icon: 'success',
				title: '刷新成功'
			})
			uni.stopPullDownRefresh();
		},
		methods: {
			clear() {
				console.log('执行了clear')
			}
		}
	}
</script>

# 加载更多数据 (vue生效)

<template>
	<view class="waterfall">
		<uv-waterfall ref="waterfall">...</uv-waterfall>
		<!-- 加载更多组件 -->
		<uv-load-more :status="loadStatus"></uv-load-more>
	</view>
</template>
<script>
	export default {
		data() {
			return {
				// ...
				loadStatus: 'loadmore'
			}
		}
		// 触底加载更多
		async onReachBottom() {
			if(this.loadStatus == 'loadmore') {
				this.loadStatus = 'loading';
				const { data } = await this.getData();
				this.list.push.apply(this.list,data);
				this.loadStatus = 'loadmore';
			}
		}
	}
</script>

# tab切换案例

设计思路:瀑布流是属于动态去根据每项的高度去计算该显示到哪列,当tab切换数据更换时,就需要将原有的数据清除,重新赋值新数据。

该示例不完整,需要完整示例请到跳转入口下载,这里只是提供一个思路:

<template>
	<view class="waterfall">
		<uv-tabs :list="tabList" @click="clickTab"></uv-tabs>
		<uv-waterfall ...>
			...
		</uv-waterfall>
	</view>
</template>
<script>
	export default {
		data() {
			return {
				tabList:[{
					name:'选项一'
				},{
					name:'选项二'
				}],
				list: [],// 瀑布流全部数据
				list1: [],// 瀑布流第一列数据
				list2: []// 瀑布流第二列数据
			}
		},
		methods: {
			async clickTab(e){
				this.list = [];
				this.$refs.waterfall.clear();
				this.list1 = [];
				this.list2 = [];
				const { data } = await this.getData();
				this.list = data;
			}
		}
	}
</script>

# 优化方案 (vue生效)

  • 上面的示例使用image展示图片,如果需要功能更强大图片组件(加载中、加载失败反馈等),可使用uv-image (opens new window)
  • 通过addTime属性可以设置每项显示的速度,值越小体验越好,但是可能导致两列高度相差较大。这里可以提供一个优化思路:通过后端返回图片的宽高,然后再通过每列的宽度,最后计算出每张图片应该展示的宽高,然后给image套一层<view style="width:xxx;height:xxx;"></view>,提前将位置站好,这样计算出的每列的高度就能保证相近没有误差
  • 如果页面还没渲染结束,页面就跳走,但此时@changeList回调还在返回数据,可能会造成渲染出错,所以要想办法停止渲染,如下处理方式:
<script>
	export default {
		onHide() {
			this.$refs.waterfall.clear();
		}
	}
</script>

# NVUE中使用 (nvue生效)

<template>
	<view class="waterfall">
		<uv-waterfall 
		column-count="2"
		left-gap="10"
		right-gap="10"
		column-gap="8"
		@scrolltolower="init">
			<cell 
				v-for="(item,index) in list"
				class="waterfall-item-cell"
			>
				<view class="waterfall-item">
					<view class="waterfall-item__image">
							<image :src="item.image" mode="widthFix" style="border-radius: 20rpx;"></image>
					</view>
					<view class="waterfall-item__ft">
						<view class="waterfall-item__ft__title">
							<text class="value">{{item.title}}</text>
						</view>
						<view class="waterfall-item__ft__desc uv-line-2">
							<text class="value">{{item.desc}}</text>
						</view>
					</view>
				</view>
				<view style="height: 4px;width: 4px;"></view>
			</cell>
		</uv-waterfall>
	</view>
</template>
<script>
	import { guid } from '@/uni_modules/uv-ui-tools/libs/function/index.js'
	export default {
		data() {
			return {
				list: [],// 瀑布流全部数据
				loadStatus: 'loadmore'
			}
		},
		async onLoad() {
			const { data } = await this.getData();
			this.list = data;
		},
		// 下拉刷新数据
		async onPullDownRefresh() {
			this.list = [];
			this.init();
			uni.showToast({
				icon: 'success',
				title: '刷新成功'
			})
			uni.stopPullDownRefresh();
		},
		// 触底加载更多
		async onReachBottom() {
			if(this.loadStatus == 'loadmore') {
				this.init();
			}
		},
		methods: {
			async init() {
				this.loadStatus = 'loading';
				const { data } = await this.getData();
				this.list.push.apply(this.list,data);
				this.loadStatus = 'loadmore';
			},
			// 模拟的后端数据
			getData() {
				return new Promise((resolve)=>{
					const imgs = [
						{url: 'https://via.placeholder.com/100x110.png/3c9cff/fff',width: 100, height: 110},
						{url: 'https://via.placeholder.com/200x220.png/f9ae3d/fff',width: 200, height: 220},
						{url: 'https://via.placeholder.com/300x340.png/5ac725/fff',width: 300, height: 340},
						{url: 'https://via.placeholder.com/400x400.png/f56c6c/fff',width: 400, height: 400},
						{url: 'https://via.placeholder.com/500x510.png/909399/fff',width: 500, height: 510},
						{url: 'https://via.placeholder.com/600x606.png/3c9cff/fff',width: 600, height: 606},
						{url: 'https://via.placeholder.com/310x422.png/f1a532/fff',width: 310, height: 422},
						{url: 'https://via.placeholder.com/320x430.png/3c9cff/fff',width: 320, height: 430},
						{url: 'https://via.placeholder.com/330x424.png/f9ae3d/fff',width: 330, height: 424},
						{url: 'https://via.placeholder.com/340x435.png/5ac725/fff',width: 340, height: 435},
						{url: 'https://via.placeholder.com/350x440.png/f56c6c/fff',width: 350, height: 440},
						{url: 'https://via.placeholder.com/380x470.png/909399/fff',width: 380, height: 470}
					];
					let list = [];
					const doFn = (i)=>{
						const randomIndex = Math.floor(Math.random() * 10);
						return {
							id: guid(),
							allowEdit: i==0,
							image: imgs[randomIndex].url,
							w: imgs[randomIndex].width,
							h: imgs[randomIndex].height,
							title: i % 2 == 0 ? `(${this.list.length + i + 1})体验uv-ui框架`: `(${this.list.length + i +1})uv-ui支持多平台`,
							desc: i % 2 == 0 ? `(${this.list.length + i + 1})欢迎使用uv-ui,uni-app生态专用的UI框架` : 
							`(${this.list.length + i})开发者编写一套代码, 可发布到iOS、Android、H5、以及各种小程序`
						}
					};
					// 模拟异步
					setTimeout(() => {
						for (let i = 0; i < 20; i++) {
							list.push(doFn(i));
						}
						resolve({data:list});
					}, 200)
				})
			}
		}
	}
</script>
<style scoped lang="scss">
	$show-lines: 1;
	@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
	.waterfall {
		background-color: #f1f1f1;
	}
	.waterfall-item {
		overflow: hidden;
		margin-top: 10px;
		border-radius: 6px;
	}
	.waterfall-item__ft {
		padding: 20rpx;
		background: #fff;
		&__title {
			margin-bottom: 10rpx;
			line-height: 48rpx;
			font-weight: 700;
			.value {
				font-size: 32rpx;
				color: #303133;
			}
		}
		&__desc .value {
			font-size: 28rpx;
			color: #606266;
		}
		&__btn {
			padding: 10px 0;
		}
	}
</style>

# 完整示例

# API


# Waterfall Props

参数 说明 类型 默认值 可选值
v-model 瀑布流数据,(vue生效) Array [] -
idKey 数据的id值,根据id值对数据可执行删除操作。如数据为:{id: 1, name: 'uv-ui'},那么该值设置为id,(vue生效) String 'id' -
addTime 每次插入数据的事件间隔,间隔越长能保证两列高度相近,但是用户体验不好,单位ms,(vue生效) Number 200 -
columnCount 瀑布流的列数,默认2,最高为5 Number | String 2 1-5
columnGap 列与列的间隙,默认单位px Number | String 20 -
leftGap 左边和列表的间隙,默认单位px Number | String 0 -
rightGap 右边和列表的间隙,默认单位px Number | String 0 -
showScrollbar 是否显示滚动条,(nvue生效) Boolean false true | false
columnWidth 列宽,单位px,(nvue生效) Number | String 'auto' -
width 瀑布流的宽度,(nvue生效) Number | String 屏幕宽度 -
height 瀑布流的高度,(nvue生效) Number | String 屏幕高度 -

# Waterfall Methods

事件名 说明 类型
remove 清除指定的某一条数据,根据id来实现,删除后v-model绑定的数据会自动变化,(vue生效) Handler
clear 清除瀑布流数据,(vue生效) -

# Waterfall Events

事件名 说明 回调参数
@changeList 【必须使用】 处理数据时触发,为了兼容某些端不支持插槽回传参数的情况(vue生效) 列表数据,eg:columnCount=2,的、第一次返回{list1:{...}},第二次返回{list2:{...}}...。返回后需要手动追加对应的列数据
@finish 瀑布流加载完成触发事件,(vue生效) -
@clear 清空数据列表触发事件,(vue生效) -
@remove 删除列表中某条数据触发事件,(vue生效) id
@scrolltolower 滚动到底部触发事件,(nvue生效) -

# Waterfall Slots (vue生效)

事件名 说明
- 瀑布流内容