tencent.jquery.pix.component 1.0.63 → 1.0.64

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,7 +1,9 @@
1
1
  import "./banner.scss"
2
- import { $, windowEnv } from "../config";
2
+ import { windowEnv, getEnv } from "../config";
3
3
  import { addResizeFunc, nextAnimationFrame, removeResizeFunc } from "../utils/utils";
4
4
 
5
+ let $ = null;
6
+
5
7
  /**
6
8
  * 在页面上放置一个banner组件
7
9
  * @constructor
@@ -17,7 +19,9 @@ import { addResizeFunc, nextAnimationFrame, removeResizeFunc } from "../utils/ut
17
19
  * @param {function} [options.pageChanged] 翻页时触发,第一个参数为新生效页面元素的jq,第二个参数为 options.list 中对应的项,第三个参数为当前页码
18
20
  * @param {function} [options.renderCallback] 自定义渲染回调,第一个参数为 options.list 中对应的项,第二个参数为当前页码,返回值为渲染后的元素
19
21
  */
20
- export function Banner(options = {}){
22
+ export function Banner(options = {}) {
23
+ $ = getEnv().$;
24
+
21
25
  this.options = options;
22
26
  this.options.isTitleEnabled ??= true;
23
27
  this.options.autoResize ??= true;
@@ -30,7 +34,7 @@ export function Banner(options = {}){
30
34
  this.init();
31
35
  }
32
36
 
33
- Banner.prototype.setWidth = function(width) {
37
+ Banner.prototype.setWidth = function (width) {
34
38
  this.signWidth = width;
35
39
  this.allWidth = this.signWidth * (this.options.list.length + 2);
36
40
  $(this.options.container).width(width);
@@ -39,7 +43,7 @@ Banner.prototype.setWidth = function(width) {
39
43
  this.setTranslate(-this.signWidth * (this.index + 1));
40
44
  }
41
45
 
42
- Banner.prototype.fitWidth = function() {
46
+ Banner.prototype.fitWidth = function () {
43
47
  console.log('banner fitWidth');
44
48
  const newWidth = $(this.options.container).parent().width();
45
49
  if (newWidth != this.signWidth) {
@@ -50,7 +54,7 @@ Banner.prototype.fitWidth = function() {
50
54
  /**
51
55
  * 跳转到上一页
52
56
  */
53
- Banner.prototype.prevPage = async function() {
57
+ Banner.prototype.prevPage = async function () {
54
58
  this.pauseAutoPlay();
55
59
  await this.gotoPageUnchecked(this.index - 1);
56
60
  this.startAutoPlay();
@@ -59,7 +63,7 @@ Banner.prototype.prevPage = async function() {
59
63
  /**
60
64
  * 跳转到下一页
61
65
  */
62
- Banner.prototype.nextPage = async function() {
66
+ Banner.prototype.nextPage = async function () {
63
67
  this.pauseAutoPlay();
64
68
  await this.gotoPageUnchecked(this.index + 1);
65
69
  this.startAutoPlay();
@@ -69,7 +73,7 @@ Banner.prototype.nextPage = async function() {
69
73
  * 跳转到指定页
70
74
  * @param {number} newIdx 要切换到的页码 [0, len)
71
75
  */
72
- Banner.prototype.gotoPage = async function(newIdx) {
76
+ Banner.prototype.gotoPage = async function (newIdx) {
73
77
  if (newIdx < 0 || newIdx >= this.options.list.length) {
74
78
  throw new Error('index out of range');
75
79
  }
@@ -81,7 +85,7 @@ Banner.prototype.gotoPage = async function(newIdx) {
81
85
  this.startAutoPlay();
82
86
  }
83
87
 
84
- Banner.prototype.gotoPageUnchecked = async function(newIdx) {
88
+ Banner.prototype.gotoPageUnchecked = async function (newIdx) {
85
89
  const len = this.options.list.length;
86
90
  let wrap = 0;
87
91
  if (newIdx <= -1) {
@@ -100,14 +104,14 @@ Banner.prototype.gotoPageUnchecked = async function(newIdx) {
100
104
  }
101
105
 
102
106
  this.$inner.css('transition', 'transform 0.3s ease-out');
103
- const newTranslate = -this.signWidth * (newIdx+1);
107
+ const newTranslate = -this.signWidth * (newIdx + 1);
104
108
  this.setTranslate(newTranslate);
105
109
  if (this.options.isTitleEnabled) {
106
- this.$pagination.children().eq(newIdx+1).addClass('active').siblings().removeClass('active');
110
+ this.$pagination.children().eq(newIdx + 1).addClass('active').siblings().removeClass('active');
107
111
  this.$titleBox.text(this.options.list[newIdx].title);
108
112
  }
109
113
  if (this.options.pageChanged && newIdx != this.index) {
110
- this.options.pageChanged(this.$inner.children().eq(newIdx+1), this.options.list[newIdx], newIdx);
114
+ this.options.pageChanged(this.$inner.children().eq(newIdx + 1), this.options.list[newIdx], newIdx);
111
115
  }
112
116
  this.index = newIdx;
113
117
  }
@@ -116,11 +120,11 @@ Banner.prototype.gotoPageUnchecked = async function(newIdx) {
116
120
  * 获取当前页的序号
117
121
  * @returns {number} 当前页的序号 [0,len]
118
122
  */
119
- Banner.prototype.getCurrentPage = function() {
123
+ Banner.prototype.getCurrentPage = function () {
120
124
  return this.index;
121
125
  }
122
126
 
123
- Banner.prototype.init = function(){
127
+ Banner.prototype.init = function () {
124
128
  const $t = $(this.options.container)
125
129
  console.log('banner container:', $t.dom);
126
130
  const signWidth = $t.width()
@@ -139,10 +143,10 @@ Banner.prototype.init = function(){
139
143
  });
140
144
  }
141
145
  this.startAutoPlay();
142
- console.log('list:', this.options.list, ' signWidth:',signWidth)
146
+ console.log('list:', this.options.list, ' signWidth:', signWidth)
143
147
  }
144
148
 
145
- Banner.prototype.startAutoPlay = function() {
149
+ Banner.prototype.startAutoPlay = function () {
146
150
  if (this.options.durationMs > 0 && this.timer == null) {
147
151
  this.timer = setInterval(() => {
148
152
  this.nextPage()
@@ -150,7 +154,7 @@ Banner.prototype.startAutoPlay = function() {
150
154
  }
151
155
  }
152
156
 
153
- Banner.prototype.pauseAutoPlay = function() {
157
+ Banner.prototype.pauseAutoPlay = function () {
154
158
  if (this.timer == null) {
155
159
  return;
156
160
  }
@@ -158,25 +162,25 @@ Banner.prototype.pauseAutoPlay = function() {
158
162
  this.timer = null;
159
163
  }
160
164
 
161
- Banner.prototype.createHtml = function(){
165
+ Banner.prototype.createHtml = function () {
162
166
  const len = this.options.list.length
163
- if(len<1){
167
+ if (len < 1) {
164
168
  return
165
169
  }
166
170
  const $t = $(this.options.container)
167
171
  $t.empty();
168
172
  const signWidth = this.signWidth = $t.width()
169
-
173
+
170
174
  const allWidth = this.allWidth = signWidth * (len + 2)
171
175
  const $inner = this.$inner = $(`<div class="banner-inner-transform"></div>`)
172
176
  this.currentTranslate = -signWidth * (this.index + 1)
173
- $inner.width(allWidth).css('transform',`translateX(${this.currentTranslate}px)`)
177
+ $inner.width(allWidth).css('transform', `translateX(${this.currentTranslate}px)`)
174
178
 
175
179
  // 加第0个位置
176
- $inner.append(`<div class="banner-inner-li" data-background-url="${this.options.list[len-1].url}" style="width:${this.signWidth}px;">
180
+ $inner.append(`<div class="banner-inner-li" data-background-url="${this.options.list[len - 1].url}" style="width:${this.signWidth}px;">
177
181
  </div>
178
182
  `)
179
- for(let i=0;i<len;i++){
183
+ for (let i = 0; i < len; i++) {
180
184
  const item = this.options.list[i]
181
185
  const $li = $(`<div class="banner-inner-li" data-background-url="${item.url}" style="width:${this.signWidth}px;">
182
186
  </div>
@@ -210,7 +214,7 @@ Banner.prototype.createHtml = function(){
210
214
  this.$titleBox = $titleBox
211
215
  this.$pagination = $pagination
212
216
 
213
- $pagination.children().eq(this.index+1).addClass('active').siblings().removeClass('active')
217
+ $pagination.children().eq(this.index + 1).addClass('active').siblings().removeClass('active')
214
218
 
215
219
  $t.append($pagination)
216
220
  }
@@ -223,21 +227,21 @@ Banner.prototype.createHtml = function(){
223
227
  /**
224
228
  * 由 this.options.renderCallback 生成页面
225
229
  */
226
- Banner.prototype.createCustomHtml = function() {
230
+ Banner.prototype.createCustomHtml = function () {
227
231
  const len = this.options.list.length
228
- if(len<1){
232
+ if (len < 1) {
229
233
  return
230
234
  }
231
235
  const $t = $(this.options.container)
232
236
  $t.empty();
233
237
  const signWidth = this.signWidth = $t.width()
234
-
238
+
235
239
  const allWidth = this.allWidth = signWidth * (len + 2)
236
240
  const $inner = this.$inner = $(`<div class="banner-inner-transform"></div>`)
237
241
  this.currentTranslate = -signWidth * (this.index + 1)
238
- $inner.width(allWidth).css('transform',`translateX(${this.currentTranslate}px)`)
242
+ $inner.width(allWidth).css('transform', `translateX(${this.currentTranslate}px)`)
239
243
 
240
- let $li = $(this.options.renderCallback(this.options.list[len-1], len-1));
244
+ let $li = $(this.options.renderCallback(this.options.list[len - 1], len - 1));
241
245
  $li.width(signWidth);
242
246
  $inner.append($li);
243
247
 
@@ -254,7 +258,7 @@ Banner.prototype.createCustomHtml = function() {
254
258
  $t.append($inner);
255
259
  }
256
260
 
257
- Banner.prototype.bindEvent = function(){
261
+ Banner.prototype.bindEvent = function () {
258
262
  const self = this
259
263
  const len = this.options.list.length;
260
264
  const $inner = this.$inner;
@@ -317,7 +321,7 @@ Banner.prototype.bindEvent = function(){
317
321
  } else if (newTranslate < -this.signWidth * len) {
318
322
  newTranslate += this.signWidth * len;
319
323
  }
320
-
324
+
321
325
  // 在移动到新位置前,先变换当前位置到正确区域
322
326
  await this.normalizeTranslate();
323
327
  } else {
@@ -325,7 +329,7 @@ Banner.prototype.bindEvent = function(){
325
329
  }
326
330
 
327
331
  $inner.css('transition', 'transform 0.3s ease-out')
328
- $inner.css('transform',`translateX(${this.currentTranslate}px)`);
332
+ $inner.css('transform', `translateX(${this.currentTranslate}px)`);
329
333
 
330
334
  if (newTranslate !== this.currentTranslate) {
331
335
  this.setTranslate(newTranslate);
@@ -335,11 +339,11 @@ Banner.prototype.bindEvent = function(){
335
339
  // 由于 newTranslate 包含了前后的循环占位(位置 0 和 len+1 ),需要减1后取模
336
340
  const newIndex = (Math.round(-newTranslate / this.signWidth) + len - 1) % len;
337
341
  if (this.options.isTitleEnabled) {
338
- this.$pagination.children().eq(newIndex+1).addClass('active').siblings().removeClass('active');
342
+ this.$pagination.children().eq(newIndex + 1).addClass('active').siblings().removeClass('active');
339
343
  this.$titleBox.text(this.options.list[newIndex].title);
340
344
  }
341
345
  if (this.options.pageChanged && newIndex != this.index) {
342
- this.options.pageChanged(this.$inner.children().eq(newIndex+1), this.options.list[newIndex], newIndex);
346
+ this.options.pageChanged(this.$inner.children().eq(newIndex + 1), this.options.list[newIndex], newIndex);
343
347
  }
344
348
  this.index = newIndex;
345
349
  this.startAutoPlay();
@@ -350,15 +354,15 @@ Banner.prototype.bindEvent = function(){
350
354
  if (windowEnv === 'h5') { // 浏览器环境
351
355
  $inner
352
356
  .attr('draggable', 'false')
353
- .on('pointerdown', function(e) {
357
+ .on('pointerdown', function (e) {
354
358
  pStart(e.originalEvent.clientX, () => {
355
359
  this.setPointerCapture(e.originalEvent.pointerId);
356
360
  });
357
361
  })
358
- .on('pointermove', function(e) {
362
+ .on('pointermove', function (e) {
359
363
  pMove(e.originalEvent.clientX);
360
364
  })
361
- .on('pointerup', async function(e) {
365
+ .on('pointerup', async function (e) {
362
366
  await pEnd(() => {
363
367
  this.releasePointerCapture(e.originalEvent.pointerId);
364
368
  });
@@ -380,23 +384,23 @@ Banner.prototype.bindEvent = function(){
380
384
  $inner.on('transitionend', () => {
381
385
  $inner.css('transition', 'none');
382
386
  })
383
- .on('click', function(e){
384
- if(!self.options.click){
385
- return
386
- }
387
- if (windowEnv === 'h5' && Math.abs(e.clientX - originalX) > 10) {
388
- // 防止浏览器中拖动时触发click
389
- return
390
- }
387
+ .on('click', function (e) {
388
+ if (!self.options.click) {
389
+ return
390
+ }
391
+ if (windowEnv === 'h5' && Math.abs(e.clientX - originalX) > 10) {
392
+ // 防止浏览器中拖动时触发click
393
+ return
394
+ }
391
395
 
392
- const $t = self.$inner.children().eq(self.index + 1);
393
- self.options.click($t, self.options.list[self.index], self.index);
394
- });
396
+ const $t = self.$inner.children().eq(self.index + 1);
397
+ self.options.click($t, self.options.list[self.index], self.index);
398
+ });
395
399
  }
396
400
 
397
401
  Banner.prototype.setTranslate = function (transX) {
398
402
  this.currentTranslate = transX;
399
- this.$inner.css('transform',`translateX(${this.currentTranslate}px)`);
403
+ this.$inner.css('transform', `translateX(${this.currentTranslate}px)`);
400
404
  }
401
405
 
402
406
  /**
@@ -420,7 +424,7 @@ Banner.prototype.normalizeTranslate = async function () {
420
424
  * 加载所有背景图片,将data-background-url属性设置为背景图片
421
425
  */
422
426
  Banner.prototype.loadAllBackgrounds = function () {
423
- this.$inner.children().each(function() {
427
+ this.$inner.children().each(function () {
424
428
  $(this).css('background-image', `url(${$(this).attr('data-background-url')})`)
425
429
  });
426
430
  // console.log('banner images loaded');
@@ -1,4 +1,7 @@
1
- import { $, windowEnv } from "../config";
1
+ import { getEnv } from "../config";
2
+
3
+ let $ = null;
4
+
2
5
  // 默认配置
3
6
  const DEFAULTS = {
4
7
  itemHeight: 50, // 单条数据高度
@@ -14,6 +17,8 @@ const DEFAULTS = {
14
17
  };
15
18
 
16
19
  export function List(options = {}) {
20
+ $ = getEnv().$;
21
+
17
22
  this.options = Object.assign({}, DEFAULTS, options);
18
23
  // 标记是否有更新元素用的回调函数
19
24
  this.hasUpdateItem = options.updateItem && (options.updateItem.constructor === Function) ? true : false;
@@ -1,3 +1,4 @@
1
+ import "./waterfall.scss"
1
2
  import { getEnv } from "../config.js";
2
3
  import { remToPx } from "../../utils/utils.js";
3
4
 
@@ -19,6 +20,10 @@ const DEFAULTS = {
19
20
  // 传入 $node, data, index
20
21
  updateItem: null, // 元素更新时的回调函数
21
22
  onscroll: null, // 滚动事件回调函数
23
+ shouldOccupySpace: null, // 是否是静态数据的回调函数,静态数据能够占用元素
24
+ showLoading: null, // 展示loading的回调函数 params:$node
25
+ hideLoading: null, // 隐藏loading的回调函数
26
+ createLoading: null, // 创建loading的回调函数
22
27
  };
23
28
 
24
29
  export function Waterfall(optionsInput = {}) {
@@ -30,6 +35,14 @@ export function Waterfall(optionsInput = {}) {
30
35
  // 标记是否有更新元素用的回调函数
31
36
  this.hasUpdateItem = options.updateItem && (options.updateItem.constructor === Function) ? true : false;
32
37
 
38
+ // 新方案:数据ID映射机制
39
+ this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
40
+ this.nextDataId = 0; // 下一个数据ID
41
+ this.renderedDataIds = new Set(); // 已渲染的数据ID集合
42
+
43
+ this.$loadingNode = null;
44
+
45
+
33
46
  // 间隔字符串转数字
34
47
  if (options.columnGap.constructor === String) {
35
48
  // 如果是rem单位,则需要计算
@@ -85,13 +98,29 @@ export function Waterfall(optionsInput = {}) {
85
98
  this.columnItems.push(this.createColumn(i));
86
99
  }
87
100
 
88
- this.renderIndex = 0; // 渲染索引
89
- this.activeNodes = new Map(); // 当前活跃节点(索引 -> DOM)
101
+ this.renderIndex = 0; // 渲染索引(保留兼容性)
102
+ this.activeNodes = new Map(); // 当前活跃节点(数据ID -> DOM)
90
103
  this.nodePool = []; // DOM 节点池
91
104
 
105
+ // 新方案:初始化数据ID映射
106
+ this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
107
+ this.nextDataId = 0; // 下一个数据ID
108
+ this.renderedDataIds = new Set(); // 已渲染的数据ID集合
109
+
110
+ // 为初始数据分配数据ID
111
+ this.options.data.forEach((item, index) => {
112
+ const dataId = index; //this.nextDataId++;
113
+ this.dataIdMap.set(dataId, {
114
+ data: true,//item,
115
+ originalIndex: index,
116
+ layoutInfo: null // 将在布局时填充
117
+ });
118
+ });
119
+
92
120
  this.init();
93
121
  }
94
122
 
123
+
95
124
  Waterfall.prototype.init = function () {
96
125
  const self = this;
97
126
  const options = this.options;
@@ -99,8 +128,8 @@ Waterfall.prototype.init = function () {
99
128
 
100
129
  this.nodePool = []; // DOM 节点池
101
130
  this.activeNodes = new Map(); // 当前活跃节点(索引 -> DOM)
102
-
103
-
131
+ this.allReadyNodes = new Map(); // 所有节点(索引 -> DOM)
132
+ this.renderIndex = 0; // 渲染索引(保留兼容性)
104
133
 
105
134
  $container.html(`
106
135
  <div class="waterfall-list-scroll" style="">
@@ -108,6 +137,18 @@ Waterfall.prototype.init = function () {
108
137
  </div>
109
138
  `);
110
139
 
140
+
141
+
142
+ // 如果有定义loading函数 那么创建一个loading节点元素
143
+ if (options.createLoading) {
144
+ this.$loadingNode = $(
145
+ `<div class="waterfall-loading" style="transform: translate(0px, -99999px)"></div>`
146
+ );
147
+ $container.find('.waterfall-list-viewport').append(this.$loadingNode);
148
+
149
+ options.createLoading(this.$loadingNode);
150
+ }
151
+
111
152
  // 绑定滚动事件(节流处理)
112
153
  $container.off().on('scroll', function () {
113
154
  self.scrollTop = $(this).scrollTop();
@@ -134,8 +175,9 @@ Waterfall.prototype.updateVisibleItems = function (force = false) {
134
175
  const options = this.options;
135
176
  const $container = $(options.container);
136
177
 
137
- const startTop = this.scrollTop;
178
+ const startTop = self.scrollTop; // 当前滚动位置
138
179
  const endTop = startTop + $container.height();
180
+ // console.log('startTop', startTop)
139
181
 
140
182
  // 进行可见区域的渲染更新
141
183
  this.updateCardsInView({
@@ -143,23 +185,44 @@ Waterfall.prototype.updateVisibleItems = function (force = false) {
143
185
  end: endTop,
144
186
  force
145
187
  });
188
+
146
189
  }
147
190
 
148
191
  // 新增卡片
149
- Waterfall.prototype.appendCard = function (data, index, { top, left }) {
192
+ Waterfall.prototype.appendCard = function (data, dataId, { top, left }) {
150
193
  const self = this;
151
194
  const options = this.options;
152
195
  const $container = $(options.container);
153
196
  const $viewport = $container.find('.waterfall-list-viewport');
197
+
198
+ // 新方案:基于数据ID的数据验证
199
+ if (!this.dataIdMap.has(dataId)) {
200
+ console.error('Waterfall: Invalid dataId in appendCard', dataId);
201
+ return null;
202
+ }
203
+
204
+ const dataInfo = this.dataIdMap.get(dataId);
205
+ if (!dataInfo || !dataInfo.data) {
206
+ console.warn('Waterfall: Empty data for dataId', dataId);
207
+ }
208
+
209
+
154
210
  const $card = $(
155
211
  `<div class="waterfall-item"
156
- data-index="${index}"
157
- style="position: absolute;transform:translate(${left}px,${top}px); width:${this.columnWidth}px;"
212
+ data-index="${dataId}"
213
+ style="position: absolute;transform:translate(${left}px,${top}px);"
158
214
  >
159
- ${options.renderItem(data, index)}
215
+ ${options.renderItem(data, dataInfo.originalIndex)}
160
216
  </div> `
161
217
  );
162
218
 
219
+ this.renderedDataIds.add(dataId);
220
+
221
+ if (options.columns !== 1) {
222
+ $card.width(this.columnWidth + 'px');
223
+ }
224
+
225
+
163
226
  $viewport.append($card);
164
227
  return $card;
165
228
  }
@@ -174,49 +237,105 @@ Waterfall.prototype.updateCardsInView = function ({ start, end, force = false })
174
237
  this.createCards({ end: endBuffer });
175
238
  }
176
239
 
177
- // 基于已有的列信息,进行高度可视区域下的判定 操作已有信息。 如果是新建信息 则依赖上方的createCards方法进行创建,那么在上方会异步创建新卡片。
178
- // 阶段1:复用已有节点
240
+ const startNum = start - options.bufferHeight;
241
+ const endNum = end + options.bufferHeight;
242
+ // 新方案:基于数据ID映射机制
179
243
  const newActiveNodes = new Map();
180
244
  for (let i = 0; i < this.columns; i++) {
181
245
  const column = this.columnItems[i];
182
246
 
183
247
  for (let j = 0; j < column.children.length; j++) {
184
248
  const row = column.children[j];
185
- const renderIndex = row.renderIndex;
249
+ const dataId = row.dataId; // 使用dataId替代renderIndex
250
+
251
+ // 验证数据ID有效性
252
+ if (!this.dataIdMap.has(dataId)) {
253
+ console.warn('Waterfall: Invalid dataId detected', dataId);
254
+ continue;
255
+ }
256
+
257
+ const dataInfo = this.dataIdMap.get(dataId);
258
+
259
+ if (!dataInfo) {
260
+ console.warn('Waterfall: Invalid data for dataId', dataId);
261
+ continue;
262
+ }
263
+
264
+ const data = options.data[dataId]
265
+
266
+ // 如果当前这个节点是特殊节点 不用做处理
267
+ // 如果是特殊的静态占用元素卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
268
+ let specialNode = false;
269
+ if (options.shouldOccupySpace) {
270
+ specialNode = options.shouldOccupySpace(data) || false;
271
+ }
272
+ if (specialNode) {
273
+ continue;
274
+ }
275
+
186
276
  // 在可视区域内 进行有关卡片的操作
187
- if (row.top <= end && row.bottom >= start) {
277
+ const bool = row.top <= endNum && row.bottom >= startNum;
278
+ if (bool) {
188
279
  // 理论上什么都不动,因为卡片的位置不会变
189
- const $card = this.activeNodes.get(renderIndex);
280
+ const $card = this.activeNodes.get(dataId);
190
281
 
191
282
  let $node = null;
192
- // 如果卡片已经在DOM中,则不用更新位置
283
+
284
+ // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
285
+ let bool = true;
193
286
  if ($card) {
287
+ bool = hasNodeInActives(newActiveNodes, $card)
288
+ }
289
+ // 如果卡片已经在DOM中,则不用更新位置
290
+ if (bool === false) {
194
291
  $node = $card;
195
- this.activeNodes.delete(renderIndex);
292
+ this.activeNodes.delete(dataId);
293
+ // 如果是强更,这里才会采取更新
196
294
  if (force) {
197
- this.updateRenderUI($node, options.data[renderIndex], renderIndex);
295
+ this.updateRenderUI($node, data, dataId);
198
296
  }
297
+
199
298
  } else {
200
- // 卡片不在DOM中,则更新位置
201
- let nodePool = this.nodePool;
202
- if (nodePool.length === 0) {
203
- // 这里是往上方拖动时,可能需要补建的情况
204
- // 如果池子没有节点,那么进行创建
205
- $node = this.appendCard(options.data[renderIndex], renderIndex, {
206
- top: row.top, left: row.left
207
- })
208
- row.$node = $node;
299
+ const $card = this.allReadyNodes.get(dataId);
300
+ // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
301
+ let bool = true;
302
+ if ($card) {
303
+ bool = hasNodeInActives(newActiveNodes, $card)
304
+ }
305
+ if (bool === false) {
306
+ // 如果成功获取到card并没有占用 那么就复用这个card
307
+ $node = $card;
308
+
309
+ this.updateRenderUI($node, data, dataId);
310
+
209
311
  } else {
210
- $node = $(this.nodePool.pop());
211
- $node.css({
212
- 'transform': `translate(${row.left}px,${row.top}px)`,
213
- }).attr('data-index', renderIndex);
214
- this.updateRenderUI($node, options.data[renderIndex], renderIndex);
215
- row.$node = $node;
312
+ // 卡片不在DOM中,则更新位置
313
+ $node = getNodePoolPop(this.nodePool, newActiveNodes);
314
+ if ($node === null) {
315
+ // 这里是往上方拖动时,可能需要补建的情况
316
+ $node = this.appendCard(data, dataId, {
317
+ top: row.top, left: row.left
318
+ });
319
+ row.$node = $node;
320
+ } else {
321
+ this.updateRenderUI($node, data, dataId);
322
+ row.$node = $node;
323
+ }
216
324
  }
217
325
  }
326
+ $node.css({
327
+ 'transform': `translate(${row.left}px,${row.top}px)`,
328
+ }).attr('data-index', dataId);
329
+
330
+ newActiveNodes.set(dataId, $node);
331
+
332
+ // 清除掉在NodePool中的card
333
+ const index = this.nodePool.indexOf($card);
334
+ if (index !== -1) {
335
+ this.nodePool.splice(index, 1);
336
+ }
218
337
 
219
- newActiveNodes.set(renderIndex, $node);
338
+ // console.log('302-row', row.dataId, dataInfo);
220
339
  }
221
340
  }
222
341
  }
@@ -224,11 +343,16 @@ Waterfall.prototype.updateCardsInView = function ({ start, end, force = false })
224
343
  // 阶段2:处理不活跃节点
225
344
  this.activeNodes.forEach($node => {
226
345
  $node.css('transform', `translateY(-9999px)`);// 移出可视区域
227
- this.nodePool.push($node);
346
+ if (this.nodePool.indexOf($node) === -1) {
347
+ this.nodePool.push($node);
348
+ }
228
349
  });
229
350
  this.activeNodes = newActiveNodes;
351
+ // console.log('this.activeNodes', this.activeNodes);
352
+ // console.log('this.nodePool', this.nodePool);
230
353
  }
231
354
 
355
+
232
356
  Waterfall.prototype.getMaxHeight = function () {
233
357
  let maxHeight = 0;
234
358
  for (let i = 0; i < this.columns; i++) {
@@ -276,79 +400,126 @@ Waterfall.prototype.getMinHeightColumn = function () {
276
400
  }
277
401
 
278
402
  // 创建卡片
279
- Waterfall.prototype.createCards = function ({ end }) {
403
+ Waterfall.prototype.createCards = function ({ end, dataId = -1 }) {
280
404
  const self = this;
281
405
  const options = this.options;
282
406
  const $container = $(options.container);
283
- const renderIndex = this.renderIndex;
284
407
 
285
- if (renderIndex >= options.data.length) {
408
+ // 新方案:获取下一个未渲染的数据ID
409
+ let nextDataId = null;
410
+ for (let [dataId, dataInfo] of this.dataIdMap) {
411
+ if (!this.renderedDataIds.has(dataId)) {
412
+ nextDataId = dataId;
413
+ break;
414
+ }
415
+ }
416
+
417
+
418
+ // 如果没有更多数据需要渲染
419
+ if (nextDataId === null) {
286
420
  const maxHeight = this.getMaxHeight();
287
- // 设置一次整体高度
288
- $(options.container).find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom)
421
+ $container.find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom + 'px');
422
+ return;
423
+ }
424
+
425
+ const dataInfo = this.dataIdMap.get(nextDataId);
426
+ if (!dataInfo || !dataInfo.data) {
427
+ console.warn('Waterfall: Invalid data for dataId', nextDataId);
289
428
  return;
290
429
  }
291
430
 
431
+ if (this.renderIndex >= options.data.length) {
432
+ const maxHeight = this.getMaxHeight();
433
+ $container.find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom + 'px');
434
+ return
435
+ }
436
+
437
+ const data = options.data[nextDataId];
438
+
292
439
  let column = this.getMinHeightColumn();
293
440
  if (column === null) {
294
- // 没有可用的列,则在第一列创建,说明此时还没有数据
295
441
  column = this.columnItems[0];
296
442
  }
297
443
 
298
444
  const top = column.bottom === 0 ? options.marginTop : (column.bottom + options.rowGap);
445
+ const position = { top, left: column.left };
446
+ const row = createDefaultRow(position);
299
447
 
300
- // 设置卡片位置
301
- const position = {
302
- top,
303
- left: column.left,
304
- }
448
+ this.renderIndex += 1;
305
449
 
306
- const row = createDefaultRow(position);
450
+ let specialNode = false;
307
451
 
452
+ // 如果是特殊的卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
453
+ if (options.shouldOccupySpace) {
454
+ specialNode = options.shouldOccupySpace(data) || false;
455
+ }
308
456
 
309
- // 添加卡片
457
+ // 添加卡片,使用dataId作为唯一标识
310
458
  let $card = null;
311
- if (this.nodePool.length === 0) {
312
- $card = this.appendCard(options.data[renderIndex], renderIndex, position);
459
+ if (this.nodePool.length === 0 || specialNode === true) {
460
+ $card = this.appendCard(data, nextDataId, position);
313
461
  } else {
314
- $card = $(this.nodePool.pop());
315
- $card.css({
316
- 'transform': `translate(${row.left}px,${row.top}px)`,
317
- }).attr('data-index', renderIndex);
318
- this.updateRenderUI($card, options.data[renderIndex], renderIndex);
319
- }
462
+ const $tmp = getNodePoolPop(this.nodePool, this.activeNodes);
463
+ if ($tmp) {
464
+ $card = $tmp;
465
+ $card.css({
466
+ 'transform': `translate(${row.left}px,${row.top}px)`,
467
+ }).attr('data-index', nextDataId);
468
+ this.updateRenderUI($card, data, nextDataId);
469
+ } else {
470
+ $card = this.appendCard(data, nextDataId, position);
471
+ }
320
472
 
473
+ }
321
474
 
322
475
  row.$node = $card;
476
+ row.dataId = nextDataId; // 使用dataId替代renderIndex
477
+ if (dataId !== -1) {
478
+ row.dataId = dataId;
479
+ }
480
+
481
+ // 记录布局信息
482
+ // dataInfo.layoutInfo = {
483
+ // //columnIndex: this.columnItems.indexOf(column),
484
+ // //position: position,
485
+ // // row: row
486
+ // };
487
+
323
488
 
324
- // 把新增的卡片放进 activeNodes, 当前是 展示状态的
325
- this.activeNodes.set(renderIndex, $card);
326
489
 
327
490
 
491
+ if (specialNode === false) {
492
+ // 把新增的卡片放进 activeNodes 当成活跃节点元素,那么是 可以动态使用的
493
+ this.activeNodes.set(nextDataId, $card);
494
+
495
+ this.allReadyNodes.set(nextDataId, $card);
496
+ } else {
497
+ // 如果是特殊的,这里不要记录了
498
+ this.allReadyNodes.set(nextDataId, null);
499
+ }
500
+
501
+ this.renderedDataIds.add(nextDataId);
502
+
328
503
  // 更新列的底部距离
329
504
  column.bottom = top + $card.height();
330
505
  column.children.push(row);
331
-
332
- // 计算当前卡片的位置
333
506
  row.bottom = column.bottom;
334
- row.renderIndex = renderIndex;
335
-
336
- this.renderIndex += 1;
337
-
338
- let hasNextData = this.renderIndex < options.data.length;
339
507
 
508
+ // 检查是否需要继续创建卡片
340
509
  const minHeight = this.getMinHeight();
341
- if (hasNextData && (minHeight < end)) {
510
+ const hasMoreData = this.renderedDataIds.size < this.dataIdMap.size;
511
+
512
+ if (hasMoreData && (minHeight < end)) {
342
513
  window.requestAnimationFrame(() => {
343
514
  this.createCards({ end });
344
515
  });
345
516
  } else {
346
517
  const maxHeight = this.getMaxHeight();
347
- // 设置一次整体高度
348
- $(options.container).find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom)
518
+ $(options.container).find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom + 'px');
349
519
  }
350
520
  }
351
521
 
522
+
352
523
  Waterfall.prototype.createColumn = function (index) {
353
524
  const res = {
354
525
  left: 0,
@@ -365,18 +536,205 @@ Waterfall.prototype.createColumn = function (index) {
365
536
  return res;
366
537
  }
367
538
 
368
- Waterfall.prototype.updateRenderUI = function ($node, data, index) {
539
+ Waterfall.prototype.updateRenderUI = function ($node, data, dataId) {
369
540
  const options = this.options;
541
+
542
+ // 新方案:基于数据ID的数据验证
543
+ if (!this.dataIdMap.has(dataId)) {
544
+ console.error('Waterfall: Invalid dataId in updateRenderUI', dataId);
545
+ return;
546
+ }
547
+
548
+ const dataInfo = this.dataIdMap.get(dataId);
549
+ if (!dataInfo || !dataInfo.data) {
550
+ console.warn('Waterfall: Empty data for dataId', dataId);
551
+ }
552
+
370
553
  if (this.hasUpdateItem === true) {
371
- options.updateItem($node, data, index)
554
+ options.updateItem($node, data, dataInfo.originalIndex)
372
555
  } else {
373
- $node.html(options.renderItem(data, index));
556
+ $node.html(options.renderItem(data, dataInfo.originalIndex));
374
557
  }
375
558
  }
376
559
 
560
+
561
+
377
562
  Waterfall.prototype.updateData = function (newData) {
378
- this.options.data = newData;
563
+ const options = this.options;
564
+ options.data = newData;
565
+
566
+ // 新方案:重新建立数据ID映射
567
+ //this.dataIdMap.clear();
568
+ //this.renderedDataIds.clear();
569
+ //this.nextDataId = 0;
570
+
571
+ // 为每个数据项分配唯一ID
572
+ options.data.forEach((item, index) => {
573
+ const dataId = index; // this.nextDataId++;
574
+ if (!this.allReadyNodes.has(dataId)) {
575
+ this.dataIdMap.set(dataId, {
576
+ data: true,// item,
577
+ originalIndex: index,
578
+ layoutInfo: null // 将在布局时填充
579
+ });
580
+ // 如果没有准备好这个数据,这里要创建一个占位节点
581
+ this.createCards({ end: 0, dataId });
582
+ }
583
+ });
584
+
379
585
  this.updateVisibleItems(true); // 强制更新渲染
586
+
587
+ // 重新计算所有卡片位置并更新位置
588
+ // this.updatePointCards();
589
+ }
590
+
591
+ // 某个数据进行了UI变更,触发高度重新绘制
592
+ Waterfall.prototype.updateCard = function (data) {
593
+ const options = this.options;
594
+ let dataId = -1;
595
+ if (typeof data === 'number') {
596
+ dataId = data;
597
+ } else {
598
+ for (let i = 0; i < options.data.length; i++) {
599
+ if (options.data[i] === data) {
600
+ dataId = i;
601
+ break;
602
+ }
603
+ }
604
+ }
605
+ if (dataId === -1) {
606
+ // 没有匹配到数据,进行退出
607
+ console.log('Waterfall: Invalid data in options.data');
608
+ return
609
+ }
610
+
611
+ // 从activeNodes中获取该数据的节点
612
+ if (!this.activeNodes.has(dataId)) {
613
+ console.log('Waterfall: Invalid dataId in activeNodes');
614
+ return
615
+ }
616
+
617
+ const $node = this.activeNodes.get(dataId);
618
+ const height = $node.height();
619
+
620
+ // 重新计算该数据所在列的卡片位置
621
+ let needUpdate = false
622
+ const columnItems = this.columnItems;
623
+ for (let i = 0; i < columnItems.length; i++) {
624
+ const column = columnItems[i];
625
+ let bool = false
626
+ let minus = 0
627
+ // 这里为了简化,各列的瀑布流保持不动,只更新各列下节点的top位置,不做节点的跨列位移
628
+ for (let j = 0; j < column.children.length; j++) {
629
+ const row = column.children[j];
630
+ if (row.dataId === dataId) {
631
+ bool = true
632
+ const oldHeight = row.bottom - row.top;
633
+
634
+ minus = height - oldHeight;
635
+
636
+ if (minus === 0) {
637
+ // 找到了原节点数据,对比后没有变更 那么直接退出
638
+ return;
639
+ }
640
+ needUpdate = true
641
+ row.bottom += minus;
642
+ } else if (minus !== 0) {
643
+ row.top += minus;
644
+ row.bottom += minus;
645
+ }
646
+ }
647
+ if (bool) {
648
+ column.bottom += minus;
649
+ break;
650
+ }
651
+ }
652
+ if (needUpdate) {
653
+ this.updateVisibleItems(true); // 强制更新渲染
654
+ }
655
+ }
656
+
657
+ // 重新计算设置一遍所有的卡片位置
658
+ Waterfall.prototype.updatePointCards = function () {
659
+ // 有问题 不要用
660
+ return
661
+ const self = this;
662
+ const options = this.options;
663
+ const columnItems = this.columnItems;
664
+ let top = options.marginTop;
665
+ for (let i = 0; i < columnItems.length; i++) {
666
+ const column = columnItems[i];
667
+ // 这里为了简化,各列的瀑布流保持不动,只更新各列下节点的top位置,不做节点的跨列位移
668
+ for (let j = 0; j < column.children.length; j++) {
669
+ const row = column.children[j];
670
+ const $card = row.$node;
671
+
672
+ // 验证数据ID有效性
673
+ if (!this.dataIdMap.has(row.dataId)) {
674
+ console.warn('Waterfall: Invalid dataId in updatePointCards', row.dataId);
675
+ continue;
676
+ }
677
+
678
+ // 第一个的top不需要更新
679
+ if (j === 0) {
680
+ row.bottom = top + $card.height();
681
+ } else {
682
+ row.top = column.children[j - 1].bottom + options.rowGap;
683
+ row.bottom = row.top + $card.height();
684
+ // 更新卡片位置
685
+ // $card.css({
686
+ // 'transform': `translate(${row.left}px,${row.top}px)`,
687
+ // })
688
+ }
689
+
690
+ }
691
+ // 设置一次该列的bottom
692
+ column.bottom = getBottomByColumn(column);
693
+ }
694
+ }
695
+
696
+ // 展示loading的回调函数
697
+ Waterfall.prototype.showLoading = function (callback = null) {
698
+ const options = this.options;
699
+ const $container = $(options.container);
700
+ let $node = null
701
+ if (this.$loadingNode) {
702
+ let loadingTop = this.getMaxHeight() + options.rowGap
703
+ let h = loadingTop + this.$loadingNode.height() + options.marginBottom
704
+
705
+ this.$loadingNode.css('transform', `translate(0px,${loadingTop}px)`);
706
+ $(options.container).find('.waterfall-list-scroll').css('height', h + 'px');
707
+
708
+ $node = this.$loadingNode
709
+ }
710
+
711
+ if (callback) callback($node)
712
+ }
713
+
714
+ // 隐藏loading的回调函数
715
+ Waterfall.prototype.hideLoading = function (callback = null) {
716
+ const options = this.options;
717
+ const $container = $(options.container);
718
+ let $node = null
719
+ if (this.$loadingNode) {
720
+ let h1 = this.getMaxHeight() + options.marginBottom
721
+ this.$loadingNode.css('transform', `translate(0px,-99999px)`);
722
+ //如果要设置高度,那么这里判断一下当前是否正在做updata 一般这里被调用时,数据已经读到,在updata的同一时间调用了该函数
723
+ // 如果两个时刻高度是一致的 那么数据就是一致的 这里重新设置回来高度即可
724
+ window.requestAnimationFrame(() => {
725
+ let h2 = this.getMaxHeight() + options.marginBottom
726
+ if (h1 === h2) {
727
+ const $scroll = $(options.container).find('.waterfall-list-scroll')
728
+ const h = $scroll.height()
729
+ if (h !== h1) {
730
+ $scroll.css('height', h1 + 'px');
731
+ }
732
+ }
733
+ })
734
+ $node = this.$loadingNode
735
+ }
736
+
737
+ if (callback) callback($node)
380
738
  }
381
739
 
382
740
  function createDefaultRow({ top, left }) {
@@ -385,13 +743,37 @@ function createDefaultRow({ top, left }) {
385
743
  left,
386
744
  bottom: 0,
387
745
  $node: null,
388
- renderIndex: -1
746
+ dataId: -1 // 新方案:使用dataId替代renderIndex
389
747
  }
390
748
  }
391
749
 
392
- function getBottomByColumn(rows) {
393
- if (rows.children.length === 0) {
750
+
751
+ function getBottomByColumn(column) {
752
+ if (column.children.length === 0) {
394
753
  return 0;
395
754
  }
396
- return rows[rows.length - 1].bottom;
755
+ const child = column.children;
756
+ return child[child.length - 1].bottom;
757
+ }
758
+
759
+ function hasNodeInActives(mapObj, $node) {
760
+ let bool = false
761
+ mapObj.forEach((item) => {
762
+ if (item === $node) {
763
+ bool = true;
764
+ }
765
+ });
766
+ return bool;
767
+ }
768
+
769
+ // 从众多jq对象数组取得一个不重复的
770
+ function getNodePoolPop(nodePool, actives) {
771
+ for (let i = 0; i < nodePool.length; i++) {
772
+ const $node = nodePool[i];
773
+ if (!hasNodeInActives(actives, $node)) {
774
+ nodePool.splice(i, 1);
775
+ return $node;
776
+ }
777
+ }
778
+ return null;
397
779
  }
@@ -0,0 +1,17 @@
1
+ .waterfall-list-scroll {
2
+ height: 100%;
3
+ .waterfall-loading {
4
+ position: absolute;
5
+ width: 100%;
6
+ align-items: center;
7
+ justify-content: center;
8
+ }
9
+ .waterfall-item {
10
+ flex: 1;
11
+ flex-shrink: 1;
12
+ flex-grow: 1;
13
+ flex-basis: 0;
14
+ width: 100%;
15
+ position: absolute;
16
+ }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tencent.jquery.pix.component",
3
- "version": "1.0.63",
3
+ "version": "1.0.64",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [