Skip to content

Waterfall 瀑布流

高性能虚拟瀑布流 / 网格容器:内置 ShLazyImage 懒加载、触底分页加载、双滚动模式,专为图片流、商品流、内容 Feed 设计。

为什么不抖动、为什么快

零抖动 —— 卡片高度在布局阶段一次性确定(真实宽高比 → 兜底比例池按下标取模),图片以 cover 填充,加载前后 0 回流;scrollbar-gutter: stable 防止滚动条出现引起列宽跳变。 高性能 —— 布局数据绕开深层响应式;虚拟列表每列二分查找可视区间,滚动 rAF 节流一帧一算;卡片绝对定位 + translate3d GPU 合成;分页追加走增量布局,已有卡片不重排。 可靠触底 —— 加载更多基于 IntersectionObserver 哨兵,与滚动来源解耦,容器滚动 / 页面滚动都能触发;内容不足一屏自动续载首屏。

基础用法

传入 items,监听 load-more 加载下一页(追加进数组即可,组件做增量布局);用 loading / finished 控制底部状态。默认插槽缺省时,组件直接用 ShLazyImage 渲染图片。

已加载 0 条
暂无数据
vue
<script setup lang="ts">
import { ref } from 'vue'
import { ShWaterfall } from 'sh-design'

const items = ref<any[]>([])
const loading = ref(false)
const finished = ref(false)
let page = 0

async function onLoadMore() {
  loading.value = true
  const list = await api.fetchList(page++) // [{ id, src, width, height }]
  items.value.push(...list)
  loading.value = false
  if (list.length === 0) finished.value = true
}
</script>

<template>
  <!-- 默认 scroller="self":由父容器给定高度,组件内部滚动 -->
  <div style="height: 600px">
    <ShWaterfall
      :items="items"
      :cols="3"
      :gap="12"
      :loading="loading"
      :finished="finished"
      @load-more="onLoadMore"
    />
  </div>
</template>

自定义卡片(#item 插槽)

#item 插槽提供 { item, index, width, height }。卡片有图片下方的固定区域(标题/操作栏)时,用 extra-height 告知组件,布局时会为每张卡片预留该高度。

vue
<ShWaterfall :items="items" :cols="3" :extra-height="34" @load-more="onLoadMore">
  <template #item="{ item, height }">
    <div class="card">
      <ShLazyImage :src="item.src" lazy="observer" fit="cover" :radius="8" :height="height - 34" />
      <div class="card-title">{{ item.title }}</div>
    </div>
  </template>
</ShWaterfall>

双滚动模式

  • scroller="self"(默认):组件自身是滚动容器,需要父级给定高度——适合后台工作区、弹窗、固定面板。
  • scroller="window":组件自然撑开、不产生内部滚动条,跟随页面(或任意祖先滚动容器)滚动——适合 C 端长页 Feed 流。

两种模式的触底加载都由 IntersectionObserver 哨兵驱动,虚拟列表照常工作。

vue
<!-- 页面滚动模式:无需给高度 -->
<ShWaterfall scroller="window" :items="items" :cols="2" @load-more="onLoadMore" />

网格布局

layout="grid" 切换为等高网格,单元格比例由 grid-ratio(height / width)控制,如 1 为正方形、0.75 为 4:3 横图。

vue
<ShWaterfall layout="grid" :grid-ratio="0.75" :items="items" :cols="4" @load-more="onLoadMore" />

零抖动的高度策略

按优先级取卡片高度,布局时一次定死,加载前后不变

  1. 数据带真实尺寸(width / height 字段,字段名可用 width-key / height-key 改)→ 按真实宽高比精确计算,与图片完全一致;
  2. 没有尺寸 → 从 ratios 比例池按下标取模取值(确定性:重排/回看结果一致),图片 cover 填充。

建议接口尽量返回图片原始宽高(大多图床/OSS 都有),可获得像素级还原的瀑布流。

入场动画

默认开启 animate:新进入视口的卡片上滑 + 回弹入场,列数/布局切换时已有卡片平滑归位。追求极简可关闭:

vue
<ShWaterfall :animate="false" :items="items" />

底部与空状态自定义

文案用属性改:loading-text / finished-text / empty-text;结构用插槽换:

vue
<ShWaterfall :items="items" :loading="loading" :finished="finished" @load-more="onLoadMore">
  <template #loading><MySpinner /> 拼命加载中</template>
  <template #finished>—— 我也是有底线的 ——</template>
  <template #empty><MyEmpty description="什么都没有" /></template>
</ShWaterfall>

API

Props

属性说明类型默认值
items数据列表(对象或图片 URL 字符串),分页时往数组追加即可(object | string)[][]
layout布局模式'waterfall' | 'grid''waterfall'
cols列数number2
gap卡片间距(px)number12
scroller滚动容器:self 组件内滚动(父级给高度)/ window 跟随页面滚动'self' | 'window''self'
loading是否加载中(触底后不重复触发)booleanfalse
finished是否已全部加载完booleanfalse
animate卡片入场动画(上滑回弹)booleantrue
src-key图片地址字段名(默认渲染时用)string'src'
item-key唯一 key 字段名(虚拟列表节点复用)string'id'
width-key / height-key原始宽/高字段名,存在时按真实比例零抖动布局string'width' / 'height'
ratios无真实宽高时的兜底比例池(height/width),按下标取模number[][0.75, 0.9, 1, 1.2, 1.35]
grid-ratiogrid 布局单元格比例(height/width)number1
extra-height每张卡片的额外固定高度(如标题区,px)number0
buffer视口外额外渲染缓冲(px)number300
threshold哨兵提前量(px),距底部多远触发加载number200
radius默认渲染 ShLazyImage 的圆角string | number8
loading-text / finished-text / empty-text底部/空状态文案string

Events

事件说明参数
load-more触底(或内容不足一屏)时触发,在此加载下一页
item-click点击卡片{ item, index }
scroll滚动(rAF 节流){ scrollTop }

Slots

插槽说明作用域参数
item自定义卡片内容,缺省渲染 ShLazyImage{ item, index, width, height }
loading底部加载中内容
finished底部加载完成内容
empty空数据内容

Expose

方法说明
relayout()手动全量重排
scrollTo(top, smooth?)滚动到指定位置(self 模式)
check()主动检查是否需要触发 load-more

Released under the MIT License.