tencent.jquery.pix.component 1.0.91-beta1 → 1.0.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/change.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # 更新日志
2
2
 
3
+ ### 1.0.91-beta2
4
+
5
+ - list组件优化滚动事件处理和DOM引用缓存,提升性能
6
+
3
7
  ### 1.0.91-beta1
4
8
 
5
9
  - 瀑布流组件V2版本的滑动流畅度升级
@@ -55,13 +55,25 @@ List.prototype.init = function () {
55
55
  </div>
56
56
  `);
57
57
 
58
- // 绑定滚动事件(节流处理)
58
+ // 【优化】缓存 DOM 引用,避免每次滚动都执行 jQuery 查询
59
+ this.$container = $container;
60
+ this.$scrollWrapper = $container.find('.virtual-list-scroll');
61
+ this.$viewport = $container.find('.virtual-list-viewport');
62
+ // 【优化】缓存容器高度,固定高度容器无需每次重新计算(避免触发 reflow)
63
+ this._viewportHeight = $container.height();
64
+
65
+ // 【优化】绑定滚动事件(真正的 rAF 节流:一帧内只执行一次 updateVisibleItems)
66
+ this._rafPending = false;
59
67
  $container.off().on('scroll', function () {
60
68
  self.scrollTop = $(this).scrollTop();
61
69
 
62
- window.requestAnimationFrame(() => {
63
- self.updateVisibleItems();
64
- });
70
+ if (!self._rafPending) {
71
+ self._rafPending = true;
72
+ window.requestAnimationFrame(() => {
73
+ self._rafPending = false;
74
+ self.updateVisibleItems();
75
+ });
76
+ }
65
77
 
66
78
  if (options.onscroll && options.onscroll.constructor === Function) {
67
79
  options.onscroll(this, self.scrollTop);
@@ -78,12 +90,12 @@ List.prototype.init = function () {
78
90
  List.prototype.updateVisibleItems = function (force = false) {
79
91
  const self = this;
80
92
  const options = this.options;
81
- const $container = $(options.container);
82
93
 
83
94
  let startIndex = 0; // 起始索引
84
95
  let endIndex = 0; // 结束索引
85
96
 
86
- const viewportHeight = $container.height();
97
+ // 【优化】使用缓存的容器高度,避免每次滚动触发布局计算
98
+ const viewportHeight = this._viewportHeight;
87
99
  // 计算当前可见项范围(含缓冲区)
88
100
  startIndex = Math.max(0, Math.floor(self.scrollTop / options.itemHeight) - options.buffer);
89
101
  endIndex = Math.min(
@@ -91,8 +103,8 @@ List.prototype.updateVisibleItems = function (force = false) {
91
103
  Math.ceil((self.scrollTop + viewportHeight) / options.itemHeight) + options.buffer
92
104
  );
93
105
 
94
- // 生成当前可见项的 DOM
95
- const $viewport = $container.find('.virtual-list-viewport');
106
+ // 【优化】使用缓存的 $viewport 引用
107
+ const $viewport = this.$viewport;
96
108
 
97
109
  /**方案1 */
98
110
  const newActiveNodes = new Map();
@@ -128,7 +140,8 @@ List.prototype.updateVisibleItems = function (force = false) {
128
140
  );
129
141
  $viewport.append($node);
130
142
  } else {
131
- $node = $(this.nodePool.pop())
143
+ // 【优化】nodePool 中已经是 jQuery 对象,无需再次 $() 包装
144
+ $node = this.nodePool.pop();
132
145
  $node.css('transform', `translateY(${top}px)`).attr('data-index', i);
133
146
  self.updateRenderUI($node, options.data[i], i);
134
147
  }
@@ -142,67 +155,15 @@ List.prototype.updateVisibleItems = function (force = false) {
142
155
  this.nodePool.push($node);
143
156
  });
144
157
  this.activeNodes = newActiveNodes;
145
-
146
- /**方案2 */
147
- // const $existingChildren = $viewport.children(); // 获取现有 DOM 元素集合
148
- // const usedIndices = new Set(); // 记录当前需要保留的索引
149
-
150
- // // 步骤 1:更新或保留现有元素
151
- // if ($existingChildren.length > 0) {
152
- // for (let j = 0; j < $existingChildren.length; j++) {
153
- // const $el = $($existingChildren[j]);
154
- // const oldIndex = parseInt($el.data('data-index'));
155
- // if (oldIndex >= startIndex && oldIndex <= endIndex) {
156
- // // 仍在可视范围内 → 更新位置和内容
157
- // const newTop = oldIndex * options.itemHeight;
158
- // $el.css('top', `${newTop}px`);
159
- // usedIndices.add(oldIndex);
160
- // } else {
161
- // // 移出可视范围 → 从 DOM 中删除
162
- // $el.remove();
163
- // }
164
- // }
165
- // }
166
-
167
- // // 步骤 2:补充新增元素
168
- // for (let i = startIndex; i <= endIndex; i++) {
169
- // if (!usedIndices.has(i)) {
170
- // // 需要新增的元素
171
- // const top = i * options.itemHeight;
172
- // const $element = $(
173
- // `<div class="virtual-item"
174
- // style="position: absolute; top: ${top}px; height: ${options.itemHeight}px"
175
- // data-index="${i}">
176
- // ${options.renderItem(options.data[i], i)}
177
- // </div>`
178
- // );
179
- // // 插入到正确位置(按索引顺序)
180
- // let inserted = false;
181
- // for (let j = 0; j < $existingChildren.length; j++) {
182
- // const $el = $($existingChildren[j]);
183
- // const currentIndex = parseInt($el.data('data-index'));
184
- // if (currentIndex > i) {
185
- // $element.insertBefore($el);
186
- // inserted = true;
187
- // break;
188
- // }
189
- // }
190
-
191
- // if (!inserted) {
192
- // $viewport.append($element);
193
- // }
194
- // }
195
- // }
196
-
197
158
  }
