Skip to content

VirtualList 虚拟列表

AuVirtualList 面向固定行高数据,只渲染视口与 overscan 缓冲区内的项目。

基础用法

示例用 1,000 条数据展示筛选、范围变化和命令式滚动。

1,000 条项目数据

输入关键词筛选,或通过组件实例定位到列表中间;DOM 仅保留当前范围和缓冲项。

共 1000 条,当前渲染 1–0
1Aurora 项目 0001设计团队已同步
2Aurora 项目 0002前端团队待处理
3Aurora 项目 0003编辑器团队仅本地
4Aurora 项目 0004质量团队已同步
5Aurora 项目 0005设计团队待处理
6Aurora 项目 0006前端团队仅本地
7Aurora 项目 0007编辑器团队已同步
8Aurora 项目 0008质量团队待处理
9Aurora 项目 0009设计团队仅本地
10Aurora 项目 0010前端团队已同步
11Aurora 项目 0011编辑器团队待处理
12Aurora 项目 0012质量团队仅本地
13Aurora 项目 0013设计团队已同步
14Aurora 项目 0014前端团队待处理
15Aurora 项目 0015编辑器团队仅本地
16Aurora 项目 0016质量团队已同步
17Aurora 项目 0017设计团队待处理
18Aurora 项目 0018前端团队仅本地
19Aurora 项目 0019编辑器团队已同步
20Aurora 项目 0020质量团队待处理
21Aurora 项目 0021设计团队仅本地
22Aurora 项目 0022前端团队已同步
vue
<template>
  <div>
    <div class="virtual-list-demo__controls">
      <input v-model.trim="keyword" type="search" placeholder="按名称或状态筛选" />
      <AuButton size="small" @click="listRef?.scrollToTop()">回到顶部</AuButton>
      <AuButton size="small" @click="scrollToMiddle">定位到中间</AuButton>
      <span>共 {{ filteredItems.length }} 条,当前渲染 {{ renderedRangeText }}</span>
    </div>
    <div class="virtual-list-demo">
      <AuVirtualList
        ref="listRef"
        :items="filteredItems"
        :item-height="36"
        :overscan="6"
        plain
        fill
        @range-change="renderedRange = $event"
      >
        <template #default="{ item, index }">
          <div
            class="virtual-list-demo__row"
            :class="{ 'is-last': index === filteredItems.length - 1 }"
          >
            <span class="virtual-list-demo__index">{{ index + 1 }}</span>
            <span class="virtual-list-demo__identity">
              <strong>{{ item.name }}</strong>
              <small>{{ item.owner }}</small>
            </span>
            <span class="virtual-list-demo__status" :class="`is-${item.statusType}`">
              {{ item.status }}
            </span>
          </div>
        </template>
        <template #empty>
          <div class="virtual-list-demo__empty">没有匹配的数据</div>
        </template>
      </AuVirtualList>
    </div>
  </div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { AuButton, AuVirtualList } from 'aurora-plus';
