slider-captcha-sdk 1.0.19 → 1.0.21

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,1309 +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
- this.rafId && cancelAnimationFrame(this.rafId);
290
- this.rafId = requestAnimationFrame(() => this.updateSliderPosition());
291
- }
292
-
293
- handleEnd() {
294
- if (!this.state.isDragging) return
295
-
296
- this.times.push({ time: Date.now(), position: this.getPosition() });
297
- this.state.isDragging = false;
298
-
299
- if (this.rafId) {
300
- cancelAnimationFrame(this.rafId);
301
- this.rafId = null;
302
- }
303
-
304
- this.verify();
305
- }
306
-
307
- handleVisibilityChange() {
308
- const animationState = document.hidden ? 'paused' : 'running';
309
- if (this.elements.fingerAnimation) {
310
- this.elements.fingerAnimation.style.animationPlayState = animationState;
311
- }
312
- }
313
-
314
- getClientX(e) {
315
- return e.type.includes('touch') ? e.touches[0].clientX : e.clientX
316
- }
317
-
318
- setTransition(enabled) {
319
- const transition = enabled ? 'all 0.3s ease' : 'none';
320
- requestAnimationFrame(() => {
321
- this.elements.btn.style.transition = transition;
322
- this.elements.sliderImg.style.transition = transition;
323
- });
324
- }
325
-
326
- // 优化:简化UI状态更新
327
- updateUIState(state) {
328
- const { elements } = this;
329
- const updates = {
330
- dragging: () => {
331
- elements.hint.style.opacity = '0';
332
- elements.fingerAnimation.style.display = 'none';
333
- },
334
- success: () => {
335
- Object.assign(elements.btn.style, { background: 'var(--sc-success)' });
336
- Object.assign(elements.icon.style, { innerHTML: '✓', color: 'white' });
337
- elements.icon.innerHTML = '✓';
338
- },
339
- fail: () => {
340
- Object.assign(elements.btn.style, { background: 'var(--sc-danger)' });
341
- Object.assign(elements.icon.style, { innerHTML: '✗', color: 'white' });
342
- elements.icon.innerHTML = '✗';
343
- },
344
- reset: () => {
345
- Object.assign(elements.btn.style, { background: 'white' });
346
- Object.assign(elements.icon.style, { color: '#666' });
347
- elements.icon.innerHTML = '→';
348
- elements.fingerAnimation.style.display = 'block';
349
- this.updateHintText('向右滑动完成验证', 'var(--sc-text-light)');
350
- },
351
- loading: () => {
352
- elements.hint.style.opacity = '0';
353
- elements.fingerAnimation.style.display = 'none';
354
- Object.assign(elements.track.style, { pointerEvents: 'none', opacity: '0.6' });
355
- },
356
- };
357
-
358
- if (updates[state]) {
359
- requestAnimationFrame(() => {
360
- updates[state]();
361
- if (state !== 'loading') {
362
- Object.assign(elements.track.style, { pointerEvents: 'auto', opacity: '1' });
363
- }
364
- });
365
- }
366
- }
367
-
368
- updateHintText(text, color) {
369
- requestAnimationFrame(() => {
370
- Object.assign(this.elements.hint, { textContent: text });
371
- Object.assign(this.elements.hint.style, { color, opacity: '1' });
372
- });
373
- }
374
-
375
- updateSliderPosition() {
376
- const { elements, options, state } = this;
377
- const { maxX } = this.getDimensions();
378
- const pieceX = (state.currentX / maxX) * (options.width - options.sliderSize);
379
- const progress = state.currentX / maxX;
380
-
381
- requestAnimationFrame(() => {
382
- elements.btn.style.transform = `translateX(${state.currentX}px)`;
383
- elements.sliderImg.style.transform = `translateX(${pieceX}px)`;
384
- elements.fingerAnimation.style.opacity = progress >= 0.8 ? '0' : '0.6';
385
- });
386
- }
387
-
388
- // 优化:简化显示/隐藏逻辑
389
- show() {
390
- this.state.isVisible = true;
391
- this.elements.overlay.style.display = 'flex';
392
-
393
- requestAnimationFrame(() => {
394
- this.elements.overlay.classList.add('show');
395
- this.elements.modal.classList.add('show');
396
- });
397
-
398
- this.loadCaptcha();
399
- }
400
-
401
- hide() {
402
- this.state.isVisible = false;
403
- this.elements.overlay.classList.remove('show');
404
- this.elements.modal.classList.remove('show');
405
-
406
- this.safeSetTimeout(() => {
407
- this.elements.overlay.style.display = 'none';
408
- document.body.removeChild(this.elements.overlay);
409
- this.reset();
410
- this.options.onClose?.();
411
- this.elements.overlay = null;
412
- }, 300);
413
- }
414
-
415
- // 优化:添加统一错误处理方法
416
- handleError(errorType, message, originalError = null) {
417
- const errorMessages = {
418
- [PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
419
- [PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
420
- [PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]: '验证失败,请重试',
421
- [PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]: '图片加载失败,请刷新重试',
422
- [PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]: '验证码数据错误,请刷新重试',
423
- };
424
-
425
- const errorMessage = errorMessages[errorType] || message || '未知错误';
426
-
427
- // 调用用户自定义错误处理
428
- if (this.options.onError) {
429
- this.options.onError({
430
- type: errorType,
431
- message: errorMessage,
432
- originalError,
433
- });
434
- }
435
-
436
- this.showError(errorMessage);
437
- console.error(`滑块验证码错误 [${errorType}]:`, errorMessage, originalError);
438
- }
439
-
440
- // 优化:简化加载逻辑,增强错误处理
441
- async loadCaptcha() {
442
- try {
443
- this.showLoading();
444
- this.startTime = Date.now();
445
-
446
- // 取消之前的请求
447
- if (this.abortController) {
448
- this.abortController.abort();
449
- }
450
- this.abortController = new AbortController();
451
-
452
- const response = await fetch(this.options.apiUrl, {
453
- method: 'POST',
454
- headers: {
455
- 'Content-Type': 'application/json',
456
- ...this.options.headers,
457
- },
458
- body: JSON.stringify({
459
- timestamp: Date.now(),
460
- ...this.options.requestData,
461
- }),
462
- signal: this.abortController.signal,
463
- });
464
-
465
- if (!response.ok) {
466
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
467
- }
468
-
469
- const data = await response.json();
470
-
471
- // 优化:更严格的数据验证
472
- if (!this.validateCaptchaData(data)) {
473
- throw new Error('验证码数据格式错误')
474
- }
475
-
476
- this.captchaData = data.data;
477
- this.showCaptcha();
478
- await this.renderCaptcha();
479
- } catch (error) {
480
- if (error.name === 'AbortError') {
481
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '请求被取消');
482
- } else if (
483
- error.message.includes('Failed to fetch') ||
484
- error.message.includes('NetworkError')
485
- ) {
486
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
487
- } else {
488
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR, error.message, error);
489
- }
490
- }
491
- }
492
-
493
- // 优化:添加验证码数据验证方法
494
- validateCaptchaData(data) {
495
- if (!data || typeof data !== 'object') return false
496
-
497
- const requiredFields = [
498
- 'canvasSrc',
499
- 'blockSrc',
500
- 'canvasWidth',
501
- 'canvasHeight',
502
- 'blockWidth',
503
- 'blockHeight',
504
- 'blockY',
505
- 'nonceStr',
506
- ];
507
- const dataObj = data.data || data;
508
-
509
- return requiredFields.every((field) => {
510
- const value = dataObj[field];
511
- return value !== null && value !== undefined && value !== ''
512
- })
513
- }
514
-
515
- async renderCaptcha() {
516
- return new Promise((resolve, reject) => {
517
- let hasError = false;
518
-
519
- // 优化:并行加载图片,提高性能
520
- const loadPromises = [
521
- this.loadImageAsync(this.elements.backgroundImg, this.captchaData.canvasSrc, {
522
- width: this.captchaData.canvasWidth,
523
- height: this.captchaData.canvasHeight,
524
- }),
525
- this.loadImageAsync(this.elements.sliderImg, this.captchaData.blockSrc, {
526
- width: this.captchaData.blockWidth,
527
- height: this.captchaData.blockHeight,
528
- top: this.captchaData.blockY,
529
- }),
530
- ];
531
-
532
- Promise.all(loadPromises)
533
- .then(() => {
534
- if (!hasError) {
535
- this.hideLoading();
536
- resolve();
537
- }
538
- })
539
- .catch((error) => {
540
- if (!hasError) {
541
- hasError = true;
542
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR, '图片加载失败', error);
543
- reject(error);
544
- }
545
- });
546
- })
547
- }
548
-
549
- // 优化:添加异步图片加载方法
550
- async loadImageAsync(imgElement, src, styles) {
551
- return new Promise((resolve, reject) => {
552
- // 检查缓存
553
- if (this.imageCache.has(src)) {
554
- const cachedImg = this.imageCache.get(src);
555
- imgElement.src = cachedImg.src;
556
- this.applyStyles(imgElement, styles);
557
- resolve();
558
- return
559
- }
560
-
561
- const timeoutId = this.safeSetTimeout(() => {
562
- reject(new Error('图片加载超时'));
563
- }, 10000); // 10秒超时
564
-
565
- imgElement.onload = () => {
566
- this.safeClearTimeout(timeoutId);
567
- this.imageCache.set(src, imgElement.cloneNode());
568
- resolve();
569
- };
570
-
571
- imgElement.onerror = (error) => {
572
- this.safeClearTimeout(timeoutId);
573
- reject(error);
574
- };
575
-
576
- imgElement.src = src;
577
- this.applyStyles(imgElement, styles);
578
- })
579
- }
580
-
581
- // 优化:提取样式应用逻辑
582
- applyStyles(element, styles) {
583
- Object.entries(styles).forEach(([key, value]) => {
584
- element.style[key] = typeof value === 'number' ? value + 'px' : value;
585
- });
586
- }
587
-
588
- // 优化:简化状态显示方法
589
- showLoading() {
590
- this.state.isLoading = true;
591
- this.batchUpdateStyles({
592
- container: { display: 'block' },
593
- loadingText: { display: 'flex' },
594
- error: { display: 'none' },
595
- });
596
- this.updateUIState('loading');
597
- }
598
-
599
- hideLoading() {
600
- this.state.isLoading = false;
601
- this.batchUpdateStyles({ loadingText: { display: 'none' } });
602
- this.updateUIState('reset');
603
- }
604
-
605
- showCaptcha() {
606
- this.batchUpdateStyles({
607
- container: { display: 'block' },
608
- track: { display: 'block' },
609
- error: { display: 'none' },
610
- });
611
- }
612
-
613
- showError(message) {
614
- this.hideLoading();
615
- this.batchUpdateStyles({
616
- error: { display: 'block', textContent: message },
617
- });
618
- }
619
-
620
- // 优化:批量样式更新
621
- batchUpdateStyles(updates) {
622
- requestAnimationFrame(() => {
623
- Object.entries(updates).forEach(([elementKey, styles]) => {
624
- const element = this.elements[elementKey];
625
- if (element) {
626
- Object.entries(styles).forEach(([prop, value]) => {
627
- if (prop === 'textContent') {
628
- element.textContent = value;
629
- } else {
630
- element.style[prop] = value;
631
- }
632
- });
633
- }
634
- });
635
- });
636
- }
637
-
638
- async verify() {
639
- if (!this.captchaData) {
640
- this.onVerifyFail('验证码数据丢失');
641
- return
642
- }
643
-
644
- try {
645
- // 取消之前的验证请求
646
- if (this.abortController) {
647
- this.abortController.abort();
648
- }
649
- this.abortController = new AbortController();
650
-
651
- const response = await fetch(this.options.verifyUrl, {
652
- method: 'POST',
653
- headers: {
654
- 'Content-Type': 'application/json',
655
- ...this.options.headers,
656
- },
657
- body: JSON.stringify({
658
- loginVo: {
659
- nonceStr: this.captchaData.nonceStr,
660
- value: this.getPosition(),
661
- },
662
- dragEventList: [...this.times],
663
- ...this.options.verifyData,
664
- }),
665
- signal: this.abortController.signal,
666
- });
667
-
668
- if (!response.ok) {
669
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
670
- }
671
-
672
- const data = await response.json();
673
-
674
- // 优化:更灵活的验证结果判断
675
- if (this.isVerifySuccess(data)) {
676
- this.onVerifySuccess(data.data || data.result);
677
- } else {
678
- this.onVerifyFail(data.message || data.msg || '验证失败,请重试!');
679
- }
680
- } catch (error) {
681
- if (error.name === 'AbortError') {
682
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '验证请求被取消');
683
- } else if (
684
- error.message.includes('Failed to fetch') ||
685
- error.message.includes('NetworkError')
686
- ) {
687
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
688
- } else {
689
- this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR, error.message, error);
690
- }
691
- }
692
- }
693
-
694
- // 优化:添加验证成功判断方法
695
- isVerifySuccess(data) {
696
- if (!data || typeof data !== 'object') return false
697
-
698
- // 支持多种成功标识
699
- const successIndicators = [
700
- data.code === '0',
701
- data.code === 0,
702
- data.success === true,
703
- data.status === 'success',
704
- data.result === true,
705
- ];
706
-
707
- return successIndicators.some((indicator) => indicator === true)
708
- }
709
-
710
- onVerifySuccess(ticket) {
711
- const duration = this.dragStartTime
712
- ? Date.now() - this.dragStartTime
713
- : Date.now() - this.startTime;
714
- const durationText = `验证成功!耗时:${(duration / 1000).toFixed(2)}s`;
715
-
716
- this.updateUIState('success');
717
- this.showFloatingTime(durationText, 'success');
718
-
719
- this.safeSetTimeout(() => {
720
- this.options.onSuccess?.({
721
- ticket: ticket,
722
- timestamp: Date.now(),
723
- duration,
724
- });
725
- this.hide();
726
- }, 2000);
727
- }
728
-
729
- showFloatingTime(text, type = 'success') {
730
- const { elements } = this;
731
- elements.floatingTime.textContent = text;
732
- elements.floatingTime.className = `slider-captcha-floating-time ${type}`;
733
-
734
- this.safeSetTimeout(() => elements.floatingTime.classList.add('show'), 100);
735
- this.safeSetTimeout(() => {
736
- elements.floatingTime.className = 'slider-captcha-floating-time';
737
- }, 2500); // 优化:延长显示时间,避免被reset清除
738
- }
739
-
740
- onVerifyFail(message) {
741
- this.state.retryCount++;
742
- this.updateUIState('fail');
743
- this.showFloatingTime(message, 'fail');
744
-
745
- this.safeSetTimeout(() => {
746
- if (this.state.retryCount >= this.options.maxRetries) {
747
- this.refresh();
748
- } else {
749
- this.reset();
750
- }
751
- }, 2500); // 优化:延长等待时间,确保浮动提示完整显示
752
- }
753
-
754
- reset() {
755
- this.clearAllTimers();
756
-
757
- // 重置状态
758
- Object.assign(this.state, {
759
- isDragging: false,
760
- currentX: 0,
761
- startX: 0,
762
- isLoading: false,
763
- });
764
-
765
- this.times = [];
766
- this.startTime = null;
767
- this.dragStartTime = null; // 优化:重置拖拽开始时间
768
- this.cachedDimensions = null;
769
-
770
- // 重置UI
771
- requestAnimationFrame(() => {
772
- this.setTransition(true);
773
- this.elements.btn.style.transform = 'translateX(0px)';
774
- this.elements.sliderImg.style.transform = 'translateX(0px)';
775
- this.updateUIState('reset');
776
- this.elements.error.style.display = 'none';
777
- });
778
- }
779
-
780
- refresh() {
781
- this.reset();
782
- this.state.retryCount = 0;
783
- this.loadCaptcha();
784
- }
785
-
786
- // 安全的定时器管理
787
- safeSetTimeout(callback, delay) {
788
- const timerId = setTimeout(() => {
789
- this.timers.delete(timerId);
790
- callback();
791
- }, delay);
792
- this.timers.add(timerId);
793
- return timerId
794
- }
795
-
796
- safeClearTimeout(timerId) {
797
- if (timerId) {
798
- clearTimeout(timerId);
799
- this.timers.delete(timerId);
800
- }
801
- }
802
-
803
- clearAllTimers() {
804
- this.timers.forEach((timer) => {
805
- clearTimeout(timer);
806
- clearInterval(timer);
807
- });
808
- this.timers.clear();
809
-
810
- if (this.rafId) {
811
- cancelAnimationFrame(this.rafId);
812
- this.rafId = null;
813
- }
814
- }
815
-
816
- // 清理图片资源
817
- cleanupImages() {
818
- if (this.elements.backgroundImg) {
819
- this.elements.backgroundImg.src = '';
820
- this.elements.backgroundImg.onload = null;
821
- this.elements.backgroundImg.onerror = null;
822
- }
823
- if (this.elements.sliderImg) {
824
- this.elements.sliderImg.src = '';
825
- this.elements.sliderImg.onload = null;
826
- this.elements.sliderImg.onerror = null;
827
- }
828
- this.imageCache.clear();
829
- }
830
-
831
- // 工具函数:节流
832
- throttle(func, delay) {
833
- let lastCall = 0;
834
- return function (...args) {
835
- const now = Date.now();
836
- if (now - lastCall >= delay) {
837
- lastCall = now;
838
- return func.apply(this, args)
839
- }
840
- }
841
- }
842
-
843
- destroy() {
844
- try {
845
- // 取消所有进行中的请求
846
- if (this.abortController) {
847
- this.abortController.abort();
848
- this.abortController = null;
849
- }
850
-
851
- // 恢复body样式
852
- if (document.body) {
853
- document.body.style.userSelect = '';
854
- document.body.style.cursor = '';
855
- }
856
-
857
- // 清理所有定时器
858
- this.clearAllTimers();
859
-
860
- // 移除所有事件监听器
861
- this.removeAllEventListeners();
862
-
863
- // 清理图片资源
864
- this.cleanupImages();
865
-
866
- // 移除DOM元素
867
- if (this.elements?.overlay?.parentNode) {
868
- this.elements.overlay.parentNode.removeChild(this.elements.overlay);
869
- }
870
-
871
- // 清理样式表
872
- const styleElement = document.getElementById('slider-captcha-styles');
873
- if (styleElement) {
874
- styleElement.remove();
875
- }
876
-
877
- // 清空缓存
878
- this.imageCache.clear();
879
- this.cachedDimensions = null;
880
-
881
- // 清空所有属性
882
- Object.keys(this).forEach((key) => {
883
- if (key !== 'constructor') {
884
- this[key] = null;
885
- }
886
- });
887
-
888
- // 调用销毁回调
889
- if (this.options.onDestroy) {
890
- this.options.onDestroy();
891
- }
892
- } catch (error) {
893
- console.error('销毁滑块验证码时出错:', error);
894
- }
895
- }
896
-
897
- static create(options) {
898
- return new PopupSliderCaptcha(options)
899
- }
900
-
901
- static show(options) {
902
- const instance = new PopupSliderCaptcha(options);
903
- instance.show();
904
- return instance
905
- }
906
- }
907
-
908
- // 模块导出
909
- if (typeof module !== 'undefined' && module.exports) {
910
- module.exports = PopupSliderCaptcha;
911
- module.exports.default = PopupSliderCaptcha;
912
- } else if (typeof define === 'function' && define.amd) {
913
- define([], () => PopupSliderCaptcha);
914
- } else if (typeof window !== 'undefined') {
915
- window.PopupSliderCaptcha = PopupSliderCaptcha;
916
- window.SliderCaptcha = PopupSliderCaptcha;
917
- }
918
-
919
- // Add ES6 export for modern module systems
920
- var PopupSliderCaptcha$1 = PopupSliderCaptcha;
921
-
922
- /**
923
- * 密码校验工具类
924
- * 提供密码加密和校验功能
925
- */
926
- class PasswordValidator {
927
- // 优化:添加常量定义
928
- static CONSTANTS = {
929
- DEFAULT_TIMEOUT: 10000,
930
- CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
931
- MIN_PASSWORD_LENGTH: 1,
932
- }
933
-
934
- static ERROR_TYPES = {
935
- NETWORK_ERROR: 'NETWORK_ERROR',
936
- TIMEOUT_ERROR: 'TIMEOUT_ERROR',
937
- ENCRYPTION_ERROR: 'ENCRYPTION_ERROR',
938
- VALIDATION_ERROR: 'VALIDATION_ERROR',
939
- PUBLIC_KEY_ERROR: 'PUBLIC_KEY_ERROR',
940
- }
941
-
942
- constructor(options = {}) {
943
- this.options = {
944
- publicKeyUrl: options.publicKeyUrl || '/microservice/strongPassword/getPublicKey',
945
- validateUrl: options.validateUrl || '/microservice/strongPassword/checkPassword',
946
- timeout: options.timeout || PasswordValidator.CONSTANTS.DEFAULT_TIMEOUT,
947
- headers: options.headers || {},
948
- cacheDuration: options.cacheDuration || PasswordValidator.CONSTANTS.CACHE_DURATION,
949
- ...options,
950
- };
951
-
952
- // 缓存公钥,避免重复请求
953
- this.publicKeyCache = null;
954
- this.publicKeyExpiry = null;
955
- }
956
-
957
- /**
958
- * 获取公钥
959
- * @returns {Promise<string>} 公钥字符串
960
- */
961
- async getPublicKey() {
962
- // 检查缓存是否有效
963
- if (this.publicKeyCache && this.publicKeyExpiry && Date.now() < this.publicKeyExpiry) {
964
- return this.publicKeyCache
965
- }
966
-
967
- try {
968
- const response = await this.makeRequest(this.options.publicKeyUrl, {
969
- method: 'GET',
970
- });
971
-
972
- if (!this.isSuccessResponse(response)) {
973
- throw new Error('获取公钥失败:' + (response.message || response.msg || '未知错误'))
974
- }
975
-
976
- // 验证公钥格式
977
- const publicKey = response.data || response.result;
978
- if (!publicKey) {
979
- throw new Error('公钥为空')
980
- }
981
-
982
- // 缓存公钥
983
- this.publicKeyCache = publicKey;
984
- this.publicKeyExpiry = Date.now() + this.options.cacheDuration;
985
-
986
- return this.publicKeyCache
987
- } catch (error) {
988
- console.error('获取公钥失败:', error);
989
- this.handleError(PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR, error.message, error);
990
- throw new Error('获取公钥失败: ' + error.message)
991
- }
992
- }
993
-
994
- // 优化:添加响应成功判断方法
995
- isSuccessResponse(response) {
996
- if (!response || typeof response !== 'object') {
997
- return false
998
- }
999
-
1000
- const successIndicators = [
1001
- response.code === '0',
1002
- response.code === 0,
1003
- response.success === true,
1004
- response.status === 'success',
1005
- response.result === true,
1006
- ];
1007
-
1008
- return successIndicators.some((indicator) => indicator === true)
1009
- }
1010
-
1011
- handleError(errorType, message, originalError = null) {
1012
- const errorMessages = {
1013
- [PasswordValidator.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
1014
- [PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
1015
- [PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR]: '密码加密失败',
1016
- [PasswordValidator.ERROR_TYPES.VALIDATION_ERROR]: '密码验证失败',
1017
- [PasswordValidator.ERROR_TYPES.PUBLIC_KEY_ERROR]: '获取公钥失败',
1018
- };
1019
-
1020
- const errorMessage = errorMessages[errorType] || message || '未知错误';
1021
-
1022
- // 调用用户自定义错误处理
1023
- if (this.options.onError) {
1024
- this.options.onError({
1025
- type: errorType,
1026
- message: errorMessage,
1027
- originalError,
1028
- });
1029
- }
1030
-
1031
- console.error(`密码校验器错误 [${errorType}]:`, errorMessage, originalError);
1032
- }
1033
-
1034
- /**
1035
- * 使用RSA公钥加密密码
1036
- * @param {string} password 原始密码
1037
- * @param {string} publicKey 公钥字符串
1038
- * @returns {string} 加密后的密码
1039
- */
1040
- encryptPassword(password, publicKey) {
1041
- try {
1042
- if (!password) {
1043
- throw new Error('密码不能为空')
1044
- }
1045
-
1046
- if (password.length < PasswordValidator.CONSTANTS.MIN_PASSWORD_LENGTH) {
1047
- throw new Error('密码长度不足')
1048
- }
1049
-
1050
- if (!publicKey) {
1051
- throw new Error('公钥不能为空')
1052
- }
1053
-
1054
- if (typeof JSEncrypt === 'undefined') {
1055
- throw new Error('JSEncrypt库未正确加载,请确保已引入JSEncrypt库')
1056
- }
1057
-
1058
- const encrypt = new JSEncrypt();
1059
- encrypt.setPublicKey(publicKey);
1060
- const encrypted = encrypt.encrypt(password);
1061
-
1062
- if (!encrypted) {
1063
- throw new Error('密码加密失败,可能是公钥格式不正确')
1064
- }
1065
-
1066
- return encrypted
1067
- } catch (error) {
1068
- console.error('密码加密失败:', error);
1069
- this.handleError(PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR, error.message, error);
1070
- throw new Error('密码加密失败: ' + error.message)
1071
- }
1072
- }
1073
-
1074
- /**
1075
- * 校验密码
1076
- * @param {string} password 原始密码
1077
- * @param {string} userName 用户名
1078
- * @param {Object} additionalData 额外的校验数据
1079
- * @returns {Promise<Object>} 校验结果
1080
- */
1081
- async validatePassword(password, userName, additionalData = {}) {
1082
- try {
1083
- if (!password || !userName) {
1084
- throw new Error('用户名密码不能为空')
1085
- }
1086
-
1087
- // 1. 获取公钥
1088
- const publicKey = await this.getPublicKey();
1089
-
1090
- // 2. 加密密码
1091
- const encryptedPassword = this.encryptPassword(password, publicKey);
1092
-
1093
- // 3. 调用校验接口
1094
- const response = await this.makeRequest(this.options.validateUrl, {
1095
- method: 'POST',
1096
- body: JSON.stringify({
1097
- userName: userName,
1098
- password: encryptedPassword,
1099
- timestamp: Date.now(),
1100
- ...additionalData,
1101
- }),
1102
- });
1103
-
1104
- return {
1105
- success: this.isSuccessResponse(response),
1106
- data: response.data || response.result,
1107
- message: response.message || response.msg || '验证完成',
1108
- code: response.code || response.status,
1109
- originalResponse: response,
1110
- }
1111
- } catch (error) {
1112
- console.error('密码校验失败:', error);
1113
-
1114
- // 优化:根据错误类型返回不同的错误信息
1115
- let errorType = PasswordValidator.ERROR_TYPES.VALIDATION_ERROR;
1116
- if (error.message.includes('网络') || error.message.includes('fetch')) {
1117
- errorType = PasswordValidator.ERROR_TYPES.NETWORK_ERROR;
1118
- } else if (error.message.includes('超时') || error.message.includes('timeout')) {
1119
- errorType = PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR;
1120
- } else if (error.message.includes('加密')) {
1121
- errorType = PasswordValidator.ERROR_TYPES.ENCRYPTION_ERROR;
1122
- }
1123
-
1124
- this.handleError(errorType, error.message, error);
1125
-
1126
- return {
1127
- success: false,
1128
- message: error.message || '密码校验失败',
1129
- code: errorType,
1130
- error: error,
1131
- }
1132
- }
1133
- }
1134
-
1135
- /**
1136
- * 发送HTTP请求的通用方法
1137
- * @param {string} url 请求地址
1138
- * @param {Object} options 请求选项
1139
- * @returns {Promise<Object>} 响应结果
1140
- */
1141
- async makeRequest(url, options = {}) {
1142
- // 优化:为每个请求创建独立的AbortController,避免多个请求互相取消
1143
- const abortController = new AbortController();
1144
-
1145
- const timeoutId = setTimeout(() => {
1146
- abortController.abort();
1147
- }, this.options.timeout);
1148
-
1149
- try {
1150
- const response = await fetch(url, {
1151
- ...options,
1152
- headers: {
1153
- 'Content-Type': 'application/json',
1154
- ...this.options.headers,
1155
- ...options.headers,
1156
- },
1157
- signal: abortController.signal,
1158
- });
1159
-
1160
- clearTimeout(timeoutId);
1161
-
1162
- if (!response.ok) {
1163
- throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
1164
- }
1165
-
1166
- const data = await response.json();
1167
-
1168
- // 优化:记录请求日志(开发环境)
1169
- if (this.options.debug) {
1170
- console.log('密码校验器请求:', { url, options, response: data });
1171
- }
1172
-
1173
- return data
1174
- } catch (error) {
1175
- clearTimeout(timeoutId);
1176
-
1177
- if (error.name === 'AbortError') {
1178
- this.handleError(PasswordValidator.ERROR_TYPES.TIMEOUT_ERROR, '请求超时', error);
1179
- throw new Error('请求超时')
1180
- }
1181
-
1182
- if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
1183
- this.handleError(PasswordValidator.ERROR_TYPES.NETWORK_ERROR, '网络连接失败', error);
1184
- throw new Error('网络连接失败')
1185
- }
1186
-
1187
- throw error
1188
- }
1189
- }
1190
-
1191
- /**
1192
- * 清除公钥缓存
1193
- */
1194
- clearCache() {
1195
- this.publicKeyCache = null;
1196
- this.publicKeyExpiry = null;
1197
- }
1198
-
1199
- /**
1200
- * 更新配置
1201
- * @param {Object} newOptions 新的配置选项
1202
- */
1203
- updateOptions(newOptions) {
1204
- this.options = { ...this.options, ...newOptions };
1205
- // 清除缓存,使用新配置重新获取
1206
- this.clearCache();
1207
- }
1208
-
1209
- /**
1210
- * 取消当前请求
1211
- * 注意:由于现在每个请求使用独立的AbortController,此方法主要用于向后兼容
1212
- */
1213
- cancelRequest() {
1214
- // 由于现在每个请求使用独立的AbortController,无法直接取消所有请求
1215
- // 如果需要取消特定请求,建议在调用validatePassword时保存返回的Promise并手动处理
1216
- console.warn('cancelRequest方法已过时,现在每个请求使用独立的AbortController');
1217
- }
1218
-
1219
- /**
1220
- * 检查是否正在请求中
1221
- * @returns {boolean} 是否正在请求
1222
- * 注意:由于现在每个请求使用独立的AbortController,此方法无法准确判断
1223
- */
1224
- isRequesting() {
1225
- // 由于现在每个请求使用独立的AbortController,无法准确判断是否正在请求
1226
- console.warn('isRequesting方法已过时,现在每个请求使用独立的AbortController');
1227
- return false
1228
- }
1229
-
1230
- /**
1231
- * 获取缓存状态
1232
- * @returns {Object} 缓存状态信息
1233
- */
1234
- getCacheStatus() {
1235
- return {
1236
- hasCache: !!this.publicKeyCache,
1237
- isExpired: this.publicKeyExpiry ? Date.now() > this.publicKeyExpiry : true,
1238
- expiryTime: this.publicKeyExpiry,
1239
- remainingTime: this.publicKeyExpiry ? Math.max(0, this.publicKeyExpiry - Date.now()) : 0,
1240
- }
1241
- }
1242
-
1243
- /**
1244
- * 销毁实例,清理资源
1245
- */
1246
- destroy() {
1247
- try {
1248
- // 取消所有进行中的请求
1249
- this.cancelRequest();
1250
-
1251
- // 清除缓存
1252
- this.clearCache();
1253
-
1254
- // 调用销毁回调
1255
- if (this.options.onDestroy) {
1256
- this.options.onDestroy();
1257
- }
1258
- } catch (error) {
1259
- console.error('销毁密码校验器时出错:', error);
1260
- }
1261
- }
1262
- }
1263
-
1264
- // 导出类和创建实例的工厂函数
1265
- var PasswordValidator$1 = PasswordValidator;
1266
-
1267
- /**
1268
- * 创建密码校验器实例的工厂函数
1269
- * @param {Object} options 配置选项
1270
- * @returns {PasswordValidator} 密码校验器实例
1271
- */
1272
- function createPasswordValidator(options) {
1273
- return new PasswordValidator(options)
1274
- }
1275
-
1276
- /**
1277
- * 快速校验密码的便捷函数
1278
- * @param {string} password 密码
1279
- * @param {Object} options 配置选项
1280
- * @param {Object} additionalData 额外数据
1281
- * @returns {Promise<Object>} 校验结果
1282
- */
1283
- async function validatePassword(password, options = {}, additionalData = {}) {
1284
- const validator = new PasswordValidator(options);
1285
- // 修复:添加userName参数,可以从additionalData中提取或设为空字符串
1286
- const userName = additionalData.userName || '';
1287
- return await validator.validatePassword(password, userName, additionalData)
1288
- }
1289
-
1290
- // 导入滑块验证码组件(使用默认导入)
1291
-
1292
- // 默认导出(向后兼容)
1293
- var index = {
1294
- PopupSliderCaptcha: PopupSliderCaptcha$1,
1295
- PasswordValidator: PasswordValidator$1,
1296
- createPasswordValidator,
1297
- validatePassword
1298
- };
1299
-
1300
- // 全局注册(用于UMD构建)
1301
- if (typeof window !== 'undefined') {
1302
- window.SliderCaptcha = PopupSliderCaptcha$1;
1303
- window.PopupSliderCaptcha = PopupSliderCaptcha$1;
1304
- window.PasswordValidator = PasswordValidator$1;
1305
- window.createPasswordValidator = createPasswordValidator;
1306
- window.validatePassword = validatePassword;
1307
- }
1308
-
1309
- 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;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}}"}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};