slider-captcha-sdk 1.0.20 → 1.0.22

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/dist/index.esm.js CHANGED
@@ -1,1303 +1 @@
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
- // 优化:添加常量定义,提高可维护性
37
- static CONSTANTS = {
38
- CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
39
- MAX_RETRY_ATTEMPTS: 3,
40
- DEFAULT_TIMEOUT: 30000,
41
- FLOATING_TIME_DURATION: 2500,
42
- THROTTLE_DELAY: 16
43
- }
44
-
45
- static ERROR_TYPES = {
46
- NETWORK_ERROR: 'NETWORK_ERROR',
47
- TIMEOUT_ERROR: 'TIMEOUT_ERROR',
48
- VALIDATION_ERROR: 'VALIDATION_ERROR',
49
- IMAGE_LOAD_ERROR: 'IMAGE_LOAD_ERROR',
50
- CAPTCHA_DATA_ERROR: 'CAPTCHA_DATA_ERROR'
51
- }
52
-
53
- constructor(options = {}) {
54
- this.options = {
55
- ...PopupSliderCaptcha.DEFAULTS,
56
- ...options,
57
- timeout: options.timeout || PopupSliderCaptcha.CONSTANTS.DEFAULT_TIMEOUT,
58
- maxRetries: options.maxRetries || PopupSliderCaptcha.CONSTANTS.MAX_RETRY_ATTEMPTS,
59
- throttleDelay: options.throttleDelay || PopupSliderCaptcha.CONSTANTS.THROTTLE_DELAY
60
- };
61
-
62
- this.elements = {};
63
- this.state = this.createInitialState();
64
- this.captchaData = null;
65
- this.times = [];
66
- this.startTime = null;
67
- this.eventListeners = [];
68
- this.timers = new Set();
69
- this.rafId = null;
70
- this.cachedDimensions = null;
71
- this.imageCache = new Map();
72
- this.abortController = null;
73
-
74
- this.throttledHandleMove = this.throttle((e) => this.handleMove(e), this.options.throttleDelay);
75
-
76
- try {
77
- this.init();
78
- } catch (error) {
79
- console.error('滑块验证码初始化失败:', error);
80
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR, error.message);
81
- }
82
- }
83
-
84
- createInitialState() {
85
- return {
86
- isVisible: false,
87
- isDragging: false,
88
- currentX: 0,
89
- startX: 0,
90
- retryCount: 0,
91
- isLoading: false
92
- }
93
- }
94
-
95
- init() {
96
- this.injectStyles();
97
- this.createElements();
98
- this.bindEvents();
99
- }
100
-
101
- injectStyles() {
102
- if (document.querySelector('#slider-captcha-styles')) { return }
103
-
104
- const style = document.createElement('style');
105
- style.id = 'slider-captcha-styles';
106
- style.textContent = PopupSliderCaptcha.getStyles();
107
- document.head.appendChild(style);
108
- }
109
-
110
- createElements() {
111
- const { elements, options } = this;
112
-
113
- // 批量创建元素配置
114
- const elementConfigs = [
115
- ['overlay', 'div', PopupSliderCaptcha.CSS_CLASSES.overlay],
116
- ['modal', 'div', PopupSliderCaptcha.CSS_CLASSES.modal],
117
- ['header', 'div', PopupSliderCaptcha.CSS_CLASSES.header],
118
- ['title', 'h3', 'slider-captcha-title', '安全验证'],
119
- ['closeBtn', 'button', 'slider-captcha-close'],
120
- ['refreshBtn', 'button', 'slider-captcha-refresh'],
121
- ['container', 'div', PopupSliderCaptcha.CSS_CLASSES.container],
122
- ['backgroundImg', 'img', 'slider-captcha-bg'],
123
- ['sliderImg', 'img', 'slider-captcha-piece'],
124
- ['loadingText', 'div', PopupSliderCaptcha.CSS_CLASSES.loading, '加载中...'],
125
- ['floatingTime', 'div', 'slider-captcha-floating-time'],
126
- ['track', 'div', PopupSliderCaptcha.CSS_CLASSES.track],
127
- ['fingerAnimation', 'div', 'slider-captcha-finger', '👉'],
128
- ['btn', 'div', PopupSliderCaptcha.CSS_CLASSES.btn],
129
- ['icon', 'div', '', '→'],
130
- ['hint', 'div', PopupSliderCaptcha.CSS_CLASSES.hint, '向右滑动完成验证'],
131
- ['error', 'div', PopupSliderCaptcha.CSS_CLASSES.error]
132
- ];
133
-
134
- // 批量创建元素
135
- elementConfigs.forEach(([key, tag, className, textContent]) => {
136
- elements[key] = this.createElement(tag, className, textContent);
137
- });
138
-
139
- // 设置容器尺寸
140
- elements.container.style.cssText = `width:${options.width}px;height:${options.height}px`;
141
-
142
- // 添加刷新按钮图标
143
- 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>`;
144
-
145
- this.assembleDOM();
146
- this.setInitialState();
147
- }
148
-
149
- createElement(tag, className = '', textContent = '') {
150
- const element = document.createElement(tag);
151
- if (className) { element.className = className; }
152
- if (textContent) { element.textContent = textContent; }
153
- return element
154
- }
155
-
156
- assembleDOM() {
157
- const { elements } = this;
158
-
159
- // 组装头部
160
- const headerButtons = this.createElement('div', 'slider-captcha-header-buttons');
161
- headerButtons.append(elements.refreshBtn, elements.closeBtn);
162
- elements.header.append(elements.title, headerButtons);
163
-
164
- // 组装验证码容器
165
- elements.container.append(
166
- elements.backgroundImg,
167
- elements.sliderImg,
168
- elements.loadingText,
169
- elements.floatingTime
170
- );
171
-
172
- // 组装滑块轨道
173
- elements.btn.appendChild(elements.icon);
174
- elements.track.append(elements.fingerAnimation, elements.btn, elements.hint);
175
-
176
- // 组装模态框
177
- elements.modal.append(elements.header, elements.container, elements.track, elements.error);
178
-
179
- // 组装到覆盖层
180
- elements.overlay.appendChild(elements.modal);
181
- document.body.appendChild(elements.overlay);
182
- }
183
-
184
- setInitialState() {
185
- // 批量设置初始状态
186
- Object.assign(this.elements.container.style, { display: 'none' });
187
- Object.assign(this.elements.track.style, { display: 'none' });
188
- }
189
-
190
- bindEvents() {
191
- const { elements } = this;
192
-
193
- // 基础事件
194
- this.addEventListener(elements.closeBtn, 'click', () => this.hide());
195
- this.addEventListener(elements.refreshBtn, 'click', () => this.refresh());
196
- this.addEventListener(elements.overlay, 'click', (e) => {
197
- if (e.target === elements.overlay && this.options.clickMaskClose) { this.hide(); }
198
- });
199
- this.addEventListener(document, 'keydown', (e) => {
200
- if (e.key === 'Escape' && this.state.isVisible) { this.hide(); }
201
- });
202
- this.addEventListener(document, 'visibilitychange', () => this.handleVisibilityChange());
203
-
204
- this.bindSliderEvents();
205
- }
206
-
207
- bindSliderEvents() {
208
- const { elements } = this;
209
- const handlers = {
210
- start: (e) => this.handleStart(e),
211
- move: this.throttledHandleMove,
212
- end: () => this.handleEnd()
213
- };
214
-
215
- // 滑块事件
216
- this.addEventListener(elements.btn, 'mousedown', handlers.start);
217
- this.addEventListener(elements.btn, 'touchstart', handlers.start);
218
- this.addEventListener(elements.sliderImg, 'mousedown', handlers.start);
219
- this.addEventListener(elements.sliderImg, 'touchstart', handlers.start);
220
- this.addEventListener(document, 'mousemove', handlers.move, { passive: false });
221
- this.addEventListener(document, 'touchmove', handlers.move, { passive: false });
222
- this.addEventListener(document, 'mouseup', handlers.end);
223
- this.addEventListener(document, 'touchend', handlers.end);
224
- }
225
-
226
- addEventListener(element, event, handler, options = {}) {
227
- if (!element || typeof handler !== 'function') { return }
228
-
229
- element.addEventListener(event, handler, options);
230
- this.eventListeners.push({ element, event, handler, options });
231
- }
232
-
233
- removeAllEventListeners() {
234
- this.eventListeners.forEach(({ element, event, handler, options }) => {
235
- try {
236
- element?.removeEventListener?.(event, handler, options);
237
- } catch (error) {
238
- console.warn('Failed to remove event listener:', error);
239
- }
240
- });
241
- this.eventListeners.length = 0;
242
- }
243
-
244
- getDimensions() {
245
- if (!this.cachedDimensions) {
246
- const trackWidth = this.elements.track.offsetWidth;
247
- const btnWidth = this.elements.btn.offsetWidth;
248
- this.cachedDimensions = {
249
- trackWidth,
250
- btnWidth,
251
- maxX: trackWidth - btnWidth
252
- };
253
- }
254
- return this.cachedDimensions
255
- }
256
-
257
- getPosition() {
258
- const { maxX } = this.getDimensions();
259
- const percentage = this.state.currentX / maxX;
260
- return Math.round(percentage * (this.options.width - this.options.sliderSize))
261
- }
262
-
263
- handleStart(e) {
264
- if (!this.captchaData || this.state.isDragging || this.state.isLoading) { return }
265
-
266
- e.preventDefault();
267
- this.state.isDragging = true;
268
- this.state.startX = this.getClientX(e) - this.state.currentX;
269
- this.dragStartTime = Date.now();
270
- this.times = [{ time: Date.now(), position: this.getPosition() }];
271
-
272
- this.setTransition(false);
273
- this.updateUIState('dragging');
274
- this.cachedDimensions = null; // 清除缓存
275
- }
276
-
277
- handleMove(e) {
278
- if (!this.state.isDragging) { return }
279
- e.preventDefault();
280
-
281
- const clientX = this.getClientX(e);
282
- const deltaX = clientX - this.state.startX;
283
- const { maxX } = this.getDimensions();
284
-
285
- this.state.currentX = Math.max(0, Math.min(deltaX, maxX));
286
- this.times.push({ time: Date.now(), position: this.getPosition() });
287
-
288
- // 优化:使用RAF批量更新
289
- if (this.rafId) {
290
- cancelAnimationFrame(this.rafId);
291
- }
292
- this.rafId = requestAnimationFrame(() => this.updateSliderPosition());
293
- }
294
-
295
- handleEnd() {
296
- if (!this.state.isDragging) { return }
297
-
298
- this.times.push({ time: Date.now(), position: this.getPosition() });
299
- this.state.isDragging = false;
300
-
301
- if (this.rafId) {
302
- cancelAnimationFrame(this.rafId);
303
- this.rafId = null;
304
- }
305
-
306
- this.verify();
307
- }
308
-
309
- handleVisibilityChange() {
310
- const animationState = document.hidden ? 'paused' : 'running';
311
- if (this.elements.fingerAnimation) {
312
- this.elements.fingerAnimation.style.animationPlayState = animationState;
313
- }
314
- }
315
-
316
- getClientX(e) {
317
- return e.type.includes('touch') ? e.touches[0].clientX : e.clientX
318
- }
319
-
320
- setTransition(enabled) {
321
- const transition = enabled ? 'all 0.3s ease' : 'none';
322
- requestAnimationFrame(() => {
323
- this.elements.btn.style.transition = transition;
324
- this.elements.sliderImg.style.transition = transition;
325
- });
326
- }
327
-
328
- // 优化:简化UI状态更新
329
- updateUIState(state) {
330
- const { elements } = this;
331
- const updates = {
332
- dragging: () => {
333
- elements.hint.style.opacity = '0';
334
- elements.fingerAnimation.style.display = 'none';
335
- },
336
- success: () => {
337
- Object.assign(elements.btn.style, { background: 'var(--sc-success)' });
338
- Object.assign(elements.icon.style, { innerHTML: '✓', color: 'white' });
339
- elements.icon.innerHTML = '✓';
340
- },
341
- fail: () => {
342
- Object.assign(elements.btn.style, { background: 'var(--sc-danger)' });
343
- Object.assign(elements.icon.style, { innerHTML: '✗', color: 'white' });
344
- elements.icon.innerHTML = '✗';
345
- },
346
- reset: () => {
347
- Object.assign(elements.btn.style, { background: 'white' });
348
- Object.assign(elements.icon.style, { color: '#666' });
349
- elements.icon.innerHTML = '→';
350
- elements.fingerAnimation.style.display = 'block';
351
- this.updateHintText('向右滑动完成验证', 'var(--sc-text-light)');
352
- },
353
- loading: () => {
354
- elements.hint.style.opacity = '0';
355
- elements.fingerAnimation.style.display = 'none';
356
- Object.assign(elements.track.style, { pointerEvents: 'none', opacity: '0.6' });
357
- }
358
- };
359
-
360
- if (updates[state]) {
361
- requestAnimationFrame(() => {
362
- updates[state]();
363
- if (state !== 'loading') {
364
- Object.assign(elements.track.style, { pointerEvents: 'auto', opacity: '1' });
365
- }
366
- });
367
- }
368
- }
369
-
370
- updateHintText(text, color) {
371
- requestAnimationFrame(() => {
372
- Object.assign(this.elements.hint, { textContent: text });
373
- Object.assign(this.elements.hint.style, { color, opacity: '1' });
374
- });
375
- }
376
-
377
- updateSliderPosition() {
378
- const { elements, options, state } = this;
379
- const { maxX } = this.getDimensions();
380
- const pieceX = (state.currentX / maxX) * (options.width - options.sliderSize);
381
- const progress = state.currentX / maxX;
382
-
383
- requestAnimationFrame(() => {
384
- elements.btn.style.transform = `translateX(${state.currentX}px)`;
385
- elements.sliderImg.style.transform = `translateX(${pieceX}px)`;
386
- elements.fingerAnimation.style.opacity = progress >= 0.8 ? '0' : '0.6';
387
- });
388
- }
389
-
390
- // 优化:简化显示/隐藏逻辑
391
- show() {
392
- this.state.isVisible = true;
393
- this.elements.overlay.style.display = 'flex';
394
-
395
- requestAnimationFrame(() => {
396
- this.elements.overlay.classList.add('show');
397
- this.elements.modal.classList.add('show');
398
- });
399
-
400
- this.loadCaptcha();
401
- }
402
-
403
- hide() {
404
- this.state.isVisible = false;
405
- this.elements.overlay.classList.remove('show');
406
- this.elements.modal.classList.remove('show');
407
-
408
- this.safeSetTimeout(() => {
409
- this.elements.overlay.style.display = 'none';
410
- document.body.removeChild(this.elements.overlay);
411
- this.reset();
412
- this.options.onClose?.();
413
- this.elements.overlay = null;
414
- }, 300);
415
- }
416
-
417
- // 优化:添加统一错误处理方法
418
- handleError(errorType, message, originalError = null) {
419
- const errorMessages = {
420
- [PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
421
- [PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
422
- [PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]: '验证失败,请重试',
423
- [PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]: '图片加载失败,请刷新重试',
424
- [PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]: '验证码数据错误,请刷新重试'
425
- };
426
-
427
- const errorMessage = errorMessages[errorType] || message || '未知错误';
428
-
429
- // 调用用户自定义错误处理
430
- if (this.options.onError) {
431
- this.options.onError({
432
- type: errorType,
433
- message: errorMessage,
434
- originalError
435
- });
436
- }
437
-
438
- this.showError(errorMessage);
439
- console.error(`滑块验证码错误 [${errorType}]:`, errorMessage, originalError);
440
- }
441
-
442
- // 优化:简化加载逻辑,增强错误处理
443
- async loadCaptcha() {
444
- try {
445
- this.showLoading();
446
- this.startTime = Date.now();
447
-
448
- // 取消之前的请求
449
- if (this.abortController) {
450
- this.abortController.abort();
451
- }
452
- this.abortController = new AbortController();
453
-
454
- const response = await fetch(this.options.apiUrl, {
455
- method: 'POST',
456
- headers: {
457
- 'Content-Type': 'application/json',
458
- ...this.options.headers
459
- },
460
- body: JSON.stringify({
461
- timestamp: Date.now(),
462
- ...this.options.requestData
463
- }),
464
- signal: this.abortController.signal
465
- });
466
-
467
- if (!response.ok) {
468
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
469
- }
470
-
471
- const data = await response.json();
472
-
473
- // 优化:更严格的数据验证
474
- if (!this.validateCaptchaData(data)) {
475
- throw new Error('验证码数据格式错误')
476
- }
477
-
478
- this.captchaData = data.data;
479
- this.showCaptcha();
480
- await this.renderCaptcha();
481
- } catch (error) {
482
- if (error.name === 'AbortError') {
483
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '请求被取消');
484
- } else if (
485
- error.message.includes('Failed to fetch') ||
486
- error.message.includes('NetworkError')
487
- ) {
488
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
489
- } else {
490
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR, error.message, error);
491
- }
492
- }
493
- }
494
-
495
- // 优化:添加验证码数据验证方法
496
- validateCaptchaData(data) {
497
- if (!data || typeof data !== 'object') { return false }
498
-
499
- const requiredFields = [
500
- 'canvasSrc',
501
- 'blockSrc',
502
- 'canvasWidth',
503
- 'canvasHeight',
504
- 'blockWidth',
505
- 'blockHeight',
506
- 'blockY',
507
- 'nonceStr'
508
- ];
509
- const dataObj = data.data || data;
510
-
511
- return requiredFields.every((field) => {
512
- const value = dataObj[field];
513
- return value !== null && value !== undefined && value !== ''
514
- })
515
- }
516
-
517
- async renderCaptcha() {
518
- return new Promise((resolve, reject) => {
519
- let hasError = false;
520
-
521
- // const _onLoad = () => {
522
- // if (hasError) { return }
523
- // loadedCount++
524
- // if (loadedCount === 2) {
525
- // this.hideLoading()
526
- // resolve()
527
- // }
528
- // }
529
- //
530
- // const _onError = (error) => {
531
- // if (hasError) { return }
532
- // hasError = true
533
- // this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR, '图片加载失败', error)
534
- // reject(new Error('图片加载失败'))
535
- // }
536
-
537
- // 优化:并行加载图片,提高性能
538
- const loadPromises = [
539
- this.loadImageAsync(this.elements.backgroundImg, this.captchaData.canvasSrc, {
540
- width: this.captchaData.canvasWidth,
541
- height: this.captchaData.canvasHeight
542
- }),
543
- this.loadImageAsync(this.elements.sliderImg, this.captchaData.blockSrc, {
544
- width: this.captchaData.blockWidth,
545
- height: this.captchaData.blockHeight,
546
- top: this.captchaData.blockY
547
- })
548
- ];
549
-
550
- Promise.all(loadPromises)
551
- .then(() => {
552
- if (!hasError) {
553
- this.hideLoading();
554
- resolve();
555
- }
556
- })
557
- .catch((error) => {
558
- if (!hasError) {
559
- hasError = true;
560
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR, '图片加载失败', error);
561
- reject(error);
562
- }
563
- });
564
- })
565
- }
566
-
567
- // 优化:添加异步图片加载方法
568
- async loadImageAsync(imgElement, src, styles) {
569
- return new Promise((resolve, reject) => {
570
- // 检查缓存
571
- if (this.imageCache.has(src)) {
572
- const cachedImg = this.imageCache.get(src);
573
- imgElement.src = cachedImg.src;
574
- this.applyStyles(imgElement, styles);
575
- resolve();
576
- return
577
- }
578
-
579
- const timeoutId = this.safeSetTimeout(() => {
580
- reject(new Error('图片加载超时'));
581
- }, 10000); // 10秒超时
582
-
583
- imgElement.onload = function onImageLoad() {
584
- this.safeClearTimeout(timeoutId);
585
- this.imageCache.set(src, imgElement.cloneNode());
586
- resolve();
587
- }.bind(this);
588
-
589
- imgElement.onerror = function onImageError(error) {
590
- this.safeClearTimeout(timeoutId);
591
- reject(error);
592
- }.bind(this);
593
-
594
- imgElement.src = src;
595
- this.applyStyles(imgElement, styles);
596
- })
597
- }
598
-
599
- // 优化:提取样式应用逻辑
600
- applyStyles(element, styles) {
601
- Object.entries(styles).forEach(([key, value]) => {
602
- element.style[key] = typeof value === 'number' ? `${value}px` : value;
603
- });
604
- }
605
-
606
- // 优化:简化状态显示方法
607
- showLoading() {
608
- this.state.isLoading = true;
609
- this.batchUpdateStyles({
610
- container: { display: 'block' },
611
- loadingText: { display: 'flex' },
612
- error: { display: 'none' }
613
- });
614
- this.updateUIState('loading');
615
- }
616
-
617
- hideLoading() {
618
- this.state.isLoading = false;
619
- this.batchUpdateStyles({ loadingText: { display: 'none' } });
620
- this.updateUIState('reset');
621
- }
622
-
623
- showCaptcha() {
624
- this.batchUpdateStyles({
625
- container: { display: 'block' },
626
- track: { display: 'block' },
627
- error: { display: 'none' }
628
- });
629
- }
630
-
631
- showError(message) {
632
- this.hideLoading();
633
- this.batchUpdateStyles({
634
- error: { display: 'block', textContent: message }
635
- });
636
- }
637
-
638
- // 优化:批量样式更新
639
- batchUpdateStyles(updates) {
640
- requestAnimationFrame(() => {
641
- Object.entries(updates).forEach(([elementKey, styles]) => {
642
- const element = this.elements[elementKey];
643
- if (element) {
644
- Object.entries(styles).forEach(([prop, value]) => {
645
- if (prop === 'textContent') {
646
- element.textContent = value;
647
- } else {
648
- element.style[prop] = value;
649
- }
650
- });
651
- }
652
- });
653
- });
654
- }
655
-
656
- async verify() {
657
- if (!this.captchaData) {
658
- this.onVerifyFail('验证码数据丢失');
659
- return
660
- }
661
-
662
- try {
663
- // 取消之前的验证请求
664
- if (this.abortController) {
665
- this.abortController.abort();
666
- }
667
- this.abortController = new AbortController();
668
-
669
- const response = await fetch(this.options.verifyUrl, {
670
- method: 'POST',
671
- headers: {
672
- 'Content-Type': 'application/json',
673
- ...this.options.headers
674
- },
675
- body: JSON.stringify({
676
- loginVo: {
677
- nonceStr: this.captchaData.nonceStr,
678
- value: this.getPosition()
679
- },
680
- dragEventList: [...this.times],
681
- ...this.options.verifyData
682
- }),
683
- signal: this.abortController.signal
684
- });
685
-
686
- if (!response.ok) {
687
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
688
- }
689
-
690
- const data = await response.json();
691
-
692
- // 优化:更灵活的验证结果判断
693
- if (this.isVerifySuccess(data)) {
694
- this.onVerifySuccess(data.data || data.result);
695
- } else {
696
- this.onVerifyFail(data.message || data.msg || '验证失败,请重试!');
697
- }
698
- } catch (error) {
699
- if (error.name === 'AbortError') {
700
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '验证请求被取消');
701
- } else if (
702
- error.message.includes('Failed to fetch') ||
703
- error.message.includes('NetworkError')
704
- ) {
705
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
706
- } else {
707
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR, error.message, error);
708
- }
709
- }
710
- }
711
-
712
- // 优化:添加验证成功判断方法
713
- isVerifySuccess(data) {
714
- if (!data || typeof data !== 'object') { return false }
715
-
716
- // 支持多种成功标识
717
- const successIndicators = [
718
- data.code === '0',
719
- data.code === 0,
720
- data.success === true,
721
- data.status === 'success',
722
- data.result === true
723
- ];
724
-
725
- return successIndicators.some((indicator) => indicator === true)
726
- }
727
-
728
- onVerifySuccess(ticket) {
729
- const duration = this.dragStartTime ?
730
- Date.now() - this.dragStartTime :
731
- Date.now() - this.startTime;
732
- const durationText = `验证成功!耗时:${(duration / 1000).toFixed(2)}s`;
733
-
734
- this.updateUIState('success');
735
- this.showFloatingTime(durationText, 'success');
736
-
737
- this.safeSetTimeout(() => {
738
- this.options.onSuccess?.({
739
- ticket,
740
- timestamp: Date.now(),
741
- duration
742
- });
743
- this.hide();
744
- }, 2000);
745
- }
746
-
747
- showFloatingTime(text, type = 'success') {
748
- const { elements } = this;
749
- elements.floatingTime.textContent = text;
750
- elements.floatingTime.className = `slider-captcha-floating-time ${type}`;
751
-
752
- this.safeSetTimeout(() => elements.floatingTime.classList.add('show'), 100);
753
- this.safeSetTimeout(() => {
754
- elements.floatingTime.className = 'slider-captcha-floating-time';
755
- }, 2500); // 优化:延长显示时间,避免被reset清除
756
- }
757
-
758
- onVerifyFail(message) {
759
- this.state.retryCount++;
760
- this.updateUIState('fail');
761
- this.showFloatingTime(message, 'fail');
762
-
763
- this.safeSetTimeout(() => {
764
- if (this.state.retryCount >= this.options.maxRetries) {
765
- this.refresh();
766
- } else {
767
- this.reset();
768
- }
769
- }, 2500); // 优化:延长等待时间,确保浮动提示完整显示
770
- }
771
-
772
- reset() {
773
- this.clearAllTimers();
774
-
775
- // 重置状态
776
- Object.assign(this.state, {
777
- isDragging: false,
778
- currentX: 0,
779
- startX: 0,
780
- isLoading: false
781
- });
782
-
783
- this.times = [];
784
- this.startTime = null;
785
- this.dragStartTime = null; // 优化:重置拖拽开始时间
786
- this.cachedDimensions = null;
787
-
788
- // 重置UI
789
- requestAnimationFrame(() => {
790
- this.setTransition(true);
791
- this.elements.btn.style.transform = 'translateX(0px)';
792
- this.elements.sliderImg.style.transform = 'translateX(0px)';
793
- this.updateUIState('reset');
794
- this.elements.error.style.display = 'none';
795
- });
796
- }
797
-
798
- refresh() {
799
- this.reset();
800
- this.state.retryCount = 0;
801
- this.loadCaptcha();
802
- }
803
-
804
- // 安全的定时器管理
805
- safeSetTimeout(callback, delay) {
806
- const timerId = setTimeout(() => {
807
- this.timers.delete(timerId);
808
- callback();
809
- }, delay);
810
- this.timers.add(timerId);
811
- return timerId
812
- }
813
-
814
- safeClearTimeout(timerId) {
815
- if (timerId) {
816
- clearTimeout(timerId);
817
- this.timers.delete(timerId);
818
- }
819
- }
820
-
821
- clearAllTimers() {
822
- this.timers.forEach((timer) => {
823
- clearTimeout(timer);
824
- clearInterval(timer);
825
- });
826
- this.timers.clear();
827
-
828
- if (this.rafId) {
829
- cancelAnimationFrame(this.rafId);
830
- this.rafId = null;
831
- }
832
- }
833
-
834
- // 清理图片资源
835
- cleanupImages() {
836
- if (this.elements.backgroundImg) {
837
- this.elements.backgroundImg.src = '';
838
- this.elements.backgroundImg.onload = null;
839
- this.elements.backgroundImg.onerror = null;
840
- }
841
- if (this.elements.sliderImg) {
842
- this.elements.sliderImg.src = '';
843
- this.elements.sliderImg.onload = null;
844
- this.elements.sliderImg.onerror = null;
845
- }
846
- this.imageCache.clear();
847
- }
848
-
849
- // 工具函数:节流
850
- throttle(func, delay) {
851
- let lastCall = 0;
852
- const throttledFunction = (...args) => {
853
- const now = Date.now();
854
- if (now - lastCall >= delay) {
855
- lastCall = now;
856
- return func.apply(this, args)
857
- }
858
- return undefined
859
- };
860
- return throttledFunction
861
- }
862
-
863
- destroy() {
864
- try {
865
- // 取消所有进行中的请求
866
- if (this.abortController) {
867
- this.abortController.abort();
868
- this.abortController = null;
869
- }
870
-
871
- // 恢复body样式
872
- if (document.body) {
873
- document.body.style.userSelect = '';
874
- document.body.style.cursor = '';
875
- }
876
-
877
- // 清理所有定时器
878
- this.clearAllTimers();
879
-
880
- // 移除所有事件监听器
881
- this.removeAllEventListeners();
882
-
883
- // 清理图片资源
884
- this.cleanupImages();
885
-
886
- // 移除DOM元素
887
- if (this.elements?.overlay?.parentNode) {
888
- this.elements.overlay.parentNode.removeChild(this.elements.overlay);
889
- }
890
-
891
- // 清理样式表
892
- const styleElement = document.getElementById('slider-captcha-styles');
893
- if (styleElement) {
894
- styleElement.remove();
895
- }
896
-
897
- // 清空缓存
898
- this.imageCache.clear();
899
- this.cachedDimensions = null;
900
-
901
- // 清空所有属性
902
- Object.keys(this).forEach((key) => {
903
- if (key !== 'constructor') {
904
- this[key] = null;
905
- }
906
- });
907
-
908
- // 调用销毁回调
909
- if (this.options.onDestroy) {
910
- this.options.onDestroy();
911
- }
912
- } catch (error) {
913
- console.error('销毁滑块验证码时出错:', error);
914
- }
915
- }
916
-
917
- static create(options) {
918
- return new PopupSliderCaptcha(options)
919
- }
920
-
921
- static show(options) {
922
- const instance = new PopupSliderCaptcha(options);
923
- instance.show();
924
- return instance
925
- }
926
- }
927
-
928
- // 模块导出
929
- if (typeof module !== 'undefined' && module.exports) {
930
- module.exports = PopupSliderCaptcha;
931
- module.exports.default = PopupSliderCaptcha;
932
- } else if (typeof define === 'function' && define.amd) {
933
- define([], () => PopupSliderCaptcha);
934
- } else if (typeof window !== 'undefined') {
935
- window.PopupSliderCaptcha = PopupSliderCaptcha;
936
- window.SliderCaptcha = PopupSliderCaptcha;
937
- }
938
-
939
- // Add ES6 export for modern module systems
940
- var PopupSliderCaptcha$1 = PopupSliderCaptcha;
941
-
942
- /**
943
- * 密码校验工具类
944
- * 提供密码加密和校验功能
945
- */
946
- class PasswordValidator {
947
- static CONSTANTS = {
948
- DEFAULT_TIMEOUT: 10000,
949
- CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
950
- MIN_PASSWORD_LENGTH: 1
951
- }
952
-
953
- static ERROR_TYPES = {
954
- NETWORK_ERROR: 'NETWORK_ERROR',
955
- TIMEOUT_ERROR: 'TIMEOUT_ERROR',
956
- ENCRYPTION_ERROR: 'ENCRYPTION_ERROR',
957
- VALIDATION_ERROR: 'VALIDATION_ERROR',
958
- PUBLIC_KEY_ERROR: 'PUBLIC_KEY_ERROR'
959
- }
960
-
961
- constructor(options = {}) {
962
- this.options = {
963
- publicKeyUrl: options.publicKeyUrl || '/microservice/strongPassword/getPublicKey',
964
- validateUrl: options.validateUrl || '/microservice/strongPassword/checkPassword',
965
- timeout: options.timeout || PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,
966
- headers: options.headers || {},
967
- cacheDuration: options.cacheDuration || PasswordValidator.CONSTANTS.CACHE_DURATION,
968
- ...options
969
- };
970
-
971
- // 缓存公钥,避免重复请求
972
- this.publicKeyCache = null;
973
- this.publicKeyExpiry = null;
974
- }
975
-
976
- /**
977
- * 获取公钥
978
- * @returns {Promise<string>} 公钥字符串
979
- */
980
- async getPublicKey() {
981
- // 检查缓存是否有效
982
- if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
983
- return this.publicKeyCache
984
- }
985
-
986
- try {
987
- const response = await this.makeRequest(this.options.publicKeyUrl, {
988
- method: 'GET'
989
- });
990
-
991
- if (!this.isSuccessResponse(response)) {
992
- throw new Error(`获取公钥失败:${response.message || response.msg || '未知错误'}`)
993
- }
994
-
995
- // 验证公钥格式
996
- const publicKey = response.data || response.result;
997
- if (!publicKey) {
998
- throw new Error('公钥为空')
999
- }
1000
-
1001
- // 缓存公钥
1002
- this.publicKeyCache = publicKey;
1003
- this.publicKeyExpiry = Date.now() + this.options.cacheDuration;
1004
-
1005
- return this.publicKeyCache
1006
- } catch (error) {
1007
- console.error('获取公钥失败:', error);
1008
- this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR, error.message, error);
1009
- throw new Error(`获取公钥失败: ${error.message}`)
1010
- }
1011
- }
1012
-
1013
- // 优化:添加响应成功判断方法
1014
- isSuccessResponse(response) {
1015
- if (!response || typeof response !== 'object') {
1016
- return false
1017
- }
1018
-
1019
- const successIndicators = [
1020
- response.code === '0',
1021
- response.code === 0,
1022
- response.success === true,
1023
- response.status === 'success',
1024
- response.result === true
1025
- ];
1026
-
1027
- return successIndicators.some((indicator) => indicator === true)
1028
- }
1029
-
1030
- handleError(errorType, message, originalError = null) {
1031
- const errorMessages = {
1032
- [PasswordValidator.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
1033
- [PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
1034
- [PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]: '密码加密失败',
1035
- [PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]: '密码验证失败',
1036
- [PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]: '获取公钥失败'
1037
- };
1038
-
1039
- const errorMessage = errorMessages[errorType] || message || '未知错误';
1040
-
1041
- // 调用用户自定义错误处理
1042
- if (this.options.onError) {
1043
- this.options.onError({
1044
- type: errorType,
1045
- message: errorMessage,
1046
- originalError
1047
- });
1048
- }
1049
-
1050
- console.error(`密码校验器错误 [${errorType}]:`, errorMessage, originalError);
1051
- }
1052
-
1053
- /**
1054
- * 使用RSA公钥加密密码
1055
- * @param {string} password 原始密码
1056
- * @param {string} publicKey 公钥字符串
1057
- * @returns {string} 加密后的密码
1058
- */
1059
- encryptPassword(password, publicKey) {
1060
- try {
1061
- if (!password) {
1062
- throw new Error('密码不能为空')
1063
- }
1064
-
1065
- if (password.length < PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH) {
1066
- throw new Error('密码长度不足')
1067
- }
1068
-
1069
- if (!publicKey) {
1070
- throw new Error('公钥不能为空')
1071
- }
1072
-
1073
- if (typeof JSEncrypt === 'undefined') {
1074
- throw new Error('JSEncrypt库未正确加载,请确保已引入JSEncrypt库')
1075
- }
1076
-
1077
- const encrypt = new JSEncrypt();
1078
- encrypt.setPublicKey(publicKey);
1079
- const encrypted = encrypt.encrypt(password);
1080
-
1081
- if (!encrypted) {
1082
- throw new Error('密码加密失败,可能是公钥格式不正确')
1083
- }
1084
-
1085
- return encrypted
1086
- } catch (error) {
1087
- console.error('密码加密失败:', error);
1088
- this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR, error.message, error);
1089
- throw new Error(`密码加密失败: ${error.message}`)
1090
- }
1091
- }
1092
-
1093
- /**
1094
- * 校验密码
1095
- * @param {string} password 原始密码
1096
- * @param {string} userName 用户名
1097
- * @param {Object} additionalData 额外的校验数据
1098
- * @returns {Promise<Object>} 校验结果
1099
- */
1100
- async validatePassword(password, userName, additionalData = {}) {
1101
- try {
1102
- if (!password || !userName) {
1103
- throw new Error('用户名密码不能为空')
1104
- }
1105
-
1106
- // 1. 获取公钥
1107
- const publicKey = await this.getPublicKey();
1108
-
1109
- // 2. 加密密码
1110
- const encryptedPassword = this.encryptPassword(password, publicKey);
1111
-
1112
- // 3. 调用校验接口
1113
- const response = await this.makeRequest(this.options.validateUrl, {
1114
- method: 'POST',
1115
- body: JSON.stringify({
1116
- userName,
1117
- password: encryptedPassword,
1118
- timestamp: Date.now(),
1119
- ...additionalData
1120
- })
1121
- });
1122
-
1123
- return {
1124
- success: this.isSuccessResponse(response),
1125
- data: response.data || response.result,
1126
- message: response.message || response.msg || '验证完成',
1127
- code: response.code || response.status,
1128
- originalResponse: response
1129
- }
1130
- } catch (error) {
1131
- console.error('密码校验失败:', error);
1132
-
1133
- // 优化:根据错误类型返回不同的错误信息
1134
- let errorType = PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;
1135
- if (error.message.includes('网络') || error.message.includes('fetch')) {
1136
- errorType = PasswordValidator.ERROR_TYPES.NETWORK_ERROR;
1137
- } else if (error.message.includes('超时') || error.message.includes('timeout')) {
1138
- errorType = PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR;
1139
- } else if (error.message.includes('加密')) {
1140
- errorType = PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR;
1141
- }
1142
-
1143
- this.handleError(errorType, error.message, error);
1144
-
1145
- return {
1146
- success: false,
1147
- message: error.message || '密码校验失败',
1148
- code: errorType,
1149
- error
1150
- }
1151
- }
1152
- }
1153
-
1154
- /**
1155
- * 发送HTTP请求的通用方法
1156
- * @param {string} url 请求地址
1157
- * @param {Object} options 请求选项
1158
- * @returns {Promise<Object>} 响应结果
1159
- */
1160
- async makeRequest(url, options = {}) {
1161
- // 优化:为每个请求创建独立的AbortController,避免多个请求互相取消
1162
- const abortController = new AbortController();
1163
-
1164
- const timeoutId = setTimeout(() => {
1165
- abortController.abort();
1166
- }, this.options.timeout);
1167
-
1168
- try {
1169
- const response = await fetch(url, {
1170
- ...options,
1171
- headers: {
1172
- 'Content-Type': 'application/json',
1173
- ...this.options.headers,
1174
- ...options.headers
1175
- },
1176
- signal: abortController.signal
1177
- });
1178
-
1179
- clearTimeout(timeoutId);
1180
-
1181
- if (!response.ok) {
1182
- throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
1183
- }
1184
-
1185
- const data = await response.json();
1186
-
1187
- // 优化:记录请求日志(开发环境)
1188
- if (this.options.debug) {
1189
- console.log('密码校验器请求:', { url, options, response: data });
1190
- }
1191
-
1192
- return data
1193
- } catch (error) {
1194
- clearTimeout(timeoutId);
1195
-
1196
- if (error.name === 'AbortError') {
1197
- this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR, '请求超时', error);
1198
- throw new Error('请求超时')
1199
- }
1200
-
1201
- if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
1202
- this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
1203
- throw new Error('网络连接失败')
1204
- }
1205
-
1206
- throw error
1207
- }
1208
- }
1209
-
1210
- /**
1211
- * 清除公钥缓存
1212
- */
1213
- clearCache() {
1214
- this.publicKeyCache = null;
1215
- this.publicKeyExpiry = null;
1216
- }
1217
-
1218
- /**
1219
- * 更新配置
1220
- * @param {Object} newOptions 新的配置选项
1221
- */
1222
- updateOptions(newOptions) {
1223
- this.options = { ...this.options, ...newOptions };
1224
- // 清除缓存,使用新配置重新获取
1225
- this.clearCache();
1226
- }
1227
-
1228
- /**
1229
- * 获取缓存状态
1230
- * @returns {Object} 缓存状态信息
1231
- */
1232
- getCacheStatus() {
1233
- return {
1234
- hasCache: Boolean(this.publicKeyCache),
1235
- isExpired: this.publicKeyExpiry ? Date.now() > this.publicKeyExpiry : true,
1236
- expiryTime: this.publicKeyExpiry,
1237
- remainingTime: this.publicKeyExpiry ? Math.max(0, this.publicKeyExpiry - Date.now()) : 0
1238
- }
1239
- }
1240
-
1241
- /**
1242
- * 销毁实例,清理资源
1243
- */
1244
- destroy() {
1245
- try {
1246
- // 清除缓存
1247
- this.clearCache();
1248
-
1249
- // 调用销毁回调
1250
- if (this.options.onDestroy) {
1251
- this.options.onDestroy();
1252
- }
1253
- } catch (error) {
1254
- console.error('销毁密码校验器时出错:', error);
1255
- }
1256
- }
1257
- }
1258
-
1259
- // 导出类和创建实例的工厂函数
1260
- var PasswordValidator$1 = PasswordValidator;
1261
-
1262
- /**
1263
- * 创建密码校验器实例的工厂函数
1264
- * @param {Object} options 配置选项
1265
- * @returns {PasswordValidator} 密码校验器实例
1266
- */
1267
- function createPasswordValidator(options) {
1268
- return new PasswordValidator(options)
1269
- }
1270
-
1271
- /**
1272
- * 快速校验密码的便捷函数
1273
- * @param {string} password 密码
1274
- * @param {string} userName 用户名
1275
- * @param {Object} options 配置选项
1276
- * @param {Object} additionalData 额外数据
1277
- * @returns {Promise<Object>} 校验结果
1278
- */
1279
- function validatePassword(password, userName, options = {}, additionalData = {}) {
1280
- const validator = new PasswordValidator(options);
1281
- return validator.validatePassword(password, userName, additionalData)
1282
- }
1283
-
1284
- // 导入滑块验证码组件(使用默认导入)
1285
-
1286
- // 默认导出(向后兼容)
1287
- var index = {
1288
- PopupSliderCaptcha: PopupSliderCaptcha$1,
1289
- PasswordValidator: PasswordValidator$1,
1290
- createPasswordValidator,
1291
- validatePassword
1292
- };
1293
-
1294
- // 全局注册(用于UMD构建)
1295
- if (typeof window !== 'undefined') {
1296
- window.SliderCaptcha = PopupSliderCaptcha$1;
1297
- window.PopupSliderCaptcha = PopupSliderCaptcha$1;
1298
- window.PasswordValidator = PasswordValidator$1;
1299
- window.createPasswordValidator = createPasswordValidator;
1300
- window.validatePassword = validatePassword;
1301
- }
1302
-
1303
- export { PasswordValidator$1 as PasswordValidator, PopupSliderCaptcha$1 as PopupSliderCaptcha, createPasswordValidator, index as default, validatePassword };
1
+ function t(t){return new PasswordValidator(t)}function i(t,i,e={},s={}){return new PasswordValidator(e).validatePassword(t,i,s)}var e,s,r;import JSEncrypt from"jsencrypt";class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/externalapi/commonservice/captcha/get",verifyUrl:"/externalapi/commonservice/captcha/check",baseUrl:"",throttleDelay:16,clickMaskClose:!1};static CSS_CLASSES={overlay:"slider-captcha-overlay",modal:"slider-captcha-modal",header:"slider-captcha-header",container:"slider-captcha-container",track:"slider-captcha-track",btn:"slider-captcha-btn",hint:"slider-captcha-hint",loading:"slider-captcha-loading",error:"slider-captcha-error"};static getStyles(){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;line-height:12px;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:4px 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}}"}static ERROR_TYPES={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT_ERROR:"TIMEOUT_ERROR",VALIDATION_ERROR:"VALIDATION_ERROR",IMAGE_LOAD_ERROR:"IMAGE_LOAD_ERROR",CAPTCHA_DATA_ERROR:"CAPTCHA_DATA_ERROR"};constructor(t={}){this.options={...PopupSliderCaptcha.DEFAULTS,...t},this.options.apiUrl=this.normalizeUrl(this.options.apiUrl,this.options.baseUrl),this.options.verifyUrl=this.normalizeUrl(this.options.verifyUrl,this.options.baseUrl);const{elements:i={},state:e=this.createInitialState(),captchaData:s=null,times:r=[],startTime:a=null,eventListeners:o=[],timers:n=new Set,rafId:h=null,cachedDimensions:c=null,imageCache:l=new Map,abortController:d=null}=this;Object.assign(this,{elements:i,state:e,captchaData:s,times:r,startTime:a,eventListeners:o,timers:n,rafId:h,cachedDimensions:c,imageCache:l,abortController:d}),this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay);try{this.init()}catch(t){this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR,t.message)}}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}normalizeUrl(t,i){return t?/^https?:\/\//.test(t)||!i?t:(i.endsWith("/")?i:i+"/")+(t.startsWith("/")?t.substring(1):t):t}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=Object.assign(document.createElement("style"),{id:"slider-captcha-styles",textContent:PopupSliderCaptcha.getStyles()});document.head.appendChild(t)}createElements(){const{elements:t,options:i}=this,e={overlay:["div",PopupSliderCaptcha.CSS_CLASSES.overlay],modal:["div",PopupSliderCaptcha.CSS_CLASSES.modal],header:["div",PopupSliderCaptcha.CSS_CLASSES.header],title:["h3","slider-captcha-title","安全验证"],closeBtn:["button","slider-captcha-close"],refreshBtn:["button","slider-captcha-refresh"],container:["div",PopupSliderCaptcha.CSS_CLASSES.container],backgroundImg:["img","slider-captcha-bg"],sliderImg:["img","slider-captcha-piece"],loadingText:["div",PopupSliderCaptcha.CSS_CLASSES.loading,"加载中..."],floatingTime:["div","slider-captcha-floating-time"],track:["div",PopupSliderCaptcha.CSS_CLASSES.track],fingerAnimation:["div","slider-captcha-finger","👉"],btn:["div",PopupSliderCaptcha.CSS_CLASSES.btn],icon:["div","","→"],hint:["div",PopupSliderCaptcha.CSS_CLASSES.hint,"向右滑动完成验证"],error:["div",PopupSliderCaptcha.CSS_CLASSES.error]};Object.entries(e).forEach(([i,[e,s,r]])=>{t[i]=this.createElement(e,s,r)}),t.container.style.cssText=`width:${i.width}px;height:${i.height}px`,t.refreshBtn.innerHTML='\n <svg viewBox="0 0 24 24">\n <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"/>\n </svg>\n ',this.assembleDOM(),this.setInitialState()}createElement(t,i="",e=""){return Object.assign(document.createElement(t),{className:i,textContent:e})}assembleDOM(){const{elements:t}=this,i=this.createElement("div","slider-captcha-header-buttons");i.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,i),t.container.append(t.backgroundImg,t.sliderImg,t.loadingText,t.floatingTime),t.btn.appendChild(t.icon),t.track.append(t.fingerAnimation,t.btn,t.hint),t.modal.append(t.header,t.container,t.track,t.error),t.overlay.appendChild(t.modal),document.body.appendChild(t.overlay)}setInitialState(){Object.assign(this.elements.container.style,{display:"none"}),Object.assign(this.elements.track.style,{display:"none"})}bindEvents(){const{elements:t}=this;[[t.closeBtn,"click",()=>this.hide()],[t.refreshBtn,"click",()=>this.refresh()],[t.overlay,"click",i=>{i.target===t.overlay&&this.options.clickMaskClose&&this.hide()}],[document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}],[document,"visibilitychange",()=>this.handleVisibilityChange()]].forEach(([t,i,e])=>{this.addEventListener(t,i,e)}),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,i={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};[[t.btn,"mousedown",i.start],[t.btn,"touchstart",i.start],[t.sliderImg,"mousedown",i.start],[t.sliderImg,"touchstart",i.start],[document,"mousemove",i.move,{passive:!1}],[document,"touchmove",i.move,{passive:!1}],[document,"mouseup",i.end],[document,"touchend",i.end]].forEach(([t,i,e,s])=>{this.addEventListener(t,i,e,s)})}addEventListener(t,i,e,s={}){t&&"function"==typeof e&&(t.addEventListener(i,e,s),this.eventListeners.push({element:t,event:i,handler:e,options:s}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:i,handler:e,options:s})=>{try{t?.removeEventListener?.(i,e,s)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const{track:t,btn:i}=this.elements;this.cachedDimensions={trackWidth:t.offsetWidth,btnWidth:i.offsetWidth,get maxX(){return this.trackWidth-this.btnWidth}}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),i=this.state.currentX/t;return Math.round(i*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),Object.assign(this.state,{isDragging:!0,startX:this.getClientX(t)-this.state.currentX}),this.dragStartTime=Date.now(),this.times=[{time:Date.now(),position:this.getPosition()}],this.setTransition(!1),this.updateUIState("dragging"),this.cachedDimensions=null)}handleMove(t){if(!this.state.isDragging)return;t.preventDefault();const i=this.getClientX(t)-this.state.startX,{maxX:e}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(i,e)),this.times.push({time:Date.now(),position:this.getPosition()}),this.rafId&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(()=>this.updateSliderPosition())}handleEnd(){this.state.isDragging&&(this.times.push({time:Date.now(),position:this.getPosition()}),this.state.isDragging=!1,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.verify())}handleVisibilityChange(){const t=document.hidden?"paused":"running";this.elements.fingerAnimation?.style&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX=t=>t.type.includes("touch")?t.touches[0].clientX:t.clientX;setTransition(t){const i=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{const{btn:t,sliderImg:e}=this.elements;t.style.transition=e.style.transition=i})}updateUIState(t){const{elements:i}=this,e={dragging:()=>{i.hint.style.opacity="0",i.fingerAnimation.style.display="none"},success:()=>{Object.assign(i.btn.style,{background:"var(--sc-success)"}),Object.assign(i.icon.style,{color:"white"}),i.icon.textContent="✓"},fail:()=>{Object.assign(i.btn.style,{background:"var(--sc-danger)"}),Object.assign(i.icon.style,{color:"white"}),i.icon.textContent="✗"},reset:()=>{Object.assign(i.btn.style,{background:"white"}),Object.assign(i.icon.style,{color:"#666"}),i.icon.textContent="→",i.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{i.hint.style.opacity="0",i.fingerAnimation.style.display="none",Object.assign(i.track.style,{pointerEvents:"none",opacity:"0.6"})}};e[t]&&requestAnimationFrame(()=>{e[t](),"loading"!==t&&Object.assign(i.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,i){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:i,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:i,state:e}=this,{maxX:s}=this.getDimensions(),r=e.currentX/s*(i.width-i.sliderSize),a=e.currentX/s;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${e.currentX}px)`,t.sliderImg.style.transform=`translateX(${r}px)`,t.fingerAnimation.style.opacity=.8>a?"0.6":"0"})}show(){this.state.isVisible=!0,this.elements.overlay.style.display="flex",requestAnimationFrame(()=>{this.elements.overlay.classList.add("show"),this.elements.modal.classList.add("show")}),this.loadCaptcha()}hide(){this.state.isVisible=!1,this.elements.overlay.classList.remove("show"),this.elements.modal.classList.remove("show"),this.safeSetTimeout(()=>{this.elements.overlay.style.display="none",document.body.removeChild(this.elements.overlay),this.reset(),this.options.onClose?.(),this.elements.overlay=null},300)}handleError(t,i,e=null){const s={[PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]:"网络连接失败,请检查网络设置",[PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]:"请求超时,请重试",[PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]:"验证失败,请重试",[PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]:"图片加载失败,请刷新重试",[PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]:"验证码数据错误,请刷新重试"}[t]||i||"未知错误";this.options.onError&&this.options.onError({type:t,message:s,originalError:e}),this.showError(s)}async loadCaptcha(){try{this.showLoading(),this.startTime=Date.now(),this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await fetch(this.options.apiUrl,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({timestamp:Date.now(),...this.options.requestData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const i=await t.json();if(!this.validateCaptchaData(i))throw Error("验证码数据格式错误");this.captchaData=i.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"请求被取消"):t.message.includes("Failed to fetch")||t.message.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败"):this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR,t.message,t)}}validateCaptchaData(t){if(!t||"object"!=typeof t)return!1;const i=t.data||t;return["canvasSrc","blockSrc","canvasWidth","canvasHeight","blockWidth","blockHeight","blockY","nonceStr"].every(t=>{const e=i[t];return null!=e&&""!==e})}renderCaptcha(){return new Promise((t,i)=>{let e=!1;const s=[this.loadImageAsync(this.elements.backgroundImg,this.captchaData.canvasSrc,{width:this.captchaData.canvasWidth,height:this.captchaData.canvasHeight}),this.loadImageAsync(this.elements.sliderImg,this.captchaData.blockSrc,{width:this.captchaData.blockWidth,height:this.captchaData.blockHeight,top:this.captchaData.blockY})];Promise.all(s).then(()=>{e||(this.hideLoading(),t())}).catch(t=>{e||(e=!0,this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR,"图片加载失败",t),i(t))})})}loadImageAsync(t,i,e){return new Promise((s,r)=>{if(this.imageCache.has(i)){const r=this.imageCache.get(i);return t.src=r.src,this.applyStyles(t,e),void s()}const a=this.safeSetTimeout(()=>{r(Error("图片加载超时"))},1e4);t.onload=function(){this.safeClearTimeout(a),this.imageCache.set(i,t.cloneNode()),s()}.bind(this),t.onerror=function(t){this.safeClearTimeout(a),r(t)}.bind(this),t.src=i,this.applyStyles(t,e)})}applyStyles(t,i){Object.entries(i).forEach(([i,e])=>{t.style[i]="number"==typeof e?e+"px":e})}showLoading(){this.state.isLoading=!0,this.batchUpdateStyles({container:{display:"block"},loadingText:{display:"flex"},error:{display:"none"}}),this.updateUIState("loading")}hideLoading(){this.state.isLoading=!1,this.batchUpdateStyles({loadingText:{display:"none"}}),this.updateUIState("reset")}showCaptcha(){this.batchUpdateStyles({container:{display:"block"},track:{display:"block"},error:{display:"none"}})}showError(t){this.hideLoading(),this.batchUpdateStyles({error:{display:"block",textContent:t}})}batchUpdateStyles(t){requestAnimationFrame(()=>{Object.entries(t).forEach(([t,i])=>{const e=this.elements[t];e&&Object.entries(i).forEach(([t,i])=>{"textContent"===t?e.textContent=i:e.style[t]=i})})})}async verify(){if(this.captchaData)try{this.abortController&&this.abortController.abort(),this.abortController=new AbortController;const t=await fetch(this.options.verifyUrl,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times],...this.options.verifyData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const i=await t.json();this.isVerifySuccess(i)?this.onVerifySuccess(i.data||i.result):this.onVerifyFail(i.message||i.msg||"验证失败,请重试!")}catch(t){"AbortError"===t.name?this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR,"验证请求被取消"):t.message.includes("Failed to fetch")||t.message.includes("NetworkError")?this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR,"网络连接失败"):this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR,t.message,t)}else this.onVerifyFail("验证码数据丢失")}isVerifySuccess(t){return!(!t||"object"!=typeof t)&&["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}onVerifySuccess(t){const i=this.dragStartTime?Date.now()-this.dragStartTime:Date.now()-this.startTime,e=`验证成功!耗时:${(i/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(e,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:i}),this.hide()},2e3)}showFloatingTime(t,i="success"){const{elements:e}=this;e.floatingTime.textContent=t,e.floatingTime.className="slider-captcha-floating-time "+i,this.safeSetTimeout(()=>e.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{e.floatingTime.className="slider-captcha-floating-time"},2500)}onVerifyFail(t){this.state.retryCount++,this.updateUIState("fail"),this.showFloatingTime(t,"fail"),this.safeSetTimeout(()=>{this.state.retryCount<this.options.maxRetries?this.reset():this.refresh()},2500)}reset(){this.clearAllTimers(),Object.assign(this.state,{isDragging:!1,currentX:0,startX:0,isLoading:!1}),this.times=[],this.startTime=null,this.dragStartTime=null,this.cachedDimensions=null,requestAnimationFrame(()=>{this.setTransition(!0),this.elements.btn.style.transform="translateX(0px)",this.elements.sliderImg.style.transform="translateX(0px)",this.updateUIState("reset"),this.elements.error.style.display="none"})}refresh(){try{this.reset(),this.state.retryCount=0,this.loadCaptcha()}catch(t){}}safeSetTimeout(t,i){const e=setTimeout(()=>{this.timers.delete(e),t()},i);return this.timers.add(e),e}safeClearTimeout(t){t&&(clearTimeout(t),this.timers.delete(t))}clearAllTimers(){this.timers.forEach(t=>{clearTimeout(t),clearInterval(t)}),this.timers.clear(),this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null)}cleanupImages(){this.elements.backgroundImg&&(this.elements.backgroundImg.src="",this.elements.backgroundImg.onload=null,this.elements.backgroundImg.onerror=null),this.elements.sliderImg&&(this.elements.sliderImg.src="",this.elements.sliderImg.onload=null,this.elements.sliderImg.onerror=null),this.imageCache.clear()}throttle(t,i){let e=0;return(...s)=>{const r=Date.now();if(r-e>=i)return e=r,t.apply(this,s)}}destroy(){try{this.abortController&&(this.abortController.abort(),this.abortController=null),document.body&&(document.body.style.userSelect="",document.body.style.cursor=""),this.clearAllTimers(),this.removeAllEventListeners(),this.cleanupImages(),this.elements?.overlay?.parentNode&&this.elements.overlay.parentNode.removeChild(this.elements.overlay);const t=document.getElementById("slider-captcha-styles");t&&t.remove(),this.imageCache.clear(),this.cachedDimensions=null,Object.keys(this).forEach(t=>{"constructor"!==t&&(this[t]=null)}),this.options.onDestroy&&this.options.onDestroy()}catch(t){}}static create(t){return new PopupSliderCaptcha(t)}static show(t){const i=new PopupSliderCaptcha(t);return i.show(),i}}"undefined"!=typeof module&&module.exports?(module.exports=PopupSliderCaptcha,module.exports.default=PopupSliderCaptcha):"function"==typeof define&&define.amd?define([],()=>PopupSliderCaptcha):"undefined"!=typeof window&&(window.PopupSliderCaptcha=PopupSliderCaptcha,window.SliderCaptcha=PopupSliderCaptcha),e=PopupSliderCaptcha;class PasswordValidator{static CONSTANTS={DEFAULT_TIMEOUT:1e4,CACHE_DURATION:3e5,MIN_PASSWORD_LENGTH:1};static ERROR_TYPES={NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT_ERROR:"TIMEOUT_ERROR",ENCRYPTION_ERROR:"ENCRYPTION_ERROR",VALIDATION_ERROR:"VALIDATION_ERROR",PUBLIC_KEY_ERROR:"PUBLIC_KEY_ERROR"};constructor(t={}){this.options={publicKeyUrl:t.publicKeyUrl||"/externalapi/commonservice/strongPassword/getPublicKey",validateUrl:t.validateUrl||"/externalapi/commonservice/strongPassword/checkPassword",baseUrl:t.baseUrl||"",timeout:t.timeout||PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,headers:t.headers||{},cacheDuration:t.cacheDuration||PasswordValidator.CONSTANTS.CACHE_DURATION,...t},this.options.publicKeyUrl=this.normalizeUrl(this.options.publicKeyUrl,this.options.baseUrl),this.options.validateUrl=this.normalizeUrl(this.options.validateUrl,this.options.baseUrl),this.publicKeyCache=null,this.publicKeyExpiry=null}normalizeUrl(t,i){return t?/^https?:\/\//.test(t)||!i?t:(i.endsWith("/")?i:i+"/")+(t.startsWith("/")?t.substring(1):t):t}async getPublicKey(){if(this.publicKeyCache&&this.publicKeyExpiry&&Date.now()<this.publicKeyExpiry)return this.publicKeyCache;try{const t=await this.makeRequest(this.options.publicKeyUrl,{method:"GET"});if(!this.isSuccessResponse(t))throw Error("获取公钥失败:"+(t.message||t.msg||"未知错误"));const i=t.data||t.result;if(!i)throw Error("公钥为空");return this.publicKeyCache=i,this.publicKeyExpiry=Date.now()+this.options.cacheDuration,this.publicKeyCache}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR,t.message,t),Error("获取公钥失败: "+t.message)}}isSuccessResponse(t){return!(!t||"object"!=typeof t)&&["0"===t.code,0===t.code,!0===t.success,"success"===t.status,!0===t.result].some(t=>!0===t)}handleError(t,i,e=null){const s={[PasswordValidator.ERROR_TYPES.NETWORK_ERROR]:"网络连接失败,请检查网络设置",[PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]:"请求超时,请重试",[PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]:"密码加密失败",[PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]:"密码验证失败",[PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]:"获取公钥失败"}[t]||i||"未知错误";this.options.onError&&this.options.onError({type:t,message:s,originalError:e})}encryptPassword(t,i){try{if(!t)throw Error("密码不能为空");if(t.length<PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH)throw Error("密码长度不足");if(!i)throw Error("公钥不能为空");if(void 0===JSEncrypt)throw Error("JSEncrypt库未正确加载,请确保已引入JSEncrypt库");const e=new JSEncrypt;e.setPublicKey(i);const s=e.encrypt(t);if(!s)throw Error("密码加密失败,可能是公钥格式不正确");return s}catch(t){throw this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR,t.message,t),Error("密码加密失败: "+t.message)}}async validatePassword(t,i,e={}){try{if(!t||!i)throw Error("用户名密码不能为空");const s=await this.getPublicKey(),r=this.encryptPassword(t,s),a=await this.makeRequest(this.options.validateUrl,{method:"POST",body:JSON.stringify({userName:i,password:r,timestamp:Date.now(),...e})});return{success:this.isSuccessResponse(a),data:a.data||a.result,message:a.message||a.msg||"验证完成",code:a.code||a.status,originalResponse:a}}catch(t){let i=PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;return t.message.includes("网络")||t.message.includes("fetch")?i=PasswordValidator.ERROR_TYPES.NETWORK_ERROR:t.message.includes("超时")||t.message.includes("timeout")?i=PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR:t.message.includes("加密")&&(i=PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR),this.handleError(i,t.message,t),{success:!1,message:t.message||"密码校验失败",code:i,error:t}}}async makeRequest(t,i={}){const e=new AbortController,s=setTimeout(()=>{e.abort()},this.options.timeout);try{const r=await fetch(t,{...i,headers:{"Content-Type":"application/json",...this.options.headers,...i.headers},signal:e.signal});if(clearTimeout(s),!r.ok)throw Error(`HTTP错误: ${r.status} ${r.statusText}`);const a=await r.json();return this.options.debug,a}catch(t){if(clearTimeout(s),"AbortError"===t.name)throw this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR,"请求超时",t),Error("请求超时");if(t.message.includes("Failed to fetch")||t.message.includes("NetworkError"))throw this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR,"网络连接失败",t),Error("网络连接失败");throw t}}clearCache(){this.publicKeyCache=null,this.publicKeyExpiry=null}updateOptions(t){this.options={...this.options,...t},this.clearCache()}getCacheStatus(){return{hasCache:!!this.publicKeyCache,isExpired:!this.publicKeyExpiry||Date.now()>this.publicKeyExpiry,expiryTime:this.publicKeyExpiry,remainingTime:this.publicKeyExpiry?Math.max(0,this.publicKeyExpiry-Date.now()):0}}destroy(){try{this.clearCache(),this.options.onDestroy&&this.options.onDestroy()}catch(t){}}}r={PopupSliderCaptcha:e,PasswordValidator:s=PasswordValidator,createPasswordValidator:t,validatePassword:i},"undefined"!=typeof window&&(window.SliderCaptcha=e,window.PopupSliderCaptcha=e,window.PasswordValidator=s,window.createPasswordValidator=t,window.validatePassword=i);export{s as PasswordValidator,e as PopupSliderCaptcha,t as createPasswordValidator,r as default,i as validatePassword};