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 +4 -0
- package/components/list/list.js +25 -64
- package/components/swiper/swiper.js +706 -0
- package/components/swiper/swiper.scss +72 -0
- package/index.js +1 -0
- package/package.json +1 -1
package/change.md
CHANGED
package/components/list/list.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
self.
|
|
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
|
-
|
|
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
|
-
//
|
|
95
|
-
const $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
|
-
|
|
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
|
-
|
|
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';
|