MasonryGrid 瀑布流

通用数据驱动瀑布流布局组件,支持响应式列数、图片懒加载、无限滚动与空态占位。

何时使用

  • 展示图片墙、卡片流、资讯流等大量非等高内容时。
  • 需要按数据顺序横向逐行填充,并支持响应式列数与间距时。
  • 需要图片懒加载、触底无限滚动、空数据占位等能力时。

特性

  • 布局:flex 多列,第 i 项放入第 i % columns 列(横向逐行填充,顺序与数据一致)
  • 懒加载:默认插槽内用 <img :data-src> 的图片进入视口才真正加载,lazy 可关闭、lazyRootMargin 可调提前量
  • 无限滚动:底部哨兵进入视口且 hasMore && !loading 时触发 load-more
  • 空态:items 为空且非加载时显示"暂无数据",可用 #empty / #loading / #end 插槽定制

基础用法

每一项内容完全由调用方通过默认插槽渲染,作用域为 { item, index }。

<script setup lang="ts">
import { ref } from "vue";
import { MasonryGrid } from "@reglow/reui";

const cards = ref([
  { key: 1, title: "Ant Design", desc: "企业级中后台设计体系" },
  { key: 2, title: "Vue 3", desc: "渐进式 JavaScript 框架" },
  { key: 3, title: "Reui", desc: "业务级 Vue 组件库" },
]);
</script>

<template>
  <MasonryGrid :items="cards" :columns="3" :gutter="[16, 16]">
    <template #default="{ item, index }">
      <div class="rounded-lg border bg-card p-4">
        <h4>{{ index + 1 }}. {{ item.title }}</h4>
        <p class="text-sm text-muted-foreground">{{ item.desc }}</p>
      </div>
    </template>
  </MasonryGrid>
</template>

itemRender 渲染

当不便使用插槽时,可用 itemRender 函数返回 VNode(优先级低于默认插槽)。

<script setup lang="ts">
import { h } from "vue";
import { MasonryGrid } from "@reglow/reui";

const cards = [
  { key: 1, title: "Ant Design", desc: "企业级中后台设计体系" },
  { key: 2, title: "Vue 3", desc: "渐进式 JavaScript 框架" },
  { key: 3, title: "Reui", desc: "业务级 Vue 组件库" },
];

const itemRender = (item: { title: string }, index: number) =>
  h("div", { class: "rounded-lg border bg-card p-4" }, [
    h("h4", `${index + 1}. ${item.title}`),
  ]);
</script>

<template>
  <MasonryGrid :items="cards" :columns="3" :gutter="16" :item-render="itemRender" />
</template>

图片懒加载

在默认插槽内使用 data-src(组件在图片进入视口时才赋值 src),非常适合大量图片瀑布流。

<script setup lang="ts">
import { MasonryGrid } from "@reglow/reui";

// 演示用占位图:内联 SVG Data URI,无需外部网络
const palette = ["#7c8cff", "#69d2a8", "#ffb86b", "#ff7f9e", "#9ad0ff", "#e0aaff"];
const photoUrl = (i: number, height: number) => {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="${height}"><rect width="100%" height="100%" fill="${palette[i % palette.length]}"/><text x="200" y="${height / 2 + 6}" font-size="22" text-anchor="middle" fill="rgba(255,255,255,0.9)">${i + 1}</text></svg>`;
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
};

const pics = [...Array(50)].map((_, i) => ({
  key: i,
  src: photoUrl(i, 180 + (i % 5) * 60),
}));
</script>

<template>
  <div class="max-h-96 overflow-y-auto">
    <MasonryGrid :items="pics" :columns="3" :gutter="12">
      <template #default="{ item }">
        <img :data-src="item.src" alt="" class="w-full rounded-lg bg-muted" />
      </template>
    </MasonryGrid>
  </div>
</template>

无限滚动与空态

hasMore 为 true 时触底自动触发 load-more;为 false 时显示"没有更多了"。items 为空且不在加载时显示"暂无数据"(可用 #empty 覆盖)。

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { MasonryGrid } from "@reglow/reui";

const total = Array.from({ length: 48 }, (_, i) => ({
  key: i,
  title: `卡片 ${i + 1}`,
}));

const items = ref<{ key: number; title: string }[]>([]);
const loading = ref(false);
const hasMore = ref(true);
let seed = 0;

const loadMore = () => {
  if (loading.value) return;
  loading.value = true;
  setTimeout(() => {
    items.value.push(...total.slice(seed, seed + 8));
    seed += 8;
    if (seed >= total.length) hasMore.value = false;
    loading.value = false;
  }, 400);
};

// 首屏先加载一批,之后触底自动加载
onMounted(loadMore);
</script>

<template>
  <div class="max-h-72 overflow-y-auto">
    <MasonryGrid
      :items="items"
      :columns="2"
      :loading="loading"
      :has-more="hasMore"
      empty-text="这里还没有任何内容"
      @load-more="loadMore"
    >
      <template #default="{ item }">
        <div class="rounded-lg border bg-card p-4">{{ item.title }}</div>
      </template>
      <template #empty>自定义空态内容</template>
    </MasonryGrid>
  </div>
</template>

响应式列数与间距

columns / gutter 支持断点映射(xs/sm/md/lg/xl/xxl,断点取值同 Ant Design 栅格)。

<script setup lang="ts">
import { MasonryGrid } from "@reglow/reui";

// 演示用占位图:内联 SVG Data URI,无需外部网络
const palette = ["#7c8cff", "#69d2a8", "#ffb86b", "#ff7f9e", "#9ad0ff", "#e0aaff"];
const photoUrl = (i: number, height: number) => {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="${height}"><rect width="100%" height="100%" fill="${palette[i % palette.length]}"/></svg>`;
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
};

