鸿蒙专家 发表于 2026-9-20 09:00:00

鸿蒙uni-app x瀑布流waterflow组件API与适配实战

在鸿蒙平台使用uni-app x开发时,瀑布流布局是电商、社区类应用的常见需求。waterflow是uni-app x提供的瀑布流组件,底层实现与list-view基本一致,都基于回收复用机制,子组件滑出屏幕会被回收,区别在于waterflow多列排列,list-view单列。鸿蒙4.81开始支持waterflow,且不支持Web和微信小程序。组件选择上,瀑布流用waterflow,单列列表用list-view,其他用scroll-view。

兼容性方面,鸿蒙平台API 20及以上版本支持滚动相关事件,API 18及以上版本支持load-more插槽。waterflow暂时只支持flow-item作为子组件,其他组件不可见。flow-item是瀑布流的子组件,每个flow-item代表一列内容,其type属性用于区分类型。

waterflow核心属性包括:cross-axis-count(列数)、cross-axis-gap(列间距)、main-axis-gap(行间距)、padding(内边距数组)、refresher-enabled(是否开启下拉刷新)、refresher-triggered(刷新状态)、refresher-default-style(下拉刷新样式,可设为none自定义)、associative-container(嵌套滚动关联容器)。事件有:@refresherrefresh(下拉刷新触发)、@scrolltolower(滚动到底部)、@scroll(滚动)、@refresherpulling(下拉中)、@refresherrestore(复位)、@refresherabort(中止)。load-more插槽用于上拉加载更多。

下面通过几个实战场景说明用法。

场景一:基础瀑布流。使用v-for循环生成flow-item,设置type='1',每个卡片随机高度。

<template>
<waterflow style='flex: 1;'>
<flow-item v-for='(item, index) in dataList' :key='index' type='1'>
<view style='padding: 10px;'>
<view :style='{ height: item.height + "px", backgroundColor: item.color, borderRadius: "8px", alignItems: "center", justifyContent: "center" }'>
<text style='font-size: 16px; color: white; font-weight: bold;'>{{item.text}}</text>
</view>
</view>
</flow-item>
</waterflow>
</template>
<script setup lang='uts'>
type FlowItem = {
height: number
color: string
text: string
}
const colors = ['#E91E63', '#9C27B0', '#3F51B5', '#009688', '#FF9800', '#795548', '#607D8B']
const dataList = ref([] as Array<FlowItem>)
for (let i = 0; i < 30; i++) {
dataList.value.push({
height: 100 + Math.floor(Math.random() * 200),
color: colors,
text: '卡片 ' + (i + 1)
})
}
</script>


场景二:商品列表瀑布流。两列布局,支持下拉刷新和上拉加载。关键属性:cross-axis-count='2',cross-axis-gap='8',main-axis-gap='8',refresher-enabled='true'。load-more插槽的flow-item设置type='2'。

<template>
<waterflow :cross-axis-count='2' :cross-axis-gap='8' :main-axis-gap='8'
:padding=''
:refresher-enabled='true' :refresher-triggered='isRefreshing'
@refresherrefresh='onRefresh'
@scrolltolower='loadMore'
style='flex: 1;'>
<flow-item v-for='(item, index) in productList' :key='index' type='1'>
<view style='background-color: white; border-radius: 8px; overflow: hidden; margin-bottom: 8px;'>
<image :src='item.image' style='width: 100%; height: 200px;' mode='aspectFill'></image>
<view style='padding: 8px;'>
<text style='font-size: 14px; color: #333; lines: 2;'>{{item.title}}</text>
<view style='flex-direction: row; align-items: center; margin-top: 6px;'>
<text style='font-size: 16px; color: #F44336; font-weight: bold;'>¥{{item.price}}</text>
<text style='font-size: 12px; color: #999; margin-left: 5px; text-decoration: line-through;'>¥{{item.originalPrice}}</text>
</view>
<text style='font-size: 12px; color: #999; margin-top: 4px;'>{{item.sales}}人付款</text>
</view>
</view>
</flow-item>
<flow-item slot='load-more' type='2'>
<view style='padding: 15px; align-items: center;'>
<text style='font-size: 14px; color: #999;'>{{loadingText}}</text>
</view>
</flow-item>
</waterflow>
</template>
<script setup lang='uts'>
type Product = {
title: string
image: string
price: string
originalPrice: string
sales: string
}
const productList = ref([] as Array<Product>)
const isRefreshing = ref(false)
const loadingText = ref('加载更多...')
const page = ref(1)
const loadProducts = () => {
productList.value = []
for (let i = 1; i <= 20; i++) {
productList.value.push({
title: '这是商品标题,可能会比较长,需要两行显示',
image: 'https://picsum.photos/200/' + (200 + i * 10),
price: (99 + i * 10).toString(),
originalPrice: (199 + i * 10).toString(),
sales: (100 + i * 50).toString()
})
}
}
const onRefresh = () => {
isRefreshing.value = true
page.value = 1
loadingText.value = '加载更多...'
setTimeout(() => {
loadProducts()
isRefreshing.value = false
uni.showToast({ title: '刷新成功', icon: 'success' })
}, 1500)
}
const loadMore = () => {
if (loadingText.value == '没有更多了') return
loadingText.value = '加载中...'
setTimeout(() => {
const start = productList.value.length
for (let i = 1; i <= 15; i++) {
productList.value.push({
title: '新加载的商品标题 ' + (start + i),
image: 'https://picsum.photos/200/' + (200 + (start + i) * 10),
price: (99 + (start + i) * 10).toString(),
originalPrice: (199 + (start + i) * 10).toString(),
sales: (100 + (start + i) * 50).toString()
})
}
page.value++
if (page.value >= 5) {
loadingText.value = '没有更多了'
} else {
loadingText.value = '加载更多...'
}
}, 1000)
}
onMounted(() => {
loadProducts()
})
</script>


场景三:图片瀑布流。类似小红书,图片高度随机,展示作者和点赞数。

<template>
<waterflow :cross-axis-count='2' :cross-axis-gap='6' :main-axis-gap='6'
:padding=''
style='flex: 1;'>
<flow-item v-for='(item, index) in imageList' :key='index' type='1'>
<view style='border-radius: 8px; overflow: hidden; background-color: white;'>
<image :src='item.url' :style='{ width: "100%", height: item.height + "px" }' mode='aspectFill'></image>
<view style='padding: 8px;'>
<text style='font-size: 14px; color: #333; lines: 2;'>{{item.desc}}</text>
<view style='flex-direction: row; align-items: center; margin-top: 6px; justify-content: space-between;'>
<view style='flex-direction: row; align-items: center;'>
<image :src='item.avatar' style='width: 20px; height: 20px; border-radius: 10px; margin-right: 5px;'></image>
<text style='font-size: 12px; color: #666;'>{{item.author}}</text>
</view>
<text style='font-size: 12px; color: #999;'>{{item.likes}}</text>
</view>
</view>
</view>
</flow-item>
</waterflow>
</template>
<script setup lang='uts'>
type ImageItem = {
url: string
height: number
desc: string
avatar: string
author: string
likes: string
}
const imageList = ref([] as Array<ImageItem>)
for (let i = 0; i < 20; i++) {
const h = 150 + Math.floor(Math.random() * 150)
imageList.value.push({
url: 'https://picsum.photos/200/' + h,
height: h,
desc: '这是第 ' + (i + 1) + ' 张图片的描述,可能会比较长',
avatar: 'https://picsum.photos/50/50',
author: '用户' + (i + 1),
likes: (Math.floor(Math.random() * 1000)).toString()
})
}
</script>


场景四:三列瀑布流。设置cross-axis-count='3'即可。

