List 虚拟列表

高性能列表,支持分组与虚拟滚动,可命令式滚动定位。

高性能列表,支持分组,并可为长列表开启虚拟滚动(仅渲染视口内的行)。

组件 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>

命令式滚动(scrollTo)

通过 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>

API

List Props

参数说明类型默认值
items列表数据源T[][]
itemRender自定义渲染单行(item, index) => VNode-
rowKey每一项的唯一键,字段名或取值函数keyof T | (item) => Keyitem.key ?? index
height滚动容器高度,内容超出后滚动number-
virtual是否开启虚拟滚动,仅渲染视口内的行booleanfalse
itemHeight虚拟滚动时未测量行高的估算值(px)number40
group分组配置ListGroupConfig-
sticky分组标题是否吸顶booleanfalse
sortable是否可拖拽排序(仅非虚拟且未分组)boolean | ListSortableOptionsfalse
classNames语义化结构类名{ root?, item?, groupHeader? }-
styles语义化结构行内样式{ root?, item?, groupHeader? }-
class附加类名string-
style附加样式CSSProperties-

List Events

事件说明回调参数
scroll原生滚动事件(event: Event)
update:items拖拽排序后同步最新顺序(配合 v-model:items)(items: T[])
sort拖拽排序结束({ oldIndex, newIndex, item })

List Slots

名称说明
default作用域插槽 { item, index } 渲染单行
groupHeader作用域插槽 { groupKey, items } 自定义分组标题

ListGroupConfig

参数说明类型
key计算每条数据所属的分组键,返回值相同的数据归为一组(item) => K
title渲染分组标题,入参为分组键与该组数据(groupKey, items) => VNode

scrollTo(命令式)

scrollTo(config?: ListScrollToConfig):

形态说明
number滚动到某个像素位置(scrollTop)
{ top?, left? }滚动到绝对像素坐标
{ key, align?, offset? }滚动到 rowKey 等于 key 的数据项
{ groupKey, align?, offset? }滚动到某个分组标题

align 可选 'top' | 'bottom' | 'auto';offset 为对齐后额外的像素偏移。

ListItemMeta

ListItemMeta 作为行内容布局辅助组件(avatar / title / description),可与 itemRender 搭配。

语义化结构

  • root:根元素(滚动容器),设置字体与相对定位
  • item:条目元素,设置内间距、分割线与悬浮背景
  • groupHeader:分组标题元素,设置吸顶定位与背景色

说明

  • 虚拟滚动基于行高自动测量(ResizeObserver),支持行高不一致的行。
  • 组件在 SSR / 预渲染阶段不执行虚拟滚动布局(依赖 ResizeObserver),建议在客户端渲染场景使用。