# Waterfall 瀑布流 
该组件主要用于瀑布流式布局显示,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。常用于一些电商商品展示等,如某宝首页。
# 平台兼容性
App(vue) | App(nvue) | H5 | 小程序 | VUE2 | VUE3 |
---|---|---|---|---|---|
√ | √ | √ | √ | √ | √ |
注意
- 瀑布流集成了
uni-app
官方提供的nvue
组件waterfall (opens new window),在nvue
中性能更佳,nvue
和vue
中使用有些区别,下面会在示例中说明。 - 在
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生效)
- 该用法是在基本使用的代码基础上进行扩展
- 使用场景:一般使用在触底加载更多数据等场景。以下案例配合加载更多组件uv-load-more (opens new window)
<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生效)
- 在
NVUE
中使用方法比较简单,直接看下面案例就懂,需要注意的是APP-NVUE
中不能使用uv-iamge
等组件,原因:android端list中为什么不能使用uv-image等组件 (opens new window)。 - 通过
column-count
设置瀑布流列数,默认2,范围1-5。 - 在
NVUE
中使用uv-waterfall
需要配合cell
使用,相关文档可以参考waterfall (opens new window)。
<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生效)
事件名 | 说明 |
---|---|
- | 瀑布流内容 |
← QRCode 二维码 Color 色彩 →