夜雨聆风学习资料网

ARTICLE · 1031530

彻底解决 uni-app 瀑布流高度错乱 原生组件与 Vue 自定义方案全解析

彻底解决 uni-app 瀑布流高度错乱 原生组件与 Vue 自定义方案全解析

uni-app 里"瀑布流组件"其实分两条路线:Vue 侧 UI 组件(uView、插件市场轮子,跨端最稳)和原生 waterfall/waterflow 组件(nvue/App 长列表性能更好)。选哪个看你的运行平台和要不要自己控样式。

一、开箱即用:现成组件怎么选

uView u-waterfall:左右两列用作用域插槽 v-slot:left / v-slot:right 拿到 leftList / rightList 渲染,自带 v-model 接数据、支持 remove / clear 方法和懒加载、加载更多搭配,H5/App/各大小程序都支持,是 Vue 项目里最省事的方案。
插件市场轮子
fg-waterfall:零依赖、easycom 自动注册,v-model 绑数组,支持 left / right 插槽、上拉加载 @load-more、空态插槽,兼容 Vue2/Vue3 和 H5/小程序/App(vue)。
w-waterfall:支持预估高度虚拟列表、自动选较短列、页面滚动,适合卡片高度能预估的场景。
wfalls-flow:老牌图片瀑布流示例,按图片+文本真实高度动态补位、流式加载、下拉刷新都带,适合图片流。
原生 waterfall(nvue)/ waterflow(uni-app X、App)waterfall 用 column-countcellheaderrefreshloading,滑出屏幕自动回收复用;waterflow 用 flow-item 子组件,配 cross-axis-count / main-axis-gap / cross-axis-gap,底层和 list-view 一致,长列表性能明显优于 scroll-view。注意 waterfall 是区域滚动,不触发页面级下拉刷新和 onReachBottomwaterflow 的 Web、微信小程序兼容不在支持范围内,主要跑 Android/iOS/HarmonyOS App。

二、自己封装:核心就一个"最短列分配"算法

别用 index % 2 奇偶硬分,高度一参差就变"假瀑布"。正确做法是维护列高数组,新 item 永远塞进当前最矮的那一列:

<template>  <view class="waterfall">    <view class="col" v-for="(col, ci) in columns" :key="ci">      <view        v-for="item in col"        :key="item.id"        class="card"        :style="{ height: item._renderH + 'px' }"      >        <image          :src="item.cover"          mode="widthFix"          @load="onImgLoad(item, $event)"        />        <view class="meta">{{ item.title }}</view>      </view>    </view>  </view></template><script setup>import { ref, watch } from 'vue'const props = defineProps({  list: { type: Array, default: () => [] },  columnCount: { type: Number, default: 2 }})const columns = ref([])        // [[item...], [item...]]const colHeights = ref([])     // [0, 0] 各列累计高度const colWidth = ref(0)        // 单列宽度 px,onReady 里按屏宽算function reset() {  columns.value = Array.from({ length: props.columnCount }, () => [])  colHeights.value = Array.from({ length: props.columnCount }, () => 0)}// 把单个 item 塞进最矮列function placeItem(item) {  let min = 0  for (let i = 1; i < colHeights.value.length; i++) {    if (colHeights.value[i] < colHeights.value[min]) min = i  }  columns.value[min].push(item)  // 预估高度:有服务端宽高就用宽高比算,没有先按占位高  const h = item._renderH || 240  colHeights.value[min] += h + 16 // 16 为卡片间距}// 图片加载完拿到真实宽高,回填高度并重排function onImgLoad(iteme) {  const natW = e?.detail?.width || item.w || 1  const natH = e?.detail?.height || item.h || 1  const scaledH = Math.round((colWidth.value / natW) * natH)  item._renderH = scaledH + 56 // 56 为标题区预估高,按实际样式调  // 简单做法:整列重排;数据量大可只改该 item 所在列  rebuild()}function rebuild() {  reset()  props.list.forEach(placeItem)}watch(() => props.list, rebuild, { immediate: true })</script><style scoped>.waterfall { displayflexgap16rpx; align-itemsflex-start; }.col { flex1displayflexflex-directioncolumngap16rpx; }.card { background#fffborder-radius16rpx; overflowhidden; }.card image { width100%displayblock; }.meta { padding12rpx 16rpx; font-size26rpx; }</style>

触底加载用 scroll-view 的 @scrolltolower,记得设固定高度和 scroll-y,并加 loading 锁防重复请求;用页面滚动就让父页面把触底信号传进组件。

三、图片高度错位的坑(最常翻车)

后端返回 width / height 或 aspectRatio 最稳,前端用 高度 = 列宽 × 原高 / 原宽 预设占位,图片没加载完布局也不跳。
没后端配合就用 image @load 拿 e.detail.width/height,或 uni.getImageInfo 预取,拿到再回填 _renderH 触发重排,别在滚动中狂调 createSelectorQuery 查 DOM,真机会卡
加载前先给卡片一个占位高(骨架屏或固定 min-height),否则 iOS 微信容易白一帧、列高全算成 0 全堆第一列。
scroll-view 里别用 position: absolute 包卡片,iOS 微信触底事件会丢;父容器高度用 rpx 或 calc(100vh - xxx) 定死。

相关学习资料