198
159
 
199
160
  // 外部更新数据方法
200
161
  List.prototype.updateData = function (newData) {
201
162
  const options = this.options;
202
163
  options.data = newData;
203
- const totalHeight = options.data.length * options.itemHeight
204
- // 重新计算高度
205
- $(this.options.container).children().height(`${totalHeight}px`);
164
+ const totalHeight = options.data.length * options.itemHeight;
165
+ // 【优化】使用缓存的 DOM 引用,避免重新创建 jQuery 对象
166
+ this.$scrollWrapper.height(`${totalHeight}px`);
206
167
  this.updateVisibleItems(true); // 强制更新渲染
207
168
  };
208
169
 
@@ -0,0 +1,706 @@
1
+ import "./swiper.scss";
2
+ import { windowEnv, getEnv } from "../config";
3
+ import { addResizeFunc, nextAnimationFrame, removeResizeFunc } from "../utils/utils";
4
+
5
+ let $ = null;
6
+
7
+ /**
8
+ * Swiper 平滑分页滑动组件
9
+ * @constructor
10
+ * @param {object} options 选项
11
+ * @param {string} options.container 容器元素的jquery选择器
12
+ * @param {string} [options.direction='horizontal'] 滑动方向,'horizontal' | 'vertical'
13
+ * @param {number} [options.initialSlide=0] 初始页码索引
14
+ * @param {number} [options.speed=300] 切换动画时长(ms)
15
+ * @param {boolean} [options.loop=false] 是否开启循环滑动
16
+ * @param {boolean} [options.autoplay=false] 是否自动轮播
17
+ * @param {number} [options.autoplayDelay=3000] 自动轮播间隔(ms)
18
+ * @param {boolean} [options.pagination=true] 是否显示分页指示器
19
+ * @param {number} [options.threshold=0.2] 滑动翻页阈值比例(拖动距离/容器尺寸)
20
+ * @param {boolean} [options.autoResize=true] 是否自动适应容器尺寸变化
21
+ * @param {function} [options.renderSlide] 自定义slide渲染回调,参数(slideData, index),返回HTML字符串、DOM元素或jq对象
22
+ * @param {Array} [options.slides=[]] slide数据数组,支持:数据对象数组(配合renderSlide使用)、HTML字符串数组、jQuery/DOM对象数组
23
+ * @param {boolean} [options.useContainerChildren=false] 是否使用容器内已有的子元素作为slides(静态HTML模式)
24
+ * @param {function} [options.onSlideChange] 翻页后回调,参数(currentIndex, prevIndex)
25
+ * @param {function} [options.onSlideChangeStart] 翻页动画开始时回调,参数(currentIndex, prevIndex)
26
+ * @param {function} [options.onClick] 点击slide回调,参数($slide, slideData, index)
27
+ */
28
+ export function Swiper(options = {}) {
29
+ $ = getEnv().$;
30
+
31
+ this.options = Object.assign({
32
+ direction: 'horizontal',
33
+ initialSlide: 0,
34
+ speed: 300,
35
+ loop: false,
36
+ autoplay: false,
37
+ autoplayDelay: 3000,
38
+ pagination: true,
39
+ threshold: 0.2,
40
+ autoResize: true,
41
+ slides: [],
42
+ useContainerChildren: false,
43
+ renderSlide: null,
44
+ onSlideChange: null,
45
+ onSlideChangeStart: null,
46
+ onClick: null,
47
+ }, options);
48
+
49
+ this.index = this.options.initialSlide;
50
+ this.isAnimating = false;
51
+ this.autoplayTimer = null;
52
+ this.$container = null;
53
+ this.$wrapper = null;
54
+ this.$pagination = null;
55
+ this.slideSize = 0; // width(horizontal) or height(vertical)
56
+ this.totalSlides = 0;
57
+
58
+ this.init();
59
+ }
60
+
61
+ /**
62
+ * 初始化组件
63
+ */
64
+ Swiper.prototype.init = function () {
65
+ this.$container = $(this.options.container);
66
+ this.$container.css('touch-action', 'none');
67
+
68
+ // 静态DOM模式:使用wrap方式在已有子元素上层包裹结构
69
+ if (this.options.useContainerChildren && this.options.slides.length === 0) {
70
+ this._initFromContainerChildren();
71
+ } else {
72
+ this.totalSlides = this.options.slides.length;
73
+ this.createHtml();
74
+ }
75
+
76
+ this.updateSize();
77
+ this.bindEvent();
78
+
79
+ if (this.options.autoResize) {
80
+ addResizeFunc({
81
+ obj: this,
82
+ func: this.updateSize,
83
+ });
84
+ }
85
+
86
+ if (this.options.autoplay && this.totalSlides > 1) {
87
+ this.startAutoplay();
88
+ }
89
+ };
90
+
91
+ /**
92
+ * 静态DOM模式初始化:通过wrap在已有子元素上层包裹swiper结构
93
+ * 不使用empty,保留原有DOM节点不被销毁
94
+ * @private
95
+ */
96
+ Swiper.prototype._initFromContainerChildren = function () {
97
+ const dir = this.options.direction;
98
+
99
+ // 确保每个子元素都有 swiper-slide 类
100
+ this.$container.children().each(function (index,dom) {
101
+ console.log(dom)
102
+ const $dom = $(dom);
103
+ if (!$dom.hasClass('swiper-slide')) {
104
+ $dom.addClass('swiper-slide');
105
+ }
106
+ });
107
+
108
+ // 统计slide数量
109
+ this.totalSlides = this.$container.children().length;
110
+
111
+ // 用 wrapAll 思路:创建 wrapper 和 container,将子元素整体包裹
112
+ // 1. 创建 wrapper,把所有子元素移入其中
113
+ const $wrapper = this.$wrapper = $(`<div class="swiper-wrapper ${dir}"></div>`);
114
+
115
+ // 将容器的子元素逐个 append 到 wrapper(append会自动从原位置移走)
116
+ const childDoms = [];
117
+ this.$container.children().each(function (index, dom) {
118
+ childDoms.push(dom);
119
+ });
120
+ for (let i = 0; i < childDoms.length; i++) {
121
+ $wrapper.append($(childDoms[i]));
122
+ }
123
+
124
+ // 2. 创建 swiper-container 包裹 wrapper
125
+ const $swiperContainer = $('<div class="swiper-container"></div>');
126
+ $swiperContainer.append($wrapper);
127
+
128
+ // 3. 创建分页器
129
+ if (this.options.pagination && this.totalSlides > 1) {
130
+ const $pagination = this.$pagination = $(`<div class="swiper-pagination ${dir}"></div>`);
131
+ for (let i = 0; i < this.totalSlides; i++) {
132
+ $pagination.append(`<div class="swiper-pagination-bullet ${i === this.index ? 'active' : ''}"></div>`);
133
+ }
134
+ $swiperContainer.append($pagination);
135
+ }
136
+
137
+ // 4. 将 swiper-container 放入原容器
138
+ this.$container.append($swiperContainer);
139
+
140
+ // loop模式:在首尾添加克隆占位
141
+ if (this.options.loop && this.totalSlides > 1) {
142
+ const $firstClone = $(childDoms[this.totalSlides - 1].outerHTML);
143
+ $firstClone.attr('data-swiper-clone', 'true');
144
+ $wrapper.prepend($firstClone);
145
+
146
+ const $lastClone = $(childDoms[0].outerHTML);
147
+ $lastClone.attr('data-swiper-clone', 'true');
148
+ $wrapper.append($lastClone);
149
+ }
150
+ };
151
+
152
+ /**
153
+ * 创建DOM结构
154
+ */
155
+ Swiper.prototype.createHtml = function () {
156
+ const dir = this.options.direction;
157
+ this.$container.empty();
158
+
159
+ // 创建swiper容器
160
+ const $swiperContainer = $('<div class="swiper-container"></div>');
161
+
162
+ // 创建wrapper
163
+ const $wrapper = this.$wrapper = $(`<div class="swiper-wrapper ${dir}"></div>`);
164
+
165
+ const slides = this.options.slides;
166
+ const len = slides.length;
167
+
168
+ // loop模式:首尾各加一个占位
169
+ if (this.options.loop && len > 1) {
170
+ $wrapper.append(this._createSlide(slides[len - 1], len - 1, true));
171
+ }
172
+
173
+ for (let i = 0; i < len; i++) {
174
+ $wrapper.append(this._createSlide(slides[i], i, false));
175
+ }
176
+
177
+ if (this.options.loop && len > 1) {
178
+ $wrapper.append(this._createSlide(slides[0], 0, true));
179
+ }
180
+
181
+ $swiperContainer.append($wrapper);
182
+
183
+ // 创建分页器
184
+ if (this.options.pagination && len > 1) {
185
+ const $pagination = this.$pagination = $(`<div class="swiper-pagination ${dir}"></div>`);
186
+ for (let i = 0; i < len; i++) {
187
+ $pagination.append(`<div class="swiper-pagination-bullet ${i === this.index ? 'active' : ''}"></div>`);
188
+ }
189
+ $swiperContainer.append($pagination);
190
+ }
191
+
192
+ this.$container.append($swiperContainer);
193
+ };
194
+
195
+ /**
196
+ * 创建单个slide
197
+ * 支持三种模式:
198
+ * 1. renderSlide回调模式:通过函数动态渲染,调用方灵活处理内容
199
+ * 2. 静态DOM模式:slides数组中传入jQuery对象/DOM元素,直接使用
200
+ * 3. 数据模式:slides数组中传入字符串或{html}/{url}对象,自动生成DOM
201
+ * @private
202
+ * @param {*} data slide数据项(可以是数据对象、HTML字符串、DOM元素、jQuery对象)
203
+ * @param {number} index 当前slide索引
204
+ * @param {boolean} isClone 是否为loop模式的克隆占位
205
+ */
206
+ Swiper.prototype._createSlide = function (data, index, isClone) {
207
+ let $slide;
208
+
209
+ // 模式1: renderSlide回调 —— 调用方通过函数动态渲染内容
210
+ if (typeof this.options.renderSlide === 'function') {
211
+ const content = this.options.renderSlide(data, index);
212
+ $slide = this._wrapAsSlide(content);
213
+ }
214
+ // 模式2: data本身是DOM元素或jQuery对象 —— 直接作为slide使用
215
+ else if (this._isDomOrJq(data)) {
216
+ if (isClone) {
217
+ // clone模式需要复制节点
218
+ const dom = data.dom ? data.dom : (data.nodeType ? data : data[0]);
219
+ $slide = this._wrapAsSlide(dom.cloneNode(true));
220
+ } else {
221
+ $slide = this._wrapAsSlide(data);
222
+ }
223
+ }
224
+ // 模式3: 数据模式 —— 字符串或对象自动生成DOM
225
+ else if (typeof data === 'string') {
226
+ // HTML字符串:通过 _wrapAsSlide 处理,如果本身已有 swiper-slide 类则直接使用
227
+ $slide = this._wrapAsSlide(data);
228
+ } else {
229
+ let html = '';
230
+ if (data && data.html) {
231
+ html = data.html;
232
+ } else if (data && data.url) {
233
+ html = `<div style="width:100%;height:100%;background-image:url(${data.url});background-size:cover;background-position:center;"></div>`;
234
+ }
235
+ $slide = $(`<div class="swiper-slide">${html}</div>`);
236
+ }
237
+
238
+ if (isClone) {
239
+ $slide.attr('data-swiper-clone', 'true');
240
+ }
241
+
242
+ return $slide;
243
+ };
244
+
245
+ /**
246
+ * 将内容包装为标准的swiper-slide元素
247
+ * @private
248
+ * @param {string|HTMLElement|jQuery} content 内容
249
+ * @returns {jQuery} 包装后的$slide
250
+ */
251
+ Swiper.prototype._wrapAsSlide = function (content) {
252
+ let $slide;
253
+
254
+ if (typeof content === 'string') {
255
+ // HTML字符串
256
+ $slide = $(content);
257
+ } else if (content && content.dom) {
258
+ // jQuery对象(jquery.pix风格,有.dom属性)
259
+ $slide = content;
260
+ } else if (content && content.nodeType) {
261
+ // 原生DOM元素
262
+ $slide = $(content);
263
+ } else {
264
+ $slide = $(content);
265
+ }
266
+
267
+ // 确保有swiper-slide类
268
+ if (!$slide.hasClass('swiper-slide')) {
269
+ const $wrap = $('<div class="swiper-slide"></div>');
270
+ $wrap.append($slide);
271
+ $slide = $wrap;
272
+ }
273
+
274
+ return $slide;
275
+ };
276
+
277
+ /**
278
+ * 判断data是否为DOM元素或jQuery对象
279
+ * @private
280
+ */
281
+ Swiper.prototype._isDomOrJq = function (data) {
282
+ if (!data) return false;
283
+ // jquery.pix对象有.dom属性
284
+ if (data.dom) return true;
285
+ // 原生DOM节点
286
+ if (data.nodeType) return true;
287
+ // 标准jQuery对象(有length和jquery属性)
288
+ if (data.jquery && data.length) return true;
289
+ return false;
290
+ };
291
+
292
+ /**
293
+ * 更新尺寸相关计算
294
+ */
295
+ Swiper.prototype.updateSize = function () {
296
+ const isHorizontal = this.options.direction === 'horizontal';
297
+ this.slideSize = isHorizontal ? this.$container.width() : this.$container.height();
298
+
299
+ // 设置每个slide的尺寸
300
+ this.$wrapper.find('.swiper-slide').each((index, dom) => {
301
+ const $s = $(dom);
302
+ if (isHorizontal) {
303
+ $s.width(this.slideSize);
304
+ } else {
305
+ $s.css('height', this.slideSize + 'px');
306
+ }
307
+ });
308
+
309
+ // 设置wrapper尺寸
310
+ const totalCount = this.options.loop && this.totalSlides > 1
311
+ ? this.totalSlides + 2
312
+ : this.totalSlides;
313
+
314
+ if (isHorizontal) {
315
+ this.$wrapper.width(this.slideSize * totalCount);
316
+ } else {
317
+ this.$wrapper.css('height', (this.slideSize * totalCount) + 'px');
318
+ }
319
+
320
+ // 设置初始位置
321
+ this._setTranslate(this._getTranslateByIndex(this.index), false);
322
+ };
323
+
324
+ /**
325
+ * 绑定交互事件
326
+ */
327
+ Swiper.prototype.bindEvent = function () {
328
+ const self = this;
329
+ const $wrapper = this.$wrapper;
330
+ const isHorizontal = this.options.direction === 'horizontal';
331
+
332
+ let startPos = 0; // 起始位置
333
+ let currentPos = 0; // 当前位置(跟随move更新)
334
+ let originalTranslate = 0;
335
+ let isDown = false;
336
+ let startTime = 0;
337
+
338
+ const getPos = (e) => {
339
+ if (e.originalEvent) {
340
+ return isHorizontal ? e.originalEvent.clientX : e.originalEvent.clientY;
341
+ }
342
+ return isHorizontal ? e.clientX : e.clientY;
343
+ };
344
+
345
+ const pStart = (e, capture) => {
346
+ if (isDown || self.isAnimating) return;
347
+ if (self.totalSlides <= 1) return;
348
+
349
+ if (capture) capture();
350
+ self.pauseAutoplay();
351
+
352
+ startPos = getPos(e);
353
+ currentPos = startPos;
354
+ originalTranslate = self.currentTranslate;
355
+ startTime = Date.now();
356
+ isDown = true;
357
+
358
+ $wrapper.css('transition', 'none');
359
+ };
360
+
361
+ const pMove = (e) => {
362
+ if (!isDown) return;
363
+
364
+ const pos = getPos(e);
365
+ const diff = pos - currentPos;
366
+ let newTranslate = self.currentTranslate + diff;
367
+
368
+ // 非loop模式边界阻尼
369
+ if (!self.options.loop) {
370
+ const minTranslate = -(self.totalSlides - 1) * self.slideSize;
371
+ if (newTranslate > 0) {
372
+ newTranslate = newTranslate * 0.3; // 阻尼系数
373
+ } else if (newTranslate < minTranslate) {
374
+ newTranslate = minTranslate + (newTranslate - minTranslate) * 0.3;
375
+ }
376
+ }
377
+
378
+ self.currentTranslate = newTranslate;
379
+ self._applyTranslate(newTranslate);
380
+ currentPos = pos;
381
+ };
382
+
383
+ const pEnd = async (e, capture) => {
384
+ if (!isDown) return;
385
+ if (capture) capture();
386
+ isDown = false;
387
+
388
+ const totalMove = self.currentTranslate - originalTranslate;
389
+ const duration = Date.now() - startTime;
390
+ const velocity = Math.abs(totalMove) / duration; // px/ms
391
+
392
+ let newIndex = self.index;
393
+
394
+ // 判断是否需要翻页:滑动距离超过阈值 或 滑动速度足够快
395
+ if (Math.abs(totalMove) > self.slideSize * self.options.threshold || velocity > 0.5) {
396
+ if (totalMove < 0) {
397
+ newIndex = self.index + 1;
398
+ } else {
399
+ newIndex = self.index - 1;
400
+ }
401
+ }
402
+
403
+ // 边界处理
404
+ if (self.options.loop) {
405
+ // loop模式下先滑动到目标位置,transitionend后再处理循环
406
+ if (newIndex < 0) {
407
+ newIndex = self.totalSlides - 1;
408
+ } else if (newIndex >= self.totalSlides) {
409
+ newIndex = 0;
410
+ }
411
+ } else {
412
+ newIndex = Math.max(0, Math.min(newIndex, self.totalSlides - 1));
413
+ }
414
+
415
+ await self.slideTo(newIndex);
416
+
417
+ if (self.options.autoplay) {
418
+ self.startAutoplay();
419
+ }
420
+ };
421
+
422
+ $wrapper.off();
423
+
424
+ if (windowEnv === 'h5') {
425
+ $wrapper
426
+ .attr('draggable', 'false')
427
+ .on('pointerdown', function (e) {
428
+ pStart(e, () => {
429
+ this.setPointerCapture(e.originalEvent.pointerId);
430
+ });
431
+ })
432
+ .on('pointermove', function (e) {
433
+ pMove(e);
434
+ })
435
+ .on('pointerup', async function (e) {
436
+ await pEnd(e, () => {
437
+ this.releasePointerCapture(e.originalEvent.pointerId);
438
+ });
439
+ });
440
+ } else {
441
+ $wrapper
442
+ .attr('draggable', 'true')
443
+ .on('dragstart', (e) => {
444
+ pStart(e);
445
+ })
446
+ .on('drag', (e) => {
447
+ pMove(e);
448
+ })
449
+ .on('dragend', async (e) => {
450
+ await pEnd(e);
451
+ });
452
+ }
453
+
454
+ // 点击事件
455
+ let clickStartPos = 0;
456
+ $wrapper.on('click', function (e) {
457
+ if (!self.options.onClick) return;
458
+ // 防止拖动后触发click
459
+ if (windowEnv === 'h5' && Math.abs(getPos(e) - startPos) > 10) return;
460
+
461
+ const slideData = self.options.slides[self.index];
462
+ const $slide = self._getActiveSlide();
463
+ self.options.onClick($slide, slideData, self.index);
464
+ });
465
+ };
466
+
467
+ /**
468
+ * 滑动到指定页
469
+ * @param {number} index 目标页码 [0, totalSlides)
470
+ * @param {number} [speed] 动画时长(ms),不传则使用options.speed
471
+ */
472
+ Swiper.prototype.slideTo = async function (index, speed) {
473
+ if (index < 0 || index >= this.totalSlides) return;
474
+
475
+ const prevIndex = this.index;
476
+ const animSpeed = speed !== undefined ? speed : this.options.speed;
477
+
478
+ if (this.options.onSlideChangeStart && index !== prevIndex) {
479
+ this.options.onSlideChangeStart(index, prevIndex);
480
+ }
481
+
482
+ this.isAnimating = true;
483
+
484
+ // loop模式特殊处理:跨边界时需要先跳到占位位置再平滑
485
+ if (this.options.loop && this.totalSlides > 1) {
486
+ // 如果从最后一个向后滑 → 使用末尾占位(clone)再跳
487
+ if (prevIndex === this.totalSlides - 1 && index === 0) {
488
+ // 先平滑到末尾占位
489
+ const cloneTranslate = -(this.totalSlides + 1) * this.slideSize;
490
+ this._setTranslate(cloneTranslate, true, animSpeed);
491
+ await this._waitTransitionEnd(animSpeed);
492
+ // 瞬移到真实位置
493
+ this._setTranslate(this._getTranslateByIndex(0), false);
494
+ await nextAnimationFrame();
495
+ }
496
+ // 如果从第一个向前滑 → 使用首位占位(clone)再跳
497
+ else if (prevIndex === 0 && index === this.totalSlides - 1) {
498
+ const cloneTranslate = 0;
499
+ this._setTranslate(cloneTranslate, true, animSpeed);
500
+ await this._waitTransitionEnd(animSpeed);
501
+ // 瞬移到真实位置
502
+ this._setTranslate(this._getTranslateByIndex(this.totalSlides - 1), false);
503
+ await nextAnimationFrame();
504
+ }
505
+ // 普通滑动
506
+ else {
507
+ const targetTranslate = this._getTranslateByIndex(index);
508
+ this._setTranslate(targetTranslate, true, animSpeed);
509
+ await this._waitTransitionEnd(animSpeed);
510
+ }
511
+ } else {
512
+ const targetTranslate = this._getTranslateByIndex(index);
513
+ this._setTranslate(targetTranslate, true, animSpeed);
514
+ await this._waitTransitionEnd(animSpeed);
515
+ }
516
+
517
+ this.index = index;
518
+ this.isAnimating = false;
519
+
520
+ // 更新分页器
521
+ this._updatePagination(index);
522
+
523
+ if (this.options.onSlideChange && index !== prevIndex) {
524
+ this.options.onSlideChange(index, prevIndex);
525
+ }
526
+ };
527
+
528
+ /**
529
+ * 滑动到下一页
530
+ */
531
+ Swiper.prototype.slideNext = function () {
532
+ if (this.isAnimating) return;
533
+ let nextIndex = this.index + 1;
534
+ if (this.options.loop) {
535
+ if (nextIndex >= this.totalSlides) nextIndex = 0;
536
+ } else {
537
+ if (nextIndex >= this.totalSlides) return;
538
+ }
539
+ this.pauseAutoplay();
540
+ this.slideTo(nextIndex);
541
+ if (this.options.autoplay) {
542
+ this.startAutoplay();
543
+ }
544
+ };
545
+
546
+ /**
547
+ * 滑动到上一页
548
+ */
549
+ Swiper.prototype.slidePrev = function () {
550
+ if (this.isAnimating) return;
551
+ let prevIndex = this.index - 1;
552
+ if (this.options.loop) {
553
+ if (prevIndex < 0) prevIndex = this.totalSlides - 1;
554
+ } else {
555
+ if (prevIndex < 0) return;
556
+ }
557
+ this.pauseAutoplay();
558
+ this.slideTo(prevIndex);
559
+ if (this.options.autoplay) {
560
+ this.startAutoplay();
561
+ }
562
+ };
563
+
564
+ /**
565
+ * 获取当前页码
566
+ * @returns {number}
567
+ */
568
+ Swiper.prototype.getActiveIndex = function () {
569
+ return this.index;
570
+ };
571
+
572
+ /**
573
+ * 获取slide总数
574
+ * @returns {number}
575
+ */
576
+ Swiper.prototype.getSlideCount = function () {
577
+ return this.totalSlides;
578
+ };
579
+
580
+ /**
581
+ * 更新slides数据并重新渲染
582
+ * @param {Array} slides 新的slides数据数组
583
+ */
584
+ Swiper.prototype.update = function (slides) {
585
+ this.pauseAutoplay();
586
+ this.options.slides = slides;
587
+ this.totalSlides = slides.length;
588
+ this.index = Math.min(this.index, this.totalSlides - 1);
589
+ this.index = Math.max(this.index, 0);
590
+ this.createHtml();
591
+ this.updateSize();
592
+ this.bindEvent();
593
+ if (this.options.autoplay && this.totalSlides > 1) {
594
+ this.startAutoplay();
595
+ }
596
+ };
597
+
598
+ /**
599
+ * 开始自动轮播
600
+ */
601
+ Swiper.prototype.startAutoplay = function () {
602
+ if (this.autoplayTimer) return;
603
+ if (this.totalSlides <= 1) return;
604
+
605
+ this.autoplayTimer = setInterval(() => {
606
+ let next = this.index + 1;
607
+ if (this.options.loop) {
608
+ if (next >= this.totalSlides) next = 0;
609
+ } else {
610
+ if (next >= this.totalSlides) next = 0; // 回到开头
611
+ }
612
+ this.slideTo(next);
613
+ }, this.options.autoplayDelay);
614
+ };
615
+
616
+ /**
617
+ * 暂停自动轮播
618
+ */
619
+ Swiper.prototype.pauseAutoplay = function () {
620
+ if (!this.autoplayTimer) return;
621
+ clearInterval(this.autoplayTimer);
622
+ this.autoplayTimer = null;
623
+ };
624
+
625
+ /**
626
+ * 销毁组件,清理事件和DOM
627
+ */
628
+ Swiper.prototype.destroy = function () {
629
+ this.pauseAutoplay();
630
+ this.$wrapper.off();
631
+ this.$container.empty();
632
+ removeResizeFunc({
633
+ obj: this,
634
+ func: this.updateSize,
635
+ });
636
+ };
637
+
638
+ // ========== 内部方法 ==========
639
+
640
+ /**
641
+ * 根据index计算translate值
642
+ * @private
643
+ */
644
+ Swiper.prototype._getTranslateByIndex = function (index) {
645
+ const offset = this.options.loop && this.totalSlides > 1 ? 1 : 0;
646
+ return -(index + offset) * this.slideSize;
647
+ };
648
+
649
+ /**
650
+ * 设置translate并可选地开启transition
651
+ * @private
652
+ */
653
+ Swiper.prototype._setTranslate = function (value, animate, speed) {
654
+ this.currentTranslate = value;
655
+ if (animate) {
656
+ const duration = speed || this.options.speed;
657
+ this.$wrapper.css('transition', `transform ${duration}ms ease-out`);
658
+ } else {
659
+ this.$wrapper.css('transition', 'none');
660
+ }
661
+ this._applyTranslate(value);
662
+ };
663
+
664
+ /**
665
+ * 应用translate到DOM
666
+ * @private
667
+ */
668
+ Swiper.prototype._applyTranslate = function (value) {
669
+ const isHorizontal = this.options.direction === 'horizontal';
670
+ if (isHorizontal) {
671
+ this.$wrapper.css('transform', `translateX(${value}px)`);
672
+ } else {
673
+ this.$wrapper.css('transform', `translateY(${value}px)`);
674
+ }
675
+ };
676
+
677
+ /**
678
+ * 等待transition结束
679
+ * @private
680
+ */
681
+ Swiper.prototype._waitTransitionEnd = function (speed) {
682
+ return new Promise(resolve => {
683
+ const duration = speed || this.options.speed;
684
+ // 使用setTimeout兜底,避免transitionend事件不触发的情况
685
+ setTimeout(resolve, duration + 50);
686
+ });
687
+ };
688
+
689
+ /**
690
+ * 更新分页器状态
691
+ * @private
692
+ */
693
+ Swiper.prototype._updatePagination = function (index) {
694
+ if (!this.$pagination) return;
695
+ this.$pagination.children().removeClass('active');
696
+ this.$pagination.children().eq(index).addClass('active');
697
+ };
698
+
699
+ /**
700
+ * 获取当前活跃的slide元素
701
+ * @private
702
+ */
703
+ Swiper.prototype._getActiveSlide = function () {
704
+ const offset = this.options.loop && this.totalSlides > 1 ? 1 : 0;
705
+ return this.$wrapper.children().eq(this.index + offset);
706
+ };
@@ -0,0 +1,72 @@
1
+ .swiper-container {
2
+ position: relative;
3
+ overflow: hidden;
4
+ width: 100%;
5
+ height: 100%;
6
+ }
7
+
8
+ .swiper-wrapper {
9
+ display: flex;
10
+ position: relative;
11
+ width: 100%;
12
+ height: 100%;
13
+ transition: transform 0.3s ease-out;
14
+ }
15
+
16
+ .swiper-wrapper.vertical {
17
+ flex-direction: column;
18
+ }
19
+
20
+ .swiper-wrapper.horizontal {
21
+ flex-direction: row;
22
+ }
23
+
24
+ .swiper-slide {
25
+ flex-shrink: 0;
26
+ width: 100%;
27
+ height: 100%;
28
+ position: relative;
29
+ }
30
+
31
+ .swiper-pagination {
32
+ position: absolute;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ z-index: 10;
37
+ }
38
+
39
+ .swiper-pagination.horizontal {
40
+ bottom: 0.2rem;
41
+ left: 0;
42
+ width: 100%;
43
+ flex-direction: row;
44
+ }
45
+
46
+ .swiper-pagination.vertical {
47
+ right: 0.2rem;
48
+ top: 0;
49
+ height: 100%;
50
+ flex-direction: column;
51
+ }
52
+
53
+ .swiper-pagination-bullet {
54
+ width: 0.16rem;
55
+ height: 0.16rem;
56
+ border-radius: 50%;
57
+ background: rgba(0, 0, 0, 0.2);
58
+ transition: background 0.3s, transform 0.3s;
59
+ }
60
+
61
+ .swiper-pagination.horizontal .swiper-pagination-bullet {
62
+ margin: 0 0.08rem;
63
+ }
64
+
65
+ .swiper-pagination.vertical .swiper-pagination-bullet {
66
+ margin: 0.08rem 0;
67
+ }
68
+
69
+ .swiper-pagination-bullet.active {
70
+ background: rgba(0, 0, 0, 0.8);
71
+ transform: scale(1.3);
72
+ }
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { VideoPlayer } from './components/video/videoplayer';
2
2
  export { tips, showTips } from './components/tips/tipv2';
3
3
  export { Banner } from './components/banner/banner';
4
+ export { Swiper } from './components/swiper/swiper';
4
5
  export { List } from './components/list/list';
5
6
  export { Waterfall } from './components/waterfall/waterfall';
6
7
  export { Waterfallv2 } from './components/waterfallv2/waterfallv2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tencent.jquery.pix.component",
3
- "version": "1.0.91-beta1",
3
+ "version": "1.0.92",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [