高性能列表,支持分组,并可为长列表开启虚拟滚动(仅渲染视口内的行)。
组件 API 参考 Ant Design Listy(List 的继任者,自 antd@6.6.0 提供)。
- 需要渲染长列表,又不想为每一行都付出挂载成本时 —— 开启
virtual 后只渲染视口内的行。 - 列表需要分组,并让分组标题吸顶时。
- 需要以命令式方式控制滚动位置(跳到某一项、某个分组或某个像素位置)时。
import { List, ListItemMeta } from "@reglow/reui";
通过 items + 默认插槽(作用域 { item, index })或 itemRender 属性渲染单行。
<script setup lang="ts">
const items = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
</script>
<template>
<List :items="items" :height="320" />
</template>
一万条数据的长列表,通过 virtual + height 开启虚拟滚动,仅渲染视口内的行。
<List :items="items" virtual :height="300">
<template #default="{ item }">{{ item }}</template>
</List>
通过 group 从数据项中取出分组键并渲染分组标题,开启 sticky 后分组标题会在滚动时吸顶(虚拟 / 非虚拟模式均支持)。
<script setup lang="ts">
const users = [
{ name: "Aaron Baker", time: "08:00", content: "commented on your merge request" },
{ name: "Alice Adams", time: "12:49", content: "invited you to the quarterly planning review" },
{ name: "Bella Carter", time: "10:00", content: "starred the report you shared yesterday" },
{ name: "Brian Diaz", time: "11:15", content: "assigned you a task that is due next Monday" },
// ...
];
const group = {
key: (i: any) => i.name[0],
title: (key: string) => key,
};
</script>
<template>
<List :items="users" :group="group" sticky :height="320" virtual>
<template #default="{ item }">
<div class="flex items-start gap-3">
<div class="flex size-9 shrink-0 items-center justify-center rounded-full bg-primary/10 font-medium text-primary">
{{ item.name[0] }}
</div>
<div>
<div class="flex items-baseline gap-2">
<span class="font-medium">{{ item.name }}</span>
<span class="text-xs text-muted-foreground">{{ item.time }}</span>
</div>
<div class="mt-1 text-sm text-muted-foreground">{{ item.content }}</div>
</div>
</div>
</template>
</List>
</template>
itemRender 可渲染任意复杂的内容,行高不必相同(自动测量校正)。
<script setup lang="ts">
import { h } from "vue";
const renderItem = (item: { title: string; desc: string }) =>
h("div", { class: "py-0.5" }, [
h("div", { class: "font-medium" }, item.title),
h("div", { class: "mt-0.5 text-sm text-muted-foreground" }, item.desc),
]);
</script>
<template>
<List :items="items" virtual :height="320" :item-render="renderItem" />
</template>
开启 sortable 后行可拖拽排序(基于 sortablejs,仅非虚拟且未分组时生效),拖拽结束通过 sort 事件上报,配合 v-model:items 同步最新顺序。传对象可指定 handle 限定可拖动区域。
<script setup lang="ts">
import { ref } from "vue";
import { GripVertical } from "lucide-vue-next";
const items = ref(Array.from({ length: 10 }, (_, i) => `Item ${i}`));
const onSort = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
console.log("排序变化:", oldIndex, "->", newIndex);
};
</script>
<template>
<List
v-model:items="items"
:height="360"
:sortable="{ handle: '.re-list-drag-handle' }"
@sort="onSort"
>
<template #default="{ item, index }">
<div class="flex items-center gap-3">
<span class="re-list-drag-handle cursor-move text-muted-foreground">
<GripVertical class="size-4" />
</span>
<span>{{ index + 1 }}</span>
<span>{{ item }}</span>
</div>
</template>
</List>
</template>
说明:sortable 与 virtual / group 互斥——拖拽排序基于流式 DOM,虚拟滚动会复用行元素,二者不可同时开启;有 group 时分组标题参与排序语义不明确,故自动忽略 sortable。不传 handle 时整行可拖。
通过 classNames / styles 定制 root / item / groupHeader 三个语义化结构的类名与行内样式。
<script setup lang="ts">
const classNames = {
item: "hover:bg-cyan-50 transition-colors",
groupHeader: "bg-cyan-100 text-cyan-900",
};
const styles = {
root: { height: "280px" },
item: { borderBottom: "1px dashed #67e8f9", background: "#ecfeff" },
groupHeader: { fontSize: "12px", letterSpacing: "0.1em" },
};
</script>
<template>
<List
:items="users"
:group="group"
sticky
:height="280"
virtual
:class-names="classNames"
:styles="styles"
/>
</template>
在 onScroll 中判断列表即将滚动到底部时,按需加载下一页数据。配合 virtual,数据积累也只渲染视口内的行。
<script setup lang="ts">
import { ref } from "vue";
const items = ref(Array.from({ length: 50 }, (_, i) => `Item ${i}`));
function onScroll(e: Event) {
const el = e.target as HTMLElement;
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 20) {
const start = items.value.length;
items.value.push(
...Array.from({ length: 50 }, (_, i) => `Item ${start + i}`),
);
}
}
</script>
<template>
<List :items="items" virtual :height="300" @scroll="onScroll" />
</template>
通过 ref 调用 scrollTo 滚动到某个位置、某一项或某个分组。
<script setup lang="ts">
import { ref } from "vue";
const listRef = ref();
const items = Array.from({ length: 100 }, (_, i) => ({
key: i,
group: String(Math.floor(i / 10)),
label: `Item ${i}`,
}));
const group = {
key: (i: any) => i.group,
title: (key: string) => `第 ${key} 组`,
};
const toTop = () => listRef.value?.scrollTo(0);
const toItem = () => listRef.value?.scrollTo({ key: 40, align: "top" });
const toGroup = () => listRef.value?.scrollTo({ groupKey: "9", align: "top" });
</script>
<template>
<List ref="listRef" :items="items" virtual :height="300" :row-key="'key'" :group="group" sticky />
</template>
| 参数 | 说明 | 类型 | 默认值 |
|---|
| items | 列表数据源 | T[] | [] |
| itemRender | 自定义渲染单行 | (item, index) => VNode | - |
| rowKey | 每一项的唯一键,字段名或取值函数 | keyof T | (item) => Key | item.key ?? index |
| height | 滚动容器高度,内容超出后滚动 | number | - |
| virtual | 是否开启虚拟滚动,仅渲染视口内的行 | boolean | false |
| itemHeight | 虚拟滚动时未测量行高的估算值(px) | number | 40 |
| group | 分组配置 | ListGroupConfig | - |
| sticky | 分组标题是否吸顶 | boolean | false |
| sortable | 是否可拖拽排序(仅非虚拟且未分组) | boolean | ListSortableOptions | false |
| classNames | 语义化结构类名 | { root?, item?, groupHeader? } | - |
| styles | 语义化结构行内样式 | { root?, item?, groupHeader? } | - |
| class | 附加类名 | string | - |
| style | 附加样式 | CSSProperties | - |
| 事件 | 说明 | 回调参数 |
|---|
| scroll | 原生滚动事件 | (event: Event) |
| update:items | 拖拽排序后同步最新顺序(配合 v-model:items) | (items: T[]) |
| sort | 拖拽排序结束 | ({ oldIndex, newIndex, item }) |
| 名称 | 说明 |
|---|
| default | 作用域插槽 { item, index } 渲染单行 |
| groupHeader | 作用域插槽 { groupKey, items } 自定义分组标题 |
| 参数 | 说明 | 类型 |
|---|
| key | 计算每条数据所属的分组键,返回值相同的数据归为一组 | (item) => K |
| title | 渲染分组标题,入参为分组键与该组数据 | (groupKey, items) => VNode |
scrollTo(config?: ListScrollToConfig):
| 形态 | 说明 |
|---|
number | 滚动到某个像素位置(scrollTop) |
{ top?, left? } | 滚动到绝对像素坐标 |
{ key, align?, offset? } | 滚动到 rowKey 等于 key 的数据项 |
{ groupKey, align?, offset? } | 滚动到某个分组标题 |
align 可选 'top' | 'bottom' | 'auto';offset 为对齐后额外的像素偏移。
ListItemMeta 作为行内容布局辅助组件(avatar / title / description),可与 itemRender 搭配。
root:根元素(滚动容器),设置字体与相对定位item:条目元素,设置内间距、分割线与悬浮背景groupHeader:分组标题元素,设置吸顶定位与背景色
- 虚拟滚动基于行高自动测量(ResizeObserver),支持行高不一致的行。
- 组件在 SSR / 预渲染阶段不执行虚拟滚动布局(依赖 ResizeObserver),建议在客户端渲染场景使用。