slider-captcha-sdk 1.0.13 → 1.0.15

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.
@@ -1,31 +1,101 @@
1
1
  declare module 'slider-captcha-sdk/slider-captcha' {
2
+ // 优化:添加更详细的类型定义
3
+ export interface CaptchaData {
4
+ canvasSrc: string
5
+ blockSrc: string
6
+ canvasWidth: number
7
+ canvasHeight: number
8
+ blockWidth: number
9
+ blockHeight: number
10
+ blockY: number
11
+ nonceStr: string
12
+ }
13
+
14
+ export interface DragEvent {
15
+ time: number
16
+ position: number
17
+ }
18
+
19
+ export interface VerifyResult {
20
+ ticket?: string
21
+ timestamp: number
22
+ duration: number
23
+ }
24
+
25
+ export interface ErrorInfo {
26
+ type: string
27
+ message: string
28
+ originalError?: Error
29
+ }
30
+
2
31
  export interface SliderCaptchaOptions {
32
+ // API配置
3
33
  apiUrl?: string
4
34
  verifyUrl?: string
35
+
36
+ // 尺寸配置
5
37
  width?: number
6
38
  height?: number
7
39
  sliderSize?: number
8
- tolerance?: number
9
- loadingText?: string
10
- failedText?: string
11
- passedText?: string
12
- clickMaskClose?: boolean
40
+
41
+ // 行为配置
42
+ maxRetries?: number
43
+ timeout?: number
13
44
  throttleDelay?: number
14
- onSuccess?: (data: any) => void
15
- onFail?: (data: any) => void
45
+ clickMaskClose?: boolean
46
+
47
+ // 自定义请求数据
48
+ requestData?: Record<string, any>
49
+ verifyData?: Record<string, any>
50
+ headers?: Record<string, string>
51
+
52
+ // 回调函数
53
+ onSuccess?: (result: VerifyResult) => void
54
+ onFail?: (message: string) => void
55
+ onError?: (error: ErrorInfo) => void
56
+ onClose?: () => void
57
+ onDestroy?: () => void
16
58
  onRefresh?: () => void
17
- getCaptcha?: () => Promise<any>
59
+
60
+ // 自定义方法(已废弃,建议使用API配置)
61
+ getCaptcha?: () => Promise<CaptchaData>
18
62
  verifyCaptcha?: (data: any) => Promise<any>
19
63
  }
20
64
 
65
+ export interface SliderCaptchaState {
66
+ isVisible: boolean
67
+ isDragging: boolean
68
+ currentX: number
69
+ startX: number
70
+ retryCount: number
71
+ isLoading: boolean
72
+ }
73
+
21
74
  export default class PopupSliderCaptcha {
22
75
  constructor(options?: SliderCaptchaOptions)
76
+
77
+ // 公共方法
23
78
  show(): void
24
79
  hide(): void
25
80
  refresh(): void
26
81
  destroy(): void
82
+ reset(): void
83
+
84
+ // 状态管理
85
+ getState(): SliderCaptchaState
86
+ isVisible(): boolean
87
+ isDragging(): boolean
88
+ isLoading(): boolean
89
+
90
+ // 静态方法
27
91
  static create(options?: SliderCaptchaOptions): PopupSliderCaptcha
28
92
  static show(options?: SliderCaptchaOptions): PopupSliderCaptcha
93
+
94
+ // 常量
95
+ static readonly DEFAULTS: Partial<SliderCaptchaOptions>
96
+ static readonly CSS_CLASSES: Record<string, string>
97
+ static readonly CONSTANTS: Record<string, number>
98
+ static readonly ERROR_TYPES: Record<string, string>
29
99
  }
30
100
 
31
101
  export { PopupSliderCaptcha }
@@ -31,8 +31,36 @@ class PopupSliderCaptcha {
31
31
  return `:root{--sc-primary:#409eff;--sc-success:#67c23a;--sc-danger:#f56c6c;--sc-border:#e4e7eb;--sc-bg:linear-gradient(90deg, #f7f9fa 0%, #e8f4fd 100%);--sc-text:#333;--sc-text-light:#999;--sc-shadow:0 4px 20px rgba(0,0,0,.3);--sc-radius:8px;--sc-transition:.3s ease}.slider-captcha-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:9999;display:none;justify-content:center;align-items:center;opacity:0;transition:opacity var(--sc-transition)}.slider-captcha-overlay.show{opacity:1}.slider-captcha-modal{background:#fff;border-radius:var(--sc-radius);padding:20px;box-shadow:var(--sc-shadow);position:relative;max-width:90vw;max-height:90vh;transform:scale(.8) translateY(-20px);opacity:0;transition:all var(--sc-transition)}.slider-captcha-modal.show{transform:scale(1) translateY(0);opacity:1}.slider-captcha-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid var(--sc-border)}.slider-captcha-container{display:flex;align-items:center;position:relative;border-radius:4px;overflow:hidden;margin-bottom:15px;background:#837a7a;justify-content:center}.slider-captcha-track{width:100%;height:40px;line-height:40px;background:var(--sc-bg);border:1px solid var(--sc-border);border-radius:20px;position:relative;margin-bottom:15px;overflow:hidden}.slider-captcha-btn{width:38px;height:38px;background:#fff;border:1px solid #ccc;border-radius:50%;position:absolute;top:0;left:0;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px rgba(0,0,0,.1);transition:all var(--sc-transition);user-select:none;z-index:1}.slider-captcha-loading{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,.6);display:flex;align-items:center;justify-content:center;flex-direction:column;color:#666;font-size:14px;z-index:10;border-radius:4px}.slider-captcha-error{color:var(--sc-danger);font-size:12px;text-align:center;margin-top:10px;display:none}.slider-captcha-title{margin:0;font-size:16px;color:var(--sc-text)}.slider-captcha-close,.slider-captcha-refresh{background:none;border:none;cursor:pointer;color:var(--sc-text-light);padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all var(--sc-transition);position:relative;font-size:0}.slider-captcha-close::before,.slider-captcha-close::after{content:'';position:absolute;width:16px;height:2px;background-color:var(--sc-text-light);border-radius:1px;transition:all var(--sc-transition)}.slider-captcha-close::before{transform:rotate(45deg)}.slider-captcha-close::after{transform:rotate(-45deg)}.slider-captcha-close:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-close:hover::before,.slider-captcha-close:hover::after{background-color:var(--sc-danger)}.slider-captcha-refresh{margin-left:10px}.slider-captcha-refresh svg{width:20px;height:20px;fill:var(--sc-text-light);transition:all var(--sc-transition)}.slider-captcha-refresh:hover{background:#f5f5f5;transform:scale(1.1)}.slider-captcha-refresh:hover svg{fill:var(--sc-primary);transform:rotate(180deg)}.slider-captcha-floating-time{position:absolute;bottom:-40px;left:50%;transform:translateX(-50%);color:#fff;font-size:12px;white-space:nowrap;opacity:0;pointer-events:none;z-index:10;transition:all var(--sc-transition);background:#fff;padding:2px 15px;border-radius:10px}.slider-captcha-floating-time.show{opacity:1;transform:translateX(-50%) translateY(-45px)}.slider-captcha-floating-time.success{color:var(--sc-success)}.slider-captcha-floating-time.fail{color:var(--sc-danger)}.slider-captcha-bg{width:100%;height:100%;object-fit:cover;display:block}.slider-captcha-piece{position:absolute;top:0;left:0;cursor:pointer;transition:none;z-index:2}.slider-captcha-finger{position:absolute;top:50%;left:10px;transform:translateY(-50%);font-size:20px;animation:fingerSlide 2s ease-in-out infinite;pointer-events:none;z-index:1;opacity:.6}.slider-captcha-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--sc-text-light);font-size:14px;pointer-events:none;z-index:1;transition:all var(--sc-transition)}.slider-captcha-header-buttons{display:flex;align-items:center}@keyframes fingerSlide{0%{left:10px;opacity:.6}50%{opacity:1}100%{left:calc(50% - 10px);opacity:.6}}`
32
32
  }
33
33
 
34
+ // 优化:添加常量定义,提高可维护性
35
+ static CONSTANTS = {
36
+ CACHE_DURATION: 5 * 60 * 1000, // 5分钟缓存
37
+ MAX_RETRY_ATTEMPTS: 3,
38
+ DEFAULT_TIMEOUT: 30000,
39
+ ANIMATION_DURATION: 300,
40
+ FLOATING_TIME_DURATION: 2500,
41
+ THROTTLE_DELAY: 16
42
+ }
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
+
34
53
  constructor(options = {}) {
35
- this.options = { ...PopupSliderCaptcha.DEFAULTS, ...options };
54
+ // 优化:合并默认配置,使用常量
55
+ this.options = {
56
+ ...PopupSliderCaptcha.DEFAULTS,
57
+ ...options,
58
+ timeout: options.timeout || PopupSliderCaptcha.CONSTANTS.DEFAULT_TIMEOUT,
59
+ maxRetries: options.maxRetries || PopupSliderCaptcha.CONSTANTS.MAX_RETRY_ATTEMPTS,
60
+ throttleDelay: options.throttleDelay || PopupSliderCaptcha.CONSTANTS.THROTTLE_DELAY
61
+ };
62
+
63
+ // 优化:初始化所有属性
36
64
  this.elements = {};
37
65
  this.state = this.createInitialState();
38
66
  this.captchaData = null;
@@ -43,11 +71,18 @@ class PopupSliderCaptcha {
43
71
  this.rafId = null;
44
72
  this.cachedDimensions = null;
45
73
  this.imageCache = new Map();
74
+ this.abortController = null;
46
75
 
47
76
  // 优化:使用箭头函数绑定this,避免重复bind调用
48
77
  this.throttledHandleMove = this.throttle((e) => this.handleMove(e), this.options.throttleDelay);
49
78
 
50
- this.init();
79
+ // 优化:添加错误处理
80
+ try {
81
+ this.init();
82
+ } catch (error) {
83
+ console.error('滑块验证码初始化失败:', error);
84
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR, error.message);
85
+ }
51
86
  }
52
87
 
53
88
  // 优化:提取初始状态创建为独立方法
@@ -374,7 +409,6 @@ class PopupSliderCaptcha {
374
409
  show() {
375
410
  this.state.isVisible = true;
376
411
  this.elements.overlay.style.display = "flex";
377
- this.elements.overlay.offsetHeight; // 强制重绘
378
412
 
379
413
  requestAnimationFrame(() => {
380
414
  this.elements.overlay.classList.add("show");
@@ -398,56 +432,158 @@ class PopupSliderCaptcha {
398
432
  }, 300);
399
433
  }
400
434
 
401
- // 优化:简化加载逻辑
435
+ // 优化:添加统一错误处理方法
436
+ handleError(errorType, message, originalError = null) {
437
+ const errorMessages = {
438
+ [PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
439
+ [PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR]: '请求超时,请重试',
440
+ [PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR]: '验证失败,请重试',
441
+ [PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR]: '图片加载失败,请刷新重试',
442
+ [PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR]: '验证码数据错误,请刷新重试'
443
+ };
444
+
445
+ const errorMessage = errorMessages[errorType] || message || '未知错误';
446
+
447
+ // 调用用户自定义错误处理
448
+ if (this.options.onError) {
449
+ this.options.onError({
450
+ type: errorType,
451
+ message: errorMessage,
452
+ originalError
453
+ });
454
+ }
455
+
456
+ this.showError(errorMessage);
457
+ console.error(`滑块验证码错误 [${errorType}]:`, errorMessage, originalError);
458
+ }
459
+
460
+ // 优化:简化加载逻辑,增强错误处理
402
461
  async loadCaptcha() {
403
462
  try {
404
463
  this.showLoading();
405
464
  this.startTime = Date.now();
406
465
 
407
- const controller = new AbortController();
408
- const timeoutId = this.safeSetTimeout(() => controller.abort(), this.options.timeout);
466
+ // 取消之前的请求
467
+ if (this.abortController) {
468
+ this.abortController.abort();
469
+ }
470
+ this.abortController = new AbortController();
409
471
 
410
472
  const response = await fetch(this.options.apiUrl, {
411
473
  method: "POST",
412
- headers: { "Content-Type": "application/json" },
413
- body: JSON.stringify({ place: 2, timestamp: Date.now() }),
414
- signal: controller.signal
474
+ headers: {
475
+ "Content-Type": "application/json",
476
+ ...this.options.headers
477
+ },
478
+ body: JSON.stringify({
479
+ place: 2,
480
+ timestamp: Date.now(),
481
+ ...this.options.requestData
482
+ }),
483
+ signal: this.abortController.signal
415
484
  });
416
485
 
417
- this.safeClearTimeout(timeoutId);
486
+ if (!response.ok) {
487
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
488
+ }
418
489
 
419
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
420
490
  const data = await response.json();
421
491
 
422
- if (data.data?.canvasSrc && data.data?.blockSrc) {
423
- this.captchaData = data.data;
424
- this.showCaptcha();
425
- await this.renderCaptcha();
426
- } else {
492
+ // 优化:更严格的数据验证
493
+ if (!this.validateCaptchaData(data)) {
427
494
  throw new Error("验证码数据格式错误")
428
495
  }
496
+
497
+ this.captchaData = data.data;
498
+ this.showCaptcha();
499
+ await this.renderCaptcha();
429
500
  } catch (error) {
430
- const message = error.name === 'AbortError' ? "请求超时,请重试" : `加载验证码失败: ${error.message}`;
431
- this.showError(message);
501
+ if (error.name === 'AbortError') {
502
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '请求被取消');
503
+ } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
504
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
505
+ } else {
506
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.CAPTCHA_DATA_ERROR, error.message, error);
507
+ }
432
508
  }
433
509
  }
434
510
 
511
+ // 优化:添加验证码数据验证方法
512
+ validateCaptchaData(data) {
513
+ if (!data || typeof data !== 'object') return false
514
+
515
+ const requiredFields = ['canvasSrc', 'blockSrc', 'canvasWidth', 'canvasHeight', 'blockWidth', 'blockHeight', 'blockY', 'nonceStr'];
516
+ const dataObj = data.data || data;
517
+
518
+ return requiredFields.every(field => {
519
+ const value = dataObj[field];
520
+ return value !== null && value !== undefined && value !== ''
521
+ })
522
+ }
523
+
435
524
  async renderCaptcha() {
436
525
  return new Promise((resolve, reject) => {
437
- let loadedCount = 0;
438
- const onLoad = () => ++loadedCount === 2 && (this.hideLoading(), resolve());
439
- const onError = () => reject(new Error("图片加载失败"));
440
-
441
- this.loadImage(this.elements.backgroundImg, this.captchaData.canvasSrc, {
442
- width: this.captchaData.canvasWidth,
443
- height: this.captchaData.canvasHeight
444
- }, onLoad, onError);
445
-
446
- this.loadImage(this.elements.sliderImg, this.captchaData.blockSrc, {
447
- width: this.captchaData.blockWidth,
448
- height: this.captchaData.blockHeight,
449
- top: this.captchaData.blockY
450
- }, onLoad, onError);
526
+ let hasError = false;
527
+
528
+ // 优化:并行加载图片,提高性能
529
+ const loadPromises = [
530
+ this.loadImageAsync(this.elements.backgroundImg, this.captchaData.canvasSrc, {
531
+ width: this.captchaData.canvasWidth,
532
+ height: this.captchaData.canvasHeight
533
+ }),
534
+ this.loadImageAsync(this.elements.sliderImg, this.captchaData.blockSrc, {
535
+ width: this.captchaData.blockWidth,
536
+ height: this.captchaData.blockHeight,
537
+ top: this.captchaData.blockY
538
+ })
539
+ ];
540
+
541
+ Promise.all(loadPromises)
542
+ .then(() => {
543
+ if (!hasError) {
544
+ this.hideLoading();
545
+ resolve();
546
+ }
547
+ })
548
+ .catch((error) => {
549
+ if (!hasError) {
550
+ hasError = true;
551
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR, '图片加载失败', error);
552
+ reject(error);
553
+ }
554
+ });
555
+ })
556
+ }
557
+
558
+ // 优化:添加异步图片加载方法
559
+ async loadImageAsync(imgElement, src, styles) {
560
+ return new Promise((resolve, reject) => {
561
+ // 检查缓存
562
+ if (this.imageCache.has(src)) {
563
+ const cachedImg = this.imageCache.get(src);
564
+ imgElement.src = cachedImg.src;
565
+ this.applyStyles(imgElement, styles);
566
+ resolve();
567
+ return
568
+ }
569
+
570
+ const timeoutId = this.safeSetTimeout(() => {
571
+ reject(new Error('图片加载超时'));
572
+ }, 10000); // 10秒超时
573
+
574
+ imgElement.onload = () => {
575
+ this.safeClearTimeout(timeoutId);
576
+ this.imageCache.set(src, imgElement.cloneNode());
577
+ resolve();
578
+ };
579
+
580
+ imgElement.onerror = (error) => {
581
+ this.safeClearTimeout(timeoutId);
582
+ reject(error);
583
+ };
584
+
585
+ imgElement.src = src;
586
+ this.applyStyles(imgElement, styles);
451
587
  })
452
588
  }
453
589
 
@@ -534,38 +670,68 @@ class PopupSliderCaptcha {
534
670
  }
535
671
 
536
672
  try {
537
- const controller = new AbortController();
538
- const timeoutId = this.safeSetTimeout(() => controller.abort(), this.options.timeout);
673
+ // 取消之前的验证请求
674
+ if (this.abortController) {
675
+ this.abortController.abort();
676
+ }
677
+ this.abortController = new AbortController();
539
678
 
540
679
  const response = await fetch(this.options.verifyUrl, {
541
680
  method: "POST",
542
- headers: { "Content-Type": "application/json" },
681
+ headers: {
682
+ "Content-Type": "application/json",
683
+ ...this.options.headers
684
+ },
543
685
  body: JSON.stringify({
544
686
  loginVo: {
545
687
  nonceStr: this.captchaData.nonceStr,
546
688
  value: this.getPosition()
547
689
  },
548
- dragEventList: [...this.times]
690
+ dragEventList: [...this.times],
691
+ ...this.options.verifyData
549
692
  }),
550
- signal: controller.signal
693
+ signal: this.abortController.signal
551
694
  });
552
695
 
553
- this.safeClearTimeout(timeoutId);
696
+ if (!response.ok) {
697
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
698
+ }
554
699
 
555
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
556
700
  const data = await response.json();
557
701
 
558
- if (data.code === "0" || data.success === true) {
559
- this.onVerifySuccess(data.data);
702
+ // 优化:更灵活的验证结果判断
703
+ if (this.isVerifySuccess(data)) {
704
+ this.onVerifySuccess(data.data || data.result);
560
705
  } else {
561
- this.onVerifyFail(data.message || "验证失败,请重试!");
706
+ this.onVerifyFail(data.message || data.msg || "验证失败,请重试!");
562
707
  }
563
708
  } catch (error) {
564
- const message = error.name === 'AbortError' ? "验证超时,请重试" : "网络错误";
565
- this.onVerifyFail(message);
709
+ if (error.name === 'AbortError') {
710
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.TIMEOUT_ERROR, '验证请求被取消');
711
+ } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
712
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.NETWORK_ERROR, '网络连接失败');
713
+ } else {
714
+ this.handleError(PopupSliderCaptcha.ERROR_TYPES.VALIDATION_ERROR, error.message, error);
715
+ }
566
716
  }