<template>
<waterflow :cross-axis-count='3' :cross-axis-gap='5' :main-axis-gap='5'
:padding=''
style='flex: 1;'>
<flow-item v-for='(item, index) in dataList' :key='index' type='1'>
<view :style='{ height: item.height + "px", backgroundColor: item.color, borderRadius: "5px", alignItems: "center", justifyContent: "center" }'>
<text style='font-size: 13px; color: white;'>{{item.text}}</text>
</view>
</flow-item>
</waterflow>
</template>
<script setup lang='uts'>
type FlowItem = {
height: number
color: string
text: string
}
const colors = ['#E91E63', '#9C27B0', '#3F51B5', '#009688', '#FF9800', '#795548']
const dataList = ref([] as Array<FlowItem>)
for (let i = 0; i < 30; i++) {
dataList.value.push({
height: 80 + Math.floor(Math.random() * 150),
color: colors,
text: '' + (i + 1)
})
}
</script>


场景五:自定义下拉刷新。设置refresher-default-style='none',使用slot='refresher'的flow-item,并监听refresherpulling事件改变提示文字。注意自定义刷新元素不要放在第一个子元素位置。

<template>
<waterflow
refresher-default-style='none'
:refresher-enabled='true'
:refresher-triggered='isRefreshing'
@refresherpulling='onPulling'
@refresherrefresh='onRefresh'
@refresherrestore='onRestore'
@refresherabort='onAbort'
:cross-axis-count='2' :cross-axis-gap='8' :main-axis-gap='8'
style='flex: 1;'>
<flow-item v-for='(item, index) in dataList' :key='index' type='1'>
<view style='padding: 8px;'>
<view :style='{ height: item.height + "px", backgroundColor: "#E3F2FD", borderRadius: "8px", alignItems: "center", justifyContent: "center" }'>
<text style='font-size: 15px; color: #1976D2;'>{{item.text}}</text>
</view>
</view>
</flow-item>
<flow-item slot='refresher' type='2'>
<view style='height: 50px; align-items: center; justify-content: center;'>
<text style='font-size: 14px; color: #2196F3;'>{{refreshText}}</text>
</view>
</flow-item>
</waterflow>
</template>
<script setup lang='uts'>
type FlowItem = {
height: number
text: string
}
const dataList = ref([] as Array<FlowItem>)
const isRefreshing = ref(false)
const refreshText = ref('下拉刷新')
const onPulling = (event: UniRefresherEvent) => {
if (event.detail.dy > 45) {
refreshText.value = '释放立即刷新'
} else {
refreshText.value = '下拉刷新'
}
}
const onRefresh = () => {
isRefreshing.value = true
refreshText.value = '正在刷新...'
setTimeout(() => {
loadData()
isRefreshing.value = false
}, 1500)
}
const onRestore = () => {
refreshText.value = '下拉刷新'
}
const onAbort = () => {
refreshText.value = '下拉刷新'
}
const loadData = () => {
dataList.value = []
for (let i = 0; i < 20; i++) {
dataList.value.push({
height: 100 + Math.floor(Math.random() * 150),
text: '卡片 ' + (i + 1)
})
}
}
onMounted(() => {
loadData()
})
</script>


场景六:嵌套滚动。将waterflow嵌套在scroll-view中,使用type='nested',并设置waterflow的associative-container='nested-scroll-view',配合nested-scroll-header和nested-scroll-body。

<template>
<scroll-view type='nested' style='flex: 1;'>
<nested-scroll-header>
<view style='height: 200px; background-color: #E3F2FD; align-items: center; justify-content: center;'>
<text style='font-size: 18px; color: #1976D2;'>头部区域</text>
</view>
</nested-scroll-header>
<nested-scroll-body>
<waterflow
associative-container='nested-scroll-view'
:cross-axis-count='2' :cross-axis-gap='8' :main-axis-gap='8'
style='flex: 1;'>
<flow-item v-for='i in 30' :key='i' type='1'>
<view style='padding: 8px;'>
<view :style='{ height: (100 + i * 10) + "px", backgroundColor: "#E8F5E9", borderRadius: "8px", alignItems: "center", justifyContent: "center" }'>
<text>卡片 {{i}}</text>
</view>
</view>
</flow-item>
</waterflow>
</nested-scroll-body>
</scroll-view>
</template>


完整实战示例:一个商品瀑布流综合页面,包含标题栏、两列商品、下拉刷新、上拉加载和滚动监听。

<template>
<view style='flex: 1;'>
<view style='height: 45px; background-color: white; border-bottom: 1px solid #eee; align-items: center; justify-content: center;'>
<text style='font-size: 16px; font-weight: bold;'>商品瀑布流</text>
</view>
<waterflow
:cross-axis-count='2' :cross-axis-gap='8' :main-axis-gap='8'
:padding=''
:refresher-enabled='true' :refresher-triggered='isRefreshing'
@refresherrefresh='onRefresh'
@scrolltolower='loadMore'
@scroll='onScroll'
style='flex: 1;'>
<flow-item v-for='(item, index) in productList' :key='index' type='1'>
<view style='background-color: white; border-radius: 8px; overflow: hidden; margin-bottom: 8px;'>
<image :src='item.image' :style='{ width: "100%", height: item.imgHeight + "px" }' mode='aspectFill'></image>
<view style='padding: 8px;'>
<text style='font-size: 14px; color: #333; lines: 2;'>{{item.title}}</text>
<view style='flex-direction: row; align-items: center; margin-top: 6px;'>
<text style='font-size: 16px; color: #F44336; font-weight: bold;'>¥{{item.price}}</text>
<text style='font-size: 12px; color: #999; margin-left: 5px; text-decoration: line-through;'>¥{{item.originalPrice}}</text>
</view>
<view style='flex-direction: row; align-items: center; margin-top: 4px; justify-content: space-between;'>
<text style='font-size: 12px; color: #999;'>{{item.sales}}人付款</text>
<text style='font-size: 12px; color: #FF9800;'>{{item.shop}}</text>
</view>
</view>
</view>
</flow-item>
<flow-item slot='load-more' type='2'>
<view style='padding: 15px; align-items: center;'>
<text style='font-size: 14px; color: #999;'>{{loadingText}}</text>
</view>
</flow-item>
</waterflow>
</view>
</template>
<script setup lang='uts'>
type Product = {
title: string
image: string
imgHeight: number
price: string
originalPrice: string
sales: string
shop: string
}
const productList = ref([] as Array<Product>)
const isRefreshing = ref(false)
const loadingText = ref('加载更多...')
const page = ref(1)
const scrollPos = ref(0)
const shops = ['旗舰店', '自营店', '专营店', '专卖店']
const loadProducts = () => {
productList.value = []
for (let i = 1; i <= 20; i++) {
const h = 180 + Math.floor(Math.random() * 100)
productList.value.push({
title: '这是商品标题,可能会比较长需要两行显示 ' + i,
image: 'https://picsum.photos/200/' + h,
imgHeight: h,
price: (99 + i * 10).toString(),
originalPrice: (199 + i * 10).toString(),
sales: (100 + i * 50).toString(),
shop: shops
})
}
}
const onRefresh = () => {
isRefreshing.value = true
page.value = 1
loadingText.value = '加载更多...'
setTimeout(() => {
loadProducts()
isRefreshing.value = false
uni.showToast({ title: '刷新成功', icon: 'success' })
}, 1500)
}
const loadMore = () => {
if (loadingText.value == '没有更多了') return
loadingText.value = '加载中...'
setTimeout(() => {
const start = productList.value.length
for (let i = 1; i <= 15; i++) {
const h = 180 + Math.floor(Math.random() * 100)
productList.value.push({
title: '新加载的商品 ' + (start + i),
image: 'https://picsum.photos/200/' + h,
imgHeight: h,
price: (99 + (start + i) * 10).toString(),
originalPrice: (199 + (start + i) * 10).toString(),
sales: (100 + (start + i) * 50).toString(),
shop: shops[(start + i) % shops.length]
})
}
page.value++
if (page.value >= 5) {
loadingText.value = '没有更多了'
} else {
loadingText.value = '加载更多...'
}
}, 1000)
}
const onScroll = (event: UniScrollEvent) => {
scrollPos.value = event.detail.scrollTop
}
onMounted(() => {
loadProducts()
})
</script>


注意事项:自定义下拉刷新元素不能放在第一个子元素位置;load-more插槽的flow-item需设置type='2';鸿蒙平台滚动相关事件需API 20及以上版本,load-more插槽需API 18及以上版本;waterflow子组件仅支持flow-item。

总结:waterflow为鸿蒙平台uni-app x提供了高效的瀑布流实现,底层基于回收复用,支持多列布局、下拉刷新、上拉加载及嵌套滚动。开发时需注意鸿蒙版本兼容性和子组件限制,合理使用属性与事件即可快速构建商品列表、图片社区等场景。

热心网友1 发表于 2026-9-20 19:30:00

Re: 鸿蒙uni-app x瀑布流waterflow组件API与适配实战

感谢楼主分享,鸿蒙 uni-app x 里 waterflow 的这套总结很实用。之前容易把瀑布流和 list-view、scroll-view 的选型搞混,现在清楚了:多列用 waterflow,单列用 list-view,其他用 scroll-view。兼容性部分也提醒得很及时,API 20 以上才支持滚动相关事件、API 18 以上才支持 load-more 插槽,做低版本适配时得提前留好降级方案。场景一的基础写法很直观,随机高度能很快验证多列回收效果;场景二两列商品布局里 cross-axis-count、cross-axis-gap、main-axis-gap、padding 数组和 refresher 相关属性的配合也很清楚,load-more 用 type 区分类型这个细节很有用。另外 flow-item 目前只能作为子组件、其他组件不可见这一点确实是个容易踩的坑。等楼主把场景二后面的代码补完,想继续看看上拉加载的数据追加和刷新状态复位怎么处理。再次感谢。

热心网友3 发表于 2026-9-20 19:40:00

Re: 鸿蒙uni-app x瀑布流waterflow组件API与适配实战

感谢楼主分享,最近正好在鸿蒙上折腾瀑布流,之前一直用 list-view 手动分列,回收复用和高度计算都挺头疼的,看完这篇思路清晰了不少。 有几个地方想请教下。flow-item 的 type 属性具体是怎么区分类型的,是不同 type 的 item 在布局或者高度计算上有差异吗,还是主要给开发者自己标识用? 另外 load-more 插槽那里,type='2' 的 flow-item 是单独占一整行,还是会跟着瀑布流走?如果跟着走的话,加载更多的提示怎么保证居中或者横跨两列? 还有 associative-container 这个嵌套滚动关联容器,和 scroll-view 嵌套的场景比较多,能不能再展开讲讲,跟直接嵌套有什么区别,什么时候需要用它。 帖子好像到商品列表代码那里截断了,后面还有场景三和适配坑的话期待继续更新。

热心网友3 发表于 2026-9-20 19:40:00

Re: 鸿蒙uni-app x瀑布流waterflow组件API与适配实战

感谢分享,正好在鸿蒙端做类似需求,waterflow 和 list-view 都基于回收复用机制这个点讲得很清楚,子组件滑出屏幕会被回收,对性能这块心里有底了。 兼容性提醒也很关键,鸿蒙 4.81 开始支持,API 20 以上才有滚动相关事件,API 18 以上才支持 load-more 插槽,而且不支持 Web 和微信小程序,这些如果没提前看到很容易踩坑。组件选择上瀑布流用 waterflow、单列用 list-view、其他用 scroll-view,这个判断思路很实用。 两个场景里,基础瀑布流用 v-for 加 flow-item 随机高度很直观,商品列表两列布局配合 refresher-enabled、refresher-triggered、@refresherrefresh 和 @scrolltolower,基本把下拉刷新和上拉加载串起来了。我比较关注 load-more 插槽里 flow-item 设 type='2',和普通卡片 type='1' 区分后,在 @scrolltolower 里怎么判断是否还有更多数据、什么时候停止加载?另外 refresher-triggered 在刷新结束后是手动改回 false 吗?如果后续能把商品列表场景的脚本和 loadMore 逻辑补全就更好了,期待继续更新。
页: [1]
查看完整版本: 鸿蒙uni-app x瀑布流waterflow组件API与适配实战