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,985 @@
1
+ import JSEncrypt from 'jsencrypt';
2
+
3
+ /**
4
+ * 纯JavaScript弹窗滑块验证码组件
5
+ */
6
+ class PopupSliderCaptcha {
7
+ static DEFAULTS = {
8
+ width: 320,
9
+ height: 155,
10
+ sliderSize: 38,
11
+ maxRetries: 3,
12
+ timeout: 30000,
13
+ apiUrl: "/api/captcha",
14
+ verifyUrl: "/api/captcha/verify",
15
+ throttleDelay: 16,
16
+ clickMaskClose: false
17
+ }
18
+
19
+ static CSS_CLASSES = {
20
+ overlay: "slider-captcha-overlay",
21
+ modal: "slider-captcha-modal",
22
+ header: "slider-captcha-header",
23
+ container: "slider-captcha-container",
24
+ track: "slider-captcha-track",
25
+ btn: "slider-captcha-btn",
26
+ hint: "slider-captcha-hint",
27
+ loading: "slider-captcha-loading",
28
+ error: "slider-captcha-error"
29
+ }
30
+
31
+ // 优化:提取CSS样式为独立方法,减少主体代码长度
32
+ static getStyles() {
33
+ 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}}`
34
+ }
35
+
36
+ constructor(options = {}) {
37
+ this.options = { ...PopupSliderCaptcha.DEFAULTS, ...options };
38
+ this.elements = {};
39
+ this.state = this.createInitialState();
40
+ this.captchaData = null;
41
+ this.times = [];
42
+ this.startTime = null;
43
+ this.eventListeners = [];
44
+ this.timers = new Set();
45
+ this.rafId = null;
46
+ this.cachedDimensions = null;
47
+ this.imageCache = new Map();
48
+
49
+ // 优化:使用箭头函数绑定this,避免重复bind调用
50
+ this.throttledHandleMove = this.throttle((e) => this.handleMove(e), this.options.throttleDelay);
51
+
52
+ this.init();
53
+ }
54
+
55
+ // 优化:提取初始状态创建为独立方法
56
+ createInitialState() {
57
+ return {
58
+ isVisible: false,
59
+ isDragging: false,
60
+ currentX: 0,
61
+ startX: 0,
62
+ retryCount: 0,
63
+ isLoading: false
64
+ }
65
+ }
66
+
67
+ init() {
68
+ this.injectStyles();
69
+ this.createElements();
70
+ this.bindEvents();
71
+ }
72
+
73
+ injectStyles() {
74
+ if (document.querySelector("#slider-captcha-styles")) return
75
+
76
+ const style = document.createElement("style");
77
+ style.id = "slider-captcha-styles";
78
+ style.textContent = PopupSliderCaptcha.getStyles();
79
+ document.head.appendChild(style);
80
+ }
81
+
82
+ // 优化:简化元素创建逻辑
83
+ createElements() {
84
+ const { elements, options } = this;
85
+
86
+ // 批量创建元素配置
87
+ const elementConfigs = [
88
+ ['overlay', 'div', PopupSliderCaptcha.CSS_CLASSES.overlay],
89
+ ['modal', 'div', PopupSliderCaptcha.CSS_CLASSES.modal],
90
+ ['header', 'div', PopupSliderCaptcha.CSS_CLASSES.header],
91
+ ['title', 'h3', 'slider-captcha-title', '安全验证'],
92
+ ['closeBtn', 'button', 'slider-captcha-close'],
93
+ ['refreshBtn', 'button', 'slider-captcha-refresh'],
94
+ ['container', 'div', PopupSliderCaptcha.CSS_CLASSES.container],
95
+ ['backgroundImg', 'img', 'slider-captcha-bg'],
96
+ ['sliderImg', 'img', 'slider-captcha-piece'],
97
+ ['loadingText', 'div', PopupSliderCaptcha.CSS_CLASSES.loading, '加载中...'],
98
+ ['floatingTime', 'div', 'slider-captcha-floating-time'],
99
+ ['track', 'div', PopupSliderCaptcha.CSS_CLASSES.track],
100
+ ['fingerAnimation', 'div', 'slider-captcha-finger', '👉'],
101
+ ['btn', 'div', PopupSliderCaptcha.CSS_CLASSES.btn],
102
+ ['icon', 'div', '', '→'],
103
+ ['hint', 'div', PopupSliderCaptcha.CSS_CLASSES.hint, '向右滑动完成验证'],
104
+ ['error', 'div', PopupSliderCaptcha.CSS_CLASSES.error]
105
+ ];
106
+
107
+ // 批量创建元素
108
+ elementConfigs.forEach(([key, tag, className, textContent]) => {
109
+ elements[key] = this.createElement(tag, className, textContent);
110
+ });
111
+
112
+ // 设置容器尺寸
113
+ elements.container.style.cssText = `width:${options.width}px;height:${options.height}px`;
114
+
115
+ // 添加刷新按钮图标
116
+ 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>`;
117
+
118
+ this.assembleDOM();
119
+ this.setInitialState();
120
+ }
121
+
122
+ createElement(tag, className = "", textContent = "") {
123
+ const element = document.createElement(tag);
124
+ if (className) element.className = className;
125
+ if (textContent) element.textContent = textContent;
126
+ return element
127
+ }
128
+
129
+ // 优化:简化DOM组装逻辑
130
+ assembleDOM() {
131
+ const { elements } = this;
132
+
133
+ // 组装头部
134
+ const headerButtons = this.createElement("div", "slider-captcha-header-buttons");
135
+ headerButtons.append(elements.refreshBtn, elements.closeBtn);
136
+ elements.header.append(elements.title, headerButtons);
137
+
138
+ // 组装验证码容器
139
+ elements.container.append(
140
+ elements.backgroundImg,
141
+ elements.sliderImg,
142
+ elements.loadingText,
143
+ elements.floatingTime
144
+ );
145
+
146
+ // 组装滑块轨道
147
+ elements.btn.appendChild(elements.icon);
148
+ elements.track.append(
149
+ elements.fingerAnimation,
150
+ elements.btn,
151
+ elements.hint
152
+ );
153
+
154
+ // 组装模态框
155
+ elements.modal.append(
156
+ elements.header,
157
+ elements.container,
158
+ elements.track,
159
+ elements.error
160
+ );
161
+
162
+ // 组装到覆盖层
163
+ elements.overlay.appendChild(elements.modal);
164
+ document.body.appendChild(elements.overlay);
165
+ }
166
+
167
+ setInitialState() {
168
+ // 批量设置初始状态
169
+ Object.assign(this.elements.container.style, { display: "none" });
170
+ Object.assign(this.elements.track.style, { display: "none" });
171
+ }
172
+
173
+ // 优化:简化事件绑定
174
+ bindEvents() {
175
+ const { elements } = this;
176
+
177
+ // 基础事件
178
+ this.addEventListener(elements.closeBtn, "click", () => this.hide());
179
+ this.addEventListener(elements.refreshBtn, "click", () => this.refresh());
180
+ this.addEventListener(elements.overlay, "click", (e) => {
181
+ if (e.target === elements.overlay && this.options.clickMaskClose) this.hide();
182
+ });
183
+ this.addEventListener(document, "keydown", (e) => {
184
+ if (e.key === "Escape" && this.state.isVisible) this.hide();
185
+ });
186
+ this.addEventListener(document, "visibilitychange", () => this.handleVisibilityChange());
187
+
188
+ this.bindSliderEvents();
189
+ }
190
+
191
+ bindSliderEvents() {
192
+ const { elements } = this;
193
+ const handlers = {
194
+ start: (e) => this.handleStart(e),
195
+ move: this.throttledHandleMove,
196
+ end: () => this.handleEnd()
197
+ };
198
+
199
+ // 滑块事件
200
+ this.addEventListener(elements.btn, "mousedown", handlers.start);
201
+ this.addEventListener(elements.btn, "touchstart", handlers.start);
202
+ this.addEventListener(elements.sliderImg, "mousedown", handlers.start);
203
+ this.addEventListener(elements.sliderImg, "touchstart", handlers.start);
204
+ this.addEventListener(document, "mousemove", handlers.move, { passive: false });
205
+ this.addEventListener(document, "touchmove", handlers.move, { passive: false });
206
+ this.addEventListener(document, "mouseup", handlers.end);
207
+ this.addEventListener(document, "touchend", handlers.end);
208
+ }
209
+
210
+ // 优化:改进事件管理
211
+ addEventListener(element, event, handler, options = {}) {
212
+ if (!element || typeof handler !== 'function') return
213
+
214
+ element.addEventListener(event, handler, options);
215
+ this.eventListeners.push({ element, event, handler, options });
216
+ }
217
+
218
+ removeAllEventListeners() {
219
+ this.eventListeners.forEach(({ element, event, handler, options }) => {
220
+ try {
221
+ element?.removeEventListener?.(event, handler, options);
222
+ } catch (error) {
223
+ console.warn('Failed to remove event listener:', error);
224
+ }
225
+ });
226
+ this.eventListeners.length = 0;
227
+ }
228
+
229
+ // 优化:缓存尺寸计算
230
+ getDimensions() {
231
+ if (!this.cachedDimensions) {
232
+ const trackWidth = this.elements.track.offsetWidth;
233
+ const btnWidth = this.elements.btn.offsetWidth;
234
+ this.cachedDimensions = {
235
+ trackWidth,
236
+ btnWidth,
237
+ maxX: trackWidth - btnWidth
238
+ };
239
+ }
240
+ return this.cachedDimensions
241
+ }
242
+
243
+ getPosition() {
244
+ const { maxX } = this.getDimensions();
245
+ const percentage = this.state.currentX / maxX;
246
+ return Math.round(percentage * (this.options.width - this.options.sliderSize))
247
+ }
248
+
249
+ // 优化:简化拖拽处理
250
+ handleStart(e) {
251
+ if (!this.captchaData || this.state.isDragging || this.state.isLoading) return
252
+
253
+ e.preventDefault();
254
+ this.state.isDragging = true;
255
+ this.state.startX = this.getClientX(e) - this.state.currentX;
256
+ this.startTime = this.startTime || Date.now();
257
+ this.times = [{ time: Date.now(), position: this.getPosition() }];
258
+
259
+ this.setTransition(false);
260
+ this.updateUIState("dragging");
261
+ this.cachedDimensions = null; // 清除缓存
262
+ }
263
+
264
+ handleMove(e) {
265
+ if (!this.state.isDragging) return
266
+ e.preventDefault();
267
+
268
+ const clientX = this.getClientX(e);
269
+ const deltaX = clientX - this.state.startX;
270
+ const { maxX } = this.getDimensions();
271
+
272
+ this.state.currentX = Math.max(0, Math.min(deltaX, maxX));
273
+ this.times.push({ time: Date.now(), position: this.getPosition() });
274
+
275
+ // 优化:使用RAF批量更新
276
+ this.rafId && cancelAnimationFrame(this.rafId);
277
+ this.rafId = requestAnimationFrame(() => this.updateSliderPosition());
278
+ }
279
+
280
+ handleEnd() {
281
+ if (!this.state.isDragging) return
282
+
283
+ this.times.push({ time: Date.now(), position: this.getPosition() });
284
+ this.state.isDragging = false;
285
+
286
+ if (this.rafId) {
287
+ cancelAnimationFrame(this.rafId);
288
+ this.rafId = null;
289
+ }
290
+
291
+ this.verify();
292
+ }
293
+
294
+ handleVisibilityChange() {
295
+ const animationState = document.hidden ? 'paused' : 'running';
296
+ if (this.elements.fingerAnimation) {
297
+ this.elements.fingerAnimation.style.animationPlayState = animationState;
298
+ }
299
+ }
300
+
301
+ getClientX(e) {
302
+ return e.type.includes("touch") ? e.touches[0].clientX : e.clientX
303
+ }
304
+
305
+ setTransition(enabled) {
306
+ const transition = enabled ? "all 0.3s ease" : "none";
307
+ requestAnimationFrame(() => {
308
+ this.elements.btn.style.transition = transition;
309
+ this.elements.sliderImg.style.transition = transition;
310
+ });
311
+ }
312
+
313
+ // 优化:简化UI状态更新
314
+ updateUIState(state) {
315
+ const { elements } = this;
316
+ const updates = {
317
+ dragging: () => {
318
+ elements.hint.style.opacity = "0";
319
+ elements.fingerAnimation.style.display = "none";
320
+ },
321
+ success: () => {
322
+ Object.assign(elements.btn.style, { background: "var(--sc-success)" });
323
+ Object.assign(elements.icon.style, { innerHTML: "✓", color: "white" });
324
+ elements.icon.innerHTML = "✓";
325
+ },
326
+ fail: () => {
327
+ Object.assign(elements.btn.style, { background: "var(--sc-danger)" });
328
+ Object.assign(elements.icon.style, { innerHTML: "✗", color: "white" });
329
+ elements.icon.innerHTML = "✗";
330
+ },
331
+ reset: () => {
332
+ Object.assign(elements.btn.style, { background: "white" });
333
+ Object.assign(elements.icon.style, { color: "#666" });
334
+ elements.icon.innerHTML = "→";
335
+ elements.fingerAnimation.style.display = "block";
336
+ this.updateHintText("向右滑动完成验证", "var(--sc-text-light)");
337
+ },
338
+ loading: () => {
339
+ elements.hint.style.opacity = "0";
340
+ elements.fingerAnimation.style.display = "none";
341
+ Object.assign(elements.track.style, { pointerEvents: "none", opacity: "0.6" });
342
+ }
343
+ };
344
+
345
+ if (updates[state]) {
346
+ requestAnimationFrame(() => {
347
+ updates[state]();
348
+ if (state !== "loading") {
349
+ Object.assign(elements.track.style, { pointerEvents: "auto", opacity: "1" });
350
+ }
351
+ });
352
+ }
353
+ }
354
+
355
+ updateHintText(text, color) {
356
+ requestAnimationFrame(() => {
357
+ Object.assign(this.elements.hint, { textContent: text });
358
+ Object.assign(this.elements.hint.style, { color, opacity: "1" });
359
+ });
360
+ }
361
+
362
+ updateSliderPosition() {
363
+ const { elements, options, state } = this;
364
+ const { maxX } = this.getDimensions();
365
+ const pieceX = (state.currentX / maxX) * (options.width - options.sliderSize);
366
+ const progress = state.currentX / maxX;
367
+
368
+ requestAnimationFrame(() => {
369
+ elements.btn.style.transform = `translateX(${state.currentX}px)`;
370
+ elements.sliderImg.style.transform = `translateX(${pieceX}px)`;
371
+ elements.fingerAnimation.style.opacity = progress >= 0.8 ? "0" : "0.6";
372
+ });
373
+ }
374
+
375
+ // 优化:简化显示/隐藏逻辑
376
+ show() {
377
+ this.state.isVisible = true;
378
+ this.elements.overlay.style.display = "flex";
379
+ this.elements.overlay.offsetHeight; // 强制重绘
380
+
381
+ requestAnimationFrame(() => {
382
+ this.elements.overlay.classList.add("show");
383
+ this.elements.modal.classList.add("show");
384
+ });
385
+
386
+ this.loadCaptcha();
387
+ }
388
+
389
+ hide() {
390
+ this.state.isVisible = false;
391
+ this.elements.overlay.classList.remove("show");
392
+ this.elements.modal.classList.remove("show");
393
+
394
+ this.safeSetTimeout(() => {
395
+ this.elements.overlay.style.display = "none";
396
+ document.body.removeChild(this.elements.overlay);
397
+ this.reset();
398
+ this.options.onClose?.();
399
+ this.elements.overlay = null;
400
+ }, 300);
401
+ }
402
+
403
+ // 优化:简化加载逻辑
404
+ async loadCaptcha() {
405
+ try {
406
+ this.showLoading();
407
+ this.startTime = Date.now();
408
+
409
+ const controller = new AbortController();
410
+ const timeoutId = this.safeSetTimeout(() => controller.abort(), this.options.timeout);
411
+
412
+ const response = await fetch(this.options.apiUrl, {
413
+ method: "POST",
414
+ headers: { "Content-Type": "application/json" },
415
+ body: JSON.stringify({ place: 2, timestamp: Date.now() }),
416
+ signal: controller.signal
417
+ });
418
+
419
+ this.safeClearTimeout(timeoutId);
420
+
421
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
422
+ const data = await response.json();
423
+
424
+ if (data.data?.canvasSrc && data.data?.blockSrc) {
425
+ this.captchaData = data.data;
426
+ this.showCaptcha();
427
+ await this.renderCaptcha();
428
+ } else {
429
+ throw new Error("验证码数据格式错误")
430
+ }
431
+ } catch (error) {
432
+ const message = error.name === 'AbortError' ? "请求超时,请重试" : `加载验证码失败: ${error.message}`;
433
+ this.showError(message);
434
+ }
435
+ }
436
+
437
+ async renderCaptcha() {
438
+ return new Promise((resolve, reject) => {
439
+ let loadedCount = 0;
440
+ const onLoad = () => ++loadedCount === 2 && (this.hideLoading(), resolve());
441
+ const onError = () => reject(new Error("图片加载失败"));
442
+
443
+ this.loadImage(this.elements.backgroundImg, this.captchaData.canvasSrc, {
444
+ width: this.captchaData.canvasWidth,
445
+ height: this.captchaData.canvasHeight
446
+ }, onLoad, onError);
447
+
448
+ this.loadImage(this.elements.sliderImg, this.captchaData.blockSrc, {
449
+ width: this.captchaData.blockWidth,
450
+ height: this.captchaData.blockHeight,
451
+ top: this.captchaData.blockY
452
+ }, onLoad, onError);
453
+ })
454
+ }
455
+
456
+ loadImage(imgElement, src, styles, onLoad, onError) {
457
+ // 检查缓存
458
+ if (this.imageCache.has(src)) {
459
+ const cachedImg = this.imageCache.get(src);
460
+ imgElement.src = cachedImg.src;
461
+ this.applyStyles(imgElement, styles);
462
+ onLoad();
463
+ return
464
+ }
465
+
466
+ imgElement.onload = () => {
467
+ this.imageCache.set(src, imgElement.cloneNode());
468
+ onLoad();
469
+ };
470
+ imgElement.onerror = onError;
471
+ imgElement.src = src;
472
+ this.applyStyles(imgElement, styles);
473
+ }
474
+
475
+ // 优化:提取样式应用逻辑
476
+ applyStyles(element, styles) {
477
+ Object.entries(styles).forEach(([key, value]) => {
478
+ element.style[key] = typeof value === "number" ? value + "px" : value;
479
+ });
480
+ }
481
+
482
+ // 优化:简化状态显示方法
483
+ showLoading() {
484
+ this.state.isLoading = true;
485
+ this.batchUpdateStyles({
486
+ container: { display: "block" },
487
+ loadingText: { display: "flex" },
488
+ error: { display: "none" }
489
+ });
490
+ this.updateUIState("loading");
491
+ }
492
+
493
+ hideLoading() {
494
+ this.state.isLoading = false;
495
+ this.batchUpdateStyles({ loadingText: { display: "none" } });
496
+ this.updateUIState("reset");
497
+ }
498
+
499
+ showCaptcha() {
500
+ this.batchUpdateStyles({
501
+ container: { display: "block" },
502
+ track: { display: "block" },
503
+ error: { display: "none" }
504
+ });
505
+ }
506
+
507
+ showError(message) {
508
+ this.hideLoading();
509
+ this.batchUpdateStyles({
510
+ error: { display: "block", textContent: message }
511
+ });
512
+ }
513
+
514
+ // 优化:批量样式更新
515
+ batchUpdateStyles(updates) {
516
+ requestAnimationFrame(() => {
517
+ Object.entries(updates).forEach(([elementKey, styles]) => {
518
+ const element = this.elements[elementKey];
519
+ if (element) {
520
+ Object.entries(styles).forEach(([prop, value]) => {
521
+ if (prop === 'textContent') {
522
+ element.textContent = value;
523
+ } else {
524
+ element.style[prop] = value;
525
+ }
526
+ });
527
+ }
528
+ });
529
+ });
530
+ }
531
+
532
+ async verify() {
533
+ if (!this.captchaData) {
534
+ this.onVerifyFail("验证码数据丢失");
535
+ return
536
+ }
537
+
538
+ try {
539
+ const controller = new AbortController();
540
+ const timeoutId = this.safeSetTimeout(() => controller.abort(), this.options.timeout);
541
+
542
+ const response = await fetch(this.options.verifyUrl, {
543
+ method: "POST",
544
+ headers: { "Content-Type": "application/json" },
545
+ body: JSON.stringify({
546
+ loginVo: {
547
+ nonceStr: this.captchaData.nonceStr,
548
+ value: this.getPosition()
549
+ },
550
+ dragEventList: [...this.times]
551
+ }),
552
+ signal: controller.signal
553
+ });
554
+
555
+ this.safeClearTimeout(timeoutId);
556
+
557
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
558
+ const data = await response.json();
559
+
560
+ if (data.code === "0" || data.success === true) {
561
+ this.onVerifySuccess();
562
+ } else {
563
+ this.onVerifyFail(data.message || "验证失败,请重试!");
564
+ }
565
+ } catch (error) {
566
+ const message = error.name === 'AbortError' ? "验证超时,请重试" : "网络错误";
567
+ this.onVerifyFail(message);
568
+ }
569
+ }
570
+
571
+ onVerifySuccess() {
572
+ const duration = Date.now() - this.startTime;
573
+ const durationText = `验证成功!耗时:${(duration / 1000).toFixed(2)}s`;
574
+
575
+ this.updateUIState("success");
576
+ this.showFloatingTime(durationText, "success");
577
+
578
+ this.safeSetTimeout(() => {
579
+ this.options.onSuccess?.({
580
+ captchaId: this.captchaData.captchaId,
581
+ timestamp: Date.now(),
582
+ duration
583
+ });
584
+ this.hide();
585
+ }, 2000);
586
+ }
587
+
588
+ showFloatingTime(text, type = "success") {
589
+ const { elements } = this;
590
+ elements.floatingTime.textContent = text;
591
+ elements.floatingTime.className = `slider-captcha-floating-time ${type}`;
592
+
593
+ this.safeSetTimeout(() => elements.floatingTime.classList.add("show"), 100);
594
+ this.safeSetTimeout(() => {
595
+ elements.floatingTime.className = "slider-captcha-floating-time";
596
+ }, 2500); // 优化:延长显示时间,避免被reset清除
597
+ }
598
+
599
+ onVerifyFail(message) {
600
+ this.state.retryCount++;
601
+ this.updateUIState("fail");
602
+ this.showFloatingTime(message, "fail");
603
+
604
+ this.safeSetTimeout(() => {
605
+ if (this.state.retryCount >= this.options.maxRetries) {
606
+ this.refresh();
607
+ } else {
608
+ this.reset();
609
+ }
610
+ }, 2500); // 优化:延长等待时间,确保浮动提示完整显示
611
+ }
612
+
613
+ reset() {
614
+ this.clearAllTimers();
615
+
616
+ // 重置状态
617
+ Object.assign(this.state, {
618
+ isDragging: false,
619
+ currentX: 0,
620
+ startX: 0,
621
+ isLoading: false
622
+ });
623
+
624
+ this.times = [];
625
+ this.startTime = null;
626
+ this.cachedDimensions = null;
627
+
628
+ // 重置UI
629
+ requestAnimationFrame(() => {
630
+ this.setTransition(true);
631
+ this.elements.btn.style.transform = "translateX(0px)";
632
+ this.elements.sliderImg.style.transform = "translateX(0px)";
633
+ this.updateUIState("reset");
634
+ this.elements.error.style.display = "none";
635
+ });
636
+ }
637
+
638
+ refresh() {
639
+ this.reset();
640
+ this.state.retryCount = 0;
641
+ this.loadCaptcha();
642
+ }
643
+
644
+ // 安全的定时器管理
645
+ safeSetTimeout(callback, delay) {
646
+ const timerId = setTimeout(() => {
647
+ this.timers.delete(timerId);
648
+ callback();
649
+ }, delay);
650
+ this.timers.add(timerId);
651
+ return timerId
652
+ }
653
+
654
+ safeClearTimeout(timerId) {
655
+ if (timerId) {
656
+ clearTimeout(timerId);
657
+ this.timers.delete(timerId);
658
+ }
659
+ }
660
+
661
+ clearAllTimers() {
662
+ this.timers.forEach(timer => {
663
+ clearTimeout(timer);
664
+ clearInterval(timer);
665
+ });
666
+ this.timers.clear();
667
+
668
+ if (this.rafId) {
669
+ cancelAnimationFrame(this.rafId);
670
+ this.rafId = null;
671
+ }
672
+ }
673
+
674
+ // 清理图片资源
675
+ cleanupImages() {
676
+ if (this.elements.backgroundImg) {
677
+ this.elements.backgroundImg.src = '';
678
+ this.elements.backgroundImg.onload = null;
679
+ this.elements.backgroundImg.onerror = null;
680
+ }
681
+ if (this.elements.sliderImg) {
682
+ this.elements.sliderImg.src = '';
683
+ this.elements.sliderImg.onload = null;
684
+ this.elements.sliderImg.onerror = null;
685
+ }
686
+ this.imageCache.clear();
687
+ }
688
+
689
+ // 工具函数:节流
690
+ throttle(func, delay) {
691
+ let lastCall = 0;
692
+ return function (...args) {
693
+ const now = Date.now();
694
+ if (now - lastCall >= delay) {
695
+ lastCall = now;
696
+ return func.apply(this, args)
697
+ }
698
+ }
699
+ }
700
+
701
+ // 工具函数:防抖
702
+ debounce(func, delay) {
703
+ let timeoutId;
704
+ return function (...args) {
705
+ clearTimeout(timeoutId);
706
+ timeoutId = setTimeout(() => func.apply(this, args), delay);
707
+ }
708
+ }
709
+
710
+ destroy() {
711
+ // 恢复body样式
712
+ document.body.style.userSelect = "";
713
+ document.body.style.cursor = "";
714
+
715
+ // 清理所有定时器
716
+ this.clearAllTimers();
717
+
718
+ // 移除所有事件监听器
719
+ this.removeAllEventListeners();
720
+
721
+ // 清理图片资源
722
+ this.cleanupImages();
723
+
724
+ // 移除DOM元素
725
+ if (this.elements.overlay?.parentNode) {
726
+ this.elements.overlay.parentNode.removeChild(this.elements.overlay);
727
+ }
728
+
729
+ // 清理样式表
730
+ const styleElement = document.getElementById('slider-captcha-styles');
731
+ if (styleElement) {
732
+ styleElement.remove();
733
+ }
734
+
735
+ // 清空所有属性
736
+ Object.keys(this).forEach(key => {
737
+ if (key !== 'constructor') {
738
+ this[key] = null;
739
+ }
740
+ });
741
+ }
742
+
743
+ static create(options) {
744
+ return new PopupSliderCaptcha(options)
745
+ }
746
+
747
+ static show(options) {
748
+ const instance = new PopupSliderCaptcha(options);
749
+ instance.show();
750
+ return instance
751
+ }
752
+ }
753
+
754
+ // 模块导出
755
+ if (typeof module !== "undefined" && module.exports) {
756
+ module.exports = PopupSliderCaptcha;
757
+ module.exports.default = PopupSliderCaptcha;
758
+ } else if (typeof define === "function" && define.amd) {
759
+ define([], () => PopupSliderCaptcha);
760
+ } else if (typeof window !== "undefined") {
761
+ window.PopupSliderCaptcha = PopupSliderCaptcha;
762
+ window.SliderCaptcha = PopupSliderCaptcha;
763
+ }
764
+
765
+ /**
766
+ * 密码校验工具类
767
+ * 提供密码加密和校验功能
768
+ */
769
+ class PasswordValidator {
770
+ constructor(options = {}) {
771
+ this.options = {
772
+ // 获取公钥的接口地址
773
+ publicKeyUrl: options.publicKeyUrl || '/microservice/strongPassword/getPublicKey',
774
+ // 密码校验接口地址
775
+ validateUrl: options.validateUrl || '/microservice/strongPassword/checkPassword',
776
+ // 请求超时时间
777
+ timeout: options.timeout || 10000,
778
+ // 自定义请求头
779
+ headers: options.headers || {},
780
+ ...options
781
+ };
782
+
783
+ // 缓存公钥,避免重复请求
784
+ this.publicKeyCache = null;
785
+ this.publicKeyExpiry = null;
786
+ }
787
+
788
+ /**
789
+ * 获取公钥
790
+ * @returns {Promise<string>} 公钥字符串
791
+ */
792
+ async getPublicKey() {
793
+ // 检查缓存是否有效(5分钟有效期)
794
+ if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
795
+ return this.publicKeyCache
796
+ }
797
+
798
+ try {
799
+ const response = await this.makeRequest(this.options.publicKeyUrl, {
800
+ method: 'GET'
801
+ });
802
+
803
+ if (response.code !== '0' ) {
804
+ throw new Error('获取公钥失败:' + (response.message || '未知错误'))
805
+ }
806
+
807
+ // 缓存公钥,设置5分钟过期时间
808
+ this.publicKeyCache = response.data;
809
+ this.publicKeyExpiry = Date.now() + 5 * 60 * 1000;
810
+
811
+ return this.publicKeyCache
812
+ } catch (error) {
813
+ console.error('获取公钥失败:', error);
814
+ throw new Error('获取公钥失败: ' + error.message)
815
+ }
816
+ }
817
+
818
+ /**
819
+ * 使用RSA公钥加密密码
820
+ * @param {string} password 原始密码
821
+ * @param {string} publicKey 公钥字符串
822
+ * @returns {string} 加密后的密码
823
+ */
824
+ encryptPassword(password, publicKey) {
825
+ try {
826
+ // 使用导入的JSEncrypt进行RSA加密
827
+ if (!JSEncrypt) {
828
+ throw new Error('JSEncrypt库未正确加载')
829
+ }
830
+
831
+ const encrypt = new JSEncrypt();
832
+ encrypt.setPublicKey(publicKey);
833
+ const encrypted = encrypt.encrypt(password);
834
+
835
+ if (!encrypted) {
836
+ throw new Error('密码加密失败')
837
+ }
838
+
839
+ return encrypted
840
+ } catch (error) {
841
+ console.error('密码加密失败:', error);
842
+ throw new Error('密码加密失败: ' + error.message)
843
+ }
844
+ }
845
+
846
+ /**
847
+ * 校验密码
848
+ * @param {string} password 原始密码
849
+ * @param {string} userName 用户
850
+ * @param {Object} additionalData 额外的校验数据
851
+ * @returns {Promise<Object>} 校验结果
852
+ */
853
+ async validatePassword(password, userName, additionalData = {}) {
854
+ try {
855
+ // 1. 获取公钥
856
+ const publicKey = await this.getPublicKey();
857
+
858
+ // 2. 加密密码
859
+ const encryptedPassword = this.encryptPassword(password, publicKey);
860
+
861
+ // 3. 调用校验接口
862
+ const response = await this.makeRequest(this.options.validateUrl, {
863
+ method: 'POST',
864
+ body: JSON.stringify({
865
+ userName: userName,
866
+ password: encryptedPassword,
867
+ timestamp: Date.now(),
868
+ ...additionalData
869
+ })
870
+ });
871
+
872
+ return {
873
+ ...response
874
+ }
875
+ } catch (error) {
876
+ console.error('密码校验失败:', error);
877
+ return {
878
+ success: false,
879
+ message: error.msg,
880
+ code: 'VALIDATION_ERROR'
881
+ }
882
+ }
883
+ }
884
+
885
+ /**
886
+ * 发送HTTP请求的通用方法
887
+ * @param {string} url 请求地址
888
+ * @param {Object} options 请求选项
889
+ * @returns {Promise<Object>} 响应结果
890
+ */
891
+ async makeRequest(url, options = {}) {
892
+ const controller = new AbortController();
893
+ const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
894
+
895
+ try {
896
+ const response = await fetch(url, {
897
+ ...options,
898
+ headers: {
899
+ 'Content-Type': 'application/json',
900
+ ...this.options.headers,
901
+ ...options.headers
902
+ },
903
+ signal: controller.signal
904
+ });
905
+
906
+ clearTimeout(timeoutId);
907
+
908
+ if (!response.ok) {
909
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
910
+ }
911
+
912
+ return await response.json()
913
+ } catch (error) {
914
+ clearTimeout(timeoutId);
915
+
916
+ if (error.name === 'AbortError') {
917
+ throw new Error('请求超时')
918
+ }
919
+
920
+ throw error
921
+ }
922
+ }
923
+
924
+ /**
925
+ * 清除公钥缓存
926
+ */
927
+ clearCache() {
928
+ this.publicKeyCache = null;
929
+ this.publicKeyExpiry = null;
930
+ }
931
+
932
+ /**
933
+ * 更新配置
934
+ * @param {Object} newOptions 新的配置选项
935
+ */
936
+ updateOptions(newOptions) {
937
+ this.options = { ...this.options, ...newOptions };
938
+ // 清除缓存,使用新配置重新获取
939
+ this.clearCache();
940
+ }
941
+ }
942
+
943
+ /**
944
+ * 创建密码校验器实例的工厂函数
945
+ * @param {Object} options 配置选项
946
+ * @returns {PasswordValidator} 密码校验器实例
947
+ */
948
+ function createPasswordValidator(options) {
949
+ return new PasswordValidator(options)
950
+ }
951
+
952
+ /**
953
+ * 快速校验密码的便捷函数
954
+ * @param {string} password 密码
955
+ * @param {Object} options 配置选项
956
+ * @param {Object} additionalData 额外数据
957
+ * @returns {Promise<Object>} 校验结果
958
+ */
959
+ async function validatePassword(password, options = {}, additionalData = {}) {
960
+ const validator = new PasswordValidator(options);
961
+ // 修复:添加userName参数,可以从additionalData中提取或设为空字符串
962
+ const userName = additionalData.userName || '';
963
+ return await validator.validatePassword(password, userName, additionalData)
964
+ }
965
+
966
+ // 导入滑块验证码组件(使用默认导入)
967
+
968
+ // 默认导出(向后兼容)
969
+ var index = {
970
+ PopupSliderCaptcha,
971
+ PasswordValidator,
972
+ createPasswordValidator,
973
+ validatePassword
974
+ };
975
+
976
+ // 全局注册(用于UMD构建)
977
+ if (typeof window !== 'undefined') {
978
+ window.SliderCaptcha = PopupSliderCaptcha;
979
+ window.PopupSliderCaptcha = PopupSliderCaptcha;
980
+ window.PasswordValidator = PasswordValidator;
981
+ window.createPasswordValidator = createPasswordValidator;
982
+ window.validatePassword = validatePassword;
983
+ }
984
+
985
+ export { PasswordValidator, PopupSliderCaptcha, createPasswordValidator, index as default, validatePassword };