Masonry 瀑布流
import { Masonry } from '@apollo-design/ui';瀑布流布局:条目按「当前最矮的一列」依次落位,高度实测而非预设。
何时使用
- 内容高度参差不齐,但希望整体视觉上「填满」容器(图片墙、卡片流、商品列表)。
- 需要按断点控制列数与间距。
代码演示
基本 #
瀑布流布局:条目按「当前最矮的一列」依次落位。
⚠️ antd 的 demo 用 Card 渲染条目,本仓 Card 尚未落地 ⇒ 用原生 div + 等价内联样式替换。
响应式 #
列数与间距都支持按断点配置 —— 命中规则是「从大到小取第一个已配置的断点」。
动态 #
增删条目;onLayoutChange 把算出来的列号写回 items,写回后条目就「钉」在该列。
⚠️ 与 antd 的 demo 有两处有意差异:① 新增条目的高度用确定值
(上游用 Math.random(),每次渲染都不同 ⇒ demo 冒烟测试不可复现);
② Card / CloseOutlined 用原生 div / 文本 × 替换(本仓 Card 尚未落地)。
内容变化 #
条目内容高度会变时用 fresh —— 它给每个条目各挂一个 ResizeObserver,
点一下就重新量测并重排。
⚠️ fresh 不改变 DOM 结构(观察器不产节点,上游的 ResizeObserver 是 cloneElement)。
与 antd 的 demo 的差异:高度用确定值而不是 Math.random()。
图片 #
使用不同宽高比的本地 SVG 图片;图片原生 load 事件触发重新布局,不依赖外部图片服务。
Local SVG images have different aspect ratios. Their native load events trigger layout updates without an external image service.
自定义样式 #
classNames / styles 两个语义槽都支持对象与函数两种形态;
函数形态的入参 props.columns 是解析后的列数(不是原始 prop)。
styles 使用原生 CSS 值:height、padding、borderRadius 等非零长度需显式单位,
如 '260px'、'20px'、'12px';opacity 等无单位属性仍可用数字。
这与 gutter 等组件尺寸参数的数字像素约定不同。显式根高度会覆盖测量高度,须能容纳所有卡片。
styles uses native CSS values. Give nonzero lengths such as height, padding and border radius explicit units;
unitless properties such as opacity may remain numeric. An explicit root height overrides the measured height and must contain every tile.
⚠️ 与 antd 的 demo 的差异:上游用 antd-style 的 createStaticStyles 生成类名,
本仓没有这个依赖 ⇒ 用普通类名替换;Flex / Divider / Typography 换成原生元素。
缺口登记在 README.md §7。
见 demo/(6 个,覆盖 antd 的主要用户场景)。
| demo | 内容 |
|---|---|
basic |
基本(4 列 + gutter 16) |
responsive |
列数与间距都按断点配置 |
dynamic |
增删条目 + onLayoutChange 把列号写回 items |
fresh |
条目内容高度会变时用 fresh(每个条目各挂 ResizeObserver) |
image |
条目是图片(加载后高度才确定) |
style-class |
classNames / styles 的对象与函数两种形态 |
API
Props
| 参数 | 说明 | 类型 | 默认值 | 全局配置 |
|---|---|---|---|---|
| items | 数据源。每项 { key, data, column?, height?, children? } |
MasonryItemType<T>[] |
— | × |
| columns | 列数。对象形态按断点解析(从大到小取第一个已配置的断点;都没命中 ⇒ columns.xs ?? 1) |
number | Partial<Record<Breakpoint, number>> |
3 |
× |
| gutter | 间距。复用 Grid 的 Gutter(数字 / 字符串 / 断点对象 / [水平, 纵向]) |
Gutter |
0 |
× |
| fresh | 为每个条目各挂一个 ResizeObserver(内容高度会变时用)。⚠️ 不改变 DOM 结构 |
boolean |
false |
× |
| onLayoutChange | layoutChange 的 Vue 事件监听器;当前条目全部完成测量后接收 {...item, column} |
(sortInfo: MasonryLayoutItem<T>[]) => void |
— | × |
| classNames | 语义化类名(root / item),支持函数形态 |
MasonrySemanticClassNames | ((info) => …) |
— | ✅(masonry.classNames) |
| styles | 语义化样式(root / item),支持函数形态 |
MasonrySemanticStyles | ((info) => …) |
— | ✅(masonry.styles) |
| prefixCls | 类名前缀 | string |
从 ConfigProvider 取,兜底 apollo-masonry |
× |
| class / style | 根元素原生 attrs(不是 Props);调用方样式覆盖同名语义样式及计算得到的容器高度 | Vue class/style 绑定 | — | × |
gutter 数字按像素处理;CSS 字符串如 ['2rem', 'calc(0.5rem + 4px)']、var() 和百分比保留 CSS 单位。横向百分比以容器宽度为基准;纵向百分比以当前条目零间距排布的高度为基准,提供显式根高度时优先使用该高度,避免间距与根高相互反馈。局部隐藏测量节点观察字体、视口和 CSS 变量变化,双轴均切回数字或组件卸载时释放。
MasonryItemType
| 字段 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| key | 唯一标识(数字会内部 String() 化) |
string | number |
— |
| data | 业务数据,泛型 T 由 items 推导并传给 #item | T |
— |
| column | 指定落在第几列(0 基)。超界会夹到最后一列 | number |
自动选最矮列 |
| height | 传给 #item 和 layoutChange 的可选元数据,不设置实测布局高度 | number |
— |
| children | 未提供 #item 时使用的回退内容 | VNodeChild |
— |
Events
| 名称 | 说明 | 参数 |
|---|---|---|
| layoutChange | 完整实测布局变化。原生 Vue 监听器(包括 .once)接收同一个事件,事件发布不依赖 onLayoutChange prop |
(sortInfo: MasonryLayoutItem<T>[]) |
Ref
| 名称 | 类型 | 说明 |
|---|---|---|
| nativeElement | () => HTMLDivElement | null |
当前根元素;卸载后返回 null |
开发版 API 修正(0.0.0):MasonryRef 现与 MasonryExpose 一致,使用 masonryRef.value?.nativeElement() 获取根元素;旧 MasonryRef.nativeElement 的直接元素类型与已文档化的 getter 不符。本次直接修正,无兼容层或弃用期。
Slots
| 名称 | 参数 | 说明 |
|---|---|---|
| item | MasonryItemSlotProps<T>:{ key, data, index, column, children?, height? } |
当前条目;优先于 item.children,空槽也覆盖回退内容 |
column 为当前测量列号,首次测量前暂为 0。条件插槽、同 key 替换数据(包括响应式数组原地替换条目对象)和语义样式函数均读取当前输入;组件更新后会重测量。CSS 过渡或后代独立尺寸变化可用 fresh 持续观察。泛型也传给 layoutChange。旧 itemRender prop 已移除,见迁移说明。
Theme
Component Token
Masonry 没有 Component Token(与 antd 逐字一致:ComponentToken 是空接口)。
它只消费全局 alias:
| token | 用在哪 |
|---|---|
motionDurationSlow |
条目淡入(-fade-appear)与位置过渡 |
motionDurationFast |
条目淡出(-fade-leave) |
motionEaseOut |
上述三条的缓动 |
设计说明
- 排布算法:按
items顺序,每项落到当前累计高度最小的那一列(平局取最左); 容器高度 = 最高列 − 一个纵向间距。详见hooks/positions.ts的文件头(含三处易错点)。 - SSR 直接输出内容:初始 items 与 #item 在服务端渲染;测量前采用正常网格流和自然高度,客户端挂载后按实际高度切换为瀑布流定位。网格按行排列,测量后才采用最短列算法。水合保留条目、输入节点和输入值,不再复制 React effect 的空首屏。
- 高度是实测的:
item.height从不被读;首帧量测在 Vue 更新 DOM 之后。 - 图片事件参与测量:根节点以原生捕获监听 load/error;非冒泡的 img 事件也会触发去抖量测与重排,fresh=false 同样有效。一般内容的尺寸变化仍可用 fresh 的逐项 ResizeObserver。
- 本组件零 ARIA:容器与条目都是普通
div(无role、无tabindex)—— 语义由 #item 内容和调用方的原生 attrs 提供。 - 原生 attrs 透传:id/title/事件以及字符串、对象、数组 class/style 按 Vue 规则路由到根节点。attrs 在 render 期读取,调用方 style 覆盖同名语义 root 样式,更新不替换根 DOM。默认不添加 role/ARIA,调用方可通过原生 attrs 提供必要语义。