virtual-seamless-scrolling
Version:
vue虚拟滚无缝动组件
242 lines (172 loc) • 9.07 kB
Markdown
# virtual-seamless-scrolling
## 介绍
基于 **Vue 3** 的**虚拟列表 + 无缝自动滚动**组件:在只渲染可视区域附近若干行的前提下,通过 **`transform`** 位移与 **GSAP** 动画实现连续滚动;适合公告、榜单、大屏等需要循环展示大量数据的场景。
> 说明:当前仓库实现为 **Vue 3** 版本。若需 Vue 2,请自行拉取源码改造。
---
## 技术栈与依赖
| 项 | 说明 |
|----|------|
| Vue | 3.x(组合式 API) |
| 动画 | [GSAP](https://greensock.com/gsap/) 3.x,用于行级位移动画与行间停顿计时 |
| 工具 | [@vueuse/core](https://vueuse.org/)(尺寸监听、元素/文档可见性等) |
发布包中会**打包**上述运行时依赖;业务项目只需安装本包与 **Vue 3**。
**本地开发 / 构建**建议使用 **Node.js `^20.19.0` 或 `>=22.12.0`**(与 Vite 8 要求一致)。
---
## 安装
```bash
npm install virtual-seamless-scrolling -S
```
```bash
pnpm add virtual-seamless-scrolling
```
也可从 Gitee 安装:
```bash
npm install git+https://gitee.com/strivelei/virtual-list-scroll.git -S
```
### 引入样式
组件样式需单独引入(若未引入,列表容器可能无高度或布局异常):
```ts
import 'virtual-seamless-scrolling/dist/style.css';
```
### 按需引入组件
```ts
import { VirtualListScroll, ListHeader } from 'virtual-seamless-scrolling';
import type { TitltListItem } from 'virtual-seamless-scrolling';
```
---
## 架构与原理(简要)
1. **虚拟窗口**:根据容器高度与首行测量高度估算 `maxCount`,只渲染 `startIndex ~ endIndex` 区间的数据行。
2. **无缝带**:在可视列表下方再渲染一段「头部数据」副本,视觉上形成循环;滚动到底后内部会将位移重置并触发 `scroll-end`,由业务决定是否追加数据等。
3. **位移方式**:列表在 **`overflow: hidden`** 容器内通过外层 **`translate3d`** 移动,避免高频修改 `scrollTop`;**鼠标滚轮**通过监听 `wheel` 手动累加内部位移,与自动滚动共用同一套 `scrollPixel` 逻辑。
4. **自动滚动**:使用 **GSAP** 的 `gsap.to` 对位移做插值;**`transitionDuration`** 控制「滚过一行高度」所需时间;**`interval`** 控制「每滚完一段后的静止等待」时间(详见下文)。
---
## VirtualListScroll 组件
### Props
| 属性 | 说明 | 类型 | 默认值 | 必填 |
|------|------|------|--------|------|
| `dataKey` | 数据源对象上用作 `v-for` `:key` 的字段名,需在每条数据中唯一 | `string` | `'id'` | 是(建议始终传入业务主键) |
| `dataSource` | 列表数据数组,每项为对象且包含 `dataKey` 对应字段 | `Array` | — | 是 |
| `loading` | 加载中。为 **`true` 时停止自动滚动**;为 **`false` 且未悬停**时才会自动滚 | `boolean` | — | 是 |
| `interval` | **行间停顿**时长(**毫秒**)。为 `0` 时连续滚动、**不触发** `line-scroll-end`;大于 `0` 时,每滚完一段后静止 `interval` 再滚下一段,并在满足条件时触发 `line-scroll-end` | `number` | `0` | 否 |
| `transitionDuration` | **单行过渡动画**基准时长(**毫秒**)。表示滚过「**一整行行高**」所用的动画时间;若本段实际位移不足一行(例如滚轮停在半行后先「补到行首」),动画时长会按距离比例缩短,以保持线速度一致 | `number` | `1000` | 否 |
| `refresh` | `true`:数据源变化时**清空内部累积列表**再按新数据渲染;`false`:在原有基础上**追加**新条目(适合分页追加) | `boolean` | `false` | 否 |
#### `interval` 与 `transitionDuration` 的区别
- **`transitionDuration`**:内容**在动**的时间。内部对应 GSAP 对 `scrollPixel` 的补间时长,按 `(实际像素位移 / 行高) × transitionDuration` 计算。
- **`interval`**:内容**停住不动**的时间。在 `interval > 0` 时,每段滚动结束后会再执行一次「仅占位、不位移」的 `gsap.to`,时长为 `interval`,用于间隔下一行滚动。
二者**数值可以不同**,分别控制「动多快」与「停多久」。
#### `interval > 0` 时的额外行为
- **首次**开始自动滚动前,会先**静止等待一个完整的 `interval`**,再开始第一段位移动画(与「每行滚完再停」的节奏一致)。
- 将 `refresh` 设为 `true` 并清空/替换数据时,会重置「首段等待」状态,新数据加载后仍会先等 `interval` 再滚。
### 事件
| 事件名 | 说明 | 触发时机 |
|--------|------|----------|
| `scroll-end` | 列表在内部逻辑中判定「滚到本轮末尾」并回到起点时触发 | 虚拟窗口滑出数据末尾、`scrollPixel` 被重置为 `0` 时;可用于请求下一页、拼接数据等 |
| `line-scroll-end` | **单行(整行高)滚动完成** | 仅当 **`interval > 0`**,且本段动画位移**约等于一行高**时触发;**补行首的短距离**段**不会**触发,避免与下一整行连续触发两次 |
在模板中使用 **kebab-case**:
```vue
<VirtualListScroll
@scroll-end="onScrollEnd"
@line-scroll-end="onLineScrollEnd"
/>
```
### 插槽
| 插槽名 | 作用域参数 | 说明 |
|--------|------------|------|
| `item` | `{ item }` | 渲染**一行**内容;`item` 为 `dataSource` 中对应数据对象 |
### 交互行为
| 行为 | 说明 |
|------|------|
| 鼠标进入列表区域 | **暂停**自动滚动 |
| 鼠标离开 | **`loading` 为 false** 时**恢复**自动滚动 |
| 鼠标滚轮 | 在列表上滚动时**阻止默认冒泡**,改为更新内部位移;非悬停且非 loading 时会**打断当前 GSAP 动画并重新排队**自动滚动链 |
| 元素离开视口 / 页签隐藏 | 会按内部逻辑暂停;再次可见时尝试恢复(与 `mouseenter` 状态叠加时以组件内判断为准) |
### 样式与布局建议
- 组件根节点使用 **`height: 100%`**,请保证**父级有明确高度**(如固定 `height` 或 flex 子项拉伸),否则可视高度为 0 会导致无法正确测量行高。
- 列表项建议 **`box-sizing: border-box`**,且同一列表内各行**高度一致**时测量最准确(当前实现按首行高度估算虚拟行高)。
---
## ListHeader(可选)
与示例配套的表头组件,通过 `titleList` 配置列标题与宽度,类型为 `TitltListItem[]`。具体字段见源码 `src/components/ListHeader/data.d.ts`。
---
## 完整示例
更完整的交互可参考仓库内 **`src/examples/App.vue`**。
```vue
<template>
<div class="virtual-list-content">
<ListHeader :title-list="titleList" />
<VirtualListScroll
data-key="project_id"
:data-source="dataSource"
:loading="loading"
:interval="3000"
:transition-duration="1000"
:refresh="false"
class="virtual-list"
@scroll-end="scrollEnd"
@line-scroll-end="lineScrollEnd"
>
<template #item="{ item }">
<div class="virtual-list-item">
<span>{{ item.name }}</span>
</div>
</template>
</VirtualListScroll>
</div>
</template>
<script setup lang="ts">
import { VirtualListScroll, ListHeader } from 'virtual-seamless-scrolling';
import type { TitltListItem } from 'virtual-seamless-scrolling';
import 'virtual-seamless-scrolling/dist/style.css';
import { ref } from 'vue';
const titleList: TitltListItem[] = [
{ label: '项目名', width: '20%' },
// ...
];
const loading = ref(true);
const dataSource = ref<Array<{ project_id: number; name: string }>>([]);
setTimeout(() => {
for (let i = 0; i < 35; i++) {
dataSource.value.push({ project_id: i, name: 'Item ' + i });
}
loading.value = false;
}, 100);
function scrollEnd() {
console.log('列表本轮滚到底并复位,可在此拉取更多数据');
}
function lineScrollEnd() {
console.log('完成一整行位移后的停顿前回调(需 interval > 0)');
}
</script>
<style scoped>
.virtual-list-content {
display: flex;
flex-direction: column;
height: 500px;
}
.virtual-list {
flex: 1;
min-height: 0;
}
</style>
```
---
## 本地开发
```bash
pnpm install
pnpm dev
```
## 构建库
```bash
pnpm run build
```
产物在 **`dist/`**(含 ES / UMD 与 `style.css`)。
---
## 常见问题(FAQ)
**Q:`loading` 已经 `false` 了,为什么不滚?**
A:请确认已有数据且容器已布局完成(行高能测到)。组件会在量完行高后排队启动;若仍异常,检查父级高度是否为 0。
**Q:`line-scroll-end` 为什么不触发?**
A:需要 **`interval > 0`**,且本段为**约一整行高**的滚动;`interval === 0` 时不会发该事件。
**Q:`transitionDuration` 和 `interval` 能写成一样吗?**
A:可以,但语义不同:前者是**动画时长**,后者是**停顿时长**;相同数值表示「动多久、就停多久」的节奏感。
---
## 反馈与贡献
有其他需求或 Bug,欢迎在 **Gitee** 提 Issue,后续会持续迭代优化。