vue-virtual-flow 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +808 -0
- package/README.vi.md +805 -0
- package/dist/components/ChatVirtualScroll.d.cts +46 -0
- package/dist/components/ChatVirtualScroll.d.ts +46 -0
- package/dist/components/DynamicVirtualScroll.d.cts +50 -0
- package/dist/components/DynamicVirtualScroll.d.ts +50 -0
- package/dist/components/ShortMediaFeed.d.cts +37 -0
- package/dist/components/ShortMediaFeed.d.ts +37 -0
- package/dist/components/VirtualCarousel.d.cts +41 -0
- package/dist/components/VirtualCarousel.d.ts +41 -0
- package/dist/components/VirtualScroll.d.cts +50 -0
- package/dist/components/VirtualScroll.d.ts +50 -0
- package/dist/components/WindowDynamicVirtualScroll.d.cts +49 -0
- package/dist/components/WindowDynamicVirtualScroll.d.ts +49 -0
- package/dist/composables/useItemKey.d.cts +10 -0
- package/dist/composables/useItemKey.d.ts +10 -0
- package/dist/composables/usePullToRefresh.d.cts +20 -0
- package/dist/composables/usePullToRefresh.d.ts +20 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/types.d.cts +119 -0
- package/dist/types.d.ts +119 -0
- package/dist/utils/layout.d.cts +2 -0
- package/dist/utils/layout.d.ts +2 -0
- package/dist/vue-virtual-flow.cjs +1 -0
- package/dist/vue-virtual-flow.css +1 -0
- package/dist/vue-virtual-flow.js +1628 -0
- package/package.json +82 -0
package/README.vi.md
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
# vue-virtual-flow
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [Tiếng Việt](./README.vi.md)
|
|
4
|
+
|
|
5
|
+
Bộ component virtual scrolling nhỏ gọn, có type đầy đủ và hỗ trợ khả năng tiếp
|
|
6
|
+
cận cho Vue 3. Thư viện hỗ trợ danh sách chiều cao cố định hoặc động, cuộn theo
|
|
7
|
+
cửa sổ, giao diện chat, feed media ngắn và carousel ngang.
|
|
8
|
+
|
|
9
|
+
Chỉ các item cần thiết cho viewport hiện tại được mount, nhờ đó tập dữ liệu lớn
|
|
10
|
+
vẫn phản hồi nhanh mà component không áp đặt giao diện của từng item.
|
|
11
|
+
|
|
12
|
+
## Mục lục
|
|
13
|
+
|
|
14
|
+
- [Cài đặt](#cài-đặt)
|
|
15
|
+
- [Bắt đầu nhanh](#bắt-đầu-nhanh)
|
|
16
|
+
- [Chọn component](#chọn-component)
|
|
17
|
+
- [Hướng dẫn từng component](#hướng-dẫn-từng-component)
|
|
18
|
+
- [VirtualList và DynamicVirtualScroll](#virtuallist-và-dynamicvirtualscroll)
|
|
19
|
+
- [VirtualScroll](#virtualscroll)
|
|
20
|
+
- [WindowDynamicVirtualScroll](#windowdynamicvirtualscroll)
|
|
21
|
+
- [ChatVirtualScroll](#chatvirtualscroll)
|
|
22
|
+
- [ShortMediaFeed](#shortmediafeed)
|
|
23
|
+
- [VirtualCarousel](#virtualcarousel)
|
|
24
|
+
- [Cách dùng chung](#cách-dùng-chung)
|
|
25
|
+
- [Chiều cao viewport](#chiều-cao-viewport)
|
|
26
|
+
- [Key ổn định cho item](#key-ổn-định-cho-item)
|
|
27
|
+
- [Tải thêm dữ liệu](#tải-thêm-dữ-liệu)
|
|
28
|
+
- [Kéo để làm mới](#kéo-để-làm-mới)
|
|
29
|
+
- [Các type được export](#các-type-được-export)
|
|
30
|
+
- [Phát triển và phát hành](#phát-triển-và-phát-hành)
|
|
31
|
+
- [Đóng góp](#đóng-góp)
|
|
32
|
+
- [Giấy phép](#giấy-phép)
|
|
33
|
+
|
|
34
|
+
## Cài đặt
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install vue-virtual-flow
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Import stylesheet của thư viện một lần tại entry point của ứng dụng:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import 'vue-virtual-flow/style.css'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Package yêu cầu Vue `^3.4.0` và Node.js `>=18`.
|
|
47
|
+
|
|
48
|
+
## Bắt đầu nhanh
|
|
49
|
+
|
|
50
|
+
Import component cục bộ:
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
import { VirtualList } from 'vue-virtual-flow'
|
|
55
|
+
import 'vue-virtual-flow/style.css'
|
|
56
|
+
|
|
57
|
+
const users = Array.from({ length: 10_000 }, (_, id) => ({
|
|
58
|
+
id,
|
|
59
|
+
name: `Người dùng ${id + 1}`,
|
|
60
|
+
}))
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<VirtualList :items="users" item-key="id">
|
|
65
|
+
<template #default="{ item, index }">
|
|
66
|
+
<div>{{ index + 1 }}. {{ item.name }}</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<template #empty>Không tìm thấy người dùng.</template>
|
|
70
|
+
</VirtualList>
|
|
71
|
+
</template>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Hoặc đăng ký toàn bộ component ở cấp global:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { createApp } from 'vue'
|
|
78
|
+
import VueVirtualScroll from 'vue-virtual-flow'
|
|
79
|
+
import 'vue-virtual-flow/style.css'
|
|
80
|
+
import App from './App.vue'
|
|
81
|
+
|
|
82
|
+
createApp(App).use(VueVirtualScroll).mount('#app')
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Plugin đăng ký `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
|
|
86
|
+
`WindowDynamicVirtualScroll`, `ChatVirtualScroll`, `ShortMediaFeed` và
|
|
87
|
+
`VirtualCarousel`.
|
|
88
|
+
|
|
89
|
+
## Chọn component
|
|
90
|
+
|
|
91
|
+
| Component | Nên dùng khi | Viewport cuộn | Kích thước item |
|
|
92
|
+
| --- | --- | --- | --- |
|
|
93
|
+
| `VirtualList` | Cần danh sách đa dụng với cách dùng đơn giản nhất. | Container của component | Động, tự đo |
|
|
94
|
+
| `DynamicVirtualScroll` | Giống `VirtualList` nhưng tên gọi thể hiện rõ chiều cao động. | Container của component | Động, tự đo |
|
|
95
|
+
| `VirtualScroll` | Tất cả hàng có cùng chiều cao đã biết. | Container của component | Cố định |
|
|
96
|
+
| `WindowDynamicVirtualScroll` | Muốn trang cuộn thay vì tạo một container cuộn lồng bên trong. | Cửa sổ trình duyệt | Động, tự đo |
|
|
97
|
+
| `ChatVirtualScroll` | Tin nhắn mới được thêm ở cuối và lịch sử cũ được thêm ở đầu. | Container của component | Động, tự đo |
|
|
98
|
+
| `ShortMediaFeed` | Mỗi lần cần snap một item đầy viewport. | Container của component | Một viewport cho mỗi item |
|
|
99
|
+
| `VirtualCarousel` | Cần hiển thị và snap nhiều slide theo chiều ngang. | Container của component | Tính từ chiều rộng container |
|
|
100
|
+
|
|
101
|
+
## Hướng dẫn từng component
|
|
102
|
+
|
|
103
|
+
### VirtualList và DynamicVirtualScroll
|
|
104
|
+
|
|
105
|
+
`VirtualList` là alias của `DynamicVirtualScroll`. Hai tên này render cùng một
|
|
106
|
+
component và dùng chung API. Component phù hợp cho card, kết quả tìm kiếm,
|
|
107
|
+
activity feed hoặc danh sách có các hàng cao thấp khác nhau.
|
|
108
|
+
|
|
109
|
+
Ban đầu mỗi hàng dùng `estimatedItemSize`. Sau đó `ResizeObserver` thay giá trị
|
|
110
|
+
ước tính bằng chiều cao đã đo. Khi chiều cao của item phía trên viewport thay
|
|
111
|
+
đổi, component bù lại vị trí cuộn để tránh nội dung bị nhảy.
|
|
112
|
+
|
|
113
|
+
#### Ví dụ
|
|
114
|
+
|
|
115
|
+
```vue
|
|
116
|
+
<script setup lang="ts">
|
|
117
|
+
import { ref } from 'vue'
|
|
118
|
+
import {
|
|
119
|
+
VirtualList,
|
|
120
|
+
type VirtualScrollEvent,
|
|
121
|
+
type VirtualListExpose,
|
|
122
|
+
} from 'vue-virtual-flow'
|
|
123
|
+
|
|
124
|
+
const list = ref<VirtualListExpose>()
|
|
125
|
+
const posts = ref([
|
|
126
|
+
{ id: 'p1', title: 'Bài viết ngắn', body: 'Chỉ có một dòng.' },
|
|
127
|
+
{
|
|
128
|
+
id: 'p2',
|
|
129
|
+
title: 'Bài viết dài',
|
|
130
|
+
body: 'Nội dung xuống nhiều dòng làm cho hàng này có chiều cao khác.',
|
|
131
|
+
},
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
function onScroll(event: VirtualScrollEvent) {
|
|
135
|
+
console.log(event.scrollTop, event.startIndex, event.endIndex)
|
|
136
|
+
}
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<template>
|
|
140
|
+
<VirtualList
|
|
141
|
+
ref="list"
|
|
142
|
+
:items="posts"
|
|
143
|
+
:estimated-item-size="96"
|
|
144
|
+
:height="500"
|
|
145
|
+
:overscan="4"
|
|
146
|
+
item-key="id"
|
|
147
|
+
aria-label="Danh sách bài viết"
|
|
148
|
+
@scroll="onScroll"
|
|
149
|
+
>
|
|
150
|
+
<template #default="{ item, index }">
|
|
151
|
+
<article>
|
|
152
|
+
<small>#{{ index + 1 }}</small>
|
|
153
|
+
<h2>{{ item.title }}</h2>
|
|
154
|
+
<p>{{ item.body }}</p>
|
|
155
|
+
</article>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<template #empty>Chưa có bài viết.</template>
|
|
159
|
+
</VirtualList>
|
|
160
|
+
|
|
161
|
+
<button @click="list?.scrollToIndex(1, { align: 'center' })">
|
|
162
|
+
Đi tới bài viết thứ hai
|
|
163
|
+
</button>
|
|
164
|
+
</template>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Nên chọn giá trị ước tính gần với chiều cao trung bình sau khi render. Các hàng
|
|
168
|
+
không cần bằng đúng giá trị này; ước tính hợp lý chủ yếu giúp scrollbar ban đầu
|
|
169
|
+
và thao tác nhảy tới item chưa được đo chính xác hơn.
|
|
170
|
+
|
|
171
|
+
#### Props
|
|
172
|
+
|
|
173
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
174
|
+
| --- | --- | --- | --- |
|
|
175
|
+
| `items` | `readonly T[]` | bắt buộc | Dữ liệu truyền vào default slot. |
|
|
176
|
+
| `estimatedItemSize` | `number` | `48` | Chiều cao trung bình ban đầu của hàng, tính bằng pixel. |
|
|
177
|
+
| `height` | `number \| string` | `400` | Chiều cao viewport. Số được hiểu là pixel; `"fill"` tương đương `100%`. |
|
|
178
|
+
| `overscan` | `number` | `3` | Số hàng mount thêm trước và sau vùng đang hiển thị. |
|
|
179
|
+
| `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định dùng cho key và kết quả đo. |
|
|
180
|
+
| `ariaLabel` | `string` | `"Dynamic virtual list"` | Nhãn hỗ trợ khả năng tiếp cận. |
|
|
181
|
+
| `hasMore` | `boolean` | `false` | Cho biết vẫn còn trang dữ liệu tiếp theo. |
|
|
182
|
+
| `loading` | `boolean` | `false` | Ngăn request trùng và hiển thị hàng loading. |
|
|
183
|
+
| `loadingItemSize` | `number` | `estimatedItemSize` | Chiều cao dành cho hàng loading, tính bằng pixel. |
|
|
184
|
+
| `loadMoreThreshold` | `number` | `200` | Khoảng cách tới cuối danh sách để emit `load-more`, tính bằng pixel. |
|
|
185
|
+
| `pullToRefresh` | `boolean` | `false` | Bật thao tác kéo xuống ở đầu danh sách trên thiết bị cảm ứng. |
|
|
186
|
+
| `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
|
|
187
|
+
| `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
|
|
188
|
+
|
|
189
|
+
#### Slots
|
|
190
|
+
|
|
191
|
+
| Slot | Scope | Mô tả |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `default` | `{ item, index }` | Render từng hàng đang được mount. |
|
|
194
|
+
| `empty` | không có | Render khi `items` rỗng và `loading` là false. |
|
|
195
|
+
| `loading` | không có | Hàng loading; mặc định là “Loading more…”. |
|
|
196
|
+
| `refresh` | `{ pullDistance, progress, refreshing }` | Indicator kéo để làm mới. `progress` được giới hạn từ `0` tới `1`. |
|
|
197
|
+
|
|
198
|
+
#### Events
|
|
199
|
+
|
|
200
|
+
| Event | Payload | Thời điểm emit |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. `endIndex` là mốc không bao gồm item tại index đó. |
|
|
203
|
+
| `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. |
|
|
204
|
+
| `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. |
|
|
205
|
+
|
|
206
|
+
#### API qua ref
|
|
207
|
+
|
|
208
|
+
| Method | Mô tả |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| `scrollTo(position, options?)` | Cuộn tới offset pixel bên trong danh sách. |
|
|
211
|
+
| `scrollToIndex(index, { align?, behavior? })` | Cuộn tới index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. |
|
|
212
|
+
| `scrollToTop(behavior?)` | Cuộn về đầu danh sách. |
|
|
213
|
+
|
|
214
|
+
Smooth scroll ở khoảng cách xa tự chuyển thành nhảy tức thời vì các hàng chưa
|
|
215
|
+
đo có thể làm thay đổi offset đích trong lúc cuộn.
|
|
216
|
+
|
|
217
|
+
### VirtualScroll
|
|
218
|
+
|
|
219
|
+
Dùng `VirtualScroll` khi mọi hàng có cùng một chiều cao chính xác. Component
|
|
220
|
+
không cần đo từng hàng, nên rất phù hợp cho bảng, menu, log hoặc danh sách kết
|
|
221
|
+
quả dày đặc.
|
|
222
|
+
|
|
223
|
+
#### Ví dụ
|
|
224
|
+
|
|
225
|
+
```vue
|
|
226
|
+
<script setup lang="ts">
|
|
227
|
+
import { ref } from 'vue'
|
|
228
|
+
import { VirtualScroll, type VirtualScrollExpose } from 'vue-virtual-flow'
|
|
229
|
+
|
|
230
|
+
const list = ref<VirtualScrollExpose>()
|
|
231
|
+
const rows = Array.from({ length: 50_000 }, (_, id) => ({
|
|
232
|
+
id,
|
|
233
|
+
label: `Hàng ${id + 1}`,
|
|
234
|
+
}))
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<template>
|
|
238
|
+
<VirtualScroll
|
|
239
|
+
ref="list"
|
|
240
|
+
:items="rows"
|
|
241
|
+
:item-size="44"
|
|
242
|
+
:height="480"
|
|
243
|
+
:overscan="5"
|
|
244
|
+
item-key="id"
|
|
245
|
+
aria-label="Kết quả"
|
|
246
|
+
>
|
|
247
|
+
<template #default="{ item }">
|
|
248
|
+
<div class="result-row">{{ item.label }}</div>
|
|
249
|
+
</template>
|
|
250
|
+
</VirtualScroll>
|
|
251
|
+
|
|
252
|
+
<button @click="list?.scrollToIndex(999, { align: 'center' })">
|
|
253
|
+
Đi tới hàng 1.000
|
|
254
|
+
</button>
|
|
255
|
+
</template>
|
|
256
|
+
|
|
257
|
+
<style scoped>
|
|
258
|
+
.result-row {
|
|
259
|
+
height: 44px;
|
|
260
|
+
box-sizing: border-box;
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Hàng đã render phải giữ đúng `itemSize`. Nếu nội dung có thể xuống dòng hoặc
|
|
266
|
+
thay đổi kích thước, hãy dùng `VirtualList`.
|
|
267
|
+
|
|
268
|
+
#### Props
|
|
269
|
+
|
|
270
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
271
|
+
| --- | --- | --- | --- |
|
|
272
|
+
| `items` | `readonly T[]` | bắt buộc | Dữ liệu truyền vào default slot. |
|
|
273
|
+
| `itemSize` | `number` | bắt buộc | Chiều cao chính xác của mọi hàng, tính bằng pixel. |
|
|
274
|
+
| `height` | `number \| string` | `400` | Chiều cao viewport. Số được hiểu là pixel; `"fill"` tương đương `100%`. |
|
|
275
|
+
| `overscan` | `number` | `3` | Số hàng mount thêm trước và sau vùng đang hiển thị. |
|
|
276
|
+
| `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định của mỗi hàng. |
|
|
277
|
+
| `ariaLabel` | `string` | `"Virtual list"` | Nhãn hỗ trợ khả năng tiếp cận. |
|
|
278
|
+
| `hasMore` | `boolean` | `false` | Cho biết vẫn còn trang dữ liệu tiếp theo. |
|
|
279
|
+
| `loading` | `boolean` | `false` | Ngăn request trùng và hiển thị hàng loading. |
|
|
280
|
+
| `loadingItemSize` | `number` | `itemSize` | Chiều cao dành cho hàng loading, tính bằng pixel. |
|
|
281
|
+
| `loadMoreThreshold` | `number` | `200` | Khoảng cách tới cuối danh sách để emit `load-more`, tính bằng pixel. |
|
|
282
|
+
| `pullToRefresh` | `boolean` | `false` | Bật thao tác kéo xuống ở đầu danh sách. |
|
|
283
|
+
| `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
|
|
284
|
+
| `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
|
|
285
|
+
|
|
286
|
+
#### Slots, events và API qua ref
|
|
287
|
+
|
|
288
|
+
- Slots: `default({ item, index })`, `empty`, `loading` và
|
|
289
|
+
`refresh({ pullDistance, progress, refreshing })`.
|
|
290
|
+
- Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more` và
|
|
291
|
+
`refresh`.
|
|
292
|
+
- Methods: `scrollTo`, `scrollToIndex` và `scrollToTop`.
|
|
293
|
+
|
|
294
|
+
### WindowDynamicVirtualScroll
|
|
295
|
+
|
|
296
|
+
Dùng `WindowDynamicVirtualScroll` cho trang hoặc feed dài khi muốn cửa sổ
|
|
297
|
+
trình duyệt là viewport cuộn thay vì một phần tử bên trong. Component đo các
|
|
298
|
+
hàng có chiều cao động và theo dõi `window.scrollY` cùng
|
|
299
|
+
`window.innerHeight`. Component này không có prop `height`.
|
|
300
|
+
|
|
301
|
+
#### Ví dụ
|
|
302
|
+
|
|
303
|
+
```vue
|
|
304
|
+
<script setup lang="ts">
|
|
305
|
+
import { ref } from 'vue'
|
|
306
|
+
import {
|
|
307
|
+
WindowDynamicVirtualScroll,
|
|
308
|
+
type VirtualScrollExpose,
|
|
309
|
+
} from 'vue-virtual-flow'
|
|
310
|
+
|
|
311
|
+
const feed = ref(loadInitialFeed())
|
|
312
|
+
const list = ref<VirtualScrollExpose>()
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
<template>
|
|
316
|
+
<header>Nội dung phía trước virtual list</header>
|
|
317
|
+
|
|
318
|
+
<WindowDynamicVirtualScroll
|
|
319
|
+
ref="list"
|
|
320
|
+
:items="feed"
|
|
321
|
+
:estimated-item-size="320"
|
|
322
|
+
:overscan="3"
|
|
323
|
+
item-key="id"
|
|
324
|
+
aria-label="Feed bài viết"
|
|
325
|
+
>
|
|
326
|
+
<template #default="{ item, index }">
|
|
327
|
+
<article>
|
|
328
|
+
<small>{{ index + 1 }}</small>
|
|
329
|
+
<h2>{{ item.title }}</h2>
|
|
330
|
+
<p>{{ item.summary }}</p>
|
|
331
|
+
</article>
|
|
332
|
+
</template>
|
|
333
|
+
</WindowDynamicVirtualScroll>
|
|
334
|
+
|
|
335
|
+
<footer>Nội dung phía sau virtual list</footer>
|
|
336
|
+
</template>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Props
|
|
340
|
+
|
|
341
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
342
|
+
| --- | --- | --- | --- |
|
|
343
|
+
| `items` | `readonly T[]` | bắt buộc | Dữ liệu truyền vào default slot. |
|
|
344
|
+
| `estimatedItemSize` | `number` | `48` | Chiều cao trung bình ban đầu của hàng, tính bằng pixel. |
|
|
345
|
+
| `overscan` | `number` | `3` | Số hàng mount thêm trước và sau vùng đang hiển thị. |
|
|
346
|
+
| `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định dùng cho key và kết quả đo. |
|
|
347
|
+
| `ariaLabel` | `string` | `"Window dynamic virtual list"` | Nhãn hỗ trợ khả năng tiếp cận. |
|
|
348
|
+
| `hasMore` | `boolean` | `false` | Cho biết vẫn còn trang dữ liệu tiếp theo. |
|
|
349
|
+
| `loading` | `boolean` | `false` | Ngăn request trùng và hiển thị hàng loading. |
|
|
350
|
+
| `loadingItemSize` | `number` | `estimatedItemSize` | Chiều cao dành cho hàng loading, tính bằng pixel. |
|
|
351
|
+
| `loadMoreThreshold` | `number` | `200` | Khoảng cách giữa đáy cửa sổ và cuối danh sách để emit `load-more`. |
|
|
352
|
+
| `pullToRefresh` | `boolean` | `false` | Bật refresh khi trang đang ở `window.scrollY = 0`. |
|
|
353
|
+
| `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
|
|
354
|
+
| `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
|
|
355
|
+
|
|
356
|
+
#### Slots, events và API qua ref
|
|
357
|
+
|
|
358
|
+
- Slots: `default({ item, index })`, `empty`, `loading` và
|
|
359
|
+
`refresh({ pullDistance, progress, refreshing })`.
|
|
360
|
+
- Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more` và
|
|
361
|
+
`refresh`. `scrollTop` được tính tương đối từ đầu component.
|
|
362
|
+
- Methods: `scrollTo`, `scrollToIndex` và `scrollToTop`. Các method này cuộn
|
|
363
|
+
cửa sổ trình duyệt; `scrollTo` nhận offset tương đối so với đầu danh sách.
|
|
364
|
+
|
|
365
|
+
### ChatVirtualScroll
|
|
366
|
+
|
|
367
|
+
`ChatVirtualScroll` dành cho hội thoại có tin nhắn cao thấp khác nhau. Mặc định
|
|
368
|
+
component mở tại tin nhắn mới nhất, chỉ bám theo tin nhắn mới khi người dùng
|
|
369
|
+
đang gần cuối và giữ nguyên nội dung đang xem khi lịch sử cũ được thêm vào đầu.
|
|
370
|
+
|
|
371
|
+
#### Ví dụ
|
|
372
|
+
|
|
373
|
+
```vue
|
|
374
|
+
<script setup lang="ts">
|
|
375
|
+
import { ref } from 'vue'
|
|
376
|
+
import {
|
|
377
|
+
ChatVirtualScroll,
|
|
378
|
+
type ChatVirtualScrollExpose,
|
|
379
|
+
} from 'vue-virtual-flow'
|
|
380
|
+
|
|
381
|
+
const chat = ref<ChatVirtualScrollExpose>()
|
|
382
|
+
const loadingOlder = ref(false)
|
|
383
|
+
const messages = ref(loadLatestMessages())
|
|
384
|
+
const hasOlder = ref(true)
|
|
385
|
+
|
|
386
|
+
async function loadOlder() {
|
|
387
|
+
if (loadingOlder.value || !hasOlder.value) return
|
|
388
|
+
loadingOlder.value = true
|
|
389
|
+
const page = await fetchOlderPage(messages.value[0]?.id)
|
|
390
|
+
messages.value.unshift(...page.items)
|
|
391
|
+
hasOlder.value = page.hasMore
|
|
392
|
+
loadingOlder.value = false
|
|
393
|
+
}
|
|
394
|
+
</script>
|
|
395
|
+
|
|
396
|
+
<template>
|
|
397
|
+
<ChatVirtualScroll
|
|
398
|
+
ref="chat"
|
|
399
|
+
:items="messages"
|
|
400
|
+
:estimated-item-size="72"
|
|
401
|
+
:height="600"
|
|
402
|
+
:has-older="hasOlder"
|
|
403
|
+
:loading-older="loadingOlder"
|
|
404
|
+
:load-older-threshold="120"
|
|
405
|
+
item-key="id"
|
|
406
|
+
aria-label="Hội thoại hỗ trợ"
|
|
407
|
+
@load-older="loadOlder"
|
|
408
|
+
>
|
|
409
|
+
<template #default="{ item }">
|
|
410
|
+
<MessageBubble :message="item" />
|
|
411
|
+
</template>
|
|
412
|
+
|
|
413
|
+
<template #loadingOlder>Đang tải tin nhắn cũ…</template>
|
|
414
|
+
<template #empty>Chưa có tin nhắn.</template>
|
|
415
|
+
</ChatVirtualScroll>
|
|
416
|
+
|
|
417
|
+
<button v-if="!chat?.isAtBottom" @click="chat?.scrollToBottom('smooth')">
|
|
418
|
+
Xem tin nhắn mới nhất
|
|
419
|
+
</button>
|
|
420
|
+
</template>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Hãy thêm dữ liệu cũ vào đầu cùng một collection có key ổn định. Component tính
|
|
424
|
+
chiều cao được thêm phía trên viewport và khôi phục vị trí của người đọc.
|
|
425
|
+
|
|
426
|
+
#### Props
|
|
427
|
+
|
|
428
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
429
|
+
| --- | --- | --- | --- |
|
|
430
|
+
| `items` | `readonly T[]` | bắt buộc | Tin nhắn được sắp từ cũ nhất tới mới nhất. |
|
|
431
|
+
| `estimatedItemSize` | `number` | `48` | Chiều cao trung bình ban đầu của tin nhắn, tính bằng pixel. |
|
|
432
|
+
| `height` | `number \| string` | `400` | Chiều cao viewport. Số được hiểu là pixel; `"fill"` tương đương `100%`. |
|
|
433
|
+
| `overscan` | `number` | `5` | Số tin nhắn mount thêm trước và sau vùng đang hiển thị. |
|
|
434
|
+
| `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định, đặc biệt quan trọng khi thêm vào đầu. |
|
|
435
|
+
| `ariaLabel` | `string` | `"Chat messages"` | Nhãn hỗ trợ khả năng tiếp cận cho live log. |
|
|
436
|
+
| `stickToBottom` | `boolean` | `true` | Bám theo tin nhắn mới nếu hiện tại đang gần cuối. |
|
|
437
|
+
| `bottomThreshold` | `number` | `80` | Khoảng cách vẫn được xem là “ở cuối”, tính bằng pixel. |
|
|
438
|
+
| `initialScroll` | `"top" \| "bottom"` | `"bottom"` | Vị trí ban đầu sau khi mount. |
|
|
439
|
+
| `hasOlder` | `boolean` | `false` | Cho biết vẫn còn lịch sử cũ. |
|
|
440
|
+
| `loadingOlder` | `boolean` | `false` | Ngăn request trùng và hiển thị indicator phía trên. |
|
|
441
|
+
| `loadOlderThreshold` | `number` | `120` | Khoảng cách tới đầu để emit `load-older`, tính bằng pixel. |
|
|
442
|
+
|
|
443
|
+
#### Slots
|
|
444
|
+
|
|
445
|
+
| Slot | Scope | Mô tả |
|
|
446
|
+
| --- | --- | --- |
|
|
447
|
+
| `default` | `{ item, index }` | Render từng tin nhắn đang được mount. |
|
|
448
|
+
| `empty` | không có | Render khi chưa có tin nhắn. |
|
|
449
|
+
| `loadingOlder` | không có | Status sticky phía trên; mặc định “Loading older messages…”. |
|
|
450
|
+
|
|
451
|
+
#### Events
|
|
452
|
+
|
|
453
|
+
| Event | Payload | Thời điểm emit |
|
|
454
|
+
| --- | --- | --- |
|
|
455
|
+
| `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. |
|
|
456
|
+
| `load-older` | không có | Gần đầu khi `hasOlder` là true và `loadingOlder` là false. |
|
|
457
|
+
| `bottom-change` | `boolean` | Khi viewport đi vào hoặc rời ngưỡng ở cuối. |
|
|
458
|
+
|
|
459
|
+
#### API qua ref
|
|
460
|
+
|
|
461
|
+
| Member | Mô tả |
|
|
462
|
+
| --- | --- |
|
|
463
|
+
| `isAtBottom` | Boolean readonly cho biết người dùng có đang gần cuối hay không. |
|
|
464
|
+
| `scrollTo(position, options?)` | Cuộn tới offset pixel. |
|
|
465
|
+
| `scrollToIndex(index, { align?, behavior? })` | Cuộn tới index tin nhắn bắt đầu từ 0. |
|
|
466
|
+
| `scrollToTop(behavior?)` | Cuộn tới tin nhắn cũ nhất đã tải. |
|
|
467
|
+
| `scrollToBottom(behavior?)` | Cuộn tới tin nhắn mới nhất đã tải. |
|
|
468
|
+
|
|
469
|
+
### ShortMediaFeed
|
|
470
|
+
|
|
471
|
+
`ShortMediaFeed` là viewport headless có scroll snap theo chiều dọc cho reels,
|
|
472
|
+
stories hoặc media ngắn. Mỗi item chiếm trọn một viewport. Component quản lý
|
|
473
|
+
việc mount và điều hướng; slot của bạn chịu trách nhiệm cho video, hình ảnh,
|
|
474
|
+
playback, âm thanh và các nút điều khiển.
|
|
475
|
+
|
|
476
|
+
#### Ví dụ
|
|
477
|
+
|
|
478
|
+
```vue
|
|
479
|
+
<script setup lang="ts">
|
|
480
|
+
import { ref } from 'vue'
|
|
481
|
+
import { ShortMediaFeed, type ShortMediaFeedExpose } from 'vue-virtual-flow'
|
|
482
|
+
|
|
483
|
+
const feed = ref<ShortMediaFeedExpose>()
|
|
484
|
+
const activeIndex = ref(0)
|
|
485
|
+
const media = ref([
|
|
486
|
+
{ id: 1, src: '/media/one.mp4' },
|
|
487
|
+
{ id: 2, src: '/media/two.mp4' },
|
|
488
|
+
{ id: 3, src: '/media/three.mp4' },
|
|
489
|
+
])
|
|
490
|
+
</script>
|
|
491
|
+
|
|
492
|
+
<template>
|
|
493
|
+
<ShortMediaFeed
|
|
494
|
+
ref="feed"
|
|
495
|
+
v-model:active-index="activeIndex"
|
|
496
|
+
:items="media"
|
|
497
|
+
:buffer="1"
|
|
498
|
+
height="100dvh"
|
|
499
|
+
item-key="id"
|
|
500
|
+
aria-label="Feed video"
|
|
501
|
+
>
|
|
502
|
+
<template #default="{ item, active }">
|
|
503
|
+
<video
|
|
504
|
+
:src="item.src"
|
|
505
|
+
:autoplay="active"
|
|
506
|
+
:muted="!active"
|
|
507
|
+
playsinline
|
|
508
|
+
controls
|
|
509
|
+
/>
|
|
510
|
+
</template>
|
|
511
|
+
|
|
512
|
+
<template #empty>Không có media.</template>
|
|
513
|
+
</ShortMediaFeed>
|
|
514
|
+
</template>
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Với `buffer="1"`, component chỉ mount tối đa item active, một item phía trước
|
|
518
|
+
và một item phía sau. State cục bộ trong slot bị mất khi item rời buffer, vì
|
|
519
|
+
vậy hãy lưu playback state cần duy trì ở component cha.
|
|
520
|
+
|
|
521
|
+
#### Props
|
|
522
|
+
|
|
523
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
524
|
+
| --- | --- | --- | --- |
|
|
525
|
+
| `items` | `readonly T[]` | bắt buộc | Dữ liệu media truyền vào default slot. |
|
|
526
|
+
| `activeIndex` | `number` | `0` | Index active bắt đầu từ 0; hỗ trợ `v-model:active-index`. |
|
|
527
|
+
| `buffer` | `number` | `1` | Số item mount thêm trước và sau item active. |
|
|
528
|
+
| `height` | `number \| string` | `"100dvh"` | Chiều cao viewport của feed. |
|
|
529
|
+
| `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định cho mỗi item. |
|
|
530
|
+
| `ariaLabel` | `string` | `"Short media feed"` | Nhãn hỗ trợ khả năng tiếp cận. |
|
|
531
|
+
| `hasMore` | `boolean` | `false` | Cho biết vẫn còn trang dữ liệu tiếp theo. |
|
|
532
|
+
| `loading` | `boolean` | `false` | Ngăn request `load-more` bị lặp. |
|
|
533
|
+
| `loadMoreThreshold` | `number` | `2` | Số item còn lại để kích hoạt `load-more`. |
|
|
534
|
+
|
|
535
|
+
#### Slots và events
|
|
536
|
+
|
|
537
|
+
| Loại | Tên | Payload/scope | Mô tả |
|
|
538
|
+
| --- | --- | --- | --- |
|
|
539
|
+
| Slot | `default` | `{ item, index, active }` | Render item đang mount; `active` đánh dấu item đang snap. |
|
|
540
|
+
| Slot | `empty` | không có | Render khi `items` rỗng. |
|
|
541
|
+
| Event | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. |
|
|
542
|
+
| Event | `change` | `{ index, item }` | Khi item active thay đổi. |
|
|
543
|
+
| Event | `reach-start` | không có | Khi điều hướng chuyển tới item đầu. |
|
|
544
|
+
| Event | `reach-end` | không có | Khi điều hướng chuyển tới item cuối. |
|
|
545
|
+
| Event | `load-more` | không có | Khi số item còn lại đạt `loadMoreThreshold`. |
|
|
546
|
+
|
|
547
|
+
#### API qua ref và bàn phím
|
|
548
|
+
|
|
549
|
+
`scrollToIndex(index, options?)` cuộn tới item bằng `ScrollToOptions` tiêu
|
|
550
|
+
chuẩn. Viewport hỗ trợ Arrow Up/Down, Page Up/Down, Home và End.
|
|
551
|
+
|
|
552
|
+
Với feed không giới hạn, hãy tải theo trang bằng `load-more` và chỉ giữ một cửa
|
|
553
|
+
sổ dữ liệu hợp lý ở component cha. Virtualization giới hạn DOM và media element
|
|
554
|
+
đang mount, nhưng không xóa object khỏi `items`.
|
|
555
|
+
|
|
556
|
+
### VirtualCarousel
|
|
557
|
+
|
|
558
|
+
`VirtualCarousel` là carousel headless có scroll snap ngang và virtualize các
|
|
559
|
+
slide. `activeIndex` là index của slide đầu tiên trong vùng nhìn hiện tại.
|
|
560
|
+
|
|
561
|
+
#### Ví dụ
|
|
562
|
+
|
|
563
|
+
```vue
|
|
564
|
+
<script setup lang="ts">
|
|
565
|
+
import { ref } from 'vue'
|
|
566
|
+
import { VirtualCarousel, type VirtualCarouselExpose } from 'vue-virtual-flow'
|
|
567
|
+
|
|
568
|
+
const carousel = ref<VirtualCarouselExpose>()
|
|
569
|
+
const activeIndex = ref(0)
|
|
570
|
+
const products = ref(loadProducts())
|
|
571
|
+
</script>
|
|
572
|
+
|
|
573
|
+
<template>
|
|
574
|
+
<VirtualCarousel
|
|
575
|
+
ref="carousel"
|
|
576
|
+
v-model:active-index="activeIndex"
|
|
577
|
+
:items="products"
|
|
578
|
+
:slides-per-view="3"
|
|
579
|
+
:slides-to-scroll="1"
|
|
580
|
+
:gap="16"
|
|
581
|
+
:buffer="1"
|
|
582
|
+
:height="320"
|
|
583
|
+
item-key="id"
|
|
584
|
+
aria-label="Sản phẩm nổi bật"
|
|
585
|
+
>
|
|
586
|
+
<template #default="{ item, index, active, visible }">
|
|
587
|
+
<ProductCard
|
|
588
|
+
:product="item"
|
|
589
|
+
:position="index"
|
|
590
|
+
:active="active"
|
|
591
|
+
:visible="visible"
|
|
592
|
+
/>
|
|
593
|
+
</template>
|
|
594
|
+
|
|
595
|
+
<template #empty>Không có sản phẩm.</template>
|
|
596
|
+
</VirtualCarousel>
|
|
597
|
+
|
|
598
|
+
<button @click="carousel?.previous()">Trước</button>
|
|
599
|
+
<button @click="carousel?.next()">Sau</button>
|
|
600
|
+
</template>
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Chiều rộng slide được tính theo công thức:
|
|
604
|
+
|
|
605
|
+
```text
|
|
606
|
+
(chiều rộng viewport - (slidesPerView - 1) × gap) / slidesPerView
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
`active` chỉ là true với slide đầu tiên trong vùng nhìn. `visible` là true với
|
|
610
|
+
mọi slide thuộc vùng nhìn đó. Khi hiển thị ba slide và buffer bằng một, thông
|
|
611
|
+
thường tối đa năm slide được mount ở giữa collection.
|
|
612
|
+
|
|
613
|
+
#### Props
|
|
614
|
+
|
|
615
|
+
| Prop | Kiểu | Mặc định | Mô tả |
|
|
616
|
+
| --- | --- | --- | --- |
|
|
617
|
+
| `items` | `readonly T[]` | bắt buộc | Dữ liệu slide truyền vào default slot. |
|
|
618
|
+
| `activeIndex` | `number` | `0` | Slide đầu tiên đang hiển thị; hỗ trợ `v-model:active-index`. |
|
|
619
|
+
| `slidesPerView` | `number` | `1` | Số slide hiển thị cùng lúc. |
|
|
620
|
+
| `slidesToScroll` | `number` | `1` | Số slide di chuyển bởi `next()` và `previous()`. |
|
|
621
|
+
| `gap` | `number` | `0` | Khoảng cách giữa các slide, tính bằng pixel. |
|
|
622
|
+
| `buffer` | `number` | `1` | Số slide mount thêm trước và sau nhóm đang hiển thị. |
|
|
623
|
+
| `height` | `number \| string` | `"auto"` | Chiều cao carousel. |
|
|
624
|
+
| `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định cho mỗi slide. |
|
|
625
|
+
| `ariaLabel` | `string` | `"Carousel"` | Nhãn hỗ trợ khả năng tiếp cận. |
|
|
626
|
+
| `hasMore` | `boolean` | `false` | Cho biết vẫn còn trang dữ liệu tiếp theo. |
|
|
627
|
+
| `loading` | `boolean` | `false` | Ngăn request `load-more` bị lặp. |
|
|
628
|
+
| `loadMoreThreshold` | `number` | `2` | Số slide còn lại sau vùng nhìn để kích hoạt `load-more`. |
|
|
629
|
+
|
|
630
|
+
#### Slots và events
|
|
631
|
+
|
|
632
|
+
| Loại | Tên | Payload/scope | Mô tả |
|
|
633
|
+
| --- | --- | --- | --- |
|
|
634
|
+
| Slot | `default` | `{ item, index, active, visible }` | Render từng slide đang mount. |
|
|
635
|
+
| Slot | `empty` | không có | Render khi `items` rỗng. |
|
|
636
|
+
| Event | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. |
|
|
637
|
+
| Event | `change` | `{ index, item }` | Khi slide đầu tiên trong vùng nhìn thay đổi. |
|
|
638
|
+
| Event | `reach-start` | không có | Khi điều hướng chuyển tới vị trí đầu. |
|
|
639
|
+
| Event | `reach-end` | không có | Khi điều hướng chuyển tới vị trí đầu-slide hợp lệ cuối cùng. |
|
|
640
|
+
| Event | `load-more` | không có | Khi số slide còn lại đạt `loadMoreThreshold`. |
|
|
641
|
+
|
|
642
|
+
#### API qua ref và bàn phím
|
|
643
|
+
|
|
644
|
+
| Method | Mô tả |
|
|
645
|
+
| --- | --- |
|
|
646
|
+
| `next(behavior?)` | Tiến thêm `slidesToScroll` slide; mặc định cuộn mượt. |
|
|
647
|
+
| `previous(behavior?)` | Lùi lại `slidesToScroll` slide. |
|
|
648
|
+
| `scrollToIndex(index, options?)` | Đưa index thành slide đầu tiên trong vùng nhìn. |
|
|
649
|
+
|
|
650
|
+
Viewport hỗ trợ Arrow Left/Right, Page Up/Down, Home và End. Smooth scroll ở
|
|
651
|
+
khoảng cách xa tự chuyển thành tức thời để không lộ khoảng trống virtualized.
|
|
652
|
+
|
|
653
|
+
## Cách dùng chung
|
|
654
|
+
|
|
655
|
+
### Chiều cao viewport
|
|
656
|
+
|
|
657
|
+
Các component dùng container nhận một số hoặc chuỗi chiều cao CSS. Giá trị số
|
|
658
|
+
được đổi thành pixel. `height="fill"` là cách viết ngắn của
|
|
659
|
+
`height="100%"`:
|
|
660
|
+
|
|
661
|
+
```vue
|
|
662
|
+
<div class="list-container">
|
|
663
|
+
<VirtualList :items="items" height="fill">
|
|
664
|
+
<template #default="{ item }"><ResultRow :item="item" /></template>
|
|
665
|
+
</VirtualList>
|
|
666
|
+
</div>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
```css
|
|
670
|
+
.list-container {
|
|
671
|
+
height: 100%;
|
|
672
|
+
min-height: 0;
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Chiều cao phần trăm chỉ hoạt động khi thẻ cha có chiều cao xác định. Bản
|
|
677
|
+
development cảnh báo nếu viewport được tính thành `0px`.
|
|
678
|
+
`WindowDynamicVirtualScroll` dùng cửa sổ trình duyệt nên không có prop
|
|
679
|
+
`height`.
|
|
680
|
+
|
|
681
|
+
### Key ổn định cho item
|
|
682
|
+
|
|
683
|
+
Với object, component tự thử `item.id` rồi tới `item.key`. Item nguyên thủy dùng
|
|
684
|
+
index làm fallback. Hãy truyền `itemKey` nếu định danh có tên khác:
|
|
685
|
+
|
|
686
|
+
```vue
|
|
687
|
+
<VirtualList :items="users" item-key="uuid" />
|
|
688
|
+
|
|
689
|
+
<VirtualList
|
|
690
|
+
:items="rows"
|
|
691
|
+
:item-key="(row) => `${row.accountId}:${row.sequence}`"
|
|
692
|
+
/>
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Key duy nhất và ổn định rất quan trọng khi thêm, xóa, thêm vào đầu hoặc sắp xếp
|
|
696
|
+
lại dữ liệu. Key cũng gắn kết quả đo chiều cao với đúng item.
|
|
697
|
+
|
|
698
|
+
### Tải thêm dữ liệu
|
|
699
|
+
|
|
700
|
+
Các component dạng list dùng ngưỡng khoảng cách pixel. `ShortMediaFeed` và
|
|
701
|
+
`VirtualCarousel` dùng ngưỡng số item còn lại.
|
|
702
|
+
|
|
703
|
+
```vue
|
|
704
|
+
<script setup lang="ts">
|
|
705
|
+
import { computed, ref } from 'vue'
|
|
706
|
+
|
|
707
|
+
const items = ref(loadFirstPage())
|
|
708
|
+
const loading = ref(false)
|
|
709
|
+
const hasMore = computed(() => items.value.length < 1_000)
|
|
710
|
+
|
|
711
|
+
async function loadMore() {
|
|
712
|
+
if (loading.value || !hasMore.value) return
|
|
713
|
+
loading.value = true
|
|
714
|
+
items.value.push(...(await fetchNextPage()))
|
|
715
|
+
loading.value = false
|
|
716
|
+
}
|
|
717
|
+
</script>
|
|
718
|
+
|
|
719
|
+
<template>
|
|
720
|
+
<VirtualList
|
|
721
|
+
:items="items"
|
|
722
|
+
:has-more="hasMore"
|
|
723
|
+
:loading="loading"
|
|
724
|
+
:load-more-threshold="400"
|
|
725
|
+
@load-more="loadMore"
|
|
726
|
+
>
|
|
727
|
+
<template #default="{ item }"><FeedCard :item="item" /></template>
|
|
728
|
+
<template #loading>Đang tải trang tiếp theo…</template>
|
|
729
|
+
</VirtualList>
|
|
730
|
+
</template>
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Event chỉ emit tối đa một lần với số lượng item hiện tại. Nối thêm item sẽ cho
|
|
734
|
+
phép request tiếp theo. Giữ `loading` là true trong toàn bộ request và đặt
|
|
735
|
+
`hasMore` thành false sau trang cuối.
|
|
736
|
+
|
|
737
|
+
### Kéo để làm mới
|
|
738
|
+
|
|
739
|
+
Pull-to-refresh có trên `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`
|
|
740
|
+
và `WindowDynamicVirtualScroll`:
|
|
741
|
+
|
|
742
|
+
```vue
|
|
743
|
+
<VirtualList
|
|
744
|
+
:items="items"
|
|
745
|
+
pull-to-refresh
|
|
746
|
+
:refreshing="refreshing"
|
|
747
|
+
:pull-refresh-threshold="72"
|
|
748
|
+
@refresh="refreshFirstPage"
|
|
749
|
+
>
|
|
750
|
+
<template #default="{ item }"><FeedCard :item="item" /></template>
|
|
751
|
+
<template #refresh="{ progress, refreshing: busy }">
|
|
752
|
+
{{ busy ? 'Đang làm mới…' : progress >= 1 ? 'Thả để làm mới' : 'Kéo xuống' }}
|
|
753
|
+
</template>
|
|
754
|
+
</VirtualList>
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Thao tác một ngón chỉ bắt đầu khi đang ở đầu danh sách. Đặt `refreshing` thành
|
|
758
|
+
true trong lúc chạy refresh bất đồng bộ, sau đó trả về false để đóng indicator.
|
|
759
|
+
|
|
760
|
+
## Các type được export
|
|
761
|
+
|
|
762
|
+
```ts
|
|
763
|
+
import type {
|
|
764
|
+
ChatVirtualScrollExpose,
|
|
765
|
+
ChatVirtualScrollProps,
|
|
766
|
+
DynamicVirtualScrollProps,
|
|
767
|
+
ItemKey,
|
|
768
|
+
ScrollAlignment,
|
|
769
|
+
ShortMediaFeedChangeEvent,
|
|
770
|
+
ShortMediaFeedExpose,
|
|
771
|
+
ShortMediaFeedProps,
|
|
772
|
+
VirtualCarouselChangeEvent,
|
|
773
|
+
VirtualCarouselExpose,
|
|
774
|
+
VirtualCarouselProps,
|
|
775
|
+
VirtualListExpose,
|
|
776
|
+
VirtualListProps,
|
|
777
|
+
VirtualScrollEvent,
|
|
778
|
+
VirtualScrollExpose,
|
|
779
|
+
VirtualScrollProps,
|
|
780
|
+
WindowDynamicVirtualScrollProps,
|
|
781
|
+
} from 'vue-virtual-flow'
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
## Phát triển và phát hành
|
|
785
|
+
|
|
786
|
+
```bash
|
|
787
|
+
bun install
|
|
788
|
+
bunx playwright install chromium firefox webkit
|
|
789
|
+
bun run dev
|
|
790
|
+
bun run check
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Playground có các demo riêng cho chiều cao cố định, chiều cao động, cuộn theo
|
|
794
|
+
cửa sổ, chat, load-more, short-media và carousel. Trước khi publish, hãy chạy
|
|
795
|
+
thêm `npm pack --dry-run`. GitHub Release được publish qua
|
|
796
|
+
`.github/workflows/publish.yml`.
|
|
797
|
+
|
|
798
|
+
## Đóng góp
|
|
799
|
+
|
|
800
|
+
Mọi issue và pull request đều được chào đón. Vui lòng đọc
|
|
801
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md) trước khi gửi thay đổi.
|
|
802
|
+
|
|
803
|
+
## Giấy phép
|
|
804
|
+
|
|
805
|
+
[MIT](./LICENSE)
|