tencent.jquery.pix.component 1.0.77 → 1.0.79

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.
@@ -1,878 +1,1254 @@
1
- import "./waterfall.scss"
2
- import { getEnv } from "../config.js";
3
- import { remToPx } from "../../utils/utils.js";
4
-
5
- let $ = null;
6
-
7
- // 默认配置
8
- const DEFAULTS = {
9
- columns: 2, // 列数
10
- columnGap: 10, // 左右间隔
11
- rowGap: 12, // 上下间隔
12
- marginTop: 0, // 距离顶部距离
13
- marginBottom: 12, // 最后一行距离底部距离
14
- bufferHeight: '2rem', // 缓冲高度,进行可能的预先添加高度
15
- startPoints: [], // 起始点距离
16
- data: [], // 数据源
17
- container: '', // 容器元素
18
- renderItem(data, index, $card) { // 元素首次渲染时的回调函数, 如果把updateItem设置为空,那么更新时则会兜底触发renderItem
19
- return '<div class="waterfall-item"></div>';
20
- },
21
- scrollDom: null, // 滚动元素,如果传入了滚动元素,那么用来计算的窗口高度就以滚动元素的高度为准
22
- // 传入 $node, data, index
23
- updateItem: null, // 元素更新时的回调函数
24
- onscroll: null, // 滚动事件回调函数
25
- shouldOccupySpace: null, // 是否是静态数据的回调函数,静态数据能够占用元素
26
- showLoading: null, // 展示loading的回调函数 params:$node
27
- hideLoading: null, // 隐藏loading的回调函数
28
- createLoading: null, // 创建loading的回调函数
29
- };
30
-
31
- export function Waterfall(optionsInput = {}) {
32
- $ = getEnv().$;
33
-
34
- this.optionsInput = optionsInput
35
-
36
- this.init();
37
- }
38
-
39
-
40
- Waterfall.prototype.init = function (optionsMain = null) {
41
- if (optionsMain) {
42
- this.options = Object.assign({}, DEFAULTS, optionsMain);
43
- } else {
44
- this.options = Object.assign({}, DEFAULTS, this.optionsInput);
45
- }
46
- const self = this;
47
- const options = this.options;
48
- const $container = $(options.container);
49
- const $scrollDom = options.scrollDom ? $(options.scrollDom) : $container;
50
-
51
-
52
- // 标记是否有更新元素用的回调函数
53
- this.hasUpdateItem = options.updateItem && (options.updateItem.constructor === Function) ? true : false;
54
-
55
- // 新方案:数据ID映射机制
56
- this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
57
- this.nextDataId = 0; // 下一个数据ID
58
- this.renderedDataIds = new Set(); // 已渲染的数据ID集合
59
-
60
- this.$loadingNode = null;
61
-
62
- this.isShowLoading = false; // 是否展示loading
63
-
64
-
65
- // 间隔字符串转数字
66
- if (options.columnGap.constructor === String) {
67
- // 如果是rem单位,则需要计算
68
- if (options.columnGap.indexOf('rem') > -1) {
69
- options.columnGap = remToPx(options.columnGap);
70
- } else {
71
- options.columnGap = parseFloat(options.columnGap);
72
- }
73
- }
74
- if (options.rowGap.constructor === String) {
75
- // 如果是rem单位,则需要计算
76
- if (options.rowGap.indexOf('rem') > -1) {
77
- options.rowGap = remToPx(options.rowGap);
78
- } else {
79
- options.rowGap = parseFloat(options.rowGap);
80
- }
81
- }
82
- if (options.bufferHeight.constructor === String) {
83
- // 如果是rem单位,则需要计算
84
- if (options.bufferHeight.indexOf('rem') > -1) {
85
- options.bufferHeight = remToPx(options.bufferHeight);
86
- } else {
87
- options.bufferHeight = parseFloat(options.bufferHeight);
88
- }
89
- }
90
- if (options.marginTop.constructor === String) {
91
- // 如果是rem单位,则需要计算
92
- if (options.marginTop.indexOf('rem') > -1) {
93
- options.marginTop = remToPx(options.marginTop);
94
- } else {
95
- options.marginTop = parseFloat(options.marginTop);
96
- }
97
- }
98
- if (options.marginBottom.constructor === String) {
99
- // 如果是rem单位,则需要计算
100
- if (options.marginBottom.indexOf('rem') > -1) {
101
- options.marginBottom = remToPx(options.marginBottom);
102
- } else {
103
- options.marginBottom = parseFloat(options.marginBottom);
104
- }
105
- }
106
- options.startPoints.forEach((item, index) => {
107
- if (item.constructor === String) {
108
- options.startPoints[index] = remToPx(item);
109
- } else {
110
- item = parseFloat(item);
111
- }
112
- });
113
-
114
- const allWidth = $container.width();
115
- this.allWidth = allWidth;
116
- // 计算每列的宽度
117
- this.columnWidth = (allWidth - (options.columns - 1) * options.columnGap) / options.columns;
118
-
119
- // 记录列数
120
- this.columns = options.columns;
121
-
122
- this.columnItems = []; // 列元素数组
123
- for (let i = 0; i < this.columns; i++) {
124
- this.columnItems.push(this.createColumn(i));
125
- }
126
-
127
- this.$scrollDom = $scrollDom;
128
-
129
- this.nodePool = []; // DOM 节点池
130
- this.activeNodes = new Map(); // 当前活跃节点(索引 -> DOM)
131
- this.allReadyNodes = new Map(); // 所有节点(索引 -> DOM)
132
- this.renderIndex = 0; // 渲染索引(保留兼容性)
133
-
134
-
135
-
136
- // 新方案:初始化数据ID映射
137
- this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
138
- this.nextDataId = 0; // 下一个数据ID
139
- this.renderedDataIds = new Set(); // 已渲染的数据ID集合
140
-
141
- // 为初始数据分配数据ID
142
- this.options.data.forEach((item, index) => {
143
- const dataId = index; //this.nextDataId++;
144
- this.dataIdMap.set(dataId, {
145
- data: true,//item,
146
- originalIndex: index,
147
- layoutInfo: null // 将在布局时填充
148
- });
149
- });
150
-
151
- $container.html(`
152
- <div class="waterfall-list-scroll" style="">
153
- <div class="waterfall-list-viewport"></div>
154
- </div>
155
- `);
156
-
157
-
158
-
159
- // 如果有定义loading函数 那么创建一个loading节点元素
160
- console.log('options.createLoading', options.createLoading)
161
- if (options.createLoading) {
162
- this.$loadingNode = $(
163
- `<div class="waterfall-loading" style="transform: translate(0px, -99999px)"></div>`
164
- );
165
- $container.find('.waterfall-list-viewport').append(this.$loadingNode);
166
-
167
- options.createLoading(this.$loadingNode);
168
- }
169
-
170
- // 绑定滚动事件(节流处理)
171
- $scrollDom.off().on('scroll', function () {
172
- self.scrollTop = $(this).scrollTop();
173
-
174
- window.requestAnimationFrame(() => {
175
- self.updateVisibleItems();
176
- });
177
-
178
- if (options.onscroll && options.onscroll.constructor === Function) {
179
- options.onscroll(this, self.scrollTop);
180
- }
181
- });
182
-
183
- this.scrollTop = $scrollDom.scrollTop(); // 当前滚动位置
184
-
185
- // 首次渲染
186
- self.updateVisibleItems();
187
-
188
- }
189
-
190
- // force 强制更新渲染
191
- Waterfall.prototype.updateVisibleItems = function (force = false) {
192
- const self = this;
193
- const options = this.options;
194
- let h = 0;
195
- if (options.scrollDom) {
196
- h = $(options.scrollDom).height();
197
- } else {
198
- h = $(options.container).height();
199
- }
200
-
201
- const startTop = self.scrollTop; // 当前滚动位置
202
- const endTop = startTop + h;
203
- // console.log('startTop', startTop)
204
- // console.log('endTop', endTop)
205
-
206
- // 进行可见区域的渲染更新
207
- this.updateCardsInView({
208
- start: startTop,
209
- end: endTop,
210
- force
211
- });
212
-
213
- }
214
-
215
- // 新增卡片
216
- Waterfall.prototype.appendCard = function (data, dataId, { top, left }) {
217
- const self = this;
218
- const options = this.options;
219
- const $container = $(options.container);
220
- const $viewport = $container.find('.waterfall-list-viewport');
221
-
222
- // 新方案:基于数据ID的数据验证
223
- if (!this.dataIdMap.has(dataId)) {
224
- console.error('Waterfall: Invalid dataId in appendCard', dataId);
225
- return null;
226
- }
227
-
228
- const dataInfo = this.dataIdMap.get(dataId);
229
- if (!dataInfo || !dataInfo.data) {
230
- console.warn('Waterfall: Empty data for dataId', dataId);
231
- }
232
-
233
-
234
- const $card = $(
235
- `<div class="waterfall-item"
236
- data-index="${dataId}"
237
- style="position: absolute;transform:translate(${left}px,${top}px);"
238
- >
239
- </div> `
240
- );
241
-
242
- $viewport.append($card);
243
-
244
- const str = options.renderItem(data, dataInfo.originalIndex, $card);
245
-
246
- if (str) {
247
- $card.html(str);
248
- }
249
-
250
- this.renderedDataIds.add(dataId);
251
-
252
- if (options.columns !== 1) {
253
- $card.width(this.columnWidth + 'px');
254
- }
255
-
256
-
257
- return $card;
258
- }
259
-
260
- // 获取指定高度下的卡片索引
261
- Waterfall.prototype.updateCardsInView = async function ({ start, end, force = false }) {
262
- const options = this.options;
263
- const minHeight = this.getMinHeight();
264
- const endBuffer = end + options.bufferHeight;
265
- if (minHeight < endBuffer) {
266
- // 如果不够 进行补建
267
- await this.createCards({ end: endBuffer });
268
- }
269
-
270
- const startNum = start - options.bufferHeight;
271
- const endNum = end + options.bufferHeight;
272
- // 新方案:基于数据ID映射机制
273
- const newActiveNodes = new Map();
274
- for (let i = 0; i < this.columns; i++) {
275
- const column = this.columnItems[i];
276
-
277
- for (let j = 0; j < column.children.length; j++) {
278
- const row = column.children[j];
279
- const dataId = row.dataId; // 使用dataId替代renderIndex
280
-
281
- // 验证数据ID有效性
282
- if (!this.dataIdMap.has(dataId)) {
283
- console.warn('Waterfall: Invalid dataId detected', dataId);
284
- continue;
285
- }
286
-
287
- const dataInfo = this.dataIdMap.get(dataId);
288
-
289
- if (!dataInfo) {
290
- console.warn('Waterfall: Invalid data for dataId', dataId);
291
- continue;
292
- }
293
-
294
- const data = options.data[dataId]
295
-
296
- // 如果当前这个节点是特殊节点 不用做处理
297
- // 如果是特殊的静态占用元素卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
298
- let specialNode = false;
299
- if (options.shouldOccupySpace) {
300
- specialNode = options.shouldOccupySpace(data) || false;
301
- }
302
- if (specialNode) {
303
- continue;
304
- }
305
-
306
- // 在可视区域内 进行有关卡片的操作
307
- const bool = row.top <= endNum && row.bottom >= startNum;
308
- if (bool) {
309
- // 理论上什么都不动,因为卡片的位置不会变
310
- const $card = this.activeNodes.get(dataId);
311
-
312
- let $node = null;
313
-
314
- // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
315
- let bool = true;
316
- if ($card) {
317
- bool = hasNodeInActives(newActiveNodes, $card)
318
- }
319
- // 如果卡片已经在DOM中,则不用更新位置
320
- if (bool === false) {
321
- $node = $card;
322
- this.activeNodes.delete(dataId);
323
- // 如果是强更,这里才会采取更新
324
- if (force) {
325
- this.updateRenderUI($node, data, dataId);
326
- }
327
-
328
- } else {
329
- const $card = this.allReadyNodes.get(dataId);
330
- // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
331
- let bool = true;
332
- if ($card) {
333
- bool = hasNodeInActives(newActiveNodes, $card)
334
- }
335
- if (bool === false) {
336
- // 如果成功获取到card并没有占用 那么就复用这个card
337
- $node = $card;
338
-
339
- this.updateRenderUI($node, data, dataId);
340
-
341
- } else {
342
- // 卡片不在DOM中,则更新位置
343
- $node = getNodePoolPop(this.nodePool, newActiveNodes);
344
- if ($node === null) {
345
- // 这里是往上方拖动时,可能需要补建的情况
346
- $node = this.appendCard(data, dataId, {
347
- top: row.top, left: row.left
348
- });
349
- row.$node = $node;
350
- } else {
351
- this.updateRenderUI($node, data, dataId);
352
- row.$node = $node;
353
- }
354
- }
355
- }
356
- $node.css({
357
- 'transform': `translate(${row.left}px,${row.top}px)`,
358
- }).attr('data-index', dataId);
359
-
360
- newActiveNodes.set(dataId, $node);
361
-
362
- // 清除掉在NodePool中的card
363
- const index = this.nodePool.indexOf($card);
364
- if (index !== -1) {
365
- this.nodePool.splice(index, 1);
366
- }
367
-
368
- // console.log('302-row', row.dataId, dataInfo);
369
- }
370
- }
371
- }
372
-
373
- // 阶段2:处理不活跃节点
374
- this.activeNodes.forEach($node => {
375
- $node.css('transform', `translateY(-9999px)`);// 移出可视区域
376
- if (this.nodePool.indexOf($node) === -1) {
377
- this.nodePool.push($node);
378
- }
379
- });
380
- this.activeNodes = newActiveNodes;
381
- // console.log('this.activeNodes', this.activeNodes);
382
- // console.log('this.nodePool', this.nodePool);
383
- }
384
-
385
-
386
- Waterfall.prototype.getMaxHeight = function () {
387
- let maxHeight = 0;
388
- for (let i = 0; i < this.columns; i++) {
389
- const column = this.columnItems[i]
390
-
391
- // 获取每组元素列表的最后一个bottom值
392
- maxHeight = Math.max(maxHeight, column.bottom);
393
-
394
- }
395
- return maxHeight;
396
- }
397
-
398
- Waterfall.prototype.getMinHeight = function () {
399
- let minHeight = 0;
400
- for (let i = 0; i < this.columns; i++) {
401
- const column = this.columnItems[i]
402
- if (minHeight === 0) {
403
- minHeight = column.bottom;
404
- }
405
- // 获取每组元素列表的最后一个bottom值
406
- minHeight = Math.min(minHeight, column.bottom);
407
-
408
- }
409
- return minHeight;
410
- }
411
-
412
- Waterfall.prototype.getMinHeightColumn = function () {
413
- let minHeight = -1;
414
- let mimHeightColumn = null;
415
- let index = -1;
416
- const startPoints = this.options.startPoints || [];
417
- for (let i = 0; i < this.columns; i++) {
418
- const column = this.columnItems[i]
419
- if (minHeight === -1) {
420
- minHeight = column.bottom;
421
- mimHeightColumn = column;
422
- index = i;
423
- } else if (minHeight > column.bottom) {
424
- // 获取每组元素列表的最后一个bottom值
425
- minHeight = column.bottom;
426
- mimHeightColumn = column;
427
- index = i;
428
- }
429
-
430
- }
431
-
432
- if (minHeight === 0 && index > -1) {
433
- if (startPoints.length > index) {
434
- // 设置一下起始点的初始化
435
- mimHeightColumn.bottom = startPoints[index];
436
- }
437
- }
438
-
439
- return mimHeightColumn;
440
- }
441
-
442
- // 创建卡片
443
- Waterfall.prototype.createCards = function ({ end, dataId = -1 }, callback) {
444
- const self = this;
445
- const options = this.options;
446
-
447
- return new Promise((resolve) => {
448
- // 新方案:获取下一个未渲染的数据ID
449
- let nextDataId = null;
450
- for (let [dataId, dataInfo] of this.dataIdMap) {
451
- if (!this.renderedDataIds.has(dataId)) {
452
- nextDataId = dataId;
453
- break;
454
- }
455
- }
456
-
457
-
458
- // 如果没有更多数据需要渲染
459
- if (nextDataId === null) {
460
- this.setScrollHeight();
461
- return resolve();
462
- }
463
-
464
- const dataInfo = this.dataIdMap.get(nextDataId);
465
- if (!dataInfo || !dataInfo.data) {
466
- console.warn('Waterfall: Invalid data for dataId', nextDataId);
467
- return resolve();
468
- }
469
-
470
- if (this.renderIndex >= options.data.length) {
471
- this.setScrollHeight();
472
- return resolve();
473
- }
474
-
475
- const data = options.data[nextDataId];
476
-
477
- let column = this.getMinHeightColumn();
478
- if (column === null) {
479
- column = this.columnItems[0];
480
- }
481
-
482
- const top = column.bottom === 0 ? options.marginTop : (column.bottom + options.rowGap);
483
- const position = { top, left: column.left };
484
- const row = createDefaultRow(position);
485
-
486
- this.renderIndex += 1;
487
-
488
- let specialNode = false;
489
-
490
- // 如果是特殊的卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
491
- if (options.shouldOccupySpace) {
492
- specialNode = options.shouldOccupySpace(data) || false;
493
- }
494
-
495
- // 添加卡片,使用dataId作为唯一标识
496
- let $card = null;
497
- if (this.nodePool.length === 0 || specialNode === true) {
498
- $card = this.appendCard(data, nextDataId, position);
499
- } else {
500
- const $tmp = getNodePoolPop(this.nodePool, this.activeNodes);
501
- if ($tmp) {
502
- $card = $tmp;
503
- $card.css({
504
- 'transform': `translate(${row.left}px,${row.top}px)`,
505
- }).attr('data-index', nextDataId);
506
- this.updateRenderUI($card, data, nextDataId);
507
-
508
- } else {
509
- $card = this.appendCard(data, nextDataId, position);
510
- }
511
-
512
- }
513
-
514
- row.$node = $card;
515
- row.dataId = nextDataId; // 使用dataId替代renderIndex
516
- if (dataId !== -1) {
517
- row.dataId = dataId;
518
- }
519
-
520
- if (specialNode === false) {
521
- // 把新增的卡片放进 activeNodes 当成活跃节点元素,那么是 可以动态使用的
522
- this.activeNodes.set(nextDataId, $card);
523
-
524
- this.allReadyNodes.set(nextDataId, $card);
525
- } else {
526
- // 如果是特殊的,这里不要记录了
527
- this.allReadyNodes.set(nextDataId, null);
528
- }
529
-
530
- this.renderedDataIds.add(nextDataId);
531
-
532
- // setTimeout(() => {
533
- //window.requestAnimationFrame(() => {
534
-
535
- column.bottom = top + $card.height();
536
-
537
- column.children.push(row);
538
- row.bottom = column.bottom;
539
-
540
- // 检查是否需要继续创建卡片
541
- const minHeight = this.getMinHeight();
542
- const hasMoreData = this.renderedDataIds.size < this.dataIdMap.size;
543
-
544
- if (hasMoreData && (minHeight < end)) {
545
-
546
- this.createCards({ end }, () => {
547
- resolve();
548
- if (callback) {
549
- callback();
550
- }
551
- });
552
- } else {
553
- this.setScrollHeight();
554
- resolve();
555
- if (callback) {
556
- callback();
557
- }
558
- }
559
- //});
560
- // }, 42);
561
- });
562
-
563
- }
564
-
565
-
566
- Waterfall.prototype.createColumn = function (index) {
567
- const res = {
568
- left: 0,
569
- bottom: 0,
570
- width: 0,
571
- children: []
572
- }
573
-
574
- const options = this.options;
575
- const columnWidth = this.columnWidth;
576
-
577
- res.width = this.columnWidth;
578
- res.left = (index * (columnWidth + options.columnGap));
579
- return res;
580
- }
581
-
582
- Waterfall.prototype.updateRenderUI = function ($node, data, dataId) {
583
- const options = this.options;
584
-
585
- // 新方案:基于数据ID的数据验证
586
- if (!this.dataIdMap.has(dataId)) {
587
- console.error('Waterfall: Invalid dataId in updateRenderUI', dataId);
588
- return;
589
- }
590
-
591
- const dataInfo = this.dataIdMap.get(dataId);
592
- if (!dataInfo || !dataInfo.data) {
593
- console.warn('Waterfall: Empty data for dataId', dataId);
594
- }
595
-
596
- if (this.hasUpdateItem === true) {
597
- options.updateItem($node, data, dataInfo.originalIndex)
598
- } else {
599
- const str = options.renderItem(data, dataInfo.originalIndex, $node);
600
- if (str) {
601
- $node.html(str);
602
- }
603
- }
604
- }
605
-
606
-
607
-
608
- Waterfall.prototype.updateData = async function (newData) {
609
- const options = this.options;
610
- options.data = newData;
611
-
612
- // 新方案:重新建立数据ID映射
613
- //this.dataIdMap.clear();
614
- //this.renderedDataIds.clear();
615
- //this.nextDataId = 0;
616
-
617
- // 为每个数据项分配唯一ID
618
- let bool = true
619
- let count = options.data.length - 1;
620
- let index = 0;
621
-
622
- while (bool) {
623
- if (index > count) {
624
- bool = false
625
- this.updateVisibleItems(true); // 强制更新渲染
626
- break;
627
- }
628
-
629
- const dataId = index;
630
- this.dataIdMap.set(dataId, {
631
- data: true,
632
- originalIndex: dataId,
633
- layoutInfo: null
634
- });
635
-
636
- await this.createCards({ end: 0, dataId })
637
-
638
- index += 1;
639
-
640
- }
641
- // options.data.forEach((item, index) => {
642
- // const dataId = index; // this.nextDataId++;
643
- // if (!this.allReadyNodes.has(dataId)) {
644
- // this.dataIdMap.set(dataId, {
645
- // data: true,// item,
646
- // originalIndex: index,
647
- // layoutInfo: null // 将在布局时填充
648
- // });
649
- // // 如果没有准备好这个数据,这里要创建一个占位节点
650
- // this.createCards({ end: 0, dataId });
651
- // }
652
- // });
653
-
654
- // this.updateVisibleItems(true); // 强制更新渲染
655
-
656
-
657
- }
658
-
659
- // 某个数据进行了UI变更,触发高度重新绘制
660
- Waterfall.prototype.updateCard = function (data) {
661
- const options = this.options;
662
- let dataId = -1;
663
- if (typeof data === 'number') {
664
- dataId = data;
665
- } else {
666
- for (let i = 0; i < options.data.length; i++) {
667
- if (options.data[i] === data) {
668
- dataId = i;
669
- break;
670
- }
671
- }
672
- }
673
- if (dataId === -1) {
674
- // 没有匹配到数据,进行退出
675
- console.log('Waterfall: Invalid data in options.data');
676
- return
677
- }
678
-
679
- // 从activeNodes中获取该数据的节点
680
- if (!this.activeNodes.has(dataId)) {
681
- console.log('Waterfall: Invalid dataId in activeNodes');
682
- return
683
- }
684
-
685
- const $node = this.activeNodes.get(dataId);
686
- const height = $node.height();
687
-
688
- // 重新计算该数据所在列的卡片位置
689
- let needUpdate = false
690
- const columnItems = this.columnItems;
691
- for (let i = 0; i < columnItems.length; i++) {
692
- const column = columnItems[i];
693
- let bool = false
694
- let minus = 0
695
- // 这里为了简化,各列的瀑布流保持不动,只更新各列下节点的top位置,不做节点的跨列位移
696
- for (let j = 0; j < column.children.length; j++) {
697
- const row = column.children[j];
698
- if (row.dataId === dataId) {
699
- bool = true
700
- const oldHeight = row.bottom - row.top;
701
-
702
- minus = height - oldHeight;
703
-
704
- if (minus === 0) {
705
- // 找到了原节点数据,对比后没有变更 那么直接退出
706
- return;
707
- }
708
- needUpdate = true
709
- row.bottom += minus;
710
- } else if (minus !== 0) {
711
- row.top += minus;
712
- row.bottom += minus;
713
- }
714
- }
715
- if (bool) {
716
- column.bottom += minus;
717
- break;
718
- }
719
- }
720
- if (needUpdate) {
721
- this.updateVisibleItems(true); // 强制更新渲染
722
- }
723
- }
724
-
725
- // 重新计算设置一遍所有的卡片位置
726
- Waterfall.prototype.updatePointCards = function () {
727
- // 有问题 不要用
728
- return
729
- const self = this;
730
- const options = this.options;
731
- const columnItems = this.columnItems;
732
- let top = options.marginTop;
733
- for (let i = 0; i < columnItems.length; i++) {
734
- const column = columnItems[i];
735
- // 这里为了简化,各列的瀑布流保持不动,只更新各列下节点的top位置,不做节点的跨列位移
736
- for (let j = 0; j < column.children.length; j++) {
737
- const row = column.children[j];
738
- const $card = row.$node;
739
-
740
- // 验证数据ID有效性
741
- if (!this.dataIdMap.has(row.dataId)) {
742
- console.warn('Waterfall: Invalid dataId in updatePointCards', row.dataId);
743
- continue;
744
- }
745
-
746
- // 第一个的top不需要更新
747
- if (j === 0) {
748
- row.bottom = top + $card.height();
749
- } else {
750
- row.top = column.children[j - 1].bottom + options.rowGap;
751
- row.bottom = row.top + $card.height();
752
- // 更新卡片位置
753
- // $card.css({
754
- // 'transform': `translate(${row.left}px,${row.top}px)`,
755
- // })
756
- }
757
-
758
- }
759
- // 设置一次该列的bottom
760
- column.bottom = getBottomByColumn(column);
761
- }
762
- }
763
-
764
- // 展示loading的回调函数
765
- Waterfall.prototype.showLoading = function (callback = null) {
766
- this.isShowLoading = true;
767
- const options = this.options;
768
- let $node = null
769
- if (this.$loadingNode) {
770
- let loadingTop = this.getMaxHeight() + options.rowGap
771
- this.$loadingNode.css('transform', `translate(0px,${loadingTop}px)`);
772
- $node = this.$loadingNode;
773
- // window.requestAnimationFrame(() => {
774
- // setTimeout(() => {
775
- // this.$scrollDom.scrollTop(loadingTop + this.$loadingNode.height());
776
- // });
777
- // });
778
- }
779
-
780
- if (callback) callback($node)
781
- this.setScrollHeight();
782
- }
783
-
784
- // 隐藏loading的回调函数
785
- Waterfall.prototype.hideLoading = function (callback = null) {
786
- this.isShowLoading = false;
787
- const options = this.options;
788
- let $node = null
789
- if (this.$loadingNode) {
790
- let h1 = this.getMaxHeight() + options.marginBottom
791
- this.$loadingNode.css('transform', `translate(0px,-99999px)`);
792
- //如果要设置高度,那么这里判断一下当前是否正在做updata 一般这里被调用时,数据已经读到,在updata的同一时间调用了该函数
793
- // 如果两个时刻高度是一致的 那么数据就是一致的 这里重新设置回来高度即可
794
- window.requestAnimationFrame(() => {
795
- this.setScrollHeight();
796
- })
797
- $node = this.$loadingNode
798
- }
799
-
800
- if (callback) callback($node)
801
- this.setScrollHeight();
802
- }
803
-
804
- // 设置滚动条列表的高度
805
- Waterfall.prototype.setScrollHeight = function () {
806
- const options = this.options;
807
- const $container = $(options.container);
808
- let h = this.getMaxHeight();
809
- if (this.isShowLoading === true) {
810
- if (this.$loadingNode) {
811
- h += options.rowGap + this.$loadingNode.height();
812
- }
813
- }
814
- h += options.marginBottom;
815
- $container.find('.waterfall-list-scroll').css('height', h + 'px');
816
- }
817
-
818
- // 销毁自己
819
- Waterfall.prototype.destroy = function () {
820
- const options = this.options;
821
- const $container = $(options.container);
822
- $container.html('');
823
- this.activeNodes.clear();
824
- this.allReadyNodes.clear();
825
- this.dataIdMap.clear();
826
- this.renderedDataIds.clear();
827
- this.nodePool = [];
828
- this.columnItems = [];
829
- this.columns = 0;
830
- this.columnWidth = 0;
831
- this.renderIndex = 0;
832
- this.scrollTop = 0;
833
- this.isShowLoading = false;
834
- this.$loadingNode = null;
835
- this.$scrollDom = null;
836
- }
837
-
838
- function createDefaultRow({ top, left }) {
839
- return {
840
- top,
841
- left,
842
- bottom: 0,
843
- $node: null,
844
- dataId: -1 // 新方案:使用dataId替代renderIndex
845
- }
846
- }
847
-
848
-
849
- function getBottomByColumn(column) {
850
- if (column.children.length === 0) {
851
- return 0;
852
- }
853
- const child = column.children;
854
- return child[child.length - 1].bottom;
855
- }
856
-
857
- function hasNodeInActives(mapObj, $node) {
858
- let bool = false
859
- mapObj.forEach((item) => {
860
- if (item === $node) {
861
- bool = true;
862
- }
863
- });
864
- return bool;
865
- }
866
-
867
- // 从众多jq对象数组取得一个不重复的
868
- function getNodePoolPop(nodePool, actives) {
869
- for (let i = 0; i < nodePool.length; i++) {
870
- const $node = nodePool[i];
871
- if (!hasNodeInActives(actives, $node)) {
872
- nodePool.splice(i, 1);
873
- return $node;
874
- }
875
- }
876
- return null;
877
- }
878
-
1
+ import { remToPx } from "../../utils/utils.js";
2
+ import { getEnv } from "../config.js";
3
+ import "./waterfall.scss";
4
+
5
+ let $ = null;
6
+
7
+ // 默认配置
8
+ const DEFAULTS = {
9
+ columns: 2, // 列数
10
+ columnGap: 10, // 左右间隔
11
+ rowGap: 12, // 上下间隔
12
+ marginTop: 0, // 距离顶部距离
13
+ marginBottom: 12, // 最后一行距离底部距离
14
+ bufferHeight: '2rem', // 缓冲高度,进行可能的预先添加高度
15
+ startPoints: [], // 起始点距离
16
+ data: [], // 数据源
17
+ container: '', // 容器元素
18
+ renderItem(data, index, $card) { // 元素首次渲染时的回调函数, 如果把updateItem设置为空,那么更新时则会兜底触发renderItem
19
+ return '<div class="waterfall-item"></div>';
20
+ },
21
+ scrollDom: null, // 滚动元素,如果传入了滚动元素,那么用来计算的窗口高度就以滚动元素的高度为准
22
+ // 传入 $node, data, index
23
+ updateItem: null, // 元素更新时的回调函数
24
+ onscroll: null, // 滚动事件回调函数
25
+ shouldOccupySpace: null, // 是否是静态数据的回调函数,静态数据能够占用元素
26
+ showLoading: null, // 展示loading的回调函数 params:$node
27
+ hideLoading: null, // 隐藏loading的回调函数
28
+ createLoading: null, // 创建loading的回调函数
29
+ };
30
+
31
+ export function Waterfall(optionsInput = {}) {
32
+ $ = getEnv().$;
33
+
34
+ this.optionsInput = optionsInput
35
+
36
+ this.init();
37
+ }
38
+
39
+
40
+ Waterfall.prototype.init = function (optionsMain = null) {
41
+ if (optionsMain) {
42
+ this.options = Object.assign({}, DEFAULTS, optionsMain);
43
+ } else {
44
+ this.options = Object.assign({}, DEFAULTS, this.optionsInput);
45
+ }
46
+ const self = this;
47
+ const options = this.options;
48
+ const $container = $(options.container);
49
+ const $scrollDom = options.scrollDom ? $(options.scrollDom) : $container;
50
+
51
+
52
+ // 标记是否有更新元素用的回调函数
53
+ this.hasUpdateItem = options.updateItem && (options.updateItem.constructor === Function) ? true : false;
54
+
55
+ // 新方案:数据ID映射机制
56
+ this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
57
+ this.nextDataId = 0; // 下一个数据ID
58
+ this.renderedDataIds = new Set(); // 已渲染的数据ID集合
59
+
60
+ this.$loadingNode = null;
61
+
62
+ this.isShowLoading = false; // 是否展示loading
63
+
64
+
65
+ // 间隔字符串转数字
66
+ if (options.columnGap.constructor === String) {
67
+ // 如果是rem单位,则需要计算
68
+ if (options.columnGap.indexOf('rem') > -1) {
69
+ options.columnGap = remToPx(options.columnGap);
70
+ } else {
71
+ options.columnGap = parseFloat(options.columnGap);
72
+ }
73
+ }
74
+ if (options.rowGap.constructor === String) {
75
+ // 如果是rem单位,则需要计算
76
+ if (options.rowGap.indexOf('rem') > -1) {
77
+ options.rowGap = remToPx(options.rowGap);
78
+ } else {
79
+ options.rowGap = parseFloat(options.rowGap);
80
+ }
81
+ }
82
+ if (options.bufferHeight.constructor === String) {
83
+ // 如果是rem单位,则需要计算
84
+ if (options.bufferHeight.indexOf('rem') > -1) {
85
+ options.bufferHeight = remToPx(options.bufferHeight);
86
+ } else {
87
+ options.bufferHeight = parseFloat(options.bufferHeight);
88
+ }
89
+ }
90
+ if (options.marginTop.constructor === String) {
91
+ // 如果是rem单位,则需要计算
92
+ if (options.marginTop.indexOf('rem') > -1) {
93
+ options.marginTop = remToPx(options.marginTop);
94
+ } else {
95
+ options.marginTop = parseFloat(options.marginTop);
96
+ }
97
+ }
98
+ if (options.marginBottom.constructor === String) {
99
+ // 如果是rem单位,则需要计算
100
+ if (options.marginBottom.indexOf('rem') > -1) {
101
+ options.marginBottom = remToPx(options.marginBottom);
102
+ } else {
103
+ options.marginBottom = parseFloat(options.marginBottom);
104
+ }
105
+ }
106
+ options.startPoints.forEach((item, index) => {
107
+ if (item.constructor === String) {
108
+ options.startPoints[index] = remToPx(item);
109
+ } else {
110
+ item = parseFloat(item);
111
+ }
112
+ });
113
+
114
+ const allWidth = $container.width();
115
+ this.allWidth = allWidth;
116
+ // 计算每列的宽度
117
+ this.columnWidth = (allWidth - (options.columns - 1) * options.columnGap) / options.columns;
118
+
119
+ // 记录列数
120
+ this.columns = options.columns;
121
+
122
+ this.columnItems = []; // 列元素数组
123
+ for (let i = 0; i < this.columns; i++) {
124
+ this.columnItems.push(this.createColumn(i));
125
+ }
126
+
127
+ this.$scrollDom = $scrollDom;
128
+
129
+ this.nodePool = []; // DOM 节点池
130
+ this.activeNodes = new Map(); // 当前活跃节点(索引 -> DOM)
131
+ this.allReadyNodes = new Map(); // 所有节点(索引 -> DOM)
132
+ this.renderIndex = 0; // 渲染索引(保留兼容性)
133
+
134
+
135
+
136
+ // 新方案:初始化数据ID映射
137
+ this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
138
+ this.nextDataId = 0; // 下一个数据ID
139
+ this.renderedDataIds = new Set(); // 已渲染的数据ID集合
140
+
141
+ // 为初始数据分配数据ID
142
+ this.options.data.forEach((item, index) => {
143
+ const dataId = index; //this.nextDataId++;
144
+ this.dataIdMap.set(dataId, {
145
+ data: true,//item,
146
+ originalIndex: index,
147
+ layoutInfo: null // 将在布局时填充
148
+ });
149
+ });
150
+
151
+ $container.html(`
152
+ <div class="waterfall-list-scroll" style="">
153
+ <div class="waterfall-list-viewport"></div>
154
+ </div>
155
+ `);
156
+
157
+
158
+
159
+ // 如果有定义loading函数 那么创建一个loading节点元素
160
+ console.log('options.createLoading', options.createLoading)
161
+ if (options.createLoading) {
162
+ this.$loadingNode = $(
163
+ `<div class="waterfall-loading" style="transform: translate(0px, -99999px)"></div>`
164
+ );
165
+ $container.find('.waterfall-list-viewport').append(this.$loadingNode);
166
+
167
+ options.createLoading(this.$loadingNode);
168
+ }
169
+
170
+ // 绑定滚动事件(节流处理)
171
+ $scrollDom.off().on('scroll', function () {
172
+ self.scrollTop = $(this).scrollTop();
173
+
174
+ window.requestAnimationFrame(() => {
175
+ self.updateVisibleItems();
176
+ });
177
+
178
+ if (options.onscroll && options.onscroll.constructor === Function) {
179
+ options.onscroll(this, self.scrollTop);
180
+ }
181
+ });
182
+
183
+ this.scrollTop = $scrollDom.scrollTop(); // 当前滚动位置
184
+
185
+ // 首次渲染
186
+ self.updateVisibleItems();
187
+
188
+ }
189
+
190
+ // force 强制更新渲染
191
+ Waterfall.prototype.updateVisibleItems = function (force = false) {
192
+ const self = this;
193
+ const options = this.options;
194
+ let h = 0;
195
+ if (options.scrollDom) {
196
+ h = $(options.scrollDom).height();
197
+ } else {
198
+ h = $(options.container).height();
199
+ }
200
+
201
+ const startTop = self.scrollTop; // 当前滚动位置
202
+ const endTop = startTop + h;
203
+ // console.log('startTop', startTop)
204
+ // console.log('endTop', endTop)
205
+
206
+ // 进行可见区域的渲染更新
207
+ this.updateCardsInView({
208
+ start: startTop,
209
+ end: endTop,
210
+ force
211
+ });
212
+
213
+ }
214
+
215
+ // 新增卡片
216
+ Waterfall.prototype.appendCard = function (data, dataId, { top, left }) {
217
+ const self = this;
218
+ const options = this.options;
219
+ const $container = $(options.container);
220
+ const $viewport = $container.find('.waterfall-list-viewport');
221
+
222
+ // 新方案:基于数据ID的数据验证
223
+ if (!this.dataIdMap.has(dataId)) {
224
+ console.error('Waterfall: Invalid dataId in appendCard', dataId);
225
+ return null;
226
+ }
227
+
228
+ const dataInfo = this.dataIdMap.get(dataId);
229
+ if (!dataInfo || !dataInfo.data) {
230
+ console.warn('Waterfall: Empty data for dataId', dataId);
231
+ }
232
+
233
+
234
+ const $card = $(
235
+ `<div class="waterfall-item"
236
+ data-index="${dataId}"
237
+ style="position: absolute;transform:translate(${left}px,${top}px);"
238
+ >
239
+ </div> `
240
+ );
241
+
242
+ $viewport.append($card);
243
+
244
+ const str = options.renderItem(data, dataInfo.originalIndex, $card);
245
+
246
+ if (str) {
247
+ $card.html(str);
248
+ }
249
+
250
+ this.renderedDataIds.add(dataId);
251
+
252
+ if (options.columns !== 1) {
253
+ $card.width(this.columnWidth + 'px');
254
+ }
255
+
256
+
257
+ return $card;
258
+ }
259
+
260
+ // 获取指定高度下的卡片索引
261
+ Waterfall.prototype.updateCardsInView = async function ({ start, end, force = false }) {
262
+ const options = this.options;
263
+ const minHeight = this.getMinHeight();
264
+ const endBuffer = end + options.bufferHeight;
265
+ if (minHeight < endBuffer) {
266
+ // 如果不够 进行补建
267
+ await this.createCards({ end: endBuffer });
268
+ }
269
+
270
+ const startNum = start - options.bufferHeight;
271
+ const endNum = end + options.bufferHeight;
272
+ // 新方案:基于数据ID映射机制
273
+ const newActiveNodes = new Map();
274
+ for (let i = 0; i < this.columns; i++) {
275
+ const column = this.columnItems[i];
276
+
277
+ for (let j = 0; j < column.children.length; j++) {
278
+ const row = column.children[j];
279
+ const dataId = row.dataId; // 使用dataId替代renderIndex
280
+
281
+ // 验证数据ID有效性
282
+ if (!this.dataIdMap.has(dataId)) {
283
+ console.warn('Waterfall: Invalid dataId detected', dataId);
284
+ continue;
285
+ }
286
+
287
+ const dataInfo = this.dataIdMap.get(dataId);
288
+
289
+ if (!dataInfo) {
290
+ console.warn('Waterfall: Invalid data for dataId', dataId);
291
+ continue;
292
+ }
293
+
294
+ const data = options.data[dataId]
295
+
296
+ // 如果当前这个节点是特殊节点,只更新位置,不参与节点复用
297
+ // 如果是特殊的静态占用元素卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
298
+ let specialNode = false;
299
+ if (options.shouldOccupySpace) {
300
+ specialNode = options.shouldOccupySpace(data) || false;
301
+ }
302
+ if (specialNode) {
303
+ // 特殊节点:只更新位置,不参与复用逻辑
304
+ if (row.$node && row.$node.length) {
305
+ // 直接更新特殊节点的位置
306
+ row.$node.css({
307
+ 'transform': `translate(${row.left}px,${row.top}px)`,
308
+ }).attr('data-index', dataId);
309
+
310
+ // 如果是强制更新,也重新渲染内容
311
+ if (force) {
312
+ this.updateRenderUI(row.$node, data, dataId);
313
+ }
314
+ } else {
315
+ console.warn('Waterfall: Special node DOM not found for dataId', dataId);
316
+ }
317
+ continue; // 跳过普通节点的复用逻辑
318
+ }
319
+
320
+ // 在可视区域内 进行有关卡片的操作
321
+ const bool = row.top <= endNum && row.bottom >= startNum;
322
+ if (bool) {
323
+ // 理论上什么都不动,因为卡片的位置不会变
324
+ const $card = this.activeNodes.get(dataId);
325
+
326
+ let $node = null;
327
+
328
+ // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
329
+ let bool = true;
330
+ if ($card) {
331
+ bool = hasNodeInActives(newActiveNodes, $card)
332
+ }
333
+ // 如果卡片已经在DOM中,则不用更新位置
334
+ if (bool === false) {
335
+ $node = $card;
336
+ this.activeNodes.delete(dataId);
337
+ // 如果是强更,这里才会采取更新
338
+ if (force) {
339
+ this.updateRenderUI($node, data, dataId);
340
+ }
341
+
342
+ } else {
343
+ const $card = this.allReadyNodes.get(dataId);
344
+ // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
345
+ let bool = true;
346
+ if ($card) {
347
+ bool = hasNodeInActives(newActiveNodes, $card)
348
+ }
349
+ if (bool === false) {
350
+ // 如果成功获取到card并没有占用 那么就复用这个card
351
+ $node = $card;
352
+
353
+ this.updateRenderUI($node, data, dataId);
354
+
355
+ } else {
356
+ // 卡片不在DOM中,则更新位置
357
+ $node = getNodePoolPop(this.nodePool, newActiveNodes);
358
+ if ($node === null) {
359
+ // 这里是往上方拖动时,可能需要补建的情况
360
+ $node = this.appendCard(data, dataId, {
361
+ top: row.top, left: row.left
362
+ });
363
+ row.$node = $node;
364
+ } else {
365
+ this.updateRenderUI($node, data, dataId);
366
+ row.$node = $node;
367
+ }
368
+ }
369
+ }
370
+ $node.css({
371
+ 'transform': `translate(${row.left}px,${row.top}px)`,
372
+ }).attr('data-index', dataId);
373
+
374
+ newActiveNodes.set(dataId, $node);
375
+
376
+ // 清除掉在NodePool中的card
377
+ const index = this.nodePool.indexOf($card);
378
+ if (index !== -1) {
379
+ this.nodePool.splice(index, 1);
380
+ }
381
+
382
+ // console.log('302-row', row.dataId, dataInfo);
383
+ }
384
+ }
385
+ }
386
+
387
+ // 阶段2:处理不活跃节点
388
+ this.activeNodes.forEach($node => {
389
+ $node.css('transform', `translateY(-9999px)`);// 移出可视区域
390
+ if (this.nodePool.indexOf($node) === -1) {
391
+ this.nodePool.push($node);
392
+ }
393
+ });
394
+ this.activeNodes = newActiveNodes;
395
+ // console.log('this.activeNodes', this.activeNodes);
396
+ // console.log('this.nodePool', this.nodePool);
397
+ }
398
+
399
+
400
+ Waterfall.prototype.getMaxHeight = function () {
401
+ let maxHeight = 0;
402
+ for (let i = 0; i < this.columns; i++) {
403
+ const column = this.columnItems[i]
404
+
405
+ // 获取每组元素列表的最后一个bottom值
406
+ maxHeight = Math.max(maxHeight, column.bottom);
407
+
408
+ }
409
+ return maxHeight;
410
+ }
411
+
412
+ Waterfall.prototype.getMinHeight = function () {
413
+ let minHeight = 0;
414
+ for (let i = 0; i < this.columns; i++) {
415
+ const column = this.columnItems[i]
416
+ if (minHeight === 0) {
417
+ minHeight = column.bottom;
418
+ }
419
+ // 获取每组元素列表的最后一个bottom值
420
+ minHeight = Math.min(minHeight, column.bottom);
421
+
422
+ }
423
+ return minHeight;
424
+ }
425
+
426
+ Waterfall.prototype.getMinHeightColumn = function () {
427
+ let minHeight = -1;
428
+ let mimHeightColumn = null;
429
+ let index = -1;
430
+ const startPoints = this.options.startPoints || [];
431
+ for (let i = 0; i < this.columns; i++) {
432
+ const column = this.columnItems[i]
433
+ if (minHeight === -1) {
434
+ minHeight = column.bottom;
435
+ mimHeightColumn = column;
436
+ index = i;
437
+ } else if (minHeight > column.bottom) {
438
+ // 获取每组元素列表的最后一个bottom值
439
+ minHeight = column.bottom;
440
+ mimHeightColumn = column;
441
+ index = i;
442
+ }
443
+
444
+ }
445
+
446
+ if (minHeight === 0 && index > -1) {
447
+ if (startPoints.length > index) {
448
+ // 设置一下起始点的初始化
449
+ mimHeightColumn.bottom = startPoints[index];
450
+ }
451
+ }
452
+
453
+ return mimHeightColumn;
454
+ }
455
+
456
+ // 创建卡片
457
+ Waterfall.prototype.createCards = function ({ end, dataId = -1 }, callback) {
458
+ const self = this;
459
+ const options = this.options;
460
+
461
+ return new Promise((resolve) => {
462
+ // 新方案:获取下一个未渲染的数据ID
463
+ let nextDataId = null;
464
+ for (let [dataId, dataInfo] of this.dataIdMap) {
465
+ if (!this.renderedDataIds.has(dataId)) {
466
+ nextDataId = dataId;
467
+ break;
468
+ }
469
+ }
470
+
471
+
472
+ // 如果没有更多数据需要渲染
473
+ if (nextDataId === null) {
474
+ this.setScrollHeight();
475
+ return resolve();
476
+ }
477
+
478
+ const dataInfo = this.dataIdMap.get(nextDataId);
479
+ if (!dataInfo || !dataInfo.data) {
480
+ console.warn('Waterfall: Invalid data for dataId', nextDataId);
481
+ return resolve();
482
+ }
483
+
484
+ if (this.renderIndex >= options.data.length) {
485
+ this.setScrollHeight();
486
+ return resolve();
487
+ }
488
+
489
+ const data = options.data[nextDataId];
490
+
491
+ let column = this.getMinHeightColumn();
492
+ if (column === null) {
493
+ column = this.columnItems[0];
494
+ }
495
+
496
+ const top = column.bottom === 0 ? options.marginTop : (column.bottom + options.rowGap);
497
+ const position = { top, left: column.left };
498
+ const row = createDefaultRow(position);
499
+
500
+ this.renderIndex += 1;
501
+
502
+ let specialNode = false;
503
+
504
+ // 如果是特殊的卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
505
+ if (options.shouldOccupySpace) {
506
+ specialNode = options.shouldOccupySpace(data) || false;
507
+ }
508
+
509
+ // 添加卡片,使用dataId作为唯一标识
510
+ let $card = null;
511
+ if (this.nodePool.length === 0 || specialNode === true) {
512
+ $card = this.appendCard(data, nextDataId, position);
513
+ } else {
514
+ const $tmp = getNodePoolPop(this.nodePool, this.activeNodes);
515
+ if ($tmp) {
516
+ $card = $tmp;
517
+ $card.css({
518
+ 'transform': `translate(${row.left}px,${row.top}px)`,
519
+ }).attr('data-index', nextDataId);
520
+ this.updateRenderUI($card, data, nextDataId);
521
+
522
+ } else {
523
+ $card = this.appendCard(data, nextDataId, position);
524
+ }
525
+
526
+ }
527
+
528
+ row.$node = $card;
529
+ row.dataId = nextDataId; // 使用dataId替代renderIndex
530
+ if (dataId !== -1) {
531
+ row.dataId = dataId;
532
+ }
533
+
534
+ if (specialNode === false) {
535
+ // 把新增的卡片放进 activeNodes 当成活跃节点元素,那么是 可以动态使用的
536
+ this.activeNodes.set(nextDataId, $card);
537
+
538
+ this.allReadyNodes.set(nextDataId, $card);
539
+ } else {
540
+ // 如果是特殊的,这里不要记录了
541
+ this.allReadyNodes.set(nextDataId, null);
542
+ }
543
+
544
+ this.renderedDataIds.add(nextDataId);
545
+
546
+ // setTimeout(() => {
547
+ //window.requestAnimationFrame(() => {
548
+
549
+ column.bottom = top + $card.height();
550
+
551
+ column.children.push(row);
552
+ row.bottom = column.bottom;
553
+
554
+ // 建立 dataIdMap -> row 的引用,用于快速访问布局信息
555
+ dataInfo.layoutInfo = row;
556
+
557
+ // 检查是否需要继续创建卡片
558
+ const minHeight = this.getMinHeight();
559
+ const hasMoreData = this.renderedDataIds.size < this.dataIdMap.size;
560
+
561
+ if (hasMoreData && (minHeight < end)) {
562
+
563
+ this.createCards({ end }, () => {
564
+ resolve();
565
+ if (callback) {
566
+ callback();
567
+ }
568
+ });
569
+ } else {
570
+ this.setScrollHeight();
571
+ resolve();
572
+ if (callback) {
573
+ callback();
574
+ }
575
+ }
576
+ //});
577
+ // }, 42);
578
+ });
579
+
580
+ }
581
+
582
+
583
+ Waterfall.prototype.createColumn = function (index) {
584
+ const res = {
585
+ left: 0,
586
+ bottom: 0,
587
+ width: 0,
588
+ children: []
589
+ }
590
+
591
+ const options = this.options;
592
+ const columnWidth = this.columnWidth;
593
+
594
+ res.width = this.columnWidth;
595
+ res.left = (index * (columnWidth + options.columnGap));
596
+ return res;
597
+ }
598
+
599
+ Waterfall.prototype.updateRenderUI = function ($node, data, dataId) {
600
+ const options = this.options;
601
+
602
+ // 新方案:基于数据ID的数据验证
603
+ if (!this.dataIdMap.has(dataId)) {
604
+ console.error('Waterfall: Invalid dataId in updateRenderUI', dataId);
605
+ return;
606
+ }
607
+
608
+ const dataInfo = this.dataIdMap.get(dataId);
609
+ if (!dataInfo || !dataInfo.data) {
610
+ console.warn('Waterfall: Empty data for dataId', dataId);
611
+ }
612
+
613
+ if (this.hasUpdateItem === true) {
614
+ options.updateItem($node, data, dataInfo.originalIndex)
615
+ } else {
616
+ const str = options.renderItem(data, dataInfo.originalIndex, $node);
617
+ if (str) {
618
+ $node.html(str);
619
+ }
620
+ }
621
+ }
622
+
623
+ // 辅助方法:使用 renderItem 渲染节点(用于全新节点)
624
+ Waterfall.prototype.renderUI = function ($node, data, dataId) {
625
+ const options = this.options;
626
+
627
+ // 新方案:基于数据ID的数据验证
628
+ if (!this.dataIdMap.has(dataId)) {
629
+ console.error('Waterfall: Invalid dataId in renderUI', dataId);
630
+ return;
631
+ }
632
+
633
+ const dataInfo = this.dataIdMap.get(dataId);
634
+ if (!dataInfo || !dataInfo.data) {
635
+ console.warn('Waterfall: Empty data for dataId', dataId);
636
+ }
637
+
638
+ // 始终使用 renderItem,因为这是全新节点
639
+ const str = options.renderItem(data, dataInfo.originalIndex, $node);
640
+ if (str) {
641
+ $node.html(str);
642
+ }
643
+ }
644
+
645
+
646
+
647
+ Waterfall.prototype.updateData = async function (newData) {
648
+ const options = this.options;
649
+ options.data = newData;
650
+
651
+ // 步骤1: 批量获取所有卡片的新高度(只需一次 requestAnimationFrame)
652
+ const dataIds = this.renderedDataIds;
653
+ const newHeightsMap = await this.getBatchCardNewHeights(dataIds, newData);
654
+
655
+ // 步骤2: 收集高度变化
656
+ const heightChanges = new Map(); // dataId -> {oldHeight, newHeight, heightDiff}
657
+
658
+ for (let dataId of dataIds) {
659
+ const oldHeight = this.getCardOldHeight(dataId);
660
+ const newHeight = newHeightsMap.get(dataId);
661
+
662
+ if (oldHeight !== newHeight) {
663
+ heightChanges.set(dataId, {
664
+ oldHeight,
665
+ newHeight,
666
+ heightDiff: newHeight - oldHeight
667
+ });
668
+ }
669
+ }
670
+
671
+ // 步骤3: 使用累积差分算法更新每一列的布局
672
+ for (let i = 0; i < this.columnItems.length; i++) {
673
+ const column = this.columnItems[i];
674
+ let accumulatedDiff = 0; // 累积的高度变化
675
+
676
+ for (let j = 0; j < column.children.length; j++) {
677
+ const row = column.children[j];
678
+ const dataId = row.dataId;
679
+
680
+ // 先应用之前累积的差分
681
+ if (accumulatedDiff !== 0) {
682
+ row.top += accumulatedDiff;
683
+ row.bottom += accumulatedDiff;
684
+ }
685
+
686
+ // 如果当前卡片有高度变化,累积到差分中
687
+ if (heightChanges.has(dataId)) {
688
+ const change = heightChanges.get(dataId);
689
+ accumulatedDiff += change.heightDiff;
690
+ // 更新当前卡片的 bottom
691
+ row.bottom += change.heightDiff;
692
+ }
693
+ }
694
+
695
+ // 更新列的总高度
696
+ if (accumulatedDiff !== 0) {
697
+ column.bottom += accumulatedDiff;
698
+ }
699
+ }
700
+
701
+ // 步骤4: 同步视图
702
+ this.setScrollHeight();
703
+ this.updateVisibleItems(true); // 这里会处理内容更新和DOM位置同步
704
+ }
705
+
706
+ // 辅助方法:获取卡片的旧高度(从布局信息)
707
+ Waterfall.prototype.getCardOldHeight = function(dataId) {
708
+ const dataInfo = this.dataIdMap.get(dataId);
709
+
710
+ if (!dataInfo) {
711
+ // dataId 不存在
712
+ return 0;
713
+ }
714
+
715
+ if (!dataInfo.layoutInfo) {
716
+ // 布局信息未初始化(卡片还未创建)
717
+ return 0;
718
+ }
719
+
720
+ const row = dataInfo.layoutInfo;
721
+ return row.bottom - row.top;
722
+ }
723
+
724
+ // 辅助方法:获取特殊节点的 DOM 引用
725
+ Waterfall.prototype.getSpecialNodeDOM = function(dataId) {
726
+ const dataInfo = this.dataIdMap.get(dataId);
727
+ if (dataInfo && dataInfo.layoutInfo && dataInfo.layoutInfo.$node) {
728
+ return dataInfo.layoutInfo.$node;
729
+ }
730
+ return null;
731
+ }
732
+
733
+ // 辅助方法:批量获取卡片的新高度(用于 updateData)
734
+ Waterfall.prototype.getBatchCardNewHeights = async function(dataIds, newData) {
735
+ const options = this.options;
736
+ const $ = getEnv().$;
737
+ const $container = $(options.container);
738
+ const $viewport = $container.find('.waterfall-list-viewport');
739
+
740
+ // 用于记录每个卡片的信息
741
+ const cardInfos = new Map(); // dataId -> { $node, needCleanup, cleanupType }
742
+
743
+ // 记录从节点池借用的节点,需要在完成后归还
744
+ const borrowedNodes = [];
745
+ // 记录新创建的临时节点,需要在完成后删除
746
+ const tempNodesToDelete = [];
747
+
748
+ // 第一步:优先处理所有可视区域节点(activeNodes)
749
+ for (let dataId of dataIds) {
750
+ if (this.activeNodes.has(dataId)) {
751
+ const $node = this.activeNodes.get(dataId);
752
+ const data = newData[dataId];
753
+
754
+ // 重新渲染内容
755
+ this.updateRenderUI($node, data, dataId);
756
+
757
+ cardInfos.set(dataId, {
758
+ $node: $node,
759
+ needCleanup: false,
760
+ cleanupType: null
761
+ });
762
+ }
763
+ }
764
+
765
+ // 第二步:处理所有非可视区域节点
766
+ for (let dataId of dataIds) {
767
+ // 跳过已在第一步处理的可视区域节点
768
+ if (this.activeNodes.has(dataId)) {
769
+ continue;
770
+ }
771
+
772
+ const data = newData[dataId];
773
+
774
+ // 情况1: 卡片在 allReadyNodes 中
775
+ if (this.allReadyNodes.has(dataId)) {
776
+ const $existingNode = this.allReadyNodes.get(dataId);
777
+
778
+ // 特殊节点(null):通过 layoutInfo 获取 DOM 引用
779
+ if ($existingNode === null) {
780
+ const $specialNode = this.getSpecialNodeDOM(dataId);
781
+
782
+ if ($specialNode) {
783
+ // 特殊节点永远独占,无需检查重复
784
+ // 直接重新渲染特殊节点的内容
785
+ this.updateRenderUI($specialNode, data, dataId);
786
+
787
+ cardInfos.set(dataId, {
788
+ $node: $specialNode,
789
+ needCleanup: false,
790
+ cleanupType: 'special'
791
+ });
792
+ } else {
793
+ // 无法获取特殊节点DOM,使用旧高度
794
+ console.warn('Waterfall: Special node DOM not found for dataId', dataId);
795
+ cardInfos.set(dataId, {
796
+ $node: null,
797
+ needCleanup: false,
798
+ cleanupType: 'special-notfound'
799
+ });
800
+ }
801
+ continue;
802
+ }
803
+
804
+ // 情况2: 普通节点 - 从节点池获取或创建临时节点
805
+ let $node = getNodePoolPop(this.nodePool, this.activeNodes);
806
+
807
+ if ($node) {
808
+ // 从节点池借用节点
809
+ borrowedNodes.push($node);
810
+
811
+ // 设置位置和样式
812
+ $node.css({
813
+ 'transform': 'translate(-9999px, -9999px)',
814
+ });
815
+
816
+ // 设置宽度
817
+ if (options.columns !== 1) {
818
+ $node.width(this.columnWidth + 'px');
819
+ }
820
+
821
+ // 使用 updateRenderUI(节点池的节点已有结构)
822
+ this.updateRenderUI($node, data, dataId);
823
+
824
+ cardInfos.set(dataId, {
825
+ $node: $node,
826
+ needCleanup: false,
827
+ cleanupType: 'borrowed'
828
+ });
829
+ } else {
830
+ // 节点池空了,创建临时节点
831
+ $node = $(
832
+ `<div class="waterfall-item"
833
+ data-index="${dataId}"
834
+ style="position: absolute; transform: translate(-9999px, -9999px);"
835
+ >
836
+ </div>`
837
+ );
838
+ $viewport.append($node);
839
+ tempNodesToDelete.push($node);
840
+
841
+ // 设置宽度
842
+ if (options.columns !== 1) {
843
+ $node.width(this.columnWidth + 'px');
844
+ }
845
+
846
+ // 使用 renderUI(临时节点是全新的)
847
+ this.renderUI($node, data, dataId);
848
+
849
+ cardInfos.set(dataId, {
850
+ $node: $node,
851
+ needCleanup: false,
852
+ cleanupType: 'temp'
853
+ });
854
+ }
855
+ continue;
856
+ }
857
+
858
+ // 情况3: 未找到节点,标记为使用旧高度
859
+ cardInfos.set(dataId, {
860
+ $node: null,
861
+ needCleanup: false,
862
+ cleanupType: 'notfound'
863
+ });
864
+ }
865
+
866
+ // 第三步:在单个 requestAnimationFrame 中统一获取所有高度
867
+ return new Promise(resolve => {
868
+ window.requestAnimationFrame(() => {
869
+ const heightMap = new Map(); // dataId -> newHeight
870
+
871
+ for (let [dataId, info] of cardInfos) {
872
+ if (info.$node) {
873
+ // 从DOM获取高度
874
+ heightMap.set(dataId, info.$node.height());
875
+ } else {
876
+ // 使用旧高度
877
+ heightMap.set(dataId, this.getCardOldHeight(dataId));
878
+ }
879
+ }
880
+
881
+ // 清理:归还借用的节点到节点池
882
+ for (let $node of borrowedNodes) {
883
+ $node.css('transform', 'translateY(-9999px)');
884
+ this.nodePool.push($node);
885
+ }
886
+
887
+ // 清理:删除临时节点
888
+ for (let $node of tempNodesToDelete) {
889
+ $node.remove();
890
+ }
891
+
892
+ resolve(heightMap);
893
+ });
894
+ });
895
+ }
896
+
897
+ // 辅助方法:获取卡片的新高度(通过渲染)- 用于单卡片更新
898
+ Waterfall.prototype.getCardNewHeight = async function(dataId, data) {
899
+ const options = this.options;
900
+ const $ = getEnv().$;
901
+
902
+ // 情况1: 卡片在可视区域(activeNodes)
903
+ if (this.activeNodes.has(dataId)) {
904
+ const $node = this.activeNodes.get(dataId);
905
+
906
+ // 重新渲染内容
907
+ this.updateRenderUI($node, data, dataId);
908
+
909
+ // 等待DOM更新
910
+ return new Promise(resolve => {
911
+ window.requestAnimationFrame(() => {
912
+ resolve($node.height());
913
+ });
914
+ });
915
+ }
916
+
917
+ // 情况2: 卡片不在可视区域(allReadyNodes)
918
+ if (this.allReadyNodes.has(dataId)) {
919
+ const $existingNode = this.allReadyNodes.get(dataId);
920
+
921
+ // 特殊节点(null):通过 layoutInfo 获取 DOM 引用并更新
922
+ if ($existingNode === null) {
923
+ const $specialNode = this.getSpecialNodeDOM(dataId);
924
+
925
+ if ($specialNode) {
926
+ // 重新渲染特殊节点的内容
927
+ this.updateRenderUI($specialNode, data, dataId);
928
+
929
+ // 等待DOM更新后获取新高度
930
+ return new Promise(resolve => {
931
+ window.requestAnimationFrame(() => {
932
+ resolve($specialNode.height());
933
+ });
934
+ });
935
+ }
936
+
937
+ // 无法获取特殊节点DOM,返回旧高度
938
+ console.warn('Waterfall: Special node DOM not found for dataId', dataId);
939
+ return this.getCardOldHeight(dataId);
940
+ }
941
+
942
+ // 普通节点:从节点池获取或创建临时节点
943
+ const $container = $(options.container);
944
+ const $viewport = $container.find('.waterfall-list-viewport');
945
+
946
+ let $node = getNodePoolPop(this.nodePool, this.activeNodes);
947
+ let isBorrowed = false;
948
+
949
+ if ($node) {
950
+ // 从节点池借用节点
951
+ isBorrowed = true;
952
+
953
+ // 设置位置和样式
954
+ $node.css({
955
+ 'transform': 'translate(-9999px, -9999px)',
956
+ });
957
+
958
+ // 设置宽度
959
+ if (options.columns !== 1) {
960
+ $node.width(this.columnWidth + 'px');
961
+ }
962
+
963
+ // 使用 updateRenderUI(节点池的节点已有结构)
964
+ this.updateRenderUI($node, data, dataId);
965
+ } else {
966
+ // 节点池空了,创建临时节点
967
+ $node = $(
968
+ `<div class="waterfall-item"
969
+ data-index="${dataId}"
970
+ style="position: absolute; transform: translate(-9999px, -9999px);"
971
+ >
972
+ </div>`
973
+ );
974
+ $viewport.append($node);
975
+
976
+ // 设置宽度
977
+ if (options.columns !== 1) {
978
+ $node.width(this.columnWidth + 'px');
979
+ }
980
+
981
+ // 使用 renderUI(临时节点是全新的)
982
+ this.renderUI($node, data, dataId);
983
+ }
984
+
985
+ // 等待DOM更新
986
+ return new Promise(resolve => {
987
+ window.requestAnimationFrame(() => {
988
+ const newHeight = $node.height();
989
+
990
+ // 清理
991
+ if (isBorrowed) {
992
+ // 归还借用的节点到节点池
993
+ $node.css('transform', 'translateY(-9999px)');
994
+ this.nodePool.push($node);
995
+ } else {
996
+ // 删除临时节点
997
+ $node.remove();
998
+ }
999
+
1000
+ resolve(newHeight);
1001
+ });
1002
+ });
1003
+ }
1004
+
1005
+ // 情况3: 未找到节点,返回旧高度
1006
+ return this.getCardOldHeight(dataId);
1007
+ }
1008
+
1009
+ // 某个数据进行了UI变更,触发高度重新绘制
1010
+ Waterfall.prototype.updateCard = async function (data) {
1011
+ const self = this;
1012
+ const options = this.options;
1013
+ let dataId = -1;
1014
+ if (typeof data === 'number') {
1015
+ dataId = data;
1016
+ } else {
1017
+ for (let i = 0; i < options.data.length; i++) {
1018
+ if (options.data[i] === data) {
1019
+ dataId = i;
1020
+ break;
1021
+ }
1022
+ }
1023
+ }
1024
+ if (dataId === -1) {
1025
+ // 没有匹配到数据,进行退出
1026
+ console.log('Waterfall: Invalid data in options.data');
1027
+ return
1028
+ }
1029
+
1030
+ // 检查数据是否已经渲染过
1031
+ if (!this.renderedDataIds.has(dataId)) {
1032
+ console.log('Waterfall: dataId not rendered yet', dataId);
1033
+ return
1034
+ }
1035
+
1036
+ const itemData = options.data[dataId];
1037
+
1038
+ // 获取旧高度
1039
+ const oldHeight = this.getCardOldHeight(dataId);
1040
+
1041
+ // 获取新高度(会自动处理渲染和临时节点)
1042
+ const newHeight = await this.getCardNewHeight(dataId, itemData);
1043
+
1044
+ // 应用高度变化
1045
+ this.applyHeightChange(dataId, oldHeight, newHeight);
1046
+ }
1047
+
1048
+ // 应用高度变化到布局
1049
+ Waterfall.prototype.applyHeightChange = function (dataId, oldHeight, newHeight) {
1050
+ const minus = newHeight - oldHeight;
1051
+
1052
+ // 如果高度没有变化,直接退出
1053
+ if (minus === 0) {
1054
+ console.log('Waterfall: Card height unchanged for dataId', dataId);
1055
+ return;
1056
+ }
1057
+
1058
+ console.log('Waterfall: Card height changed for dataId', dataId, 'from', oldHeight, 'to', newHeight, 'diff', minus);
1059
+
1060
+ // 重新计算该数据所在列的卡片位置
1061
+ let needUpdate = false;
1062
+ const columnItems = this.columnItems;
1063
+
1064
+ for (let i = 0; i < columnItems.length; i++) {
1065
+ const column = columnItems[i];
1066
+ let foundCard = false;
1067
+
1068
+ // 这里为了简化,各列的瀑布流保持不动,只更新各列下节点的top位置,不做节点的跨列位移
1069
+ for (let j = 0; j < column.children.length; j++) {
1070
+ const row = column.children[j];
1071
+
1072
+ if (row.dataId === dataId) {
1073
+ foundCard = true;
1074
+ needUpdate = true;
1075
+
1076
+ // 更新当前卡片的bottom
1077
+ row.bottom += minus;
1078
+
1079
+ } else if (foundCard) {
1080
+ // 更新该卡片以下所有卡片的位置(同列)
1081
+ row.top += minus;
1082
+ row.bottom += minus;
1083
+ }
1084
+ }
1085
+
1086
+ if (foundCard) {
1087
+ // 更新列的总高度
1088
+ column.bottom += minus;
1089
+ break;
1090
+ }
1091
+ }
1092
+
1093
+ if (needUpdate) {
1094
+ // 更新滚动容器高度
1095
+ this.setScrollHeight();
1096
+
1097
+ // 强制更新渲染(更新所有卡片的实际位置)
1098
+ this.updateVisibleItems(true);
1099
+ }
1100
+ }
1101
+
1102
+ // 重新计算设置一遍所有的卡片位置
1103
+ Waterfall.prototype.updatePointCards = function () {
1104
+ // 有问题 不要用
1105
+ return
1106
+ const self = this;
1107
+ const options = this.options;
1108
+ const columnItems = this.columnItems;
1109
+ let top = options.marginTop;
1110
+ for (let i = 0; i < columnItems.length; i++) {
1111
+ const column = columnItems[i];
1112
+ // 这里为了简化,各列的瀑布流保持不动,只更新各列下节点的top位置,不做节点的跨列位移
1113
+ for (let j = 0; j < column.children.length; j++) {
1114
+ const row = column.children[j];
1115
+ const $card = row.$node;
1116
+
1117
+ // 验证数据ID有效性
1118
+ if (!this.dataIdMap.has(row.dataId)) {
1119
+ console.warn('Waterfall: Invalid dataId in updatePointCards', row.dataId);
1120
+ continue;
1121
+ }
1122
+
1123
+ // 第一个的top不需要更新
1124
+ if (j === 0) {
1125
+ row.bottom = top + $card.height();
1126
+ } else {
1127
+ row.top = column.children[j - 1].bottom + options.rowGap;
1128
+ row.bottom = row.top + $card.height();
1129
+ // 更新卡片位置
1130
+ // $card.css({
1131
+ // 'transform': `translate(${row.left}px,${row.top}px)`,
1132
+ // })
1133
+ }
1134
+
1135
+ }
1136
+ // 设置一次该列的bottom
1137
+ column.bottom = getBottomByColumn(column);
1138
+ }
1139
+ }
1140
+
1141
+ // 展示loading的回调函数
1142
+ Waterfall.prototype.showLoading = function (callback = null) {
1143
+ this.isShowLoading = true;
1144
+ const options = this.options;
1145
+ let $node = null
1146
+ if (this.$loadingNode) {
1147
+ let loadingTop = this.getMaxHeight() + options.rowGap
1148
+ this.$loadingNode.css('transform', `translate(0px,${loadingTop}px)`);
1149
+ $node = this.$loadingNode;
1150
+ // window.requestAnimationFrame(() => {
1151
+ // setTimeout(() => {
1152
+ // this.$scrollDom.scrollTop(loadingTop + this.$loadingNode.height());
1153
+ // });
1154
+ // });
1155
+ }
1156
+
1157
+ if (callback) callback($node)
1158
+ this.setScrollHeight();
1159
+ }
1160
+
1161
+ // 隐藏loading的回调函数
1162
+ Waterfall.prototype.hideLoading = function (callback = null) {
1163
+ this.isShowLoading = false;
1164
+ const options = this.options;
1165
+ let $node = null
1166
+ if (this.$loadingNode) {
1167
+ let h1 = this.getMaxHeight() + options.marginBottom
1168
+ this.$loadingNode.css('transform', `translate(0px,-99999px)`);
1169
+ //如果要设置高度,那么这里判断一下当前是否正在做updata 一般这里被调用时,数据已经读到,在updata的同一时间调用了该函数
1170
+ // 如果两个时刻高度是一致的 那么数据就是一致的 这里重新设置回来高度即可
1171
+ window.requestAnimationFrame(() => {
1172
+ this.setScrollHeight();
1173
+ })
1174
+ $node = this.$loadingNode
1175
+ }
1176
+
1177
+ if (callback) callback($node)
1178
+ this.setScrollHeight();
1179
+ }
1180
+
1181
+ // 设置滚动条列表的高度
1182
+ Waterfall.prototype.setScrollHeight = function () {
1183
+ const options = this.options;
1184
+ const $container = $(options.container);
1185
+ let h = this.getMaxHeight();
1186
+ if (this.isShowLoading === true) {
1187
+ if (this.$loadingNode) {
1188
+ h += options.rowGap + this.$loadingNode.height();
1189
+ }
1190
+ }
1191
+ h += options.marginBottom;
1192
+ $container.find('.waterfall-list-scroll').css('height', h + 'px');
1193
+ }
1194
+
1195
+ // 销毁自己
1196
+ Waterfall.prototype.destroy = function () {
1197
+ const options = this.options;
1198
+ const $container = $(options.container);
1199
+ $container.html('');
1200
+ this.activeNodes.clear();
1201
+ this.allReadyNodes.clear();
1202
+ this.dataIdMap.clear();
1203
+ this.renderedDataIds.clear();
1204
+ this.nodePool = [];
1205
+ this.columnItems = [];
1206
+ this.columns = 0;
1207
+ this.columnWidth = 0;
1208
+ this.renderIndex = 0;
1209
+ this.scrollTop = 0;
1210
+ this.isShowLoading = false;
1211
+ this.$loadingNode = null;
1212
+ this.$scrollDom = null;
1213
+ }
1214
+
1215
+ function createDefaultRow({ top, left }) {
1216
+ return {
1217
+ top,
1218
+ left,
1219
+ bottom: 0,
1220
+ $node: null,
1221
+ dataId: -1 // 新方案:使用dataId替代renderIndex
1222
+ }
1223
+ }
1224
+
1225
+
1226
+ function getBottomByColumn(column) {
1227
+ if (column.children.length === 0) {
1228
+ return 0;
1229
+ }
1230
+ const child = column.children;
1231
+ return child[child.length - 1].bottom;
1232
+ }
1233
+
1234
+ function hasNodeInActives(mapObj, $node) {
1235
+ let bool = false
1236
+ mapObj.forEach((item) => {
1237
+ if (item === $node) {
1238
+ bool = true;
1239
+ }
1240
+ });
1241
+ return bool;
1242
+ }
1243
+
1244
+ // 从众多jq对象数组取得一个不重复的
1245
+ function getNodePoolPop(nodePool, actives) {
1246
+ for (let i = 0; i < nodePool.length; i++) {
1247
+ const $node = nodePool[i];
1248
+ if (!hasNodeInActives(actives, $node)) {
1249
+ nodePool.splice(i, 1);
1250
+ return $node;
1251
+ }
1252
+ }
1253
+ return null;
1254
+ }