567
717
  }
568
718
 
719
+ // 优化:添加验证成功判断方法
720
+ isVerifySuccess(data) {
721
+ if (!data || typeof data !== 'object') return false
722
+
723
+ // 支持多种成功标识
724
+ const successIndicators = [
725
+ data.code === "0",
726
+ data.code === 0,
727
+ data.success === true,
728
+ data.status === 'success',
729
+ data.result === true
730
+ ];
731
+
732
+ return successIndicators.some(indicator => indicator === true)
733
+ }
734
+
569
735
  onVerifySuccess(ticket) {
570
736
  const duration = Date.now() - this.startTime;
571
737
  const durationText = `验证成功!耗时:${(duration / 1000).toFixed(2)}s`;
@@ -706,36 +872,66 @@ class PopupSliderCaptcha {
706
872
  }
707
873
 
708
874
  destroy() {
709
- // 恢复body样式
710
- document.body.style.userSelect = "";
711
- document.body.style.cursor = "";
875
+ try {
876
+ // 取消所有进行中的请求
877
+ if (this.abortController) {
878
+ this.abortController.abort();
879
+ this.abortController = null;
880
+ }
712
881
 
713
- // 清理所有定时器
714
- this.clearAllTimers();
882
+ // 恢复body样式
883
+ if (document.body) {
884
+ document.body.style.userSelect = "";
885
+ document.body.style.cursor = "";
886
+ }
715
887
 
716
- // 移除所有事件监听器
717
- this.removeAllEventListeners();
888
+ // 清理所有定时器
889
+ this.clearAllTimers();
718
890
 
719
- // 清理图片资源
720
- this.cleanupImages();
891
+ // 移除所有事件监听器
892
+ this.removeAllEventListeners();
721
893
 
722
- // 移除DOM元素
723
- if (this.elements.overlay?.parentNode) {
724
- this.elements.overlay.parentNode.removeChild(this.elements.overlay);
725
- }
894
+ // 清理图片资源
895
+ this.cleanupImages();
726
896
 
727
- // 清理样式表
728
- const styleElement = document.getElementById('slider-captcha-styles');
729
- if (styleElement) {
730
- styleElement.remove();
731
- }
897
+ // 移除DOM元素
898
+ if (this.elements?.overlay?.parentNode) {
899
+ this.elements.overlay.parentNode.removeChild(this.elements.overlay);
900
+ }
732
901
 
733
- // 清空所有属性
734
- Object.keys(this).forEach(key => {
735
- if (key !== 'constructor') {
736
- this[key] = null;
902
+ // 清理样式表(只有在没有其他实例使用时才删除)
903
+ const styleElement = document.getElementById('slider-captcha-styles');
904
+ if (styleElement && !this.hasOtherInstances()) {
905
+ styleElement.remove();
737
906
  }
738
- });
907
+
908
+ // 清空缓存
909
+ this.imageCache.clear();
910
+ this.cachedDimensions = null;
911
+
912
+ // 清空所有属性
913
+ Object.keys(this).forEach(key => {
914
+ if (key !== 'constructor') {
915
+ this[key] = null;
916
+ }
917
+ });
918
+
919
+ // 调用销毁回调
920
+ if (this.options.onDestroy) {
921
+ this.options.onDestroy();
922
+ }
923
+ } catch (error) {
924
+ console.error('销毁滑块验证码时出错:', error);
925
+ }
926
+ }
927
+
928
+ // 优化:检查是否有其他实例在使用样式
929
+ hasOtherInstances() {
930
+ // 简单的检查方式,可以通过全局计数器实现
931
+ if (typeof window !== 'undefined') {
932
+ return window.sliderCaptchaInstanceCount > 1
933
+ }
934
+ return false
739
935
  }
740
936
 
741
937
  static create(options) {
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).SliderCaptcha=e()}(this,function(){"use strict";class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/api/captcha",verifyUrl:"/api/captcha/verify",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}}"}constructor(t={}){this.options={...PopupSliderCaptcha.DEFAULTS,...t},this.elements={},this.state=this.createInitialState(),this.captchaData=null,this.times=[],this.startTime=null,this.eventListeners=[],this.timers=new Set,this.rafId=null,this.cachedDimensions=null,this.imageCache=new Map,this.throttledHandleMove=this.throttle(t=>this.handleMove(t),this.options.throttleDelay),this.init()}createInitialState(){return{isVisible:!1,isDragging:!1,currentX:0,startX:0,retryCount:0,isLoading:!1}}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=document.createElement("style");t.id="slider-captcha-styles",t.textContent=PopupSliderCaptcha.getStyles(),document.head.appendChild(t)}createElements(){const{elements:t,options:e}=this;[["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]].forEach(([e,i,s,a])=>{t[e]=this.createElement(i,s,a)}),t.container.style.cssText=`width:${e.width}px;height:${e.height}px`,t.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>',this.assembleDOM(),this.setInitialState()}createElement(t,e="",i=""){const s=document.createElement(t);return e&&(s.className=e),i&&(s.textContent=i),s}assembleDOM(){const{elements:t}=this,e=this.createElement("div","slider-captcha-header-buttons");e.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,e),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;this.addEventListener(t.closeBtn,"click",()=>this.hide()),this.addEventListener(t.refreshBtn,"click",()=>this.refresh()),this.addEventListener(t.overlay,"click",e=>{e.target===t.overlay&&this.options.clickMaskClose&&this.hide()}),this.addEventListener(document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}),this.addEventListener(document,"visibilitychange",()=>this.handleVisibilityChange()),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,e={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};this.addEventListener(t.btn,"mousedown",e.start),this.addEventListener(t.btn,"touchstart",e.start),this.addEventListener(t.sliderImg,"mousedown",e.start),this.addEventListener(t.sliderImg,"touchstart",e.start),this.addEventListener(document,"mousemove",e.move,{passive:!1}),this.addEventListener(document,"touchmove",e.move,{passive:!1}),this.addEventListener(document,"mouseup",e.end),this.addEventListener(document,"touchend",e.end)}addEventListener(t,e,i,s={}){t&&"function"==typeof i&&(t.addEventListener(e,i,s),this.eventListeners.push({element:t,event:e,handler:i,options:s}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:e,handler:i,options:s})=>{try{t?.removeEventListener?.(e,i,s)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const t=this.elements.track.offsetWidth,e=this.elements.btn.offsetWidth;this.cachedDimensions={trackWidth:t,btnWidth:e,maxX:t-e}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),e=this.state.currentX/t;return Math.round(e*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),this.state.isDragging=!0,this.state.startX=this.getClientX(t)-this.state.currentX,this.startTime=this.startTime||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 e=this.getClientX(t)-this.state.startX,{maxX:i}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(e,i)),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&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX(t){return t.type.includes("touch")?t.touches[0].clientX:t.clientX}setTransition(t){const e=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{this.elements.btn.style.transition=e,this.elements.sliderImg.style.transition=e})}updateUIState(t){const{elements:e}=this,i={dragging:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none"},success:()=>{Object.assign(e.btn.style,{background:"var(--sc-success)"}),Object.assign(e.icon.style,{innerHTML:"✓",color:"white"}),e.icon.innerHTML="✓"},fail:()=>{Object.assign(e.btn.style,{background:"var(--sc-danger)"}),Object.assign(e.icon.style,{innerHTML:"✗",color:"white"}),e.icon.innerHTML="✗"},reset:()=>{Object.assign(e.btn.style,{background:"white"}),Object.assign(e.icon.style,{color:"#666"}),e.icon.innerHTML="→",e.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none",Object.assign(e.track.style,{pointerEvents:"none",opacity:"0.6"})}};i[t]&&requestAnimationFrame(()=>{i[t](),"loading"!==t&&Object.assign(e.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,e){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:e,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:e,state:i}=this,{maxX:s}=this.getDimensions(),a=i.currentX/s*(e.width-e.sliderSize),n=i.currentX/s;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${i.currentX}px)`,t.sliderImg.style.transform=`translateX(${a}px)`,t.fingerAnimation.style.opacity=.8>n?"0.6":"0"})}show(){this.state.isVisible=!0,this.elements.overlay.style.display="flex",this.elements.overlay.offsetHeight,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)}async loadCaptcha(){try{this.showLoading(),this.startTime=Date.now();const t=new AbortController,e=this.safeSetTimeout(()=>t.abort(),this.options.timeout),i=await fetch(this.options.apiUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({place:2,timestamp:Date.now()}),signal:t.signal});if(this.safeClearTimeout(e),!i.ok)throw Error("HTTP "+i.status);const s=await i.json();if(!s.data?.canvasSrc||!s.data?.blockSrc)throw Error("验证码数据格式错误");this.captchaData=s.data,this.showCaptcha(),await this.renderCaptcha()}catch(t){const e="AbortError"===t.name?"请求超时,请重试":"加载验证码失败: "+t.message;this.showError(e)}}async renderCaptcha(){return new Promise((t,e)=>{let i=0;const s=()=>2===++i&&(this.hideLoading(),t()),a=()=>e(Error("图片加载失败"));this.loadImage(this.elements.backgroundImg,this.captchaData.canvasSrc,{width:this.captchaData.canvasWidth,height:this.captchaData.canvasHeight},s,a),this.loadImage(this.elements.sliderImg,this.captchaData.blockSrc,{width:this.captchaData.blockWidth,height:this.captchaData.blockHeight,top:this.captchaData.blockY},s,a)})}loadImage(t,e,i,s,a){if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,i),void s()}t.onload=()=>{this.imageCache.set(e,t.cloneNode()),s()},t.onerror=a,t.src=e,this.applyStyles(t,i)}applyStyles(t,e){Object.entries(e).forEach(([e,i])=>{t.style[e]="number"==typeof i?i+"px":i})}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,e])=>{const i=this.elements[t];i&&Object.entries(e).forEach(([t,e])=>{"textContent"===t?i.textContent=e:i.style[t]=e})})})}async verify(){if(this.captchaData)try{const t=new AbortController,e=this.safeSetTimeout(()=>t.abort(),this.options.timeout),i=await fetch(this.options.verifyUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({loginVo:{nonceStr:this.captchaData.nonceStr,value:this.getPosition()},dragEventList:[...this.times]}),signal:t.signal});if(this.safeClearTimeout(e),!i.ok)throw Error("HTTP "+i.status);const s=await i.json();"0"===s.code||!0===s.success?this.onVerifySuccess(s.data):this.onVerifyFail(s.message||"验证失败,请重试!")}catch(t){const e="AbortError"===t.name?"验证超时,请重试":"网络错误";this.onVerifyFail(e)}else this.onVerifyFail("验证码数据丢失")}onVerifySuccess(t){const e=Date.now()-this.startTime,i=`验证成功!耗时:${(e/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(i,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:e}),this.hide()},2e3)}showFloatingTime(t,e="success"){const{elements:i}=this;i.floatingTime.textContent=t,i.floatingTime.className="slider-captcha-floating-time "+e,this.safeSetTimeout(()=>i.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{i.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.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(){this.reset(),this.state.retryCount=0,this.loadCaptcha()}safeSetTimeout(t,e){const i=setTimeout(()=>{this.timers.delete(i),t()},e);return this.timers.add(i),i}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,e){let i=0;return function(...s){const a=Date.now();if(a-i>=e)return i=a,t.apply(this,s)}}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout(()=>t.apply(this,s),e)}}destroy(){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(),Object.keys(this).forEach(t=>{"constructor"!==t&&(this[t]=null)})}static create(t){return new PopupSliderCaptcha(t)}static show(t){const e=new PopupSliderCaptcha(t);return e.show(),e}}return"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),PopupSliderCaptcha});
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).SliderCaptcha=e()}(this,function(){"use strict";class PopupSliderCaptcha{static DEFAULTS={width:320,height:155,sliderSize:38,maxRetries:3,timeout:3e4,apiUrl:"/api/captcha",verifyUrl:"/api/captcha/verify",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 CONSTANTS={CACHE_DURATION:3e5,MAX_RETRY_ATTEMPTS:3,DEFAULT_TIMEOUT:3e4,ANIMATION_DURATION:300,FLOATING_TIME_DURATION:2500,THROTTLE_DELAY:16};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,timeout:t.timeout||PopupSliderCaptcha.CONSTANTS.DEFAULT_TIMEOUT,maxRetries:t.maxRetries||PopupSliderCaptcha.CONSTANTS.MAX_RETRY_ATTEMPTS,throttleDelay:t.throttleDelay||PopupSliderCaptcha.CONSTANTS.THROTTLE_DELAY},this.elements={},this.state=this.createInitialState(),this.captchaData=null,this.times=[],this.startTime=null,this.eventListeners=[],this.timers=new Set,this.rafId=null,this.cachedDimensions=null,this.imageCache=new Map,this.abortController=null,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}}init(){this.injectStyles(),this.createElements(),this.bindEvents()}injectStyles(){if(document.querySelector("#slider-captcha-styles"))return;const t=document.createElement("style");t.id="slider-captcha-styles",t.textContent=PopupSliderCaptcha.getStyles(),document.head.appendChild(t)}createElements(){const{elements:t,options:e}=this;[["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]].forEach(([e,s,i,a])=>{t[e]=this.createElement(s,i,a)}),t.container.style.cssText=`width:${e.width}px;height:${e.height}px`,t.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>',this.assembleDOM(),this.setInitialState()}createElement(t,e="",s=""){const i=document.createElement(t);return e&&(i.className=e),s&&(i.textContent=s),i}assembleDOM(){const{elements:t}=this,e=this.createElement("div","slider-captcha-header-buttons");e.append(t.refreshBtn,t.closeBtn),t.header.append(t.title,e),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;this.addEventListener(t.closeBtn,"click",()=>this.hide()),this.addEventListener(t.refreshBtn,"click",()=>this.refresh()),this.addEventListener(t.overlay,"click",e=>{e.target===t.overlay&&this.options.clickMaskClose&&this.hide()}),this.addEventListener(document,"keydown",t=>{"Escape"===t.key&&this.state.isVisible&&this.hide()}),this.addEventListener(document,"visibilitychange",()=>this.handleVisibilityChange()),this.bindSliderEvents()}bindSliderEvents(){const{elements:t}=this,e={start:t=>this.handleStart(t),move:this.throttledHandleMove,end:()=>this.handleEnd()};this.addEventListener(t.btn,"mousedown",e.start),this.addEventListener(t.btn,"touchstart",e.start),this.addEventListener(t.sliderImg,"mousedown",e.start),this.addEventListener(t.sliderImg,"touchstart",e.start),this.addEventListener(document,"mousemove",e.move,{passive:!1}),this.addEventListener(document,"touchmove",e.move,{passive:!1}),this.addEventListener(document,"mouseup",e.end),this.addEventListener(document,"touchend",e.end)}addEventListener(t,e,s,i={}){t&&"function"==typeof s&&(t.addEventListener(e,s,i),this.eventListeners.push({element:t,event:e,handler:s,options:i}))}removeAllEventListeners(){this.eventListeners.forEach(({element:t,event:e,handler:s,options:i})=>{try{t?.removeEventListener?.(e,s,i)}catch(t){}}),this.eventListeners.length=0}getDimensions(){if(!this.cachedDimensions){const t=this.elements.track.offsetWidth,e=this.elements.btn.offsetWidth;this.cachedDimensions={trackWidth:t,btnWidth:e,maxX:t-e}}return this.cachedDimensions}getPosition(){const{maxX:t}=this.getDimensions(),e=this.state.currentX/t;return Math.round(e*(this.options.width-this.options.sliderSize))}handleStart(t){!this.captchaData||this.state.isDragging||this.state.isLoading||(t.preventDefault(),this.state.isDragging=!0,this.state.startX=this.getClientX(t)-this.state.currentX,this.startTime=this.startTime||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 e=this.getClientX(t)-this.state.startX,{maxX:s}=this.getDimensions();this.state.currentX=Math.max(0,Math.min(e,s)),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&&(this.elements.fingerAnimation.style.animationPlayState=t)}getClientX(t){return t.type.includes("touch")?t.touches[0].clientX:t.clientX}setTransition(t){const e=t?"all 0.3s ease":"none";requestAnimationFrame(()=>{this.elements.btn.style.transition=e,this.elements.sliderImg.style.transition=e})}updateUIState(t){const{elements:e}=this,s={dragging:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none"},success:()=>{Object.assign(e.btn.style,{background:"var(--sc-success)"}),Object.assign(e.icon.style,{innerHTML:"✓",color:"white"}),e.icon.innerHTML="✓"},fail:()=>{Object.assign(e.btn.style,{background:"var(--sc-danger)"}),Object.assign(e.icon.style,{innerHTML:"✗",color:"white"}),e.icon.innerHTML="✗"},reset:()=>{Object.assign(e.btn.style,{background:"white"}),Object.assign(e.icon.style,{color:"#666"}),e.icon.innerHTML="→",e.fingerAnimation.style.display="block",this.updateHintText("向右滑动完成验证","var(--sc-text-light)")},loading:()=>{e.hint.style.opacity="0",e.fingerAnimation.style.display="none",Object.assign(e.track.style,{pointerEvents:"none",opacity:"0.6"})}};s[t]&&requestAnimationFrame(()=>{s[t](),"loading"!==t&&Object.assign(e.track.style,{pointerEvents:"auto",opacity:"1"})})}updateHintText(t,e){requestAnimationFrame(()=>{Object.assign(this.elements.hint,{textContent:t}),Object.assign(this.elements.hint.style,{color:e,opacity:"1"})})}updateSliderPosition(){const{elements:t,options:e,state:s}=this,{maxX:i}=this.getDimensions(),a=s.currentX/i*(e.width-e.sliderSize),n=s.currentX/i;requestAnimationFrame(()=>{t.btn.style.transform=`translateX(${s.currentX}px)`,t.sliderImg.style.transform=`translateX(${a}px)`,t.fingerAnimation.style.opacity=.8>n?"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,e,s=null){const i={[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]||e||"未知错误";this.options.onError&&this.options.onError({type:t,message:i,originalError:s}),this.showError(i)}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({place:2,timestamp:Date.now(),...this.options.requestData}),signal:this.abortController.signal});if(!t.ok)throw Error(`HTTP ${t.status}: ${t.statusText}`);const e=await t.json();if(!this.validateCaptchaData(e))throw Error("验证码数据格式错误");this.captchaData=e.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 e=t.data||t;return["canvasSrc","blockSrc","canvasWidth","canvasHeight","blockWidth","blockHeight","blockY","nonceStr"].every(t=>{const s=e[t];return null!=s&&""!==s})}async renderCaptcha(){return new Promise((t,e)=>{let s=!1;const i=[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(i).then(()=>{s||(this.hideLoading(),t())}).catch(t=>{s||(s=!0,this.handleError(PopupSliderCaptcha.ERROR_TYPES.IMAGE_LOAD_ERROR,"图片加载失败",t),e(t))})})}async loadImageAsync(t,e,s){return new Promise((i,a)=>{if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,s),void i()}const n=this.safeSetTimeout(()=>{a(Error("图片加载超时"))},1e4);t.onload=()=>{this.safeClearTimeout(n),this.imageCache.set(e,t.cloneNode()),i()},t.onerror=t=>{this.safeClearTimeout(n),a(t)},t.src=e,this.applyStyles(t,s)})}loadImage(t,e,s,i,a){if(this.imageCache.has(e)){const a=this.imageCache.get(e);return t.src=a.src,this.applyStyles(t,s),void i()}t.onload=()=>{this.imageCache.set(e,t.cloneNode()),i()},t.onerror=a,t.src=e,this.applyStyles(t,s)}applyStyles(t,e){Object.entries(e).forEach(([e,s])=>{t.style[e]="number"==typeof s?s+"px":s})}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,e])=>{const s=this.elements[t];s&&Object.entries(e).forEach(([t,e])=>{"textContent"===t?s.textContent=e:s.style[t]=e})})})}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 e=await t.json();this.isVerifySuccess(e)?this.onVerifySuccess(e.data||e.result):this.onVerifyFail(e.message||e.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 e=Date.now()-this.startTime,s=`验证成功!耗时:${(e/1e3).toFixed(2)}s`;this.updateUIState("success"),this.showFloatingTime(s,"success"),this.safeSetTimeout(()=>{this.options.onSuccess?.({ticket:t,timestamp:Date.now(),duration:e}),this.hide()},2e3)}showFloatingTime(t,e="success"){const{elements:s}=this;s.floatingTime.textContent=t,s.floatingTime.className="slider-captcha-floating-time "+e,this.safeSetTimeout(()=>s.floatingTime.classList.add("show"),100),this.safeSetTimeout(()=>{s.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.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(){this.reset(),this.state.retryCount=0,this.loadCaptcha()}safeSetTimeout(t,e){const s=setTimeout(()=>{this.timers.delete(s),t()},e);return this.timers.add(s),s}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,e){let s=0;return function(...i){const a=Date.now();if(a-s>=e)return s=a,t.apply(this,i)}}debounce(t,e){let s;return function(...i){clearTimeout(s),s=setTimeout(()=>t.apply(this,i),e)}}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&&!this.hasOtherInstances()&&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){}}hasOtherInstances(){return"undefined"!=typeof window&&window.sliderCaptchaInstanceCount>1}static create(t){return new PopupSliderCaptcha(t)}static show(t){const e=new PopupSliderCaptcha(t);return e.show(),e}}return"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),PopupSliderCaptcha});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slider-captcha-sdk",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "纯JavaScript滑块验证码SDK和密码校验工具,无依赖,支持多种模块格式",
5
5
  "type": "module",
6
6
  "main": "dist/index.min.js",