customer-chat-sdk 1.1.0 → 1.1.1

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.
@@ -13,8 +13,12 @@ class IconManager {
13
13
  this.iconStartY = 0;
14
14
  this.dragMoveHandler = null;
15
15
  this.dragEndHandler = null;
16
+ this.checkDragHandler = null; // 临时拖动检测监听器
17
+ this.dragStartHandler = null; // 拖动开始事件监听器
18
+ this.touchStartHandler = null; // 触摸开始事件监听器
16
19
  this.iconPosition = null; // 图标位置配置
17
20
  this.debug = false; // debug 模式标志
21
+ this.isClickEnabled = true; // 是否允许点击(iframe 打开时禁用)
18
22
  this.iconPosition = position || null;
19
23
  this.debug = debug;
20
24
  }
@@ -30,7 +34,7 @@ class IconManager {
30
34
  this.iconElement.className = 'customer-sdk-icon';
31
35
  // 直接设置样式 - 图标容器
32
36
  const defaultStyle = {
33
- position: 'fixed',
37
+ position: 'absolute',
34
38
  width: '30px',
35
39
  height: '30px',
36
40
  backgroundColor: 'transparent', // 移除背景色,让图片直接显示
@@ -39,7 +43,7 @@ class IconManager {
39
43
  alignItems: 'center',
40
44
  justifyContent: 'center',
41
45
  cursor: 'pointer',
42
- zIndex: '999999',
46
+ zIndex: '1000002', // 确保图标始终在最上层(遮罩层 999998,iframe 容器 999999)
43
47
  boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
44
48
  userSelect: 'none',
45
49
  transition: 'transform 0.2s ease',
@@ -266,9 +270,11 @@ class IconManager {
266
270
  // 绑定事件处理器(用于后续清理)
267
271
  this.dragMoveHandler = this.handleDragMove.bind(this);
268
272
  this.dragEndHandler = this.handleDragEnd.bind(this);
269
- // 只在图标上监听开始事件
270
- this.iconElement.addEventListener('mousedown', this.handleDragStart.bind(this));
271
- this.iconElement.addEventListener('touchstart', this.handleDragStart.bind(this), { passive: false });
273
+ this.dragStartHandler = this.handleDragStart.bind(this);
274
+ // 只在图标上监听开始事件(保存引用以便后续移除)
275
+ this.iconElement.addEventListener('mousedown', this.dragStartHandler);
276
+ this.touchStartHandler = this.handleDragStart.bind(this);
277
+ this.iconElement.addEventListener('touchstart', this.touchStartHandler, { passive: false });
272
278
  }
273
279
  /**
274
280
  * 开始拖动
@@ -291,12 +297,33 @@ class IconManager {
291
297
  const rect = this.iconElement.getBoundingClientRect();
292
298
  this.iconStartX = rect.left;
293
299
  this.iconStartY = rect.top;
294
- // 添加拖动样式
295
- this.iconElement.style.transition = 'none';
296
- this.iconElement.style.cursor = 'grabbing';
300
+ // 注意:不要在这里立即移除 transition 和设置 cursor
301
+ // 只有在真正开始拖动时才修改样式,避免点击时图标位置跳动
297
302
  // 只在真正开始拖动时添加document事件监听
298
303
  // 先添加一个临时的move监听器来检测是否真的在拖动
299
304
  const checkDrag = (moveEvent) => {
305
+ // 检测事件目标:如果事件发生在iframe或其他元素上,停止检测拖动
306
+ const target = moveEvent.target;
307
+ if (target && target !== this.iconElement && !this.iconElement?.contains(target)) {
308
+ // 检查是否是iframe相关元素
309
+ const isIframeElement = target.tagName === 'IFRAME' ||
310
+ target.closest('iframe') ||
311
+ target.closest('.customer-sdk-container') ||
312
+ target.closest('.customer-sdk-overlay');
313
+ if (isIframeElement) {
314
+ // 如果事件发生在iframe相关元素上,停止检测并清理监听器
315
+ if (this.checkDragHandler) {
316
+ if ('touches' in moveEvent) {
317
+ document.removeEventListener('touchmove', this.checkDragHandler);
318
+ }
319
+ else {
320
+ document.removeEventListener('mousemove', this.checkDragHandler);
321
+ }
322
+ this.checkDragHandler = null;
323
+ }
324
+ return;
325
+ }
326
+ }
300
327
  const moveX = 'touches' in moveEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
301
328
  const moveY = 'touches' in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY;
302
329
  const deltaX = moveX - this.dragStartX;
@@ -304,12 +331,20 @@ class IconManager {
304
331
  // 如果移动距离超过5px,开始拖动
305
332
  if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
306
333
  this.isDragging = true;
307
- // 移除临时监听器
308
- if ('touches' in moveEvent) {
309
- document.removeEventListener('touchmove', checkDrag);
334
+ // 只有在真正开始拖动时才移除 transition 和设置 cursor
335
+ if (this.iconElement) {
336
+ this.iconElement.style.transition = 'none';
337
+ this.iconElement.style.cursor = 'grabbing';
310
338
  }
311
- else {
312
- document.removeEventListener('mousemove', checkDrag);
339
+ // 移除临时监听器
340
+ if (this.checkDragHandler) {
341
+ if ('touches' in moveEvent) {
342
+ document.removeEventListener('touchmove', this.checkDragHandler);
343
+ }
344
+ else {
345
+ document.removeEventListener('mousemove', this.checkDragHandler);
346
+ }
347
+ this.checkDragHandler = null;
313
348
  }
314
349
  // 添加正式的事件监听器
315
350
  if (this.dragMoveHandler && this.dragEndHandler) {
@@ -324,13 +359,15 @@ class IconManager {
324
359
  }
325
360
  }
326
361
  };
362
+ // 保存 checkDrag 引用,以便后续清理
363
+ this.checkDragHandler = checkDrag;
327
364
  // 添加临时检测监听器
328
365
  if ('touches' in e) {
329
- document.addEventListener('touchmove', checkDrag, { passive: false });
366
+ document.addEventListener('touchmove', this.checkDragHandler, { passive: false });
330
367
  document.addEventListener('touchend', this.dragEndHandler);
331
368
  }
332
369
  else {
333
- document.addEventListener('mousemove', checkDrag);
370
+ document.addEventListener('mousemove', this.checkDragHandler);
334
371
  document.addEventListener('mouseup', this.dragEndHandler);
335
372
  }
336
373
  }
@@ -342,20 +379,9 @@ class IconManager {
342
379
  return;
343
380
  const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
344
381
  const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
345
- // 检测事件目标:如果事件发生在iframe或其他元素上,停止拖动
346
- const target = e.target;
347
- if (target && target !== this.iconElement && !this.iconElement.contains(target)) {
348
- // 检查是否是iframe相关元素
349
- const isIframeElement = target.tagName === 'IFRAME' ||
350
- target.closest('iframe') ||
351
- target.closest('.customer-sdk-container') ||
352
- target.closest('.customer-sdk-overlay');
353
- if (isIframeElement) {
354
- // 如果事件发生在iframe相关元素上,停止拖动
355
- this.handleDragEnd();
356
- return;
357
- }
358
- }
382
+ // 注意:拖动过程中不检测 iframe 元素,因为用户可能只是想将图标拖动到 iframe 附近或上方
383
+ // 只有在拖动开始时(checkDrag 阶段)才检测 iframe,防止误判为拖动
384
+ // 一旦开始拖动,就应该允许拖动到任何位置,包括 iframe 上方
359
385
  // 计算从拖动开始位置到当前位置的距离
360
386
  const deltaX = clientX - this.dragStartX;
361
387
  const deltaY = clientY - this.dragStartY;
@@ -390,10 +416,15 @@ class IconManager {
390
416
  /**
391
417
  * 结束拖动
392
418
  */
393
- handleDragEnd(e) {
419
+ handleDragEnd(_e) {
394
420
  if (!this.iconElement)
395
421
  return;
396
- // 清理document上的事件监听器
422
+ // 清理document上的所有事件监听器(包括临时检测监听器)
423
+ if (this.checkDragHandler) {
424
+ document.removeEventListener('mousemove', this.checkDragHandler);
425
+ document.removeEventListener('touchmove', this.checkDragHandler);
426
+ this.checkDragHandler = null;
427
+ }
397
428
  if (this.dragMoveHandler) {
398
429
  document.removeEventListener('mousemove', this.dragMoveHandler);
399
430
  document.removeEventListener('touchmove', this.dragMoveHandler);
@@ -402,6 +433,8 @@ class IconManager {
402
433
  document.removeEventListener('mouseup', this.dragEndHandler);
403
434
  document.removeEventListener('touchend', this.dragEndHandler);
404
435
  }
436
+ // 恢复样式(如果之前被修改过)
437
+ // 注意:如果只是点击(没有拖动),这些样式可能没有被修改,但恢复操作是安全的
405
438
  this.iconElement.style.transition = 'transform 0.2s ease';
406
439
  this.iconElement.style.cursor = 'pointer';
407
440
  // 如果只是点击(没有拖动),触发点击事件
@@ -414,10 +447,52 @@ class IconManager {
414
447
  * 处理点击事件
415
448
  */
416
449
  handleClick() {
450
+ // 如果点击被禁用(iframe 打开时),不执行点击回调
451
+ if (!this.isClickEnabled) {
452
+ return;
453
+ }
417
454
  if (this.onClickCallback) {
418
455
  this.onClickCallback();
419
456
  }
420
457
  }
458
+ /**
459
+ * 禁用点击和拖拽(iframe 打开时调用)
460
+ */
461
+ disableClick() {
462
+ this.isClickEnabled = false;
463
+ // 移除拖动事件监听器
464
+ if (this.iconElement) {
465
+ if (this.dragStartHandler) {
466
+ this.iconElement.removeEventListener('mousedown', this.dragStartHandler);
467
+ }
468
+ if (this.touchStartHandler) {
469
+ this.iconElement.removeEventListener('touchstart', this.touchStartHandler);
470
+ }
471
+ // 禁用所有鼠标事件(包括点击和拖拽)
472
+ this.iconElement.style.pointerEvents = 'none';
473
+ this.iconElement.style.cursor = 'default';
474
+ }
475
+ // 清理可能正在进行的拖动
476
+ this.forceCleanupDragEvents();
477
+ }
478
+ /**
479
+ * 启用点击和拖拽(iframe 关闭时调用)
480
+ */
481
+ enableClick() {
482
+ this.isClickEnabled = true;
483
+ // 重新添加拖动事件监听器
484
+ if (this.iconElement) {
485
+ if (this.dragStartHandler) {
486
+ this.iconElement.addEventListener('mousedown', this.dragStartHandler);
487
+ }
488
+ if (this.touchStartHandler) {
489
+ this.iconElement.addEventListener('touchstart', this.touchStartHandler, { passive: false });
490
+ }
491
+ // 恢复鼠标事件
492
+ this.iconElement.style.pointerEvents = 'auto';
493
+ this.iconElement.style.cursor = 'pointer';
494
+ }
495
+ }
421
496
  /**
422
497
  * 创建消息徽章(简化版)
423
498
  */
@@ -483,7 +558,7 @@ class IframeManager {
483
558
  this.config = {
484
559
  src: '',
485
560
  mode: 'auto', // 默认自动检测设备类型
486
- width: 400,
561
+ width: 450, // PC 模式默认宽度
487
562
  height: 600,
488
563
  allowClose: true,
489
564
  ...config
@@ -523,9 +598,9 @@ class IframeManager {
523
598
  try {
524
599
  const actualMode = this.getActualMode();
525
600
  const isPC = actualMode === 'popup';
526
- // PC模式下创建遮罩层
601
+ // PC模式:创建或显示遮罩层
527
602
  if (isPC) {
528
- this.createOverlay();
603
+ this.createOverlay(); // createOverlay 内部会检查是否已存在
529
604
  }
530
605
  // 显示已创建的容器
531
606
  if (this.containerElement) {
@@ -540,8 +615,18 @@ class IframeManager {
540
615
  opacity: '1',
541
616
  display: 'block'
542
617
  });
543
- // 将容器移到遮罩层内
544
- this.overlayElement?.appendChild(this.containerElement);
618
+ // 关键优化:避免重复移动容器导致 iframe 重新加载
619
+ // 只有当容器不在遮罩层内时才移动,且确保遮罩层在 DOM 中
620
+ if (this.overlayElement) {
621
+ // 如果遮罩层不在 DOM 中,先添加到 DOM
622
+ if (!this.overlayElement.parentNode) {
623
+ document.body.appendChild(this.overlayElement);
624
+ }
625
+ // 只有当容器不在遮罩层内时才移动(避免重复移动导致 iframe 重新加载)
626
+ if (this.containerElement.parentNode !== this.overlayElement) {
627
+ this.overlayElement.appendChild(this.containerElement);
628
+ }
629
+ }
545
630
  }
546
631
  else {
547
632
  // 移动端模式:直接全屏显示,不需要遮罩层
@@ -578,22 +663,27 @@ class IframeManager {
578
663
  if (!this.isOpen) {
579
664
  return;
580
665
  }
581
- // 隐藏容器但保留DOM元素
666
+ // 隐藏容器但保留DOM元素(不移动容器,避免 iframe 重新加载)
582
667
  if (this.containerElement) {
583
668
  Object.assign(this.containerElement.style, {
584
669
  visibility: 'hidden',
585
670
  opacity: '0',
586
671
  display: 'none'
587
672
  });
673
+ // 注意:不移动容器,保持容器在当前位置(遮罩层或 body),避免 iframe 重新加载
588
674
  }
589
- // 移除遮罩层(仅PC模式)
675
+ // 隐藏遮罩层但不移除(仅PC模式,避免重新创建导致 iframe 重新加载)
590
676
  if (this.overlayElement) {
591
- this.overlayElement.remove();
592
- this.overlayElement = null;
677
+ Object.assign(this.overlayElement.style, {
678
+ visibility: 'hidden',
679
+ opacity: '0',
680
+ display: 'none'
681
+ });
682
+ // 不移除遮罩层,下次显示时直接显示即可
593
683
  }
594
684
  // 恢复body滚动(移动端模式)
595
- const actualMode = this.getActualMode();
596
- if (actualMode === 'fullscreen') {
685
+ const actualModeForScroll = this.getActualMode();
686
+ if (actualModeForScroll === 'fullscreen') {
597
687
  this.preventBodyScroll(false);
598
688
  }
599
689
  this.isOpen = false;
@@ -641,9 +731,29 @@ class IframeManager {
641
731
  }
642
732
  }
643
733
  /**
644
- * 创建遮罩层
734
+ * 创建遮罩层(PC模式使用)
645
735
  */
646
736
  createOverlay() {
737
+ // 如果遮罩层已存在,直接显示即可
738
+ if (this.overlayElement && this.overlayElement.parentNode) {
739
+ Object.assign(this.overlayElement.style, {
740
+ visibility: 'visible',
741
+ opacity: '1',
742
+ display: 'flex'
743
+ });
744
+ return;
745
+ }
746
+ // 如果遮罩层存在但不在 DOM 中,重新添加到 DOM
747
+ if (this.overlayElement && !this.overlayElement.parentNode) {
748
+ document.body.appendChild(this.overlayElement);
749
+ Object.assign(this.overlayElement.style, {
750
+ visibility: 'visible',
751
+ opacity: '1',
752
+ display: 'flex'
753
+ });
754
+ return;
755
+ }
756
+ // 创建新的遮罩层
647
757
  this.overlayElement = document.createElement('div');
648
758
  this.overlayElement.className = 'customer-sdk-overlay';
649
759
  Object.assign(this.overlayElement.style, {
@@ -659,7 +769,7 @@ class IframeManager {
659
769
  justifyContent: 'center',
660
770
  cursor: this.config.allowClose ? 'pointer' : 'default'
661
771
  });
662
- // 点击遮罩层关闭
772
+ // 点击遮罩层关闭(只添加一次事件监听器)
663
773
  if (this.config.allowClose) {
664
774
  this.overlayElement.addEventListener('click', (e) => {
665
775
  if (e.target === this.overlayElement) {
@@ -711,45 +821,59 @@ class IframeManager {
711
821
  'allow-pointer-lock', // 允许指针锁定
712
822
  'allow-storage-access-by-user-activation' // 允许用户激活的存储访问
713
823
  ].join(' '));
714
- // 根据设备类型设置滚动行为
824
+ // 根据设备类型设置模式
715
825
  const actualMode = this.getActualMode();
716
826
  const isPC = actualMode === 'popup';
717
827
  this.iframeElement.scrolling = isPC ? 'auto' : 'no'; // PC显示滚动条,移动端禁用
718
- const containerStyles = {
719
- width: isPC ? `${this.config.width}px` : '100%',
720
- height: isPC ? `${this.config.height}px` : '100%',
721
- maxWidth: isPC ? '450px' : '100%',
722
- maxHeight: isPC ? '700px' : '100%',
828
+ // PC 模式:使用配置的宽度和高度
829
+ // 移动端:使用全屏
830
+ const containerStyles = isPC ? {
831
+ // PC 弹窗模式
832
+ width: `${this.config.width || 450}px`,
833
+ height: `${this.config.height || 600}px`,
834
+ maxWidth: '90vw',
835
+ maxHeight: '90vh',
723
836
  backgroundColor: '#ffffff',
724
- borderRadius: isPC ? '12px' : '12px 12px 0 0',
725
- boxShadow: isPC
726
- ? '0 20px 40px rgba(0, 0, 0, 0.15)'
727
- : '0 -4px 16px rgba(0, 0, 0, 0.25)',
837
+ borderRadius: '12px',
838
+ boxShadow: '0 20px 40px rgba(0, 0, 0, 0.15)',
728
839
  border: 'none',
729
840
  position: 'fixed',
730
841
  zIndex: '999999',
731
- // PC模式下的定位
732
- ...(isPC ? {
733
- top: '50%',
734
- left: '50%',
735
- transform: 'translate(-50%, -50%)'
736
- } : {
737
- // 移动端全屏模式 - 确保占满屏幕且不滚动
738
- top: '0',
739
- left: '0',
740
- bottom: '0',
741
- right: '0',
742
- transform: 'none',
743
- overflow: 'hidden', // 防止容器本身滚动
744
- position: 'fixed' // 确保固定定位
745
- }),
842
+ // PC模式:居中显示
843
+ top: '50%',
844
+ left: '50%',
845
+ transform: 'translate(-50%, -50%)',
846
+ overflow: 'hidden',
847
+ // 初始隐藏的关键样式
848
+ visibility: 'hidden',
849
+ opacity: '0',
850
+ display: 'none'
851
+ } : {
852
+ // 移动端全屏模式(强制 100% 宽度和高度)
853
+ width: '100%',
854
+ height: '100%',
855
+ maxWidth: '100%',
856
+ maxHeight: '100%',
857
+ backgroundColor: '#ffffff',
858
+ borderRadius: '12px 12px 0 0',
859
+ boxShadow: '0 -4px 16px rgba(0, 0, 0, 0.25)',
860
+ border: 'none',
861
+ position: 'fixed',
862
+ zIndex: '999999',
863
+ // 全屏模式 - 占满整个屏幕
864
+ top: '0',
865
+ left: '0',
866
+ bottom: '0',
867
+ right: '0',
868
+ transform: 'none',
869
+ overflow: 'hidden',
746
870
  // 初始隐藏的关键样式
747
871
  visibility: 'hidden',
748
872
  opacity: '0',
749
873
  display: 'none'
750
874
  };
751
875
  Object.assign(this.containerElement.style, containerStyles);
752
- // iframe填充整个容器,根据设备类型设置滚动样式
876
+ // iframe填充整个容器
753
877
  const iframeStyles = {
754
878
  width: '100%',
755
879
  height: '100%',
@@ -883,6 +1007,7 @@ class IframeManager {
883
1007
  }
884
1008
  /**
885
1009
  * 获取当前显示模式
1010
+ * PC 模式使用弹窗,移动端使用全屏
886
1011
  */
887
1012
  getActualMode() {
888
1013
  if (this.config.mode === 'auto') {
@@ -935,9 +1060,14 @@ class IframeManager {
935
1060
  break;
936
1061
  case 'resize_iframe':
937
1062
  case 'resize':
938
- if (data.width && data.height) {
1063
+ // PC模式支持 resize,移动端忽略
1064
+ const actualMode = this.getActualMode();
1065
+ if (actualMode === 'popup' && data.width && data.height) {
939
1066
  this.resizeIframe(data.width, data.height);
940
1067
  }
1068
+ else if (this.debug) {
1069
+ console.log('Resize request ignored (fullscreen mode)');
1070
+ }
941
1071
  break;
942
1072
  case 'new-message':
943
1073
  // 新消息通知 - 触发回调让外层处理
@@ -956,12 +1086,12 @@ class IframeManager {
956
1086
  }
957
1087
  }
958
1088
  /**
959
- * 调整iframe大小
1089
+ * 调整iframe大小(PC模式支持)
960
1090
  */
961
1091
  resizeIframe(width, height) {
962
- if (this.iframeElement) {
963
- this.iframeElement.style.width = `${width}px`;
964
- this.iframeElement.style.height = `${height}px`;
1092
+ if (this.containerElement) {
1093
+ this.containerElement.style.width = `${width}px`;
1094
+ this.containerElement.style.height = `${height}px`;
965
1095
  }
966
1096
  }
967
1097
  }
@@ -20699,9 +20829,9 @@ class CustomerServiceSDK {
20699
20829
  // 创建iframe管理器(自动检测设备类型)
20700
20830
  this.iframeManager = new IframeManager({
20701
20831
  src: iframeUrl,
20702
- mode: 'auto', // 自动根据设备类型选择模式
20703
- width: 400,
20704
- height: 600,
20832
+ mode: 'auto', // 自动根据设备类型选择模式(PC弹窗,移动端全屏)
20833
+ width: options?.width || 450, // PC模式宽度(像素,默认450px),移动端不使用
20834
+ height: options?.height || 600, // PC模式高度(像素),移动端不使用(强制全屏)
20705
20835
  allowClose: true,
20706
20836
  debug: this.debug, // 传递 debug 标志
20707
20837
  onMessage: (messageType, _data) => {
@@ -20713,8 +20843,9 @@ class CustomerServiceSDK {
20713
20843
  // checkScreenshot 消息由 ScreenshotManager 处理,不需要在这里处理
20714
20844
  },
20715
20845
  onClose: () => {
20716
- // iframe关闭时,清理图标拖动事件监听器
20846
+ // iframe关闭时,清理图标拖动事件监听器,并重新启用图标点击
20717
20847
  this.iconManager?.forceCleanupDragEvents();
20848
+ this.iconManager?.enableClick();
20718
20849
  },
20719
20850
  ...options
20720
20851
  });
@@ -20725,6 +20856,8 @@ class CustomerServiceSDK {
20725
20856
  // 打开iframe时清除红点通知
20726
20857
  this.clearNotification();
20727
20858
  this.iframeManager?.show();
20859
+ // iframe 打开后,禁用图标点击(防止重复打开)
20860
+ this.iconManager?.disableClick();
20728
20861
  });
20729
20862
  // 初始化截图管理器(如果启用了截图功能)
20730
20863
  if (config.screenshot) {