zx-infinite-scroll 1.0.1 → 1.0.3
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/README.md +375 -10
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +4 -3
- package/dist/index.d.ts +0 -253
- package/dist/types/components/empty-component.vue.d.ts +0 -25
- package/dist/types/components/infinite-scroll.vue.d.ts +0 -78
- package/dist/types/components/request-loading.vue.d.ts +0 -9
- package/dist/types/index.d.ts +0 -11
package/README.md
CHANGED
|
@@ -18,6 +18,91 @@ Vue 3 无限滚动组件,支持下拉刷新、上拉加载。
|
|
|
18
18
|
npm install zx-infinite-scroll
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
### ⭐ 推荐:全局引入样式(最简单)
|
|
24
|
+
|
|
25
|
+
在 `main.ts` 中全局引入样式**一次**即可:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createApp } from 'vue';
|
|
29
|
+
import App from './App.vue';
|
|
30
|
+
import 'zx-infinite-scroll/dist/style.css'; // 👈 只需引入一次
|
|
31
|
+
|
|
32
|
+
const app = createApp(App);
|
|
33
|
+
app.mount('#app');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
之后在任何组件中使用都**不需要**再引入样式:
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<script setup>
|
|
40
|
+
import { InfiniteScrollV2 } from 'zx-infinite-scroll';
|
|
41
|
+
// ✅ 不需要再引入样式
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<InfiniteScrollV2 :fetch-fn="fetchData">
|
|
46
|
+
<!-- ... -->
|
|
47
|
+
</InfiniteScrollV2>
|
|
48
|
+
</template>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 方式一:手动引入样式(每次使用都要引入)
|
|
54
|
+
|
|
55
|
+
如果不想全局引入,每次使用时手动引入:
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<script setup>
|
|
59
|
+
import { InfiniteScrollV2 } from 'zx-infinite-scroll';
|
|
60
|
+
import 'zx-infinite-scroll/dist/style.css'; // 👈 每次都要引入
|
|
61
|
+
</script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### 方式二:使用 unplugin-vue-components 自动导入(高级)
|
|
67
|
+
|
|
68
|
+
安装插件:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install -D unplugin-vue-components
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
配置 `vite.config.ts`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { defineConfig } from 'vite';
|
|
78
|
+
import vue from '@vitejs/plugin-vue';
|
|
79
|
+
import Components from 'unplugin-vue-components/vite';
|
|
80
|
+
|
|
81
|
+
export default defineConfig({
|
|
82
|
+
plugins: [
|
|
83
|
+
vue(),
|
|
84
|
+
Components({
|
|
85
|
+
resolvers: [
|
|
86
|
+
// 自定义解析器
|
|
87
|
+
(componentName) => {
|
|
88
|
+
if (componentName.startsWith('InfiniteScroll')) {
|
|
89
|
+
return {
|
|
90
|
+
name: componentName,
|
|
91
|
+
from: 'zx-infinite-scroll',
|
|
92
|
+
sideEffects: 'zx-infinite-scroll/dist/style.css'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
这样组件和样式都会自动按需导入,无需手动 import!
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
21
106
|
## 使用
|
|
22
107
|
|
|
23
108
|
### 方式一:按需引入
|
|
@@ -84,34 +169,83 @@ app.mount('#app');
|
|
|
84
169
|
|
|
85
170
|
| 属性 | 类型 | 默认值 | 说明 |
|
|
86
171
|
|------|------|--------|------|
|
|
87
|
-
| fetchFn | `(params: FetchParams) => Promise<FetchResult<T>>` | - |
|
|
88
|
-
| params | `Record<string, any>` | `{}` |
|
|
172
|
+
| fetchFn | `(params: FetchParams) => Promise<FetchResult<T>>` | - | 数据请求函数(必填),需返回 `{ records: T[], total: number }` |
|
|
173
|
+
| params | `Record<string, any>` | `{}` | 查询参数(如筛选条件),变化时自动重新加载 |
|
|
89
174
|
| pageSize | `number` | `10` | 每页条数 |
|
|
90
175
|
| autoReload | `boolean` | `true` | params 变化时是否自动重新加载 |
|
|
91
176
|
| immediate | `boolean` | `true` | 是否在挂载时自动加载首页 |
|
|
92
177
|
| pullRefresh | `boolean` | `false` | 是否启用下拉刷新 |
|
|
93
178
|
| pullThreshold | `number` | `70` | 下拉刷新触发距离(px) |
|
|
94
|
-
| loadThreshold | `number` | `50` |
|
|
179
|
+
| loadThreshold | `number` | `50` | 上拉加载触发距离(距离底部 px) |
|
|
180
|
+
| emptyText | `string` | `'暂无数据'` | 空状态文案 |
|
|
181
|
+
| pullingText | `string` | `'下拉刷新'` | 下拉中的文案 |
|
|
182
|
+
| loosingText | `string` | `'释放即可刷新'` | 释放刷新的文案 |
|
|
183
|
+
| refreshingText | `string` | `'加载中...'` | 刷新中的文案 |
|
|
184
|
+
| loadingText | `string` | `'加载中...'` | 加载中的文案 |
|
|
185
|
+
| finishedText | `string` | `'没有更多了~'` | 加载完成的文案 |
|
|
186
|
+
| errorText | `string` | `'加载失败,点击重试'` | 加载失败的文案 |
|
|
187
|
+
|
|
188
|
+
#### FetchParams 类型
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
interface FetchParams {
|
|
192
|
+
page: number; // 当前页码,从 1 开始
|
|
193
|
+
pageSize: number; // 每页条数
|
|
194
|
+
[key: string]: any; // 其他自定义参数(从 params prop 传入)
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### FetchResult 类型
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface FetchResult<T> {
|
|
202
|
+
records: T[]; // 当前页数据列表
|
|
203
|
+
total: number; // 总条数
|
|
204
|
+
}
|
|
205
|
+
```
|
|
95
206
|
|
|
96
207
|
### InfiniteScrollV2 Events
|
|
97
208
|
|
|
98
209
|
| 事件 | 参数 | 说明 |
|
|
99
210
|
|------|------|------|
|
|
100
|
-
| success | `{ records: T[], total: number, reset: boolean }` |
|
|
101
|
-
| error | `err: unknown` |
|
|
211
|
+
| success | `{ records: T[], total: number, reset: boolean }` | 加载成功时触发,每次成功(含分页)都会触发 |
|
|
212
|
+
| error | `err: unknown` | 加载失败时触发 |
|
|
213
|
+
| update:list | `list: T[]` | 列表内容变化时触发(支持 v-model:list) |
|
|
102
214
|
|
|
103
215
|
### InfiniteScrollV2 Slots
|
|
104
216
|
|
|
105
217
|
| 插槽 | 参数 | 说明 |
|
|
106
218
|
|------|------|------|
|
|
107
|
-
| default | `{ list, loading, finished, isEmpty, error, total, reload }` |
|
|
219
|
+
| default | `{ list, loading, finished, isEmpty, error, total, reload }` | 列表内容插槽 |
|
|
220
|
+
|
|
221
|
+
#### Slot 参数说明
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
{
|
|
225
|
+
list: T[]; // 当前列表数据
|
|
226
|
+
loading: boolean; // 是否正在加载
|
|
227
|
+
finished: boolean; // 是否已加载完所有数据
|
|
228
|
+
isEmpty: boolean; // 是否为空数据
|
|
229
|
+
error: boolean; // 是否加载失败
|
|
230
|
+
total: number; // 总条数
|
|
231
|
+
reload: () => void; // 重新加载第一页的方法
|
|
232
|
+
}
|
|
233
|
+
```
|
|
108
234
|
|
|
109
235
|
### InfiniteScrollV2 Expose
|
|
110
236
|
|
|
111
|
-
| 方法 | 说明 |
|
|
112
|
-
|
|
113
|
-
| reload() | 重新加载第一页 |
|
|
114
|
-
| refresh() | 同 reload |
|
|
237
|
+
| 方法 | 类型 | 说明 |
|
|
238
|
+
|------|------|------|
|
|
239
|
+
| reload() | `() => Promise<void>` | 重新加载第一页 |
|
|
240
|
+
| refresh() | `() => Promise<void>` | 同 reload(别名) |
|
|
241
|
+
| list | `Ref<T[]>` | 当前列表数据(响应式) |
|
|
242
|
+
| loading | `Ref<boolean>` | 加载中状态(响应式) |
|
|
243
|
+
| finished | `Ref<boolean>` | 是否已加载完(响应式) |
|
|
244
|
+
| isEmpty | `Ref<boolean>` | 是否为空数据(响应式) |
|
|
245
|
+
| total | `Ref<number>` | 总条数(响应式) |
|
|
246
|
+
| finishLoad() | `() => void` | 完成上拉加载(透传 v1 方法) |
|
|
247
|
+
| finishRefresh() | `() => void` | 完成下拉刷新(透传 v1 方法) |
|
|
248
|
+
| checkContentHeight() | `() => void` | 检查内容高度(透传 v1 方法) |
|
|
115
249
|
|
|
116
250
|
## InfiniteScroll(基础组件)
|
|
117
251
|
|
|
@@ -151,6 +285,237 @@ const onRefresh = async () => {
|
|
|
151
285
|
</template>
|
|
152
286
|
```
|
|
153
287
|
|
|
288
|
+
### InfiniteScroll Props
|
|
289
|
+
|
|
290
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
291
|
+
|------|------|--------|------|
|
|
292
|
+
| loading | `boolean` | `false` | 是否处于加载中状态 |
|
|
293
|
+
| finished | `boolean` | `false` | 是否已加载完所有数据 |
|
|
294
|
+
| isEmpty | `boolean` | `false` | 是否显示空状态 |
|
|
295
|
+
| emptyText | `string` | `'暂无数据'` | 空状态文案 |
|
|
296
|
+
| pullRefresh | `boolean` | `false` | 是否启用下拉刷新 |
|
|
297
|
+
| pullThreshold | `number` | `70` | 下拉刷新触发距离(px) |
|
|
298
|
+
| loadThreshold | `number` | `50` | 上拉加载触发距离(距离底部 px) |
|
|
299
|
+
| pullingText | `string` | `'下拉刷新'` | 下拉中的文案 |
|
|
300
|
+
| loosingText | `string` | `'释放即可刷新'` | 释放刷新的文案 |
|
|
301
|
+
| refreshingText | `string` | `'加载中...'` | 刷新中的文案 |
|
|
302
|
+
| loadingText | `string` | `'加载中...'` | 加载中的文案 |
|
|
303
|
+
| finishedText | `string` | `'没有更多了~'` | 加载完成的文案 |
|
|
304
|
+
| errorText | `string` | `'加载失败,点击重试'` | 加载失败的文案 |
|
|
305
|
+
| error | `boolean` | `false` | 是否显示错误状态 |
|
|
306
|
+
| contentLoading | `boolean` | `false` | 内容区域是否显示加载状态(用于下拉刷新时的遮罩) |
|
|
307
|
+
|
|
308
|
+
### InfiniteScroll Events
|
|
309
|
+
|
|
310
|
+
| 事件 | 参数 | 说明 |
|
|
311
|
+
|------|------|------|
|
|
312
|
+
| load | - | 触发上拉加载时触发 |
|
|
313
|
+
| refresh | - | 触发下拉刷新时触发 |
|
|
314
|
+
| retry | - | 点击错误重试时触发 |
|
|
315
|
+
|
|
316
|
+
### InfiniteScroll Expose
|
|
317
|
+
|
|
318
|
+
| 方法 | 类型 | 说明 |
|
|
319
|
+
|------|------|------|
|
|
320
|
+
| finishRefresh() | `() => void` | 完成下拉刷新,调用后收起刷新指示器 |
|
|
321
|
+
| finishPullDown() | `() => void` | 同 finishRefresh(别名) |
|
|
322
|
+
| finishLoad() | `() => void` | 完成上拉加载 |
|
|
323
|
+
| finishPullUp() | `() => void` | 同 finishLoad(别名) |
|
|
324
|
+
| checkContentHeight() | `() => void` | 检查内容高度,不足一屏时自动触发加载 |
|
|
325
|
+
|
|
326
|
+
### InfiniteScroll Slots
|
|
327
|
+
|
|
328
|
+
| 插槽 | 说明 |
|
|
329
|
+
|------|------|
|
|
330
|
+
| default | 列表内容 |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 完整使用示例
|
|
335
|
+
|
|
336
|
+
### 示例 1:基础列表
|
|
337
|
+
|
|
338
|
+
```vue
|
|
339
|
+
<script setup lang="ts">
|
|
340
|
+
import { InfiniteScrollV2 } from 'zx-infinite-scroll';
|
|
341
|
+
import 'zx-infinite-scroll/dist/style.css';
|
|
342
|
+
|
|
343
|
+
interface Item {
|
|
344
|
+
id: number;
|
|
345
|
+
name: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const fetchData = async ({ page, pageSize }) => {
|
|
349
|
+
const res = await fetch(`/api/list?page=${page}&pageSize=${pageSize}`);
|
|
350
|
+
const data = await res.json();
|
|
351
|
+
return {
|
|
352
|
+
records: data.list,
|
|
353
|
+
total: data.total
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
</script>
|
|
357
|
+
|
|
358
|
+
<template>
|
|
359
|
+
<InfiniteScrollV2 :fetch-fn="fetchData">
|
|
360
|
+
<template #default="{ list }">
|
|
361
|
+
<div v-for="item in list" :key="item.id" class="item">
|
|
362
|
+
{{ item.name }}
|
|
363
|
+
</div>
|
|
364
|
+
</template>
|
|
365
|
+
</InfiniteScrollV2>
|
|
366
|
+
</template>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### 示例 2:带下拉刷新和筛选参数
|
|
370
|
+
|
|
371
|
+
```vue
|
|
372
|
+
<script setup lang="ts">
|
|
373
|
+
import { ref } from 'vue';
|
|
374
|
+
import { InfiniteScrollV2 } from 'zx-infinite-scroll';
|
|
375
|
+
import 'zx-infinite-scroll/dist/style.css';
|
|
376
|
+
|
|
377
|
+
const keyword = ref('');
|
|
378
|
+
const category = ref('');
|
|
379
|
+
|
|
380
|
+
const fetchData = async ({ page, pageSize, keyword, category }) => {
|
|
381
|
+
const res = await fetch(
|
|
382
|
+
`/api/list?page=${page}&pageSize=${pageSize}&keyword=${keyword}&category=${category}`
|
|
383
|
+
);
|
|
384
|
+
return await res.json();
|
|
385
|
+
};
|
|
386
|
+
</script>
|
|
387
|
+
|
|
388
|
+
<template>
|
|
389
|
+
<input v-model="keyword" placeholder="搜索关键词" />
|
|
390
|
+
<select v-model="category">
|
|
391
|
+
<option value="">全部分类</option>
|
|
392
|
+
<option value="1">分类1</option>
|
|
393
|
+
<option value="2">分类2</option>
|
|
394
|
+
</select>
|
|
395
|
+
|
|
396
|
+
<InfiniteScrollV2
|
|
397
|
+
:fetch-fn="fetchData"
|
|
398
|
+
:params="{ keyword, category }"
|
|
399
|
+
:pull-refresh="true"
|
|
400
|
+
:page-size="20"
|
|
401
|
+
>
|
|
402
|
+
<template #default="{ list, loading, reload }">
|
|
403
|
+
<div v-for="item in list" :key="item.id" class="item">
|
|
404
|
+
{{ item.name }}
|
|
405
|
+
</div>
|
|
406
|
+
<button v-if="!loading" @click="reload">手动刷新</button>
|
|
407
|
+
</template>
|
|
408
|
+
</InfiniteScrollV2>
|
|
409
|
+
</template>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 示例 3:监听事件
|
|
413
|
+
|
|
414
|
+
```vue
|
|
415
|
+
<script setup lang="ts">
|
|
416
|
+
import { InfiniteScrollV2 } from 'zx-infinite-scroll';
|
|
417
|
+
import 'zx-infinite-scroll/dist/style.css';
|
|
418
|
+
|
|
419
|
+
const handleSuccess = ({ records, total, reset }) => {
|
|
420
|
+
console.log('加载成功', { records, total, reset });
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const handleError = (err) => {
|
|
424
|
+
console.error('加载失败', err);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const fetchData = async ({ page, pageSize }) => {
|
|
428
|
+
// 模拟可能失败的请求
|
|
429
|
+
const res = await fetch(`/api/list?page=${page}&pageSize=${pageSize}`);
|
|
430
|
+
if (!res.ok) throw new Error('请求失败');
|
|
431
|
+
return await res.json();
|
|
432
|
+
};
|
|
433
|
+
</script>
|
|
434
|
+
|
|
435
|
+
<template>
|
|
436
|
+
<InfiniteScrollV2
|
|
437
|
+
:fetch-fn="fetchData"
|
|
438
|
+
@success="handleSuccess"
|
|
439
|
+
@error="handleError"
|
|
440
|
+
>
|
|
441
|
+
<template #default="{ list }">
|
|
442
|
+
<div v-for="item in list" :key="item.id">
|
|
443
|
+
{{ item.name }}
|
|
444
|
+
</div>
|
|
445
|
+
</template>
|
|
446
|
+
</InfiniteScrollV2>
|
|
447
|
+
</template>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### 示例 4:使用基础组件(手动控制)
|
|
451
|
+
|
|
452
|
+
```vue
|
|
453
|
+
<script setup lang="ts">
|
|
454
|
+
import { ref } from 'vue';
|
|
455
|
+
import { InfiniteScroll } from 'zx-infinite-scroll';
|
|
456
|
+
import 'zx-infinite-scroll/dist/style.css';
|
|
457
|
+
|
|
458
|
+
const scrollRef = ref();
|
|
459
|
+
const list = ref([]);
|
|
460
|
+
const loading = ref(false);
|
|
461
|
+
const finished = ref(false);
|
|
462
|
+
const error = ref(false);
|
|
463
|
+
const page = ref(1);
|
|
464
|
+
const total = ref(0);
|
|
465
|
+
|
|
466
|
+
const loadData = async () => {
|
|
467
|
+
loading.value = true;
|
|
468
|
+
error.value = false;
|
|
469
|
+
try {
|
|
470
|
+
const res = await fetch(`/api/list?page=${page.value}&pageSize=10`);
|
|
471
|
+
const data = await res.json();
|
|
472
|
+
list.value.push(...data.records);
|
|
473
|
+
total.value = data.total;
|
|
474
|
+
finished.value = list.value.length >= total.value;
|
|
475
|
+
page.value++;
|
|
476
|
+
} catch (err) {
|
|
477
|
+
error.value = true;
|
|
478
|
+
} finally {
|
|
479
|
+
loading.value = false;
|
|
480
|
+
scrollRef.value?.finishLoad();
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const refreshData = async () => {
|
|
485
|
+
page.value = 1;
|
|
486
|
+
list.value = [];
|
|
487
|
+
finished.value = false;
|
|
488
|
+
await loadData();
|
|
489
|
+
scrollRef.value?.finishRefresh();
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const retryLoad = () => {
|
|
493
|
+
if (list.value.length === 0) {
|
|
494
|
+
refreshData();
|
|
495
|
+
} else {
|
|
496
|
+
loadData();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
</script>
|
|
500
|
+
|
|
501
|
+
<template>
|
|
502
|
+
<InfiniteScroll
|
|
503
|
+
ref="scrollRef"
|
|
504
|
+
:loading="loading"
|
|
505
|
+
:finished="finished"
|
|
506
|
+
:error="error"
|
|
507
|
+
:pull-refresh="true"
|
|
508
|
+
@load="loadData"
|
|
509
|
+
@refresh="refreshData"
|
|
510
|
+
@retry="retryLoad"
|
|
511
|
+
>
|
|
512
|
+
<div v-for="item in list" :key="item.id" class="item">
|
|
513
|
+
{{ item.name }}
|
|
514
|
+
</div>
|
|
515
|
+
</InfiniteScroll>
|
|
516
|
+
</template>
|
|
517
|
+
```
|
|
518
|
+
|
|
154
519
|
## License
|
|
155
520
|
|
|
156
521
|
MIT
|
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","sources":["../src/components/request-loading.vue","../src/components/empty-component.vue","../src/components/infinite-scroll.vue","../src/components/infinite-scroll-v2.vue","../src/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\ndefineOptions({ name: 'RequestLoading' });\n\nwithDefaults(defineProps<{ text?: string; fullscreen?: boolean }>(), {\n text: '加载中...',\n fullscreen: false\n});\n</script>\n\n<template>\n <div class=\"loading-container\" :class=\"{ fullscreen: fullscreen }\">\n <div class=\"loading-content\">\n <span class=\"loading-spinner\"></span>\n <span class=\"loading-text\">{{ text }}</span>\n </div>\n </div>\n</template>\n\n<style scoped lang=\"scss\">\n.loading-content {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 6px 12px;\n border-radius: 16px;\n color: var(--zx-primary-color, #1e5efd);\n font-size: 14px;\n}\n\n.loading-spinner {\n width: 18px;\n height: 18px;\n border-radius: 50%;\n border: 2.5px solid var(--zx-primary-color, #1e5efd);\n border-top-color: transparent;\n animation: spin 0.8s linear infinite;\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.loading-container.fullscreen {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(255, 255, 255, 0.5);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\ndefineOptions({ name: 'EmptyComponent' });\n\ninterface Props {\n text?: string;\n showText?: boolean;\n /** 顶部间距,可传数字(px)或带单位字符串,默认 168px */\n paddingTop?: number | string;\n /** 自定义图片 URL */\n emptyImage?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n text: '暂无数据',\n showText: true,\n paddingTop: 168,\n emptyImage: ''\n});\n\nconst wrapStyle = computed(() => ({\n paddingTop: typeof props.paddingTop === 'number' ? `${props.paddingTop}px` : props.paddingTop\n}));\n\n// 默认空状态 SVG(简洁的空文件夹图标)\nconst defaultEmptySvg = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='80' viewBox='0 0 96 80' fill='none'%3E%3Cpath d='M8 16C8 12.6863 10.6863 10 14 10H38L44 16H82C85.3137 16 88 18.6863 88 22V66C88 69.3137 85.3137 72 82 72H14C10.6863 72 8 69.3137 8 66V16Z' fill='%23E8E8E8'/%3E%3Cpath d='M8 20C8 18.3431 9.34315 17 11 17H85C86.6569 17 88 18.3431 88 20V66C88 69.3137 85.3137 72 82 72H14C10.6863 72 8 69.3137 8 66V20Z' fill='%23F5F5F5'/%3E%3Cpath d='M38 42C38 40.8954 38.8954 40 40 40H56C57.1046 40 58 40.8954 58 42V44C58 45.1046 57.1046 46 56 46H40C38.8954 46 38 45.1046 38 44V42Z' fill='%23D9D9D9'/%3E%3C/svg%3E`;\n\nconst imageSrc = computed(() => props.emptyImage || defaultEmptySvg);\n</script>\n\n<template>\n <div class=\"empty-wrap\" :style=\"wrapStyle\">\n <img class=\"empty-image\" :src=\"imageSrc\" alt=\"\" />\n <p v-if=\"showText\" class=\"empty-text\">\n <slot>{{ text }}</slot>\n </p>\n </div>\n</template>\n\n<style scoped lang=\"scss\">\n.empty-wrap {\n display: flex;\n flex-direction: column;\n align-items: center;\n}\n\n.empty-image {\n width: 96px;\n height: 80px;\n}\n\n.empty-text {\n margin: 12px 0 0;\n color: #8c8f97;\n font-size: 12px;\n line-height: 22px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport RequestLoading from './request-loading.vue';\nimport EmptyComponent from './empty-component.vue';\n\ndefineOptions({\n name: 'InfiniteScroll'\n});\n\ninterface Props {\n /** 是否处于加载中状态 */\n loading?: boolean;\n /** 是否已加载完所有数据 */\n finished?: boolean;\n /** 是否显示空状态 */\n isEmpty?: boolean;\n /** 空状态文案 */\n emptyText?: string;\n /** 是否启用下拉刷新 */\n pullRefresh?: boolean;\n /** 下拉刷新触发距离(px) */\n pullThreshold?: number;\n /** 上拉加载触发距离(距离底部 px) */\n loadThreshold?: number;\n /** 下拉中的文案 */\n pullingText?: string;\n /** 释放刷新的文案 */\n loosingText?: string;\n /** 刷新中的文案 */\n refreshingText?: string;\n /** 加载中的文案 */\n loadingText?: string;\n /** 加载完成的文案 */\n finishedText?: string;\n /** 加载失败的文案 */\n errorText?: string;\n /** 是否显示错误状态 */\n error?: boolean;\n /** 内容区域是否显示加载状态 */\n contentLoading?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n loading: false,\n finished: false,\n isEmpty: false,\n emptyText: '暂无数据',\n pullRefresh: false,\n pullThreshold: 70,\n loadThreshold: 50,\n pullingText: '下拉刷新',\n loosingText: '释放即可刷新',\n refreshingText: '加载中...',\n loadingText: '加载中...',\n finishedText: '没有更多了~',\n errorText: '加载失败,点击重试',\n error: false,\n contentLoading: false\n});\n\nconst emit = defineEmits<{\n /** 触发下拉刷新 */\n refresh: [];\n /** 触发上拉加载 */\n load: [];\n /** 点击错误重试 */\n retry: [];\n}>();\n\nconst containerRef = ref<HTMLElement | null>(null);\nconst contentRef = ref<HTMLElement | null>(null);\nconst indicatorRef = ref<HTMLElement | null>(null);\n\n// 下拉刷新状态\nconst pullStatus = ref<'idle' | 'pulling' | 'loosing' | 'refreshing'>('idle');\nconst pullDistance = ref(0);\n// 拖动期间禁用 transition,避免 transform 变化时被 0.25s 补间拖慢\nconst isDragging = ref(false);\n\n// 触摸相关\nlet startX = 0;\nlet startY = 0;\nlet lastDistance = 0;\n// 手势方向锁:避免横向滑动被误判为下拉\nlet gestureLock: 'none' | 'vertical' | 'horizontal' = 'none';\nconst SLOPE_LOCK_DISTANCE = 8;\n\nlet isRefreshing = false;\nlet isLoading = false;\n// 鼠标拖拽下拉(PC 端)\nlet isMouseDown = false;\n\n// 节流相关\nlet scrollTimer: number | null = null;\nlet touchMoveRaf: number | null = null;\nlet idleTimer: number | null = null;\n// 监听容器尺寸变化\nlet resizeObserver: ResizeObserver | null = null;\n// 防止空页死循环:snapshot 触发 load 时的 contentHeight,下次完成后没增长就不再 auto-load\nlet lastTriggerContentHeight = -1;\n\n// 下拉刷新核心:按下(与指针类型无关,touch/mouse 共用)\nfunction pullStart(clientX: number, clientY: number) {\n if (!props.pullRefresh || isRefreshing) return;\n startX = clientX;\n startY = clientY;\n lastDistance = 0;\n gestureLock = 'none';\n pullStatus.value = 'idle';\n}\n\n// 下拉刷新核心:移动(rAF 节流,始终使用最新位置)\n// 返回 true 表示消费了本次移动(下拉进行中),调用方据此决定是否 preventDefault\nfunction pullMove(clientX: number, clientY: number, cancelable: boolean): boolean {\n if (!props.pullRefresh || isRefreshing || startY === 0) return false;\n\n const dx = clientX - startX;\n const dy = clientY - startY;\n\n // 方向判定:先到达 8px 锁定的方向才认为有意图,垂直要明显大于水平(1.5x)\n if (gestureLock === 'none') {\n const ax = Math.abs(dx);\n const ay = Math.abs(dy);\n if (ax > SLOPE_LOCK_DISTANCE || ay > SLOPE_LOCK_DISTANCE) {\n gestureLock = ay > ax * 1.5 ? 'vertical' : 'horizontal';\n } else {\n return false;\n }\n }\n if (gestureLock === 'horizontal') return false;\n\n // 子像素容差:≤1 视为已到顶,避免 X5 内核 scrollTop 偶发非整数判断失败\n const atTop = getScrollTop() <= 1;\n\n // iOS 15- 兜底:在 atBottom 且向上拖时阻止整页 rubber-band\n // (iOS 16+ overscroll-behavior 已处理,此处兜底兼容)\n if (!atTop && dy < 0) {\n const c = containerRef.value;\n if (c) {\n const atBottom = c.scrollHeight - c.scrollTop - c.clientHeight <= 1;\n if (atBottom && cancelable) {\n return true;\n }\n }\n return false;\n }\n if (!atTop) return false;\n\n // 反向拖动也要更新(让指示器跟着手指回退),夹紧到 [0, 150]\n const clamped = Math.max(0, Math.min(dy, 150));\n\n lastDistance = clamped;\n isDragging.value = true;\n\n if (touchMoveRaf === null) {\n touchMoveRaf = requestAnimationFrame(() => {\n touchMoveRaf = null;\n pullDistance.value = lastDistance * 0.5;\n if (lastDistance === 0) {\n pullStatus.value = 'idle';\n } else {\n pullStatus.value = lastDistance > props.pullThreshold ? 'loosing' : 'pulling';\n }\n });\n }\n\n // 仅在向下拖时消费事件(避免误吞用户向上滚动的意图)\n return dy > 0;\n}\n\n// 下拉刷新核心:松开\nfunction pullEnd() {\n if (!props.pullRefresh || isRefreshing) return;\n\n // 取消待执行的 rAF,避免松手后还有一帧异步更新把 UI 拉回 loosing 卡住\n if (touchMoveRaf !== null) {\n cancelAnimationFrame(touchMoveRaf);\n touchMoveRaf = null;\n }\n\n isDragging.value = false;\n\n // 用最新的 lastDistance 同步判定,不依赖可能尚未生效的 pullStatus\n if (lastDistance > props.pullThreshold) {\n isRefreshing = true;\n pullStatus.value = 'refreshing';\n pullDistance.value = 50;\n emit('refresh');\n } else {\n pullDistance.value = 0;\n pullStatus.value = 'idle';\n }\n\n resetTouch();\n}\n\n// 下拉刷新核心:取消(回到 idle)\nfunction pullCancel() {\n if (!props.pullRefresh || isRefreshing) return;\n\n if (touchMoveRaf !== null) {\n cancelAnimationFrame(touchMoveRaf);\n touchMoveRaf = null;\n }\n\n isDragging.value = false;\n pullDistance.value = 0;\n pullStatus.value = 'idle';\n resetTouch();\n}\n\n// 下拉刷新 - touchstart\nfunction handleTouchStart(e: TouchEvent) {\n pullStart(e.touches[0].clientX, e.touches[0].clientY);\n}\n\n// 下拉刷新 - touchmove\nfunction handleTouchMove(e: TouchEvent) {\n const touch = e.touches[0];\n if (pullMove(touch.clientX, touch.clientY, e.cancelable) && e.cancelable) {\n e.preventDefault();\n }\n}\n\n// 下拉刷新 - touchend\nfunction handleTouchEnd() {\n pullEnd();\n}\n\n// 下拉刷新 - touchcancel(微信 X5 内核会在系统手势介入时派发 cancel 而非 end)\nfunction handleTouchCancel() {\n pullCancel();\n}\n\n// 下拉刷新 - mousedown(PC 端鼠标拖拽)\nfunction handleMouseDown(e: MouseEvent) {\n // 仅左键触发\n if (!props.pullRefresh || isRefreshing || e.button !== 0) return;\n isMouseDown = true;\n pullStart(e.clientX, e.clientY);\n}\n\n// 下拉刷新 - mousemove(挂在 window 上,允许拖出容器)\nfunction handleMouseMove(e: MouseEvent) {\n if (!isMouseDown) return;\n if (pullMove(e.clientX, e.clientY, true)) {\n // 阻止拖拽时选中文本\n e.preventDefault();\n }\n}\n\n// 下拉刷新 - mouseup\nfunction handleMouseUp() {\n if (!isMouseDown) return;\n isMouseDown = false;\n pullEnd();\n}\n\nfunction resetTouch() {\n startX = 0;\n startY = 0;\n lastDistance = 0;\n gestureLock = 'none';\n}\n\n// 上拉加载 - scroll(节流处理)\nfunction handleScroll() {\n if (props.loading || props.finished || props.error || isLoading) return;\n if (scrollTimer) return;\n\n scrollTimer = window.setTimeout(() => {\n scrollTimer = null;\n const container = containerRef.value;\n if (!container) return;\n\n const { scrollTop, clientHeight, scrollHeight } = container;\n const contentHeight = contentRef.value?.clientHeight || 0;\n\n // 情况1:内容不足一屏,自动触发加载\n if (contentHeight <= clientHeight) {\n // 死循环防护:上一次 auto-load 后内容没增长,不再继续触发\n if (lastTriggerContentHeight === contentHeight) return;\n lastTriggerContentHeight = contentHeight;\n // 不足一屏时主动滚到底,让 loading 指示器进入视口\n triggerLoad(true);\n return;\n }\n\n // 情况2:滚动到距离底部阈值范围内\n if (scrollHeight - scrollTop - clientHeight <= props.loadThreshold) {\n // 用户已经手动滚到底,不再强制 scrollTop(避免打断惯性滚动)\n triggerLoad(false);\n }\n }, 100);\n}\n\nfunction triggerLoad(scrollToBottom = false) {\n isLoading = true;\n emit('load');\n nextTick(() => {\n const c = containerRef.value;\n if (!c) return;\n if (scrollToBottom) {\n // 内容不足一屏:直接滚到底(没有\"惯性\"可以打断)\n c.scrollTop = c.scrollHeight;\n } else {\n // 阈值触发:用 scrollIntoView 平滑进入视口,避免突兀打断;\n // 旧 WebKit 不识别 options 会降级为即时滚动,效果等价\n indicatorRef.value?.scrollIntoView({ block: 'end', behavior: 'smooth' });\n }\n });\n}\n\n// 获取滚动距离\nfunction getScrollTop(): number {\n return containerRef.value?.scrollTop ?? 0;\n}\n\n// 完成下拉刷新\nfunction finishRefresh() {\n isRefreshing = false;\n pullDistance.value = 0;\n // 让 transform snap-back 完成后再切 idle,避免 spinner 在动画中途突然消失\n if (idleTimer !== null) clearTimeout(idleTimer);\n idleTimer = window.setTimeout(() => {\n pullStatus.value = 'idle';\n idleTimer = null;\n }, 250);\n // 重置死循环计数\n lastTriggerContentHeight = -1;\n nextTick(() => {\n checkContentHeight();\n });\n}\n\n// 完成上拉加载\nfunction finishLoad() {\n isLoading = false;\n nextTick(() => {\n const contentHeight = contentRef.value?.clientHeight || 0;\n // 内容没增长就不再触发 auto-load,避免空页死循环\n if (lastTriggerContentHeight !== -1 && contentHeight === lastTriggerContentHeight) return;\n lastTriggerContentHeight = -1;\n checkContentHeight();\n });\n}\n\n// 检查内容高度,不足一屏时自动触发加载\nfunction checkContentHeight() {\n if (props.finished || props.loading || props.error) return;\n\n nextTick(() => {\n const contentHeight = contentRef.value?.clientHeight || 0;\n const clientHeight = containerRef.value?.clientHeight || 0;\n\n if (contentHeight <= clientHeight && !isLoading) {\n handleScroll();\n }\n });\n}\n\n// 点击错误重试 — 由父组件决定具体行为,避免 retry + load 双触发\nfunction handleRetry() {\n emit('retry');\n}\n\n// loading 完成时同步重置 isLoading\nwatch(\n () => props.loading,\n newVal => {\n if (!newVal) {\n isLoading = false;\n }\n }\n);\n\n// finished 翻为 true 时重置死循环计数\nwatch(\n () => props.finished,\n newVal => {\n if (newVal) {\n lastTriggerContentHeight = -1;\n }\n }\n);\n\nonMounted(() => {\n const container = containerRef.value;\n if (!container) return;\n\n if (props.pullRefresh) {\n container.addEventListener('touchstart', handleTouchStart, { passive: true });\n container.addEventListener('touchmove', handleTouchMove, { passive: false });\n container.addEventListener('touchend', handleTouchEnd, { passive: true });\n container.addEventListener('touchcancel', handleTouchCancel, { passive: true });\n // 鼠标拖拽(PC):move/up 挂在 window 上,允许拖出容器边界\n container.addEventListener('mousedown', handleMouseDown);\n window.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('mouseup', handleMouseUp);\n }\n\n container.addEventListener('scroll', handleScroll, { passive: true });\n\n // 监听容器/内容尺寸变化(旋转、WebView resize、软键盘弹出收起等)\n if (typeof ResizeObserver !== 'undefined') {\n resizeObserver = new ResizeObserver(() => {\n checkContentHeight();\n });\n resizeObserver.observe(container);\n if (contentRef.value) resizeObserver.observe(contentRef.value);\n }\n\n // 初始检查内容高度\n nextTick(() => {\n checkContentHeight();\n });\n});\n\nonBeforeUnmount(() => {\n const container = containerRef.value;\n if (container) {\n container.removeEventListener('touchstart', handleTouchStart);\n container.removeEventListener('touchmove', handleTouchMove);\n container.removeEventListener('touchend', handleTouchEnd);\n container.removeEventListener('touchcancel', handleTouchCancel);\n container.removeEventListener('mousedown', handleMouseDown);\n container.removeEventListener('scroll', handleScroll);\n }\n window.removeEventListener('mousemove', handleMouseMove);\n window.removeEventListener('mouseup', handleMouseUp);\n if (resizeObserver) {\n resizeObserver.disconnect();\n resizeObserver = null;\n }\n if (touchMoveRaf !== null) {\n cancelAnimationFrame(touchMoveRaf);\n touchMoveRaf = null;\n }\n if (scrollTimer !== null) {\n clearTimeout(scrollTimer);\n scrollTimer = null;\n }\n if (idleTimer !== null) {\n clearTimeout(idleTimer);\n idleTimer = null;\n }\n});\n\ndefineExpose({\n refresh: finishRefresh,\n finishRefresh,\n finishPullDown: finishRefresh,\n finishLoad,\n finishPullUp: finishLoad,\n checkContentHeight\n});\n</script>\n\n<template>\n <div ref=\"containerRef\" class=\"infinite-scroll\">\n <!-- 下拉刷新指示器 -->\n <div\n v-if=\"pullRefresh\"\n class=\"pull-refresh-indicator\"\n :class=\"{ 'no-transition': isDragging }\"\n :style=\"{ transform: `translateY(${pullDistance}px)` }\"\n >\n <div v-show=\"pullStatus !== 'idle'\" class=\"pull-refresh-content\">\n <!-- <RequestLoading v-if=\"pullStatus === 'refreshing'\" text=\"\" /> -->\n <span v-if=\"pullStatus === 'refreshing'\"></span>\n <span v-else-if=\"pullStatus === 'loosing'\" class=\"pull-text\">{{ loosingText }}</span>\n <span v-else class=\"pull-text\">{{ pullingText }}</span>\n </div>\n </div>\n\n <!-- 内容区域 -->\n <div\n ref=\"contentRef\"\n class=\"infinite-scroll-content\"\n :class=\"{ 'no-transition': isDragging }\"\n :style=\"pullRefresh ? { transform: `translateY(${pullDistance}px)` } : {}\"\n >\n <!-- 内容区域加载遮罩 -->\n <div v-if=\"contentLoading\" class=\"content-loading-mask\">\n <RequestLoading :text=\"refreshingText\" />\n </div>\n\n <!-- 空状态 -->\n <EmptyComponent v-if=\"isEmpty && !loading\" :text=\"emptyText\" />\n\n <!-- 默认插槽 -->\n <slot></slot>\n\n <!-- 上拉加载指示器 -->\n <div v-if=\"!isEmpty\" ref=\"indicatorRef\" class=\"load-more-indicator\">\n <RequestLoading v-if=\"loading && pullStatus !== 'refreshing'\" :text=\"loadingText\" />\n <span v-else-if=\"finished\" class=\"finished-text\">{{ finishedText }}</span>\n <span v-else-if=\"error\" class=\"error-text\" @click=\"handleRetry\">{{ errorText }}</span>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped lang=\"scss\">\n.infinite-scroll {\n height: 100%;\n position: relative;\n min-height: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n // 阻止滚动到边界时把滚动链传递给祖先(修复 iOS 16+ / 微信 X5 下拉变成整页拖拽)\n overscroll-behavior: contain;\n // 显式声明只接受纵向手势,避免被外层 touch-action 影响\n touch-action: pan-y;\n // iOS 下让原生回弹有惯性\n -webkit-overflow-scrolling: touch;\n}\n\n.pull-refresh-indicator {\n position: absolute;\n top: -50px;\n left: 0;\n right: 0;\n height: 50px;\n transition: transform 0.25s;\n\n // 拖动期间禁用过渡,让 transform 1:1 跟随手指\n &.no-transition {\n transition: none;\n }\n}\n\n.pull-refresh-content {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n font-size: 14px;\n color: #969799;\n}\n\n.pull-text {\n color: var(--zx-primary-color, #1989fa);\n font-size: 14px;\n}\n\n.infinite-scroll-content {\n position: relative;\n transition: transform 0.25s;\n min-height: 100%;\n\n &.no-transition {\n transition: none;\n }\n}\n\n.content-loading-mask {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(255, 255, 255, 0.8);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10;\n backdrop-filter: blur(2px);\n}\n\n.load-more-indicator {\n display: flex;\n align-items: center;\n justify-content: center;\n // 预留高度避免 loading 渲染后被推到视口外造成遮挡\n min-height: 50px;\n padding: 16px 0;\n font-size: 14px;\n color: #8c8f97;\n}\n\n.finished-text {\n color: #8c8f97;\n font-size: 14px;\n}\n\n.error-text {\n color: #ee0a24;\n font-size: 14px;\n cursor: pointer;\n\n &:active {\n opacity: 0.7;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"T\">\nimport { type Ref, nextTick, onMounted, ref, watch } from 'vue';\nimport InfiniteScroll from './infinite-scroll.vue';\n\ndefineOptions({\n name: 'InfiniteScrollV2'\n});\n\ninterface FetchParams {\n page: number;\n pageSize: number;\n [key: string]: any;\n}\n\ninterface FetchResult<R> {\n records: R[];\n total: number;\n}\n\ninterface Props {\n /** 数据请求函数,需返回 { records, total },失败时抛错 */\n fetchFn: (params: FetchParams) => Promise<FetchResult<T>>;\n /** 业务查询参数(如筛选条件),变化时自动 reset 重新加载 */\n params?: Record<string, any>;\n /** 每页条数 */\n pageSize?: number;\n /** params 变化时是否自动重新加载 */\n autoReload?: boolean;\n /** 是否在挂载时自动加载首页 */\n immediate?: boolean;\n /** 是否启用下拉刷新 */\n pullRefresh?: boolean;\n /** 下拉刷新触发距离(px) */\n pullThreshold?: number;\n /** 上拉加载触发距离(距离底部 px) */\n loadThreshold?: number;\n /** 空状态文案 */\n emptyText?: string;\n /** 下拉中的文案 */\n pullingText?: string;\n /** 释放刷新的文案 */\n loosingText?: string;\n /** 刷新中的文案 */\n refreshingText?: string;\n /** 加载中的文案 */\n loadingText?: string;\n /** 加载完成的文案 */\n finishedText?: string;\n /** 加载失败的文案 */\n errorText?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n params: () => ({}),\n pageSize: 10,\n autoReload: true,\n immediate: true,\n pullRefresh: false,\n pullThreshold: 70,\n loadThreshold: 50,\n emptyText: '暂无数据',\n pullingText: '下拉刷新',\n loosingText: '释放即可刷新',\n refreshingText: '加载中...',\n loadingText: '加载中...',\n finishedText: '没有更多了~',\n errorText: '加载失败,点击重试'\n});\n\nconst emit = defineEmits<{\n /** 加载成功,每次成功(含分页)都会触发 */\n success: [payload: { records: T[]; total: number; reset: boolean }];\n /** 加载失败 */\n error: [err: unknown];\n /** 列表内容变化(v-model:list 支持) */\n 'update:list': [list: T[]];\n}>();\n\nconst scrollRef = ref<InstanceType<typeof InfiniteScroll> | null>(null);\n\nconst list = ref<T[]>([]) as Ref<T[]>;\nconst loading = ref(false);\nconst finished = ref(false);\nconst isEmpty = ref(false);\nconst loadError = ref(false);\nconst total = ref(0);\nconst page = ref(1);\nconst isRefreshing = ref(false);\n\nlet requestSeq = 0;\n\nasync function load(reset = false) {\n if (reset) {\n page.value = 1;\n finished.value = false;\n isEmpty.value = false;\n // 不立即清空 list,保留旧数据直到新数据到达\n }\n loading.value = true;\n loadError.value = false;\n\n requestSeq += 1;\n const seq = requestSeq;\n\n try {\n const result = await props.fetchFn({\n page: page.value,\n pageSize: props.pageSize,\n ...props.params\n });\n\n if (seq !== requestSeq) return;\n\n const records = result?.records || [];\n if (reset) {\n // reset 时替换整个列表\n list.value = records;\n total.value = 0;\n } else {\n list.value = [...list.value, ...records];\n }\n total.value = Number(result?.total || 0);\n finished.value = list.value.length >= total.value;\n isEmpty.value = list.value.length === 0;\n\n emit('success', { records, total: total.value, reset });\n emit('update:list', list.value);\n } catch (err) {\n if (seq !== requestSeq) return;\n loadError.value = true;\n emit('error', err);\n } finally {\n if (seq === requestSeq) {\n loading.value = false;\n }\n }\n}\n\nasync function handleRefresh() {\n isRefreshing.value = true;\n // 先让下拉刷新指示器收回\n scrollRef.value?.finishRefresh();\n // 等待 250ms 让指示器完全收回(对应 infinite-scroll.vue 的 transition 时长)\n await new Promise<void>(resolve => {\n setTimeout(() => resolve(), 250);\n });\n // 指示器收回后再开始请求新数据\n await load(true);\n isRefreshing.value = false;\n}\n\nasync function handleLoad() {\n if (finished.value) {\n scrollRef.value?.finishLoad();\n return;\n }\n // 列表为空时是首次加载(page 已经是 1),其它情况 page++\n if (list.value.length > 0) {\n page.value += 1;\n }\n await load(false);\n scrollRef.value?.finishLoad();\n}\n\nfunction handleRetry() {\n loadError.value = false;\n if (list.value.length === 0) {\n load(true);\n } else {\n handleLoad();\n }\n}\n\nasync function reload() {\n await load(true);\n}\n\nwatch(\n () => props.params,\n () => {\n if (props.autoReload) {\n reload();\n }\n },\n { deep: true }\n);\n\nwatch(\n list,\n async () => {\n await nextTick();\n scrollRef.value?.checkContentHeight();\n },\n { deep: true }\n);\n\nonMounted(() => {\n if (props.immediate) {\n load(true);\n }\n});\n\ndefineExpose({\n /** 重新加载第一页 */\n reload,\n /** 兼容别名 */\n refresh: reload,\n /** 当前列表数据 */\n list,\n /** 加载中状态 */\n loading,\n /** 是否已加载完 */\n finished,\n /** 是否空数据 */\n isEmpty,\n /** 总条数 */\n total,\n /** 透传 v1 方法 */\n finishLoad: () => scrollRef.value?.finishLoad(),\n finishRefresh: () => scrollRef.value?.finishRefresh(),\n checkContentHeight: () => scrollRef.value?.checkContentHeight()\n});\n</script>\n\n<template>\n <InfiniteScroll\n ref=\"scrollRef\"\n :loading=\"loading\"\n :finished=\"finished\"\n :is-empty=\"isEmpty\"\n :error=\"loadError\"\n :pull-refresh=\"pullRefresh\"\n :pull-threshold=\"pullThreshold\"\n :load-threshold=\"loadThreshold\"\n :empty-text=\"emptyText\"\n :pulling-text=\"pullingText\"\n :loosing-text=\"loosingText\"\n :refreshing-text=\"refreshingText\"\n :loading-text=\"loadingText\"\n :finished-text=\"finishedText\"\n :error-text=\"errorText\"\n :content-loading=\"isRefreshing\"\n @load=\"handleLoad\"\n @refresh=\"handleRefresh\"\n @retry=\"handleRetry\"\n >\n <slot\n :list=\"list\"\n :loading=\"loading\"\n :finished=\"finished\"\n :is-empty=\"isEmpty\"\n :error=\"loadError\"\n :total=\"total\"\n :reload=\"reload\"\n />\n </InfiniteScroll>\n</template>\n","import { App } from 'vue';\nimport InfiniteScroll from './components/infinite-scroll.vue';\nimport InfiniteScrollV2 from './components/infinite-scroll-v2.vue';\nimport RequestLoading from './components/request-loading.vue';\nimport EmptyComponent from './components/empty-component.vue';\n\n// 单独导出组件\nexport { InfiniteScroll, InfiniteScrollV2, RequestLoading, EmptyComponent };\n\n// Vue plugin install 函数\nexport function install(app: App) {\n app.component('InfiniteScroll', InfiniteScroll);\n app.component('InfiniteScrollV2', InfiniteScrollV2);\n app.component('RequestLoading', RequestLoading);\n app.component('EmptyComponent', EmptyComponent);\n}\n\n// 默认导出(支持 app.use() 安装)\nexport default {\n install\n};\n"],"names":["_createElementBlock","_normalizeClass","__props","_createElementVNode","_hoisted_1","_hoisted_2","_toDisplayString","defaultEmptySvg","props","wrapStyle","computed","imageSrc","_openBlock","_renderSlot","_ctx","SLOPE_LOCK_DISTANCE","emit","__emit","containerRef","ref","contentRef","indicatorRef","pullStatus","pullDistance","isDragging","startX","startY","lastDistance","gestureLock","isRefreshing","isLoading","isMouseDown","scrollTimer","touchMoveRaf","idleTimer","resizeObserver","lastTriggerContentHeight","pullStart","clientX","clientY","pullMove","cancelable","dx","dy","ax","ay","atTop","getScrollTop","c","pullEnd","resetTouch","pullCancel","handleTouchStart","handleTouchMove","touch","handleTouchEnd","handleTouchCancel","handleMouseDown","handleMouseMove","handleMouseUp","handleScroll","container","scrollTop","clientHeight","scrollHeight","contentHeight","_a","triggerLoad","scrollToBottom","nextTick","finishRefresh","checkContentHeight","finishLoad","_b","handleRetry","watch","newVal","onMounted","onBeforeUnmount","__expose","_withDirectives","_hoisted_3","_hoisted_4","_normalizeStyle","_hoisted_5","_createVNode","RequestLoading","_createBlock","EmptyComponent","_hoisted_6","scrollRef","list","loading","finished","isEmpty","loadError","total","page","requestSeq","load","reset","seq","result","records","err","handleRefresh","resolve","handleLoad","reload","InfiniteScroll","install","app","InfiniteScrollV2","index"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","sources":["../src/components/request-loading.vue","../src/components/empty-component.vue","../src/components/infinite-scroll.vue","../src/components/infinite-scroll-v2.vue","../src/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\ndefineOptions({ name: 'RequestLoading' });\n\nwithDefaults(defineProps<{ text?: string; fullscreen?: boolean }>(), {\n text: '加载中...',\n fullscreen: false\n});\n</script>\n\n<template>\n <div class=\"loading-container\" :class=\"{ fullscreen: fullscreen }\">\n <div class=\"loading-content\">\n <span class=\"loading-spinner\"></span>\n <span class=\"loading-text\">{{ text }}</span>\n </div>\n </div>\n</template>\n\n<style scoped lang=\"scss\">\n.loading-content {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 6px 12px;\n border-radius: 16px;\n color: var(--zx-primary-color, #1e5efd);\n font-size: 14px;\n}\n\n.loading-spinner {\n width: 18px;\n height: 18px;\n border-radius: 50%;\n border: 2.5px solid var(--zx-primary-color, #1e5efd);\n border-top-color: transparent;\n animation: spin 0.8s linear infinite;\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.loading-container.fullscreen {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(255, 255, 255, 0.5);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\ndefineOptions({ name: 'EmptyComponent' });\n\ninterface Props {\n text?: string;\n showText?: boolean;\n /** 顶部间距,可传数字(px)或带单位字符串,默认 168px */\n paddingTop?: number | string;\n /** 自定义图片 URL */\n emptyImage?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n text: '暂无数据',\n showText: true,\n paddingTop: 168,\n emptyImage: ''\n});\n\nconst wrapStyle = computed(() => ({\n paddingTop: typeof props.paddingTop === 'number' ? `${props.paddingTop}px` : props.paddingTop\n}));\n\n// 默认空状态 SVG(简洁的空文件夹图标)\nconst defaultEmptySvg = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='80' viewBox='0 0 96 80' fill='none'%3E%3Cpath d='M8 16C8 12.6863 10.6863 10 14 10H38L44 16H82C85.3137 16 88 18.6863 88 22V66C88 69.3137 85.3137 72 82 72H14C10.6863 72 8 69.3137 8 66V16Z' fill='%23E8E8E8'/%3E%3Cpath d='M8 20C8 18.3431 9.34315 17 11 17H85C86.6569 17 88 18.3431 88 20V66C88 69.3137 85.3137 72 82 72H14C10.6863 72 8 69.3137 8 66V20Z' fill='%23F5F5F5'/%3E%3Cpath d='M38 42C38 40.8954 38.8954 40 40 40H56C57.1046 40 58 40.8954 58 42V44C58 45.1046 57.1046 46 56 46H40C38.8954 46 38 45.1046 38 44V42Z' fill='%23D9D9D9'/%3E%3C/svg%3E`;\n\nconst imageSrc = computed(() => props.emptyImage || defaultEmptySvg);\n</script>\n\n<template>\n <div class=\"empty-wrap\" :style=\"wrapStyle\">\n <img class=\"empty-image\" :src=\"imageSrc\" alt=\"\" />\n <p v-if=\"showText\" class=\"empty-text\">\n <slot>{{ text }}</slot>\n </p>\n </div>\n</template>\n\n<style scoped lang=\"scss\">\n.empty-wrap {\n display: flex;\n flex-direction: column;\n align-items: center;\n}\n\n.empty-image {\n width: 96px;\n height: 80px;\n}\n\n.empty-text {\n margin: 12px 0 0;\n color: #8c8f97;\n font-size: 12px;\n line-height: 22px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport RequestLoading from './request-loading.vue';\nimport EmptyComponent from './empty-component.vue';\n\ndefineOptions({\n name: 'InfiniteScroll'\n});\n\ninterface Props {\n /** 是否处于加载中状态 */\n loading?: boolean;\n /** 是否已加载完所有数据 */\n finished?: boolean;\n /** 是否显示空状态 */\n isEmpty?: boolean;\n /** 空状态文案 */\n emptyText?: string;\n /** 是否启用下拉刷新 */\n pullRefresh?: boolean;\n /** 下拉刷新触发距离(px) */\n pullThreshold?: number;\n /** 上拉加载触发距离(距离底部 px) */\n loadThreshold?: number;\n /** 下拉中的文案 */\n pullingText?: string;\n /** 释放刷新的文案 */\n loosingText?: string;\n /** 刷新中的文案 */\n refreshingText?: string;\n /** 加载中的文案 */\n loadingText?: string;\n /** 加载完成的文案 */\n finishedText?: string;\n /** 加载失败的文案 */\n errorText?: string;\n /** 是否显示错误状态 */\n error?: boolean;\n /** 内容区域是否显示加载状态 */\n contentLoading?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n loading: false,\n finished: false,\n isEmpty: false,\n emptyText: '暂无数据',\n pullRefresh: false,\n pullThreshold: 70,\n loadThreshold: 50,\n pullingText: '下拉刷新',\n loosingText: '释放即可刷新',\n refreshingText: '加载中...',\n loadingText: '加载中...',\n finishedText: '没有更多了~',\n errorText: '加载失败,点击重试',\n error: false,\n contentLoading: false\n});\n\nconst emit = defineEmits<{\n /** 触发下拉刷新 */\n refresh: [];\n /** 触发上拉加载 */\n load: [];\n /** 点击错误重试 */\n retry: [];\n}>();\n\nconst containerRef = ref<HTMLElement | null>(null);\nconst contentRef = ref<HTMLElement | null>(null);\nconst indicatorRef = ref<HTMLElement | null>(null);\n\n// 下拉刷新状态\nconst pullStatus = ref<'idle' | 'pulling' | 'loosing' | 'refreshing'>('idle');\nconst pullDistance = ref(0);\n// 拖动期间禁用 transition,避免 transform 变化时被 0.25s 补间拖慢\nconst isDragging = ref(false);\n\n// 触摸相关\nlet startX = 0;\nlet startY = 0;\nlet lastDistance = 0;\n// 手势方向锁:避免横向滑动被误判为下拉\nlet gestureLock: 'none' | 'vertical' | 'horizontal' = 'none';\nconst SLOPE_LOCK_DISTANCE = 8;\n\nlet isRefreshing = false;\nlet isLoading = false;\n// 鼠标拖拽下拉(PC 端)\nlet isMouseDown = false;\n\n// 节流相关\nlet scrollTimer: number | null = null;\nlet touchMoveRaf: number | null = null;\nlet idleTimer: number | null = null;\n// 监听容器尺寸变化\nlet resizeObserver: ResizeObserver | null = null;\n// 防止空页死循环:snapshot 触发 load 时的 contentHeight,下次完成后没增长就不再 auto-load\nlet lastTriggerContentHeight = -1;\n\n// 下拉刷新核心:按下(与指针类型无关,touch/mouse 共用)\nfunction pullStart(clientX: number, clientY: number) {\n if (!props.pullRefresh || isRefreshing) return;\n startX = clientX;\n startY = clientY;\n lastDistance = 0;\n gestureLock = 'none';\n pullStatus.value = 'idle';\n}\n\n// 下拉刷新核心:移动(rAF 节流,始终使用最新位置)\n// 返回 true 表示消费了本次移动(下拉进行中),调用方据此决定是否 preventDefault\nfunction pullMove(clientX: number, clientY: number, cancelable: boolean): boolean {\n if (!props.pullRefresh || isRefreshing || startY === 0) return false;\n\n const dx = clientX - startX;\n const dy = clientY - startY;\n\n // 方向判定:先到达 8px 锁定的方向才认为有意图,垂直要明显大于水平(1.5x)\n if (gestureLock === 'none') {\n const ax = Math.abs(dx);\n const ay = Math.abs(dy);\n if (ax > SLOPE_LOCK_DISTANCE || ay > SLOPE_LOCK_DISTANCE) {\n gestureLock = ay > ax * 1.5 ? 'vertical' : 'horizontal';\n } else {\n return false;\n }\n }\n if (gestureLock === 'horizontal') return false;\n\n // 子像素容差:≤1 视为已到顶,避免 X5 内核 scrollTop 偶发非整数判断失败\n const atTop = getScrollTop() <= 1;\n\n // iOS 15- 兜底:在 atBottom 且向上拖时阻止整页 rubber-band\n // (iOS 16+ overscroll-behavior 已处理,此处兜底兼容)\n if (!atTop && dy < 0) {\n const c = containerRef.value;\n if (c) {\n const atBottom = c.scrollHeight - c.scrollTop - c.clientHeight <= 1;\n if (atBottom && cancelable) {\n return true;\n }\n }\n return false;\n }\n if (!atTop) return false;\n\n // 反向拖动也要更新(让指示器跟着手指回退),夹紧到 [0, 150]\n const clamped = Math.max(0, Math.min(dy, 150));\n\n lastDistance = clamped;\n isDragging.value = true;\n\n if (touchMoveRaf === null) {\n touchMoveRaf = requestAnimationFrame(() => {\n touchMoveRaf = null;\n pullDistance.value = lastDistance * 0.5;\n if (lastDistance === 0) {\n pullStatus.value = 'idle';\n } else {\n pullStatus.value = lastDistance > props.pullThreshold ? 'loosing' : 'pulling';\n }\n });\n }\n\n // 仅在向下拖时消费事件(避免误吞用户向上滚动的意图)\n return dy > 0;\n}\n\n// 下拉刷新核心:松开\nfunction pullEnd() {\n if (!props.pullRefresh || isRefreshing) return;\n\n // 取消待执行的 rAF,避免松手后还有一帧异步更新把 UI 拉回 loosing 卡住\n if (touchMoveRaf !== null) {\n cancelAnimationFrame(touchMoveRaf);\n touchMoveRaf = null;\n }\n\n isDragging.value = false;\n\n // 用最新的 lastDistance 同步判定,不依赖可能尚未生效的 pullStatus\n if (lastDistance > props.pullThreshold) {\n isRefreshing = true;\n pullStatus.value = 'refreshing';\n pullDistance.value = 50;\n emit('refresh');\n } else {\n pullDistance.value = 0;\n pullStatus.value = 'idle';\n }\n\n resetTouch();\n}\n\n// 下拉刷新核心:取消(回到 idle)\nfunction pullCancel() {\n if (!props.pullRefresh || isRefreshing) return;\n\n if (touchMoveRaf !== null) {\n cancelAnimationFrame(touchMoveRaf);\n touchMoveRaf = null;\n }\n\n isDragging.value = false;\n pullDistance.value = 0;\n pullStatus.value = 'idle';\n resetTouch();\n}\n\n// 下拉刷新 - touchstart\nfunction handleTouchStart(e: TouchEvent) {\n pullStart(e.touches[0].clientX, e.touches[0].clientY);\n}\n\n// 下拉刷新 - touchmove\nfunction handleTouchMove(e: TouchEvent) {\n const touch = e.touches[0];\n if (pullMove(touch.clientX, touch.clientY, e.cancelable) && e.cancelable) {\n e.preventDefault();\n }\n}\n\n// 下拉刷新 - touchend\nfunction handleTouchEnd() {\n pullEnd();\n}\n\n// 下拉刷新 - touchcancel(微信 X5 内核会在系统手势介入时派发 cancel 而非 end)\nfunction handleTouchCancel() {\n pullCancel();\n}\n\n// 下拉刷新 - mousedown(PC 端鼠标拖拽)\nfunction handleMouseDown(e: MouseEvent) {\n // 仅左键触发\n if (!props.pullRefresh || isRefreshing || e.button !== 0) return;\n isMouseDown = true;\n pullStart(e.clientX, e.clientY);\n}\n\n// 下拉刷新 - mousemove(挂在 window 上,允许拖出容器)\nfunction handleMouseMove(e: MouseEvent) {\n if (!isMouseDown) return;\n if (pullMove(e.clientX, e.clientY, true)) {\n // 阻止拖拽时选中文本\n e.preventDefault();\n }\n}\n\n// 下拉刷新 - mouseup\nfunction handleMouseUp() {\n if (!isMouseDown) return;\n isMouseDown = false;\n pullEnd();\n}\n\nfunction resetTouch() {\n startX = 0;\n startY = 0;\n lastDistance = 0;\n gestureLock = 'none';\n}\n\n// 上拉加载 - scroll(节流处理)\nfunction handleScroll() {\n if (props.loading || props.finished || props.error || isLoading) return;\n if (scrollTimer) return;\n\n scrollTimer = window.setTimeout(() => {\n scrollTimer = null;\n const container = containerRef.value;\n if (!container) return;\n\n const { scrollTop, clientHeight, scrollHeight } = container;\n const contentHeight = contentRef.value?.clientHeight || 0;\n\n // 情况1:内容不足一屏,自动触发加载\n if (contentHeight <= clientHeight) {\n // 死循环防护:上一次 auto-load 后内容没增长,不再继续触发\n if (lastTriggerContentHeight === contentHeight) return;\n lastTriggerContentHeight = contentHeight;\n // 不足一屏时主动滚到底,让 loading 指示器进入视口\n triggerLoad(true);\n return;\n }\n\n // 情况2:滚动到距离底部阈值范围内\n if (scrollHeight - scrollTop - clientHeight <= props.loadThreshold) {\n // 用户已经手动滚到底,不再强制 scrollTop(避免打断惯性滚动)\n triggerLoad(false);\n }\n }, 100);\n}\n\nfunction triggerLoad(scrollToBottom = false) {\n isLoading = true;\n emit('load');\n nextTick(() => {\n const c = containerRef.value;\n if (!c) return;\n if (scrollToBottom) {\n // 内容不足一屏:直接滚到底(没有\"惯性\"可以打断)\n c.scrollTop = c.scrollHeight;\n } else {\n // 阈值触发:用 scrollIntoView 平滑进入视口,避免突兀打断;\n // 旧 WebKit 不识别 options 会降级为即时滚动,效果等价\n indicatorRef.value?.scrollIntoView({ block: 'end', behavior: 'smooth' });\n }\n });\n}\n\n// 获取滚动距离\nfunction getScrollTop(): number {\n return containerRef.value?.scrollTop ?? 0;\n}\n\n// 完成下拉刷新\nfunction finishRefresh() {\n isRefreshing = false;\n pullDistance.value = 0;\n // 让 transform snap-back 完成后再切 idle,避免 spinner 在动画中途突然消失\n if (idleTimer !== null) clearTimeout(idleTimer);\n idleTimer = window.setTimeout(() => {\n pullStatus.value = 'idle';\n idleTimer = null;\n }, 250);\n // 重置死循环计数\n lastTriggerContentHeight = -1;\n nextTick(() => {\n checkContentHeight();\n });\n}\n\n// 完成上拉加载\nfunction finishLoad() {\n isLoading = false;\n nextTick(() => {\n const contentHeight = contentRef.value?.clientHeight || 0;\n // 内容没增长就不再触发 auto-load,避免空页死循环\n if (lastTriggerContentHeight !== -1 && contentHeight === lastTriggerContentHeight) return;\n lastTriggerContentHeight = -1;\n checkContentHeight();\n });\n}\n\n// 检查内容高度,不足一屏时自动触发加载\nfunction checkContentHeight() {\n if (props.finished || props.loading || props.error) return;\n\n nextTick(() => {\n const contentHeight = contentRef.value?.clientHeight || 0;\n const clientHeight = containerRef.value?.clientHeight || 0;\n\n if (contentHeight <= clientHeight && !isLoading) {\n handleScroll();\n }\n });\n}\n\n// 点击错误重试 — 由父组件决定具体行为,避免 retry + load 双触发\nfunction handleRetry() {\n emit('retry');\n}\n\n// loading 完成时同步重置 isLoading\nwatch(\n () => props.loading,\n newVal => {\n if (!newVal) {\n isLoading = false;\n }\n }\n);\n\n// finished 翻为 true 时重置死循环计数\nwatch(\n () => props.finished,\n newVal => {\n if (newVal) {\n lastTriggerContentHeight = -1;\n }\n }\n);\n\nonMounted(() => {\n const container = containerRef.value;\n if (!container) return;\n\n if (props.pullRefresh) {\n container.addEventListener('touchstart', handleTouchStart, { passive: true });\n container.addEventListener('touchmove', handleTouchMove, { passive: false });\n container.addEventListener('touchend', handleTouchEnd, { passive: true });\n container.addEventListener('touchcancel', handleTouchCancel, { passive: true });\n // 鼠标拖拽(PC):move/up 挂在 window 上,允许拖出容器边界\n container.addEventListener('mousedown', handleMouseDown);\n window.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('mouseup', handleMouseUp);\n }\n\n container.addEventListener('scroll', handleScroll, { passive: true });\n\n // 监听容器/内容尺寸变化(旋转、WebView resize、软键盘弹出收起等)\n if (typeof ResizeObserver !== 'undefined') {\n resizeObserver = new ResizeObserver(() => {\n checkContentHeight();\n });\n resizeObserver.observe(container);\n if (contentRef.value) resizeObserver.observe(contentRef.value);\n }\n\n // 初始检查内容高度\n nextTick(() => {\n checkContentHeight();\n });\n});\n\nonBeforeUnmount(() => {\n const container = containerRef.value;\n if (container) {\n container.removeEventListener('touchstart', handleTouchStart);\n container.removeEventListener('touchmove', handleTouchMove);\n container.removeEventListener('touchend', handleTouchEnd);\n container.removeEventListener('touchcancel', handleTouchCancel);\n container.removeEventListener('mousedown', handleMouseDown);\n container.removeEventListener('scroll', handleScroll);\n }\n window.removeEventListener('mousemove', handleMouseMove);\n window.removeEventListener('mouseup', handleMouseUp);\n if (resizeObserver) {\n resizeObserver.disconnect();\n resizeObserver = null;\n }\n if (touchMoveRaf !== null) {\n cancelAnimationFrame(touchMoveRaf);\n touchMoveRaf = null;\n }\n if (scrollTimer !== null) {\n clearTimeout(scrollTimer);\n scrollTimer = null;\n }\n if (idleTimer !== null) {\n clearTimeout(idleTimer);\n idleTimer = null;\n }\n});\n\ndefineExpose({\n refresh: finishRefresh,\n finishRefresh,\n finishPullDown: finishRefresh,\n finishLoad,\n finishPullUp: finishLoad,\n checkContentHeight\n});\n</script>\n\n<template>\n <div ref=\"containerRef\" class=\"infinite-scroll\">\n <!-- 下拉刷新指示器 -->\n <div\n v-if=\"pullRefresh\"\n class=\"pull-refresh-indicator\"\n :class=\"{ 'no-transition': isDragging }\"\n :style=\"{ transform: `translateY(${pullDistance}px)` }\"\n >\n <div v-show=\"pullStatus !== 'idle'\" class=\"pull-refresh-content\">\n <!-- <RequestLoading v-if=\"pullStatus === 'refreshing'\" text=\"\" /> -->\n <span v-if=\"pullStatus === 'refreshing'\"></span>\n <span v-else-if=\"pullStatus === 'loosing'\" class=\"pull-text\">{{ loosingText }}</span>\n <span v-else class=\"pull-text\">{{ pullingText }}</span>\n </div>\n </div>\n\n <!-- 内容区域 -->\n <div\n ref=\"contentRef\"\n class=\"infinite-scroll-content\"\n :class=\"{ 'no-transition': isDragging }\"\n :style=\"pullRefresh ? { transform: `translateY(${pullDistance}px)` } : {}\"\n >\n <!-- 内容区域加载遮罩 -->\n <div v-if=\"contentLoading\" class=\"content-loading-mask\">\n <RequestLoading :text=\"refreshingText\" />\n </div>\n\n <!-- 空状态 -->\n <EmptyComponent v-if=\"isEmpty && !loading\" :text=\"emptyText\" />\n\n <!-- 默认插槽 -->\n <slot></slot>\n\n <!-- 上拉加载指示器 -->\n <div v-if=\"!isEmpty\" ref=\"indicatorRef\" class=\"load-more-indicator\">\n <RequestLoading v-if=\"loading && pullStatus !== 'refreshing'\" :text=\"loadingText\" />\n <span v-else-if=\"finished\" class=\"finished-text\">{{ finishedText }}</span>\n <span v-else-if=\"error\" class=\"error-text\" @click=\"handleRetry\">{{ errorText }}</span>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped lang=\"scss\">\n.infinite-scroll {\n height: 100%;\n position: relative;\n min-height: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n // 阻止滚动到边界时把滚动链传递给祖先(修复 iOS 16+ / 微信 X5 下拉变成整页拖拽)\n overscroll-behavior: contain;\n // 显式声明只接受纵向手势,避免被外层 touch-action 影响\n touch-action: pan-y;\n // iOS 下让原生回弹有惯性\n -webkit-overflow-scrolling: touch;\n}\n\n.pull-refresh-indicator {\n position: absolute;\n top: -50px;\n left: 0;\n right: 0;\n height: 50px;\n transition: transform 0.25s;\n\n // 拖动期间禁用过渡,让 transform 1:1 跟随手指\n &.no-transition {\n transition: none;\n }\n}\n\n.pull-refresh-content {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n font-size: 14px;\n color: #969799;\n}\n\n.pull-text {\n color: var(--zx-primary-color, #1989fa);\n font-size: 14px;\n}\n\n.infinite-scroll-content {\n position: relative;\n transition: transform 0.25s;\n min-height: 100%;\n\n &.no-transition {\n transition: none;\n }\n}\n\n.content-loading-mask {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(255, 255, 255, 0.8);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10;\n backdrop-filter: blur(2px);\n}\n\n.load-more-indicator {\n display: flex;\n align-items: center;\n justify-content: center;\n // 预留高度避免 loading 渲染后被推到视口外造成遮挡\n min-height: 50px;\n padding: 16px 0;\n font-size: 14px;\n color: #8c8f97;\n}\n\n.finished-text {\n color: #8c8f97;\n font-size: 14px;\n}\n\n.error-text {\n color: #ee0a24;\n font-size: 14px;\n cursor: pointer;\n\n &:active {\n opacity: 0.7;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"T\">\nimport { type Ref, nextTick, onMounted, ref, watch } from 'vue';\nimport InfiniteScroll from './infinite-scroll.vue';\n\ndefineOptions({\n name: 'InfiniteScrollV2'\n});\n\nconst props = withDefaults(\n defineProps<{\n /** 数据请求函数,需返回 { records, total },失败时抛错 */\n fetchFn: (params: {\n page: number;\n pageSize: number;\n [key: string]: any;\n }) => Promise<{\n records: T[];\n total: number;\n }>;\n /** 业务查询参数(如筛选条件),变化时自动 reset 重新加载 */\n params?: Record<string, any>;\n /** 每页条数 */\n pageSize?: number;\n /** params 变化时是否自动重新加载 */\n autoReload?: boolean;\n /** 是否在挂载时自动加载首页 */\n immediate?: boolean;\n /** 是否启用下拉刷新 */\n pullRefresh?: boolean;\n /** 下拉刷新触发距离(px) */\n pullThreshold?: number;\n /** 上拉加载触发距离(距离底部 px) */\n loadThreshold?: number;\n /** 空状态文案 */\n emptyText?: string;\n /** 下拉中的文案 */\n pullingText?: string;\n /** 释放刷新的文案 */\n loosingText?: string;\n /** 刷新中的文案 */\n refreshingText?: string;\n /** 加载中的文案 */\n loadingText?: string;\n /** 加载完成的文案 */\n finishedText?: string;\n /** 加载失败的文案 */\n errorText?: string;\n }>(),\n {\n params: () => ({}),\n pageSize: 10,\n autoReload: true,\n immediate: true,\n pullRefresh: false,\n pullThreshold: 70,\n loadThreshold: 50,\n emptyText: '暂无数据',\n pullingText: '下拉刷新',\n loosingText: '释放即可刷新',\n refreshingText: '加载中...',\n loadingText: '加载中...',\n finishedText: '没有更多了~',\n errorText: '加载失败,点击重试'\n});\n\nconst emit = defineEmits<{\n /** 加载成功,每次成功(含分页)都会触发 */\n success: [payload: { records: T[]; total: number; reset: boolean }];\n /** 加载失败 */\n error: [err: unknown];\n /** 列表内容变化(v-model:list 支持) */\n 'update:list': [list: T[]];\n}>();\n\nconst scrollRef = ref<InstanceType<typeof InfiniteScroll> | null>(null);\n\nconst list = ref<T[]>([]) as Ref<T[]>;\nconst loading = ref(false);\nconst finished = ref(false);\nconst isEmpty = ref(false);\nconst loadError = ref(false);\nconst total = ref(0);\nconst page = ref(1);\nconst isRefreshing = ref(false);\n\nlet requestSeq = 0;\n\nasync function load(reset = false) {\n if (reset) {\n page.value = 1;\n finished.value = false;\n isEmpty.value = false;\n // 不立即清空 list,保留旧数据直到新数据到达\n }\n loading.value = true;\n loadError.value = false;\n\n requestSeq += 1;\n const seq = requestSeq;\n\n try {\n const result = await props.fetchFn({\n page: page.value,\n pageSize: props.pageSize,\n ...props.params\n });\n\n if (seq !== requestSeq) return;\n\n const records = result?.records || [];\n if (reset) {\n // reset 时替换整个列表\n list.value = records;\n total.value = 0;\n } else {\n list.value = [...list.value, ...records];\n }\n total.value = Number(result?.total || 0);\n finished.value = list.value.length >= total.value;\n isEmpty.value = list.value.length === 0;\n\n emit('success', { records, total: total.value, reset });\n emit('update:list', list.value);\n } catch (err) {\n if (seq !== requestSeq) return;\n loadError.value = true;\n emit('error', err);\n } finally {\n if (seq === requestSeq) {\n loading.value = false;\n }\n }\n}\n\nasync function handleRefresh() {\n isRefreshing.value = true;\n // 先让下拉刷新指示器收回\n scrollRef.value?.finishRefresh();\n // 等待 250ms 让指示器完全收回(对应 infinite-scroll.vue 的 transition 时长)\n await new Promise<void>(resolve => {\n setTimeout(() => resolve(), 250);\n });\n // 指示器收回后再开始请求新数据\n await load(true);\n isRefreshing.value = false;\n}\n\nasync function handleLoad() {\n if (finished.value) {\n scrollRef.value?.finishLoad();\n return;\n }\n // 列表为空时是首次加载(page 已经是 1),其它情况 page++\n if (list.value.length > 0) {\n page.value += 1;\n }\n await load(false);\n scrollRef.value?.finishLoad();\n}\n\nfunction handleRetry() {\n loadError.value = false;\n if (list.value.length === 0) {\n load(true);\n } else {\n handleLoad();\n }\n}\n\nasync function reload() {\n await load(true);\n}\n\nwatch(\n () => props.params,\n () => {\n if (props.autoReload) {\n reload();\n }\n },\n { deep: true }\n);\n\nwatch(\n list,\n async () => {\n await nextTick();\n scrollRef.value?.checkContentHeight();\n },\n { deep: true }\n);\n\nonMounted(() => {\n if (props.immediate) {\n load(true);\n }\n});\n\ndefineExpose({\n /** 重新加载第一页 */\n reload,\n /** 兼容别名 */\n refresh: reload,\n /** 当前列表数据 */\n list,\n /** 加载中状态 */\n loading,\n /** 是否已加载完 */\n finished,\n /** 是否空数据 */\n isEmpty,\n /** 总条数 */\n total,\n /** 透传 v1 方法 */\n finishLoad: () => scrollRef.value?.finishLoad(),\n finishRefresh: () => scrollRef.value?.finishRefresh(),\n checkContentHeight: () => scrollRef.value?.checkContentHeight()\n});\n</script>\n\n<template>\n <InfiniteScroll\n ref=\"scrollRef\"\n :loading=\"loading\"\n :finished=\"finished\"\n :is-empty=\"isEmpty\"\n :error=\"loadError\"\n :pull-refresh=\"pullRefresh\"\n :pull-threshold=\"pullThreshold\"\n :load-threshold=\"loadThreshold\"\n :empty-text=\"emptyText\"\n :pulling-text=\"pullingText\"\n :loosing-text=\"loosingText\"\n :refreshing-text=\"refreshingText\"\n :loading-text=\"loadingText\"\n :finished-text=\"finishedText\"\n :error-text=\"errorText\"\n :content-loading=\"isRefreshing\"\n @load=\"handleLoad\"\n @refresh=\"handleRefresh\"\n @retry=\"handleRetry\"\n >\n <slot\n :list=\"list\"\n :loading=\"loading\"\n :finished=\"finished\"\n :is-empty=\"isEmpty\"\n :error=\"loadError\"\n :total=\"total\"\n :reload=\"reload\"\n />\n </InfiniteScroll>\n</template>\n","import { App } from 'vue';\nimport InfiniteScroll from './components/infinite-scroll.vue';\nimport InfiniteScrollV2 from './components/infinite-scroll-v2.vue';\nimport RequestLoading from './components/request-loading.vue';\nimport EmptyComponent from './components/empty-component.vue';\n\n// 单独导出组件\nexport { InfiniteScroll, InfiniteScrollV2, RequestLoading, EmptyComponent };\n\n// Vue plugin install 函数\nexport function install(app: App) {\n app.component('InfiniteScroll', InfiniteScroll);\n app.component('InfiniteScrollV2', InfiniteScrollV2);\n app.component('RequestLoading', RequestLoading);\n app.component('EmptyComponent', EmptyComponent);\n}\n\n// 默认导出(支持 app.use() 安装)\nexport default {\n install\n};\n"],"names":["_createElementBlock","_normalizeClass","__props","_createElementVNode","_hoisted_1","_hoisted_2","_toDisplayString","defaultEmptySvg","props","wrapStyle","computed","imageSrc","_openBlock","_renderSlot","_ctx","SLOPE_LOCK_DISTANCE","emit","__emit","containerRef","ref","contentRef","indicatorRef","pullStatus","pullDistance","isDragging","startX","startY","lastDistance","gestureLock","isRefreshing","isLoading","isMouseDown","scrollTimer","touchMoveRaf","idleTimer","resizeObserver","lastTriggerContentHeight","pullStart","clientX","clientY","pullMove","cancelable","dx","dy","ax","ay","atTop","getScrollTop","c","pullEnd","resetTouch","pullCancel","handleTouchStart","handleTouchMove","touch","handleTouchEnd","handleTouchCancel","handleMouseDown","handleMouseMove","handleMouseUp","handleScroll","container","scrollTop","clientHeight","scrollHeight","contentHeight","_a","triggerLoad","scrollToBottom","nextTick","finishRefresh","checkContentHeight","finishLoad","_b","handleRetry","watch","newVal","onMounted","onBeforeUnmount","__expose","_withDirectives","_hoisted_3","_hoisted_4","_normalizeStyle","_hoisted_5","_createVNode","RequestLoading","_createBlock","EmptyComponent","_hoisted_6","scrollRef","list","loading","finished","isEmpty","loadError","total","page","requestSeq","load","reset","seq","result","records","err","handleRefresh","resolve","handleLoad","reload","InfiniteScroll","install","app","InfiniteScrollV2","index"],"mappings":"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"}
|