const owners = ['设计团队', '前端团队', '编辑器团队', '质量团队'];
const statuses = [
  { label: '已同步', type: 'success' },
  { label: '待处理', type: 'warning' },
  { label: '仅本地', type: 'info' },
];
const items = Array.from({ length: 1000 }, (_, index) => {
  const status = statuses[index % statuses.length];
  return {
    id: index + 1,
    name: `Aurora 项目 ${String(index + 1).padStart(4, '0')}`,
    owner: owners[index % owners.length],
    status: status.label,
    statusType: status.type,
  };
});
const keyword = ref('');
const listRef = ref(null);
const renderedRange = ref({ start: 0, end: 0 });
const filteredItems = computed(() => {
  const query = keyword.value.toLowerCase();
  if (!query) return items;
  return items.filter((item) =>
    [item.name, item.owner, item.status].some((value) => value.toLowerCase().includes(query))
  );
});
const renderedRangeText = computed(() => {
  if (filteredItems.value.length === 0) return '0';
  return `${renderedRange.value.start + 1}–${renderedRange.value.end}`;
});
function scrollToMiddle() {
  const middleIndex = Math.floor(filteredItems.value.length / 2);
  listRef.value?.scrollToIndex(middleIndex, 'center');
}
</script>
<style scoped>
.virtual-list-demo {
  height: 330px;
  overflow: hidden;
  border: 1px solid var(--au-color-border-lighter);
  border-radius: 8px;
  background: transparent;
}
.virtual-list-demo__controls {
  display: flex;
  align-items: center;
  margin-bottom: 14px;
  flex-wrap: wrap;
  gap: 8px;
}
.virtual-list-demo__controls input {
  min-width: 210px;
  height: 28px;
  padding: 0 9px;
  border: 1px solid var(--au-color-border);
  border-radius: var(--au-border-radius-base);
  color: var(--au-color-text-primary);
  background: transparent;
  font: inherit;
  font-size: 13px;
  outline: none;
}
.virtual-list-demo__controls input:focus-visible {
  border-color: color-mix(in srgb, var(--au-color-primary) 58%, var(--au-color-border));
  outline: 2px solid color-mix(in srgb, var(--au-color-primary) 22%, transparent);
  outline-offset: 1px;
}
.virtual-list-demo__controls span {
  margin-left: auto;
  color: var(--au-color-text-secondary);
  font-size: 12px;
}
.virtual-list-demo__row {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) auto;
  align-items: center;
  height: 36px;
  padding: 0 10px;
  border-bottom: 1px solid var(--au-color-border-lighter);
  color: var(--au-color-text-regular);
  font-size: 13px;
}
.virtual-list-demo__row.is-last {
  border-bottom-color: transparent;
}
.virtual-list-demo__identity {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.virtual-list-demo__row strong {
  min-width: 0;
  overflow: hidden;
  color: var(--au-color-text-primary);
  font-weight: var(--au-font-weight-medium);
  line-height: 18px;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.virtual-list-demo__index {
  color: var(--au-color-text-secondary);
}
.virtual-list-demo__identity small {
  position: relative;
  flex: 0 1 7em;
  min-width: 0;
  padding-left: 10px;
  overflow: hidden;
  color: var(--au-color-text-secondary);
  font-size: 12px;
  line-height: 16px;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.virtual-list-demo__identity small::before {
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 0;
  width: 1px;
  background: var(--au-color-border-light);
  content: '';
}
.virtual-list-demo__status {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 46px;
  height: 20px;
  padding: 0 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: var(--au-font-weight-medium);
  line-height: 1;
}
.virtual-list-demo__status.is-success {
  color: var(--au-color-success);
  background: color-mix(in srgb, var(--au-color-success) 11%, transparent);
}
.virtual-list-demo__status.is-warning {
  color: var(--au-color-warning);
  background: color-mix(in srgb, var(--au-color-warning) 12%, transparent);
}
.virtual-list-demo__status.is-info {
  color: var(--au-color-info);
  background: color-mix(in srgb, var(--au-color-info) 11%, transparent);
}
.virtual-list-demo__empty {
  display: grid;
  height: 100%;
  min-height: 180px;
  color: var(--au-color-text-secondary);
  place-items: center;
}
@media (prefers-contrast: more) {
  .virtual-list-demo__status {
    outline: 1px solid currentColor;
    outline-offset: -1px;
  }
}
</style>

使用约束

  • 容器必须具备可计算高度,例如 height: 400px 或由父级布局确定的高度。
  • 每个列表项的实际高度必须与 itemHeight 完全一致;组件不支持动态行高。
  • items 应保持稳定引用,数据筛选可使用 computed,不应在模板表达式内反复创建数组。
  • 数据对象存在稳定 ID 时优先使用 keyField;复杂场景使用 itemKey

VirtualList API

Attributes

属性说明类型默认值
items完整数据数组T[][]
itemHeight固定行高,单位 px,必须大于 0number28
overscan视口上下额外渲染的行数number8
keyField从对象数据读取 key 的字段stringid
itemKey自定义 key 计算函数,优先级高于 keyField(item: T, index: number) => string | number
plain移除容器边框、内边距和表面阴影,供复合组件承载内容booleanfalse
fill填满父级可用宽高并参与弹性布局booleanfalse

Events

事件名说明参数
scroll原生滚动事件(event: Event)
range-change实际渲染范围变化;end 为不包含的结束索引({ start, end })

Slots

插槽名作用域参数说明
default{ item: T, index: number }每一个当前可见列表项
emptyitems 为空时的内容

Exposes

属性或方法说明
scrollToTop()滚动到顶部
scrollToIndex(index, align?)滚动到指定索引;align 支持 auto / start / center / end
scrollContainerRef内部滚动容器元素引用

Aurora Plus · Vue 3 Component Library