slider-captcha-sdk 1.0.9 → 1.0.12

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.
@@ -0,0 +1,763 @@
1
+ /**
2
+ * 纯JavaScript弹窗滑块验证码组件
3
+ */
4
+ class PopupSliderCaptcha {
5
+ static DEFAULTS = {
6
+ width: 320,
7
+ height: 155,
8
+ sliderSize: 38,
9
+ maxRetries: 3,
10
+ timeout: 30000,
11
+ apiUrl: "/api/captcha",
12
+ verifyUrl: "/api/captcha/verify",
13
+ throttleDelay: 16,
14
+ clickMaskClose: false
15
+ }
16
+
17
+ static CSS_CLASSES = {
18
+ overlay: "slider-captcha-overlay",
19
+ modal: "slider-captcha-modal",
20
+ header: "slider-captcha-header",
21
+ container: "slider-captcha-container",
22
+ track: "slider-captcha-track",
23
+ btn: "slider-captcha-btn",
24
+ hint: "slider-captcha-hint",
25
+ loading: "slider-captcha-loading",
26
+ error: "slider-captcha-error"
27
+ }
28
+
29
+ // 优化:提取CSS样式为独立方法,减少主体代码长度
30
+ static getStyles() {
31
+ return `:root{--sc-primary:#409eff;--sc-success:#67c23a;--sc-danger:#f56c6c;--sc-border:#e4e7eb;--sc-bg:linear-gradient(90deg, #f7f9fa 0%, #e8f4fd 100%);--sc-text:#333;--sc-text-light:#999;--sc-shadow:0 4px 20px rgba(0,0,0,.3);--sc-radius:8px;--sc-transition:.3s ease}.slider-captcha-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:9999;display:none;justify-content:center;align-items:center;opacity:0;transition:opacity var(--sc-transition)}.slider-captcha-overlay.show{opacity:1}.slider-captcha-modal{background:#fff;border-radius:var(--sc-radius);padding:20px;box-shadow:var(--sc-shadow);position:relative;max-width:90vw;max-height:90vh;transform:scale(.8) translateY(-20px);opacity:0;transition:all var(--sc-transition)}.slider-captcha-modal.show{transform:scale(1) translateY(0);opacity:1}.slider-captcha-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid var(--sc-border)}.slider-captcha-container{display:flex;align-items:center;position:relative;border-radius:4px;overflow:hidden;margin-bottom:15px;background:#837a7a;justify-content:center}.slider-captcha-track{width:100%;height:40px;line-height:40px;background:var(--sc-bg);border:1px solid var(--sc-border);border-radius:20px;position:relative;margin-bottom:15px;overflow:hidden}.slider-captcha-btn{width:38px;height:38px;background:#fff;border:1px solid #ccc;border-radius:50%;position:absolute;top:0;left:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px rgba(0,0,0,.1);transition:all var(--sc-transition);user-select:none;z-index:1}.slider-captcha-loading{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,.6);display:flex;align-items:center;justify-content:center;flex-direction:column;color:#666;font-size:14px;z-index:10;border-radius:4px}.slider-captcha-error{color:var(--sc-danger);font-size:12px;text-align:center;margin-top:10px;display:none}.slider-captcha-title{margin:0;font-size:16px;color:var(--sc-text)}.slider-captcha-close,.slider-captcha-refresh{background:none;border:none;cursor:pointer;color:var(--sc-text-light);padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all var(--sc-transition);position:relative;font-size:0}.slider-captcha-close::before,.slider-captcha-close::after{content:'';position:absolute;width:16px;height:2px;background-color:var(--sc-text-light);border-radius:1px;transition:all var(--sc-transition)}.slider-captcha-close::before{transform:rotate(45deg)}.slider-captcha-close::after{transform:rotate(-45deg)}.slider-captcha-close:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-close:hover::before,.slider-captcha-close:hover::after{background-color:var(--sc-danger)}.slider-captcha-refresh{margin-left:10px}.slider-captcha-refresh svg{width:20px;height:20px;fill:var(--sc-text-light);transition:all var(--sc-transition)}.slider-captcha-refresh:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-refresh:hover svg{fill:var(--sc-primary);transform:rotate(180deg)}.slider-captcha-floating-time{position:absolute;bottom:-40px;left:50%;transform:translateX(-50%);color:#fff;font-size:12px;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:2px 15px;border-radius:10px}.slider-captcha-floating-time.show{opacity:1;transform:translateX(-50%) translateY(-45px)}.slider-captcha-floating-time.success{color:var(--sc-success)}.slider-captcha-floating-time.fail{color:var(--sc-danger)}.slider-captcha-bg{width:100%;height:100%;object-fit:cover;display:block}.slider-captcha-piece{position:absolute;top:0;left:0;cursor:pointer;transition:none;z-index:2}.slider-captcha-finger{position:absolute;top:50%;left:10px;transform:translateY(-50%);font-size:20px;animation:fingerSlide 2s ease-in-out infinite;pointer-events:none;z-index:1;opacity:.6}.slider-captcha-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--sc-text-light);font-size:14px;pointer-events:none;z-index:1;transition:all var(--sc-transition)}.slider-captcha-header-buttons{display:flex;align-items:center}@keyframes fingerSlide{0%{left:10px;opacity:.6}50%{opacity:1}100%{left:calc(50% - 10px);opacity:.6}}`
32
+ }
33
+
34
+ constructor(options = {}) {
35
+ this.options = { ...PopupSliderCaptcha.DEFAULTS, ...options };
36
+ this.elements = {};
37
+ this.state = this.createInitialState();
38
+ this.captchaData = null;
39
+ this.times = [];
40
+ this.startTime = null;
41
+ this.eventListeners = [];
42
+ this.timers = new Set();
43
+ this.rafId = null;
44
+ this.cachedDimensions = null;
45
+ this.imageCache = new Map();
46
+
47
+ // 优化:使用箭头函数绑定this,避免重复bind调用
48
+ this.throttledHandleMove = this.throttle((e) => this.handleMove(e), this.options.throttleDelay);
49
+
50
+ this.init();
51
+ }
52
+
53
+ // 优化:提取初始状态创建为独立方法
54
+ createInitialState() {
55
+ return {
56
+ isVisible: false,
57
+ isDragging: false,
58
+ currentX: 0,
59
+ startX: 0,
60
+ retryCount: 0,
61
+ isLoading: false
62
+ }
63
+ }
64
+
65
+ init() {
66
+ this.injectStyles();
67
+ this.createElements();
68
+ this.bindEvents();
69
+ }
70
+
71
+ injectStyles() {
72
+ if (document.querySelector("#slider-captcha-styles")) return
73
+
74
+ const style = document.createElement("style");
75
+ style.id = "slider-captcha-styles";
76
+ style.textContent = PopupSliderCaptcha.getStyles();
77
+ document.head.appendChild(style);
78
+ }
79
+
80
+ // 优化:简化元素创建逻辑
81
+ createElements() {
82
+ const { elements, options } = this;
83
+
84
+ // 批量创建元素配置
85
+ const elementConfigs = [
86
+ ['overlay', 'div', PopupSliderCaptcha.CSS_CLASSES.overlay],
87
+ ['modal', 'div', PopupSliderCaptcha.CSS_CLASSES.modal],
88
+ ['header', 'div', PopupSliderCaptcha.CSS_CLASSES.header],
89
+ ['title', 'h3', 'slider-captcha-title', '安全验证'],
90
+ ['closeBtn', 'button', 'slider-captcha-close'],
91
+ ['refreshBtn', 'button', 'slider-captcha-refresh'],
92
+ ['container', 'div', PopupSliderCaptcha.CSS_CLASSES.container],
93
+ ['backgroundImg', 'img', 'slider-captcha-bg'],
94
+ ['sliderImg', 'img', 'slider-captcha-piece'],
95
+ ['loadingText', 'div', PopupSliderCaptcha.CSS_CLASSES.loading, '加载中...'],
96
+ ['floatingTime', 'div', 'slider-captcha-floating-time'],
97
+ ['track', 'div', PopupSliderCaptcha.CSS_CLASSES.track],
98
+ ['fingerAnimation', 'div', 'slider-captcha-finger', '👉'],
99
+ ['btn', 'div', PopupSliderCaptcha.CSS_CLASSES.btn],
100
+ ['icon', 'div', '', '→'],
101
+ ['hint', 'div', PopupSliderCaptcha.CSS_CLASSES.hint, '向右滑动完成验证'],
102
+ ['error', 'div', PopupSliderCaptcha.CSS_CLASSES.error]
103
+ ];
104
+
105
+ // 批量创建元素
106
+ elementConfigs.forEach(([key, tag, className, textContent]) => {
107
+ elements[key] = this.createElement(tag, className, textContent);
108
+ });
109
+
110
+ // 设置容器尺寸
111
+ elements.container.style.cssText = `width:${options.width}px;height:${options.height}px`;
112
+
113
+ // 添加刷新按钮图标
114
+ elements.refreshBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/></svg>`;
115
+
116
+ this.assembleDOM();
117
+ this.setInitialState();
118
+ }
119
+
120
+ createElement(tag, className = "", textContent = "") {
121
+ const element = document.createElement(tag);
122
+ if (className) element.className = className;
123
+ if (textContent) element.textContent = textContent;
124
+ return element
125
+ }
126
+
127
+ // 优化:简化DOM组装逻辑
128
+ assembleDOM() {
129
+ const { elements } = this;
130
+
131
+ // 组装头部
132
+ const headerButtons = this.createElement("div", "slider-captcha-header-buttons");
133
+ headerButtons.append(elements.refreshBtn, elements.closeBtn);
134
+ elements.header.append(elements.title, headerButtons);
135
+
136
+ // 组装验证码容器
137
+ elements.container.append(
138
+ elements.backgroundImg,
139
+ elements.sliderImg,
140
+ elements.loadingText,
141
+ elements.floatingTime
142
+ );
143
+
144
+ // 组装滑块轨道
145
+ elements.btn.appendChild(elements.icon);
146
+ elements.track.append(
147
+ elements.fingerAnimation,
148
+ elements.btn,
149
+ elements.hint
150
+ );
151
+
152
+ // 组装模态框
153
+ elements.modal.append(
154
+ elements.header,
155
+ elements.container,
156
+ elements.track,
157
+ elements.error
158
+ );
159
+
160
+ // 组装到覆盖层
161
+ elements.overlay.appendChild(elements.modal);
162
+ document.body.appendChild(elements.overlay);
163
+ }
164
+
165
+ setInitialState() {
166
+ // 批量设置初始状态
167
+ Object.assign(this.elements.container.style, { display: "none" });
168
+ Object.assign(this.elements.track.style, { display: "none" });
169
+ }
170
+
171
+ // 优化:简化事件绑定
172
+ bindEvents() {
173
+ const { elements } = this;
174
+
175
+ // 基础事件
176
+ this.addEventListener(elements.closeBtn, "click", () => this.hide());
177
+ this.addEventListener(elements.refreshBtn, "click", () => this.refresh());
178
+ this.addEventListener(elements.overlay, "click", (e) => {
179
+ if (e.target === elements.overlay && this.options.clickMaskClose) this.hide();
180
+ });
181
+ this.addEventListener(document, "keydown", (e) => {
182
+ if (e.key === "Escape" && this.state.isVisible) this.hide();
183
+ });
184
+ this.addEventListener(document, "visibilitychange", () => this.handleVisibilityChange());
185
+
186
+ this.bindSliderEvents();
187
+ }
188
+
189
+ bindSliderEvents() {
190
+ const { elements } = this;
191
+ const handlers = {
192
+ start: (e) => this.handleStart(e),
193
+ move: this.throttledHandleMove,
194
+ end: () => this.handleEnd()
195
+ };
196
+
197
+ // 滑块事件
198
+ this.addEventListener(elements.btn, "mousedown", handlers.start);
199
+ this.addEventListener(elements.btn, "touchstart", handlers.start);
200
+ this.addEventListener(elements.sliderImg, "mousedown", handlers.start);
201
+ this.addEventListener(elements.sliderImg, "touchstart", handlers.start);
202
+ this.addEventListener(document, "mousemove", handlers.move, { passive: false });
203
+ this.addEventListener(document, "touchmove", handlers.move, { passive: false });
204
+ this.addEventListener(document, "mouseup", handlers.end);
205
+ this.addEventListener(document, "touchend", handlers.end);
206
+ }
207
+
208
+ // 优化:改进事件管理
209
+ addEventListener(element, event, handler, options = {}) {
210
+ if (!element || typeof handler !== 'function') return
211
+
212
+ element.addEventListener(event, handler, options);
213
+ this.eventListeners.push({ element, event, handler, options });
214
+ }
215
+
216
+ removeAllEventListeners() {
217
+ this.eventListeners.forEach(({ element, event, handler, options }) => {
218
+ try {
219
+ element?.removeEventListener?.(event, handler, options);
220
+ } catch (error) {
221
+ console.warn('Failed to remove event listener:', error);
222
+ }
223
+ });
224
+ this.eventListeners.length = 0;
225
+ }
226
+
227
+ // 优化:缓存尺寸计算
228
+ getDimensions() {
229
+ if (!this.cachedDimensions) {
230
+ const trackWidth = this.elements.track.offsetWidth;
231
+ const btnWidth = this.elements.btn.offsetWidth;
232
+ this.cachedDimensions = {
233
+ trackWidth,
234
+ btnWidth,
235
+ maxX: trackWidth - btnWidth
236
+ };
237
+ }
238
+ return this.cachedDimensions
239
+ }
240
+
241
+ getPosition() {
242
+ const { maxX } = this.getDimensions();
243
+ const percentage = this.state.currentX / maxX;
244
+ return Math.round(percentage * (this.options.width - this.options.sliderSize))
245
+ }
246
+
247
+ // 优化:简化拖拽处理
248
+ handleStart(e) {
249
+ if (!this.captchaData || this.state.isDragging || this.state.isLoading) return
250
+
251
+ e.preventDefault();
252
+ this.state.isDragging = true;
253
+ this.state.startX = this.getClientX(e) - this.state.currentX;
254
+ this.startTime = this.startTime || Date.now();
255
+ this.times = [{ time: Date.now(), position: this.getPosition() }];
256
+
257
+ this.setTransition(false);
258
+ this.updateUIState("dragging");
259
+ this.cachedDimensions = null; // 清除缓存
260
+ }
261
+
262
+ handleMove(e) {
263
+ if (!this.state.isDragging) return
264
+ e.preventDefault();
265
+
266
+ const clientX = this.getClientX(e);
267
+ const deltaX = clientX - this.state.startX;
268
+ const { maxX } = this.getDimensions();
269
+
270
+ this.state.currentX = Math.max(0, Math.min(deltaX, maxX));
271
+ this.times.push({ time: Date.now(), position: this.getPosition() });
272
+
273
+ // 优化:使用RAF批量更新
274
+ this.rafId && cancelAnimationFrame(this.rafId);
275
+ this.rafId = requestAnimationFrame(() => this.updateSliderPosition());
276
+ }
277
+
278
+ handleEnd() {
279
+ if (!this.state.isDragging) return
280
+
281
+ this.times.push({ time: Date.now(), position: this.getPosition() });
282
+ this.state.isDragging = false;
283
+
284
+ if (this.rafId) {
285
+ cancelAnimationFrame(this.rafId);
286
+ this.rafId = null;
287
+ }
288
+
289
+ this.verify();
290
+ }
291
+
292
+ handleVisibilityChange() {
293
+ const animationState = document.hidden ? 'paused' : 'running';
294
+ if (this.elements.fingerAnimation) {
295
+ this.elements.fingerAnimation.style.animationPlayState = animationState;
296
+ }
297
+ }
298
+
299
+ getClientX(e) {
300
+ return e.type.includes("touch") ? e.touches[0].clientX : e.clientX
301
+ }
302
+
303
+ setTransition(enabled) {
304
+ const transition = enabled ? "all 0.3s ease" : "none";
305
+ requestAnimationFrame(() => {
306
+ this.elements.btn.style.transition = transition;
307
+ this.elements.sliderImg.style.transition = transition;
308
+ });
309
+ }
310
+
311
+ // 优化:简化UI状态更新
312
+ updateUIState(state) {
313
+ const { elements } = this;
314
+ const updates = {
315
+ dragging: () => {
316
+ elements.hint.style.opacity = "0";
317
+ elements.fingerAnimation.style.display = "none";
318
+ },
319
+ success: () => {
320
+ Object.assign(elements.btn.style, { background: "var(--sc-success)" });
321
+ Object.assign(elements.icon.style, { innerHTML: "✓", color: "white" });
322
+ elements.icon.innerHTML = "✓";
323
+ },
324
+ fail: () => {
325
+ Object.assign(elements.btn.style, { background: "var(--sc-danger)" });
326
+ Object.assign(elements.icon.style, { innerHTML: "✗", color: "white" });
327
+ elements.icon.innerHTML = "✗";
328
+ },
329
+ reset: () => {
330
+ Object.assign(elements.btn.style, { background: "white" });
331
+ Object.assign(elements.icon.style, { color: "#666" });
332
+ elements.icon.innerHTML = "→";
333
+ elements.fingerAnimation.style.display = "block";
334
+ this.updateHintText("向右滑动完成验证", "var(--sc-text-light)");
335
+ },
336
+ loading: () => {
337
+ elements.hint.style.opacity = "0";
338
+ elements.fingerAnimation.style.display = "none";
339
+ Object.assign(elements.track.style, { pointerEvents: "none", opacity: "0.6" });
340
+ }
341
+ };
342
+
343
+ if (updates[state]) {
344
+ requestAnimationFrame(() => {
345
+ updates[state]();
346
+ if (state !== "loading") {
347
+ Object.assign(elements.track.style, { pointerEvents: "auto", opacity: "1" });
348
+ }
349
+ });
350
+ }
351
+ }
352
+
353
+ updateHintText(text, color) {
354
+ requestAnimationFrame(() => {
355
+ Object.assign(this.elements.hint, { textContent: text });
356
+ Object.assign(this.elements.hint.style, { color, opacity: "1" });
357
+ });
358
+ }
359
+
360
+ updateSliderPosition() {
361
+ const { elements, options, state } = this;
362
+ const { maxX } = this.getDimensions();
363
+ const pieceX = (state.currentX / maxX) * (options.width - options.sliderSize);
364
+ const progress = state.currentX / maxX;
365
+
366
+ requestAnimationFrame(() => {
367
+ elements.btn.style.transform = `translateX(${state.currentX}px)`;
368
+ elements.sliderImg.style.transform = `translateX(${pieceX}px)`;
369
+ elements.fingerAnimation.style.opacity = progress >= 0.8 ? "0" : "0.6";
370
+ });
371
+ }
372
+
373
+ // 优化:简化显示/隐藏逻辑
374
+ show() {
375
+ this.state.isVisible = true;
376
+ this.elements.overlay.style.display = "flex";
377
+ this.elements.overlay.offsetHeight; // 强制重绘
378
+
379
+ requestAnimationFrame(() => {
380
+ this.elements.overlay.classList.add("show");
381
+ this.elements.modal.classList.add("show");
382
+ });
383
+
384
+ this.loadCaptcha();
385
+ }
386
+
387
+ hide() {
388
+ this.state.isVisible = false;
389
+ this.elements.overlay.classList.remove("show");
390
+ this.elements.modal.classList.remove("show");
391
+
392
+ this.safeSetTimeout(() => {
393
+ this.elements.overlay.style.display = "none";
394
+ document.body.removeChild(this.elements.overlay);
395
+ this.reset();
396
+ this.options.onClose?.();
397
+ this.elements.overlay = null;
398
+ }, 300);
399
+ }
400
+
401
+ // 优化:简化加载逻辑
402
+ async loadCaptcha() {
403
+ try {
404
+ this.showLoading();
405
+ this.startTime = Date.now();
406
+
407
+ const controller = new AbortController();
408
+ const timeoutId = this.safeSetTimeout(() => controller.abort(), this.options.timeout);
409
+
410
+ const response = await fetch(this.options.apiUrl, {
411
+ method: "POST",
412
+ headers: { "Content-Type": "application/json" },
413
+ body: JSON.stringify({ place: 2, timestamp: Date.now() }),
414
+ signal: controller.signal
415
+ });
416
+
417
+ this.safeClearTimeout(timeoutId);
418
+
419
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
420
+ const data = await response.json();
421
+
422
+ if (data.data?.canvasSrc && data.data?.blockSrc) {
423
+ this.captchaData = data.data;
424
+ this.showCaptcha();
425
+ await this.renderCaptcha();
426
+ } else {
427
+ throw new Error("验证码数据格式错误")
428
+ }
429
+ } catch (error) {
430
+ const message = error.name === 'AbortError' ? "请求超时,请重试" : `加载验证码失败: ${error.message}`;
431
+ this.showError(message);
432
+ }
433
+ }
434
+
435
+ async renderCaptcha() {
436
+ return new Promise((resolve, reject) => {
437
+ let loadedCount = 0;
438
+ const onLoad = () => ++loadedCount === 2 && (this.hideLoading(), resolve());
439
+ const onError = () => reject(new Error("图片加载失败"));
440
+
441
+ this.loadImage(this.elements.backgroundImg, this.captchaData.canvasSrc, {
442
+ width: this.captchaData.canvasWidth,
443
+ height: this.captchaData.canvasHeight
444
+ }, onLoad, onError);
445
+
446
+ this.loadImage(this.elements.sliderImg, this.captchaData.blockSrc, {
447
+ width: this.captchaData.blockWidth,
448
+ height: this.captchaData.blockHeight,
449
+ top: this.captchaData.blockY
450
+ }, onLoad, onError);
451
+ })
452
+ }
453
+
454
+ loadImage(imgElement, src, styles, onLoad, onError) {
455
+ // 检查缓存
456
+ if (this.imageCache.has(src)) {
457
+ const cachedImg = this.imageCache.get(src);
458
+ imgElement.src = cachedImg.src;
459
+ this.applyStyles(imgElement, styles);
460
+ onLoad();
461
+ return
462
+ }
463
+
464
+ imgElement.onload = () => {
465
+ this.imageCache.set(src, imgElement.cloneNode());
466
+ onLoad();
467
+ };
468
+ imgElement.onerror = onError;
469
+ imgElement.src = src;
470
+ this.applyStyles(imgElement, styles);
471
+ }
472
+
473
+ // 优化:提取样式应用逻辑
474
+ applyStyles(element, styles) {
475
+ Object.entries(styles).forEach(([key, value]) => {
476
+ element.style[key] = typeof value === "number" ? value + "px" : value;
477
+ });
478
+ }
479
+
480
+ // 优化:简化状态显示方法
481
+ showLoading() {
482
+ this.state.isLoading = true;
483
+ this.batchUpdateStyles({
484
+ container: { display: "block" },
485
+ loadingText: { display: "flex" },
486
+ error: { display: "none" }
487
+ });
488
+ this.updateUIState("loading");
489
+ }
490
+
491
+ hideLoading() {
492
+ this.state.isLoading = false;
493
+ this.batchUpdateStyles({ loadingText: { display: "none" } });
494
+ this.updateUIState("reset");
495
+ }
496
+
497
+ showCaptcha() {
498
+ this.batchUpdateStyles({
499
+ container: { display: "block" },
500
+ track: { display: "block" },
501
+ error: { display: "none" }
502
+ });
503
+ }
504
+
505
+ showError(message) {
506
+ this.hideLoading();
507
+ this.batchUpdateStyles({
508
+ error: { display: "block", textContent: message }
509
+ });
510
+ }
511
+
512
+ // 优化:批量样式更新
513
+ batchUpdateStyles(updates) {
514
+ requestAnimationFrame(() => {
515
+ Object.entries(updates).forEach(([elementKey, styles]) => {
516
+ const element = this.elements[elementKey];
517
+ if (element) {
518
+ Object.entries(styles).forEach(([prop, value]) => {
519
+ if (prop === 'textContent') {
520
+ element.textContent = value;
521
+ } else {
522
+ element.style[prop] = value;
523
+ }
524
+ });
525
+ }
526
+ });
527
+ });
528
+ }
529
+
530
+ async verify() {
531
+ if (!this.captchaData) {
532
+ this.onVerifyFail("验证码数据丢失");
533
+ return
534
+ }
535
+
536
+ try {
537
+ const controller = new AbortController();
538
+ const timeoutId = this.safeSetTimeout(() => controller.abort(), this.options.timeout);
539
+
540
+ const response = await fetch(this.options.verifyUrl, {
541
+ method: "POST",
542
+ headers: { "Content-Type": "application/json" },
543
+ body: JSON.stringify({
544
+ loginVo: {
545
+ nonceStr: this.captchaData.nonceStr,
546
+ value: this.getPosition()
547
+ },
548
+ dragEventList: [...this.times]
549
+ }),
550
+ signal: controller.signal
551
+ });
552
+
553
+ this.safeClearTimeout(timeoutId);
554
+
555
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
556
+ const data = await response.json();
557
+
558
+ if (data.code === "0" || data.success === true) {
559
+ this.onVerifySuccess();
560
+ } else {
561
+ this.onVerifyFail(data.message || "验证失败,请重试!");
562
+ }
563
+ } catch (error) {
564
+ const message = error.name === 'AbortError' ? "验证超时,请重试" : "网络错误";
565
+ this.onVerifyFail(message);
566
+ }
567
+ }
568
+
569
+ onVerifySuccess() {
570
+ const duration = Date.now() - this.startTime;
571
+ const durationText = `验证成功!耗时:${(duration / 1000).toFixed(2)}s`;
572
+
573
+ this.updateUIState("success");
574
+ this.showFloatingTime(durationText, "success");
575
+
576
+ this.safeSetTimeout(() => {
577
+ this.options.onSuccess?.({
578
+ captchaId: this.captchaData.captchaId,
579
+ timestamp: Date.now(),
580
+ duration
581
+ });
582
+ this.hide();
583
+ }, 2000);
584
+ }
585
+
586
+ showFloatingTime(text, type = "success") {
587
+ const { elements } = this;
588
+ elements.floatingTime.textContent = text;
589
+ elements.floatingTime.className = `slider-captcha-floating-time ${type}`;
590
+
591
+ this.safeSetTimeout(() => elements.floatingTime.classList.add("show"), 100);
592
+ this.safeSetTimeout(() => {
593
+ elements.floatingTime.className = "slider-captcha-floating-time";
594
+ }, 2500); // 优化:延长显示时间,避免被reset清除
595
+ }
596
+
597
+ onVerifyFail(message) {
598
+ this.state.retryCount++;
599
+ this.updateUIState("fail");
600
+ this.showFloatingTime(message, "fail");
601
+
602
+ this.safeSetTimeout(() => {
603
+ if (this.state.retryCount >= this.options.maxRetries) {
604
+ this.refresh();
605
+ } else {
606
+ this.reset();
607
+ }
608
+ }, 2500); // 优化:延长等待时间,确保浮动提示完整显示
609
+ }
610
+
611
+ reset() {
612
+ this.clearAllTimers();
613
+
614
+ // 重置状态
615
+ Object.assign(this.state, {
616
+ isDragging: false,
617
+ currentX: 0,
618
+ startX: 0,
619
+ isLoading: false
620
+ });
621
+
622
+ this.times = [];
623
+ this.startTime = null;
624
+ this.cachedDimensions = null;
625
+
626
+ // 重置UI
627
+ requestAnimationFrame(() => {
628
+ this.setTransition(true);
629
+ this.elements.btn.style.transform = "translateX(0px)";
630
+ this.elements.sliderImg.style.transform = "translateX(0px)";
631
+ this.updateUIState("reset");
632
+ this.elements.error.style.display = "none";
633
+ });
634
+ }
635
+
636
+ refresh() {
637
+ this.reset();
638
+ this.state.retryCount = 0;
639
+ this.loadCaptcha();
640
+ }
641
+
642
+ // 安全的定时器管理
643
+ safeSetTimeout(callback, delay) {
644
+ const timerId = setTimeout(() => {
645
+ this.timers.delete(timerId);
646
+ callback();
647
+ }, delay);
648
+ this.timers.add(timerId);
649
+ return timerId
650
+ }
651
+
652
+ safeClearTimeout(timerId) {
653
+ if (timerId) {
654
+ clearTimeout(timerId);
655
+ this.timers.delete(timerId);
656
+ }
657
+ }
658
+
659
+ clearAllTimers() {
660
+ this.timers.forEach(timer => {
661
+ clearTimeout(timer);
662
+ clearInterval(timer);
663
+ });
664
+ this.timers.clear();
665
+
666
+ if (this.rafId) {
667
+ cancelAnimationFrame(this.rafId);
668
+ this.rafId = null;
669
+ }
670
+ }
671
+
672
+ // 清理图片资源
673
+ cleanupImages() {
674
+ if (this.elements.backgroundImg) {
675
+ this.elements.backgroundImg.src = '';
676
+ this.elements.backgroundImg.onload = null;
677
+ this.elements.backgroundImg.onerror = null;
678
+ }
679
+ if (this.elements.sliderImg) {
680
+ this.elements.sliderImg.src = '';
681
+ this.elements.sliderImg.onload = null;
682
+ this.elements.sliderImg.onerror = null;
683
+ }
684
+ this.imageCache.clear();
685
+ }
686
+
687
+ // 工具函数:节流
688
+ throttle(func, delay) {
689
+ let lastCall = 0;
690
+ return function (...args) {
691
+ const now = Date.now();
692
+ if (now - lastCall >= delay) {
693
+ lastCall = now;
694
+ return func.apply(this, args)
695
+ }
696
+ }
697
+ }
698
+
699
+ // 工具函数:防抖
700
+ debounce(func, delay) {
701
+ let timeoutId;
702
+ return function (...args) {
703
+ clearTimeout(timeoutId);
704
+ timeoutId = setTimeout(() => func.apply(this, args), delay);
705
+ }
706
+ }
707
+
708
+ destroy() {
709
+ // 恢复body样式
710
+ document.body.style.userSelect = "";
711
+ document.body.style.cursor = "";
712
+
713
+ // 清理所有定时器
714
+ this.clearAllTimers();
715
+
716
+ // 移除所有事件监听器
717
+ this.removeAllEventListeners();
718
+
719
+ // 清理图片资源
720
+ this.cleanupImages();
721
+
722
+ // 移除DOM元素
723
+ if (this.elements.overlay?.parentNode) {
724
+ this.elements.overlay.parentNode.removeChild(this.elements.overlay);
725
+ }
726
+
727
+ // 清理样式表
728
+ const styleElement = document.getElementById('slider-captcha-styles');
729
+ if (styleElement) {
730
+ styleElement.remove();
731
+ }
732
+
733
+ // 清空所有属性
734
+ Object.keys(this).forEach(key => {
735
+ if (key !== 'constructor') {
736
+ this[key] = null;
737
+ }
738
+ });
739
+ }
740
+
741
+ static create(options) {
742
+ return new PopupSliderCaptcha(options)
743
+ }
744
+
745
+ static show(options) {
746
+ const instance = new PopupSliderCaptcha(options);
747
+ instance.show();
748
+ return instance
749
+ }
750
+ }
751
+
752
+ // 模块导出
753
+ if (typeof module !== "undefined" && module.exports) {
754
+ module.exports = PopupSliderCaptcha;
755
+ module.exports.default = PopupSliderCaptcha;
756
+ } else if (typeof define === "function" && define.amd) {
757
+ define([], () => PopupSliderCaptcha);
758
+ } else if (typeof window !== "undefined") {
759
+ window.PopupSliderCaptcha = PopupSliderCaptcha;
760
+ window.SliderCaptcha = PopupSliderCaptcha;
761
+ }
762
+
763
+ export { PopupSliderCaptcha as default };