在鸿蒙平台使用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[i % colors.length],
- 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='[0, 8, 8, 8]'
- :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='[6, 6, 6, 6]'
- 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='[5, 5, 5, 5]'
- 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[i % colors.length],
- 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='[0, 8, 8, 8]'
- :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[i % shops.length]
- })
- }
- }
- 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提供了高效的瀑布流实现,底层基于回收复用,支持多列布局、下拉刷新、上拉加载及嵌套滚动。开发时需注意鸿蒙版本兼容性和子组件限制,合理使用属性与事件即可快速构建商品列表、图片社区等场景。 |