const photos = [...Array(16)].map((_, i) => ({
  key: i,
  url: photoUrl(i, 140 + (i % 4) * 50),
}));
</script>

<template>
  <div class="max-h-96 overflow-y-auto">
    <MasonryGrid
      :items="photos"
      :columns="{ xs: 1, sm: 2, md: 3, lg: 4 }"
      :gutter="{ xs: 8, sm: 16, lg: 24 }"
    >
      <template #default="{ item }">
        <img :data-src="item.url" alt="" class="w-full rounded-lg bg-muted" />
      </template>
    </MasonryGrid>
  </div>
</template>

语义化 class / style

可用 classNames、styles 定制 root / column / item 结构。

<script setup lang="ts">
import { MasonryGrid } from "@reglow/reui";

const cards = [
  { key: 1, title: "Ant Design", desc: "企业级中后台设计体系" },
  { key: 2, title: "Vue 3", desc: "渐进式 JavaScript 框架" },
  { key: 3, title: "Reui", desc: "业务级 Vue 组件库" },
  { key: 4, title: "Masonry", desc: "瀑布流布局组件" },
  { key: 5, title: "Vite", desc: "下一代前端构建工具" },
  { key: 6, title: "TypeScript", desc: "JavaScript 的超集" },
];
</script>

<template>
  <MasonryGrid
    :items="cards"
    :columns="4"
    :class-names="{ root: 'rounded-lg p-2', item: 'hover:opacity-85 transition-opacity' }"
  >
    <template #default="{ item }">
      <div class="rounded bg-background p-3 shadow-sm">
        <h4>{{ item.title }}</h4>
        <p class="text-sm text-muted-foreground">{{ item.desc }}</p>
      </div>
    </template>
  </MasonryGrid>
</template>

导入

import { MasonryGrid } from "@reglow/reui";

Props

参数说明类型默认值
items瀑布流数据项object[][]
columns列数(数字或断点映射)number | { xs?; sm?; md?; lg?; xl?; xxl? }3
gutter间距(水平/垂直)number | { 断点: number } | [GapType, GapType]0
lazy是否开启图片懒加载booleantrue
lazyRootMargin懒加载提前量string"200px"
loading是否加载中booleanfalse
hasMore是否还有更多booleantrue
emptyText空数据文案string"暂无数据"
loadingText加载中文案string"加载中…"
endText到底提示文案string"没有更多了"
itemRender自定义项渲染(返回 VNode,优先级低于默认插槽)(item, index) => VNode—
classNames语义化结构 class(root / column / item)Partial<Record<"root" | "column" | "item", string>>—
styles语义化结构 style(root / item)Partial<Record<"root" | "item", CSSProperties>>—
class根元素附加类名string—

gutter 为数字时水平/垂直同距;传 [水平, 垂直] 分开控制;断点映射取「当前档及以下已显式设置的最大档」。

断点

断点最小容器宽度
xs0
sm576
md768
lg992
xl1200
xxl1600

Slots

插槽说明
#default每个瀑布流项,作用域 { item, index }
#empty空数据占位(覆盖默认"暂无数据")
#loading加载中占位(首屏与滚动加载共用)
#end无更多提示占位(覆盖默认"没有更多了")

Events

事件说明回调参数
load-more触底需要加载更多()
layoutChange列数变化(column: number)

设计规范

  • 数据项可用 key(string \| number)提供稳定唯一键以获得正确的复用
  • 懒加载基于 DOM 约定:图片需带 data-src(不给 src),组件在图片进入视口时才赋值 src
  • 无限滚动触发后请将 loading 置 true 以显示加载态并避免重复触发;完成后置 false 复位,hasMore 置 false 停止