customer-chat-sdk 1.1.16 → 1.1.18

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.
@@ -16,12 +16,14 @@ class IconManager {
16
16
  this.target = null; // 图标传送目标元素(可以是 HTMLElement 或选择器字符串)
17
17
  // 拖动相关状态
18
18
  this.isDragging = false;
19
- this.dragStarted = false; // 是否真正开始了拖拽
19
+ this.dragStarted = false; // 是否真正开始了拖拽(移动超过阈值)
20
+ this.dragIntent = false; // 拖动意图(mousedown/touchstart 时设置为 true,表示用户想要拖动)
20
21
  this.hasMoved = false; // 是否移动过
21
22
  this.dragOffset = { x: 0, y: 0 }; // 拖动偏移量
22
23
  this.lastTouchPosition = { x: 0, y: 0 }; // 最后触摸位置
23
24
  this.touchStartTime = 0; // 触摸开始时间
24
25
  this.clickThreshold = 15; // 点击阈值(像素)
26
+ this.stoppedByIframe = false; // 是否因为 iframe 而停止拖动(用于避免触发磁性吸附)
25
27
  // 性能优化:缓存容器信息,避免频繁查询 DOM
26
28
  this.cachedContainer = null;
27
29
  this.cachedContainerRect = null;
@@ -37,7 +39,9 @@ class IconManager {
37
39
  this.pendingPosition = { x: 0, y: 0, needsUpdate: false };
38
40
  // 主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置(即使没有 mousemove 事件)
39
41
  this.activeDetectionRafId = null;
42
+ this.activeDetectionTimeoutId = null; // setTimeout 的 ID
40
43
  this.lastMousePosition = { x: 0, y: 0, timestamp: 0 }; // 最后记录的鼠标位置和时间戳
44
+ this.mouseVelocity = { vx: 0, vy: 0 }; // 鼠标移动速度(用于预测位置)
41
45
  // 事件处理器引用(用于清理)
42
46
  this.onDragHandler = null;
43
47
  this.stopDragHandler = null;
@@ -49,6 +53,7 @@ class IconManager {
49
53
  this.mouseLeaveTimeout = null; // 鼠标离开防抖定时器
50
54
  this.pointerLeaveTimeout = null; // 指针离开防抖定时器
51
55
  this.isStoppingDrag = false; // 防止 stopDrag 重复调用
56
+ this.hasLoggedIframeWarning = false; // 防止 iframe 警告日志重复输出
52
57
  // 侧边吸附相关状态
53
58
  this.sideAttach = true; // 是否启用侧边吸附(默认 true)
54
59
  this.sideHideRatio = 0.5; // 侧边吸附时的隐藏比例(默认 0.5,显示一半)
@@ -84,6 +89,7 @@ class IconManager {
84
89
  this.iconElement = document.createElement('div');
85
90
  this.iconElement.className = 'customer-sdk-icon';
86
91
  // 直接设置样式 - 图标容器
92
+ // 关键优化:使用 pointer-events: none 让事件穿透到 iframe,子元素使用 pointer-events: auto 保持可点击
87
93
  const defaultStyle = {
88
94
  position: 'absolute',
89
95
  width: '30px',
@@ -100,7 +106,8 @@ class IconManager {
100
106
  transition: 'transform 0.2s ease',
101
107
  border: 'none',
102
108
  outline: 'none',
103
- overflow: 'visible' // 允许红点显示在图标外部
109
+ overflow: 'visible', // 允许红点显示在图标外部
110
+ pointerEvents: 'none' // 让事件穿透到 iframe,只有子元素可以接收事件
104
111
  };
105
112
  // 如果指定了位置,使用left/top;否则使用默认的bottom/right
106
113
  if (this.iconPosition) {
@@ -198,12 +205,22 @@ class IconManager {
198
205
  height: '100%',
199
206
  borderRadius: '50%',
200
207
  overflow: 'hidden', // 限制图片溢出圆形边界
201
- position: 'relative'
208
+ position: 'relative',
209
+ pointerEvents: 'auto' // 子元素启用指针事件,让 icon 可以点击和拖动
202
210
  });
203
211
  imgContainer.appendChild(iconImg);
204
212
  this.iconElement.appendChild(imgContainer);
205
213
  // 添加到目标元素(如果 target 是字符串,需要重新查找,因为可能在初始化时元素还不存在)
206
214
  const targetElement = this.getTargetElement();
215
+ if (this.debug) {
216
+ console.log('[IconManager] Target element for icon:', {
217
+ target: this.target,
218
+ targetElement: targetElement,
219
+ targetElementId: targetElement.id,
220
+ targetElementClass: targetElement.className,
221
+ isBody: targetElement === document.body
222
+ });
223
+ }
207
224
  // 确保目标容器有 overflow-x: hidden,防止图标超出边界时出现横向滚动条
208
225
  // 注意:只在启用侧边吸附时才设置,避免影响其他场景
209
226
  if (this.sideAttach && targetElement !== document.body) {
@@ -236,12 +253,20 @@ class IconManager {
236
253
  }
237
254
  if (targetElement) {
238
255
  targetElement.appendChild(this.iconElement);
256
+ if (this.debug) {
257
+ console.log('[IconManager] Icon added to target element:', {
258
+ target: this.target,
259
+ targetElement: targetElement,
260
+ targetElementId: targetElement.id,
261
+ targetElementClass: targetElement.className
262
+ });
263
+ }
239
264
  }
240
265
  else {
241
266
  // 如果目标元素不存在,回退到 document.body
242
267
  document.body.appendChild(this.iconElement);
243
268
  if (this.debug) {
244
- console.warn('Target element not found, icon added to document.body');
269
+ console.warn('[IconManager] Target element not found, icon added to document.body');
245
270
  }
246
271
  }
247
272
  // 设置拖动事件
@@ -556,12 +581,70 @@ class IconManager {
556
581
  this.iconElement.addEventListener('mousedown', this.startDragHandler);
557
582
  this.iconElement.addEventListener('touchstart', this.startDragHandler, { passive: false });
558
583
  }
584
+ /**
585
+ * 检查 SDK iframe 是否打开(PC 模式)
586
+ * PC 模式下,如果 SDK iframe 打开,禁止拖动图标
587
+ */
588
+ isSDKIframeOpen() {
589
+ const isPC = window.innerWidth > 768;
590
+ if (!isPC)
591
+ return false; // 移动端不受影响
592
+ // 检查 SDK iframe 容器
593
+ const iframeContainer = document.querySelector('.customer-sdk-container');
594
+ if (!iframeContainer) {
595
+ if (this.debug) {
596
+ console.log('[IconManager] [PC+iframe] SDK iframe container not found');
597
+ }
598
+ return false;
599
+ }
600
+ // 检查 iframe 容器是否可见
601
+ // 注意:IframeManager.show() 会设置 visibility: 'visible', opacity: '1', display: 'block'
602
+ const style = window.getComputedStyle(iframeContainer);
603
+ const visibility = style.visibility;
604
+ const opacity = style.opacity;
605
+ const display = style.display;
606
+ // 更严格的检查:必须是 visible、opacity > 0、display 不是 none
607
+ const isVisible = (visibility === 'visible' &&
608
+ parseFloat(opacity) > 0 &&
609
+ display !== 'none');
610
+ if (this.debug) {
611
+ console.log('[IconManager] [PC+iframe] Checking SDK iframe visibility:', {
612
+ visibility,
613
+ opacity,
614
+ display,
615
+ isVisible,
616
+ parsedOpacity: parseFloat(opacity)
617
+ });
618
+ }
619
+ return isVisible;
620
+ }
559
621
  /**
560
622
  * 开始拖动
561
623
  */
562
624
  startDrag(e) {
563
625
  if (!this.iconElement || !this.isClickEnabled)
564
626
  return;
627
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
628
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
629
+ // PC 模式下,如果 SDK iframe 打开,禁止拖动图标
630
+ if (this.isSDKIframeOpen()) {
631
+ if (this.debug) {
632
+ console.log('[IconManager] [PC+iframe] SDK iframe is open, icon dragging disabled');
633
+ }
634
+ return;
635
+ }
636
+ // PC 模式下,如果鼠标在任何 iframe 上(排除 SDK 自己的 iframe),禁止拖动图标
637
+ const isPC = window.innerWidth > 768;
638
+ if (isPC && this.isMouseOverIframe(clientX, clientY)) {
639
+ if (this.debug) {
640
+ console.log('[IconManager] [PC+iframe] Mouse is over iframe, icon dragging disabled', {
641
+ clientX,
642
+ clientY,
643
+ isPC
644
+ });
645
+ }
646
+ return;
647
+ }
565
648
  // 清除自动吸附定时器(用户开始拖动)
566
649
  this.clearAutoAttachTimer();
567
650
  // 如果正在停止拖动,等待完成后再允许新的拖动
@@ -582,6 +665,8 @@ class IconManager {
582
665
  }
583
666
  return;
584
667
  }
668
+ // 重置 iframe 警告标志
669
+ this.hasLoggedIframeWarning = false;
585
670
  // 如果发现残留的资源,先清理它们(防御性编程)
586
671
  if (this.dragTimeoutId !== null || this.activeDetectionRafId !== null || this.rafId !== null) {
587
672
  if (this.debug) {
@@ -601,16 +686,49 @@ class IconManager {
601
686
  target.closest('.customer-sdk-overlay'))) {
602
687
  return;
603
688
  }
689
+ // 关键优化:检查触摸点是否真的在 icon 的圆形区域内
690
+ // 如果不在,让事件穿透到 iframe,不触发拖动
691
+ if (this.iconElement) {
692
+ const iconRect = this.iconElement.getBoundingClientRect();
693
+ // 计算触摸点相对于 icon 中心的位置
694
+ const iconCenterX = iconRect.left + iconRect.width / 2;
695
+ const iconCenterY = iconRect.top + iconRect.height / 2;
696
+ const iconRadius = Math.min(iconRect.width, iconRect.height) / 2;
697
+ const distanceFromCenter = Math.sqrt(Math.pow(clientX - iconCenterX, 2) + Math.pow(clientY - iconCenterY, 2));
698
+ // 如果触摸点在 icon 圆形区域外(留一点边距,比如 10px),不处理拖动
699
+ // 这样可以让 iframe 内的滑动正常工作
700
+ // 但边距不能太大,否则无法拖动
701
+ if (distanceFromCenter > iconRadius + 10) {
702
+ if (this.debug) {
703
+ console.log('[IconManager] Touch point outside icon circle, allowing event to pass through', {
704
+ distance: distanceFromCenter,
705
+ radius: iconRadius,
706
+ touchPoint: { x: clientX, y: clientY },
707
+ iconCenter: { x: iconCenterX, y: iconCenterY }
708
+ });
709
+ }
710
+ return;
711
+ }
712
+ }
604
713
  e.preventDefault();
605
714
  e.stopPropagation();
715
+ // 拖动开始时,临时启用 pointer-events 确保拖动事件能正常接收
716
+ if (this.iconElement) {
717
+ this.iconElement.style.pointerEvents = 'auto';
718
+ }
606
719
  // 重置状态
607
720
  this.hasMoved = false;
608
721
  this.dragStarted = false;
609
722
  this.isDragging = false;
723
+ this.dragIntent = true; // 设置拖动意图,表示用户已经开始拖动意图(在 mousedown/touchstart 时)
610
724
  // 记录开始时间和位置
611
725
  this.touchStartTime = Date.now();
612
- const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
613
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
726
+ // 初始化 lastMousePosition,避免第一次 onDrag timeSinceLastUpdate 过大
727
+ this.lastMousePosition = {
728
+ x: clientX,
729
+ y: clientY,
730
+ timestamp: Date.now()
731
+ };
614
732
  this.lastTouchPosition.x = clientX;
615
733
  this.lastTouchPosition.y = clientY;
616
734
  try {
@@ -628,14 +746,21 @@ class IconManager {
628
746
  // 注意:不在这里转换位置,只在真正开始拖动时才转换(在 onDrag 中)
629
747
  // 性能优化:在拖动开始时预加载所有 iframe 位置信息
630
748
  // 这样可以避免在拖动过程中频繁查询 DOM
749
+ // 注意:排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe),因为 SDK 的 iframe 不应该影响拖动
631
750
  const allIframes = document.querySelectorAll('iframe');
632
- this.cachedIframes = Array.from(allIframes).map(iframe => ({
751
+ this.cachedIframes = Array.from(allIframes)
752
+ .filter(iframe => {
753
+ // 检查 iframe 是否在 SDK 容器内
754
+ const container = iframe.closest('.customer-sdk-container');
755
+ return !container; // 排除 SDK 容器内的 iframe
756
+ })
757
+ .map(iframe => ({
633
758
  element: iframe,
634
759
  rect: iframe.getBoundingClientRect()
635
760
  }));
636
761
  this.lastIframeUpdateTime = Date.now();
637
762
  if (this.debug) {
638
- console.log(`[IconManager] Drag start - Found ${this.cachedIframes.length} iframe(s)`, {
763
+ console.log(`[IconManager] Drag start - Found ${this.cachedIframes.length} iframe(s) (excluding SDK iframes)`, {
639
764
  iframes: this.cachedIframes.map(({ rect }) => ({
640
765
  left: rect.left,
641
766
  top: rect.top,
@@ -657,43 +782,54 @@ class IconManager {
657
782
  }
658
783
  // 添加处理 iframe 上事件丢失的机制
659
784
  // 1. 监听 mouseleave 和 pointerleave 事件(鼠标离开窗口时停止拖动)
660
- // 注意:这些事件可能会在拖动过程中多次触发,需要添加防抖机制
785
+ // 注意:当鼠标移动到 iframe 上时,这些事件可能会误触发,需要更严格的检测
661
786
  this.mouseLeaveHandler = (e) => {
662
- // 只有当鼠标真正离开窗口时才停止拖动
663
- if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
664
- e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
787
+ // 检查鼠标是否真的离开了窗口(而不是移动到 iframe 上)
788
+ // 只有当鼠标完全离开窗口边界时才停止拖动
789
+ const isReallyLeaving = (e.clientY < -10 ||
790
+ e.clientX < -10 ||
791
+ e.clientX > window.innerWidth + 10 ||
792
+ e.clientY > window.innerHeight + 10);
793
+ // 还要检查是否在 iframe 区域内(如果在 iframe 上,不应该停止拖动)
794
+ if (isReallyLeaving && !this.isMouseOverIframe(e.clientX, e.clientY)) {
665
795
  // 添加防抖,避免重复触发
666
796
  if (this.mouseLeaveTimeout) {
667
797
  clearTimeout(this.mouseLeaveTimeout);
668
798
  }
669
799
  this.mouseLeaveTimeout = window.setTimeout(() => {
670
- if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag) {
800
+ // 再次确认鼠标真的离开了窗口
801
+ if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
671
802
  if (this.debug) {
672
803
  console.log('[IconManager] Mouse left window, stopping drag');
673
804
  }
674
805
  this.stopDrag();
675
806
  }
676
807
  this.mouseLeaveTimeout = null;
677
- }, 50); // 50ms 防抖
808
+ }, 100); // 增加到 100ms 防抖,避免误判
678
809
  }
679
810
  };
680
811
  this.pointerLeaveHandler = (e) => {
681
- // 只有当指针真正离开窗口时才停止拖动
682
- if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
683
- e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
812
+ // 检查指针是否真的离开了窗口(而不是移动到 iframe 上)
813
+ const isReallyLeaving = (e.clientY < -10 ||
814
+ e.clientX < -10 ||
815
+ e.clientX > window.innerWidth + 10 ||
816
+ e.clientY > window.innerHeight + 10);
817
+ // 还要检查是否在 iframe 区域内(如果在 iframe 上,不应该停止拖动)
818
+ if (isReallyLeaving && !this.isMouseOverIframe(e.clientX, e.clientY)) {
684
819
  // 添加防抖,避免重复触发
685
820
  if (this.pointerLeaveTimeout) {
686
821
  clearTimeout(this.pointerLeaveTimeout);
687
822
  }
688
823
  this.pointerLeaveTimeout = window.setTimeout(() => {
689
- if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag) {
824
+ // 再次确认指针真的离开了窗口
825
+ if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
690
826
  if (this.debug) {
691
827
  console.log('[IconManager] Pointer left window, stopping drag');
692
828
  }
693
829
  this.stopDrag();
694
830
  }
695
831
  this.pointerLeaveTimeout = null;
696
- }, 50); // 50ms 防抖
832
+ }, 100); // 增加到 100ms 防抖,避免误判
697
833
  }
698
834
  };
699
835
  document.addEventListener('mouseleave', this.mouseLeaveHandler);
@@ -715,15 +851,97 @@ class IconManager {
715
851
  };
716
852
  window.addEventListener('blur', this.blurHandler);
717
853
  // 3. 添加超时机制(如果一段时间没有收到 mousemove 事件,自动停止拖动)
718
- // 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
719
- this.dragTimeoutId = window.setTimeout(() => {
720
- if (this.isDragging) {
721
- if (this.debug) {
722
- console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
723
- }
724
- this.stopDrag();
854
+ // 注意:在 startDrag 时,检查鼠标是否在 iframe
855
+ // 如果在 iframe 上,使用更长的超时时间(5秒),因为 mousemove 事件会丢失
856
+ // 如果不在 iframe 上,使用简单的超时逻辑(50ms)
857
+ const isPC = window.innerWidth > 768;
858
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
859
+ if (isPC) {
860
+ // ========== PC 模式 ==========
861
+ if (isOverIframe) {
862
+ // PC 模式 + 在 iframe 上:特殊处理逻辑
863
+ // 在 iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
864
+ // 所以使用更长的超时时间(5秒),等待 mouseup
865
+ const checkDragTimeoutForPCIframe = () => {
866
+ const lastPos = this.lastMousePosition;
867
+ const stillOverIframe = lastPos && this.isMouseOverIframe(lastPos.x, lastPos.y);
868
+ // 如果还在 iframe 上且还有拖动意图,继续等待 mouseup
869
+ if (stillOverIframe && (this.dragIntent || this.dragStarted || this.isDragging)) {
870
+ if (this.debug) {
871
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
872
+ console.log('[IconManager] [PC+iframe] Drag timeout but mouse is over iframe, keeping drag active (waiting for mouseup)', {
873
+ timeSinceLastMove,
874
+ lastMousePosition: this.lastMousePosition
875
+ });
876
+ }
877
+ // PC 模式 iframe 上:使用 5 秒超时,等待 mouseup
878
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForPCIframe, 5000);
879
+ return;
880
+ }
881
+ // 不在 iframe 上了,或者拖动意图已消失,停止拖动
882
+ if ((this.isDragging || this.dragStarted) && this.dragStarted) {
883
+ if (this.debug) {
884
+ console.warn('[IconManager] [PC+iframe] Drag timeout triggered, stopping drag');
885
+ }
886
+ this.stopDrag();
887
+ }
888
+ };
889
+ // PC 模式 iframe 上:使用 5 秒超时
890
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForPCIframe, 5000);
891
+ }
892
+ else {
893
+ // PC 模式 + 不在 iframe 上:还原到原来的简单逻辑
894
+ // 原来的逻辑:简单的超时,50ms(和之前的代码一致)
895
+ // 只有在真正拖动后(dragStarted = true)才停止拖动
896
+ this.dragTimeoutId = window.setTimeout(() => {
897
+ if (this.isDragging && this.dragStarted) {
898
+ if (this.debug) {
899
+ console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
900
+ }
901
+ this.stopDrag();
902
+ }
903
+ }, 50); // PC 模式非 iframe:使用 50ms 超时
725
904
  }
726
- }, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
905
+ }
906
+ else {
907
+ // ========== 手机模式 ==========
908
+ if (isOverIframe) {
909
+ // 手机模式 + 在 iframe 上:特殊处理逻辑
910
+ const checkDragTimeoutForMobileIframe = () => {
911
+ const lastPos = this.lastMousePosition;
912
+ const stillOverIframe = lastPos && this.isMouseOverIframe(lastPos.x, lastPos.y);
913
+ if (stillOverIframe && (this.dragIntent || this.dragStarted || this.isDragging)) {
914
+ if (this.debug) {
915
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
916
+ console.log('[IconManager] [Mobile+iframe] Drag timeout but mouse is over iframe, keeping drag active (waiting for mouseup)', {
917
+ timeSinceLastMove,
918
+ lastMousePosition: this.lastMousePosition
919
+ });
920
+ }
921
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
922
+ return;
923
+ }
924
+ if ((this.isDragging || this.dragStarted) && this.dragStarted) {
925
+ if (this.debug) {
926
+ console.warn('[IconManager] [Mobile+iframe] Drag timeout triggered, stopping drag');
927
+ }
928
+ this.stopDrag();
929
+ }
930
+ };
931
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
932
+ }
933
+ else {
934
+ // 手机模式 + 不在 iframe 上:使用简单的超时逻辑
935
+ this.dragTimeoutId = window.setTimeout(() => {
936
+ if (this.isDragging && this.dragStarted) {
937
+ if (this.debug) {
938
+ console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
939
+ }
940
+ this.stopDrag();
941
+ }
942
+ }, 50); // 手机模式:使用 50ms 超时
943
+ }
944
+ }
727
945
  // 4. 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
728
946
  // 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
729
947
  this.startActiveDetection();
@@ -752,27 +970,72 @@ class IconManager {
752
970
  onDrag(e) {
753
971
  if (!this.iconElement)
754
972
  return;
755
- e.preventDefault();
756
973
  const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
757
974
  const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
975
+ // PC 模式下,如果 SDK iframe 打开,直接 return,禁止拖动
976
+ const isSDKOpen = this.isSDKIframeOpen();
977
+ if (this.debug) {
978
+ console.log('[IconManager] [PC+iframe] Checking SDK iframe status:', {
979
+ isSDKOpen,
980
+ windowWidth: window.innerWidth,
981
+ isPC: window.innerWidth > 768
982
+ });
983
+ }
984
+ if (isSDKOpen) {
985
+ if (this.debug) {
986
+ console.log('[IconManager] [PC+iframe] SDK iframe is open, blocking drag - returning immediately');
987
+ }
988
+ // 直接 return,不更新位置,不调用 stopDrag(让图标保持当前位置)
989
+ return;
990
+ }
991
+ // PC 模式下,如果鼠标在任何 iframe 上(排除 SDK 自己的 iframe),禁止拖动
992
+ const isPC = window.innerWidth > 768;
993
+ if (isPC && this.isMouseOverIframe(clientX, clientY)) {
994
+ if (this.debug) {
995
+ console.log('[IconManager] [PC+iframe] Mouse is over iframe, blocking drag - returning immediately', {
996
+ clientX,
997
+ clientY,
998
+ isPC
999
+ });
1000
+ }
1001
+ // 直接 return,不更新位置,不调用 stopDrag(让图标保持当前位置)
1002
+ return;
1003
+ }
1004
+ e.preventDefault();
758
1005
  // 检测鼠标是否在任何 iframe 上(通过坐标判断)
759
- // 如果检测到鼠标在 iframe 区域,立即停止拖动
1006
+ // 注意:在 PC 模式下,当鼠标进入 iframe 后,mousemove 事件会丢失
1007
+ // 但在此之前,我们仍然可以正常更新位置
1008
+ if (this.debug) {
1009
+ console.log('[IconManager] onDrag called', {
1010
+ clientPosition: { x: clientX, y: clientY },
1011
+ isDragging: this.isDragging,
1012
+ dragStarted: this.dragStarted
1013
+ });
1014
+ }
760
1015
  // 重要:需要检测所有 iframe(包括嵌套的),因为任何 iframe 都会导致事件丢失
761
- if (this.isDragging) {
1016
+ // 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
1017
+ // 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
1018
+ if (this.isDragging || this.dragIntent || this.dragStarted) {
762
1019
  const now = Date.now();
763
1020
  // 性能优化:缓存 iframe 位置信息,避免频繁查询 DOM
764
1021
  if (this.cachedIframes.length === 0 ||
765
1022
  now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
766
- // 更新 iframe 缓存
1023
+ // 更新 iframe 缓存(排除 SDK 自己的 iframe)
767
1024
  const allIframes = document.querySelectorAll('iframe');
768
1025
  const previousCount = this.cachedIframes.length;
769
- this.cachedIframes = Array.from(allIframes).map(iframe => ({
1026
+ this.cachedIframes = Array.from(allIframes)
1027
+ .filter(iframe => {
1028
+ // 检查 iframe 是否在 SDK 容器内
1029
+ const container = iframe.closest('.customer-sdk-container');
1030
+ return !container; // 排除 SDK 容器内的 iframe
1031
+ })
1032
+ .map(iframe => ({
770
1033
  element: iframe,
771
1034
  rect: iframe.getBoundingClientRect()
772
1035
  }));
773
1036
  this.lastIframeUpdateTime = now;
774
1037
  if (this.debug && this.cachedIframes.length !== previousCount) {
775
- console.log(`[IconManager] Iframe cache updated - Found ${this.cachedIframes.length} iframe(s)`, {
1038
+ console.log(`[IconManager] Iframe cache updated - Found ${this.cachedIframes.length} iframe(s) (excluding SDK iframes)`, {
776
1039
  iframes: this.cachedIframes.map(({ rect }) => ({
777
1040
  left: rect.left,
778
1041
  top: rect.top,
@@ -782,67 +1045,103 @@ class IconManager {
782
1045
  });
783
1046
  }
784
1047
  }
785
- // 检查鼠标是否在任何 iframe
786
- for (const { rect, element } of this.cachedIframes) {
787
- if (clientX >= rect.left &&
788
- clientX <= rect.right &&
789
- clientY >= rect.top &&
790
- clientY <= rect.bottom) {
791
- // 鼠标在 iframe 上,立即停止拖动
792
- if (this.debug) {
793
- console.log('[IconManager] Mouse over iframe detected, stopping drag immediately', {
794
- mousePosition: { x: clientX, y: clientY },
795
- iframeRect: {
796
- left: rect.left,
797
- top: rect.top,
798
- right: rect.right,
799
- bottom: rect.bottom
800
- },
801
- iframeSrc: element.src || 'no src'
802
- });
803
- }
804
- this.stopDrag();
805
- return;
806
- }
807
- }
1048
+ // 移除 iframe 检测停止拖动的逻辑,让拖动可以在 iframe 上顺畅进行
1049
+ // 不再因为检测到 iframe 而停止拖动,保持拖动的流畅性
808
1050
  }
809
1051
  // 更新最后记录的鼠标位置和时间戳(用于主动检测)
810
1052
  const now = Date.now();
811
1053
  const timeSinceLastUpdate = now - this.lastMousePosition.timestamp;
1054
+ const lastX = this.lastMousePosition.x;
1055
+ const lastY = this.lastMousePosition.y;
1056
+ const lastTimestamp = this.lastMousePosition.timestamp;
812
1057
  this.lastMousePosition = {
813
1058
  x: clientX,
814
1059
  y: clientY,
815
1060
  timestamp: now
816
1061
  };
1062
+ // 计算鼠标移动速度(用于在 iframe 上时预测位置)
1063
+ if (lastTimestamp > 0 && timeSinceLastUpdate > 0 && timeSinceLastUpdate < 100) {
1064
+ // 只在时间间隔合理时计算速度(避免时间间隔过大导致速度不准确)
1065
+ const dt = timeSinceLastUpdate / 1000; // 转换为秒
1066
+ this.mouseVelocity.vx = (clientX - lastX) / dt;
1067
+ this.mouseVelocity.vy = (clientY - lastY) / dt;
1068
+ // 限制速度范围,避免异常值
1069
+ this.mouseVelocity.vx = Math.max(-2e3, Math.min(2000, this.mouseVelocity.vx));
1070
+ this.mouseVelocity.vy = Math.max(-2e3, Math.min(2000, this.mouseVelocity.vy));
1071
+ }
817
1072
  // 如果距离上次更新超过 50ms,记录警告(可能事件丢失)
818
1073
  if (this.debug && timeSinceLastUpdate > 50 && this.lastMousePosition.timestamp > 0) {
819
1074
  console.warn(`[IconManager] Long gap between mousemove events: ${timeSinceLastUpdate}ms`, {
820
- lastPosition: { x: this.lastMousePosition.x, y: this.lastMousePosition.y },
821
- currentPosition: { x: clientX, y: clientY }
1075
+ lastPosition: { x: lastX, y: lastY },
1076
+ currentPosition: { x: clientX, y: clientY },
1077
+ velocity: { vx: this.mouseVelocity.vx, vy: this.mouseVelocity.vy }
822
1078
  });
823
1079
  }
824
1080
  // 重置超时定时器(每次移动都重置,确保只有真正停止移动时才触发超时)
825
- // 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
826
1081
  if (this.dragTimeoutId !== null) {
827
1082
  window.clearTimeout(this.dragTimeoutId);
828
- this.dragTimeoutId = window.setTimeout(() => {
829
- if (this.isDragging) {
830
- if (this.debug) {
831
- const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
832
- console.warn('[IconManager] Drag timeout triggered, stopping drag (likely mouse moved over iframe)', {
833
- timeSinceLastMove,
834
- lastMousePosition: this.lastMousePosition,
835
- iframeCount: this.cachedIframes.length
836
- });
837
- }
838
- this.stopDrag();
1083
+ // 区分 PC 模式和手机模式,以及是否在 iframe 上
1084
+ const isPC = window.innerWidth > 768;
1085
+ // 检查鼠标是否在 iframe 上(简单判断)
1086
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
1087
+ if (isPC) ;
1088
+ else {
1089
+ // ========== 手机模式 ==========
1090
+ if (isOverIframe) {
1091
+ // 手机模式 + 在 iframe 上:特殊处理逻辑
1092
+ // 在 iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
1093
+ const checkDragTimeoutForMobileIframe = () => {
1094
+ const lastPos = this.lastMousePosition;
1095
+ const stillOverIframe = lastPos && this.isMouseOverIframe(lastPos.x, lastPos.y);
1096
+ // 如果还在 iframe 上且还有拖动意图,继续等待 mouseup
1097
+ if (stillOverIframe && (this.dragIntent || this.dragStarted || this.isDragging)) {
1098
+ if (this.debug) {
1099
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
1100
+ console.log('[IconManager] [Mobile+iframe] Drag timeout but mouse is over iframe, keeping drag active (waiting for mouseup)', {
1101
+ timeSinceLastMove,
1102
+ lastMousePosition: this.lastMousePosition
1103
+ });
1104
+ }
1105
+ // 手机模式 iframe 上:使用 3 秒超时(比 PC 模式稍短),等待 mouseup
1106
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
1107
+ return;
1108
+ }
1109
+ // 不在 iframe 上了,或者拖动意图已消失,停止拖动
1110
+ if (this.isDragging || this.dragStarted) {
1111
+ if (this.debug) {
1112
+ console.warn('[IconManager] [Mobile+iframe] Drag timeout triggered, stopping drag');
1113
+ }
1114
+ this.stopDrag();
1115
+ }
1116
+ };
1117
+ // 手机模式 iframe 上:使用 3 秒超时
1118
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
839
1119
  }
840
- }, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
1120
+ else {
1121
+ // 手机模式 + 不在 iframe 上:正常逻辑
1122
+ // 手机模式不在 iframe 上时,使用 200ms 超时
1123
+ this.dragTimeoutId = window.setTimeout(() => {
1124
+ if (this.isDragging) {
1125
+ if (this.debug) {
1126
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
1127
+ console.warn('[IconManager] [Mobile] Drag timeout triggered, stopping drag', {
1128
+ timeSinceLastMove,
1129
+ lastMousePosition: this.lastMousePosition,
1130
+ iframeCount: this.cachedIframes.length
1131
+ });
1132
+ }
1133
+ this.stopDrag();
1134
+ }
1135
+ }, 200);
1136
+ }
1137
+ }
841
1138
  }
842
1139
  // 检查是否有足够的移动距离
843
1140
  const deltaX = Math.abs(clientX - this.lastTouchPosition.x);
844
1141
  const deltaY = Math.abs(clientY - this.lastTouchPosition.y);
845
1142
  const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1143
+ // 只有在移动距离超过阈值时,才认为是拖动
1144
+ // 这样可以避免轻微的鼠标抖动被误判为拖动
846
1145
  if (totalMovement > this.clickThreshold) {
847
1146
  this.hasMoved = true;
848
1147
  if (!this.dragStarted) {
@@ -886,7 +1185,9 @@ class IconManager {
886
1185
  }
887
1186
  }
888
1187
  }
889
- if (!this.isDragging) {
1188
+ // 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
1189
+ // 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
1190
+ if (!this.isDragging && !this.dragIntent && !this.dragStarted) {
890
1191
  return;
891
1192
  }
892
1193
  try {
@@ -911,41 +1212,184 @@ class IconManager {
911
1212
  const containerRect = this.cachedContainerRect;
912
1213
  const iconWidth = this.cachedIconSize.width;
913
1214
  const iconHeight = this.cachedIconSize.height;
914
- // 计算新位置
1215
+ // 获取容器的实际宽度(考虑 max-width 限制)
1216
+ // 使用 offsetWidth 或 clientWidth,它们会考虑 max-width 限制
1217
+ const computedStyle = window.getComputedStyle(container);
1218
+ const maxWidth = computedStyle.maxWidth;
1219
+ const actualWidth = maxWidth && maxWidth !== 'none'
1220
+ ? Math.min(containerRect.width, parseFloat(maxWidth) || containerRect.width)
1221
+ : containerRect.width;
1222
+ const actualHeight = containerRect.height;
1223
+ // 确保 dragOffset 已经正确设置
1224
+ // 如果 dragOffset 还没有设置(在 startDrag 中设置,但可能在某些情况下未设置),在这里计算
1225
+ // 重要:只有在 dragOffset 真的为 0 且还没有开始拖动时才计算,避免在拖动过程中被重置
1226
+ if ((this.dragOffset.x === 0 && this.dragOffset.y === 0) && (this.dragIntent || this.dragStarted) && this.iconElement && !this.dragStarted) {
1227
+ // 只有在 dragIntent 为 true 但 dragStarted 为 false 时才计算 dragOffset
1228
+ // 如果 dragStarted 已经为 true,说明 dragOffset 应该已经在 startDrag 中设置了,不应该重新计算
1229
+ const currentRect = this.iconElement.getBoundingClientRect();
1230
+ this.dragOffset.x = clientX - currentRect.left;
1231
+ this.dragOffset.y = clientY - currentRect.top;
1232
+ if (this.debug) {
1233
+ console.log('[IconManager] Calculated dragOffset in onDrag (fallback)', {
1234
+ dragOffset: this.dragOffset,
1235
+ clientPosition: { x: clientX, y: clientY },
1236
+ iconRect: currentRect,
1237
+ dragStarted: this.dragStarted,
1238
+ dragIntent: this.dragIntent
1239
+ });
1240
+ }
1241
+ }
1242
+ // 计算新位置(相对于容器的实际内容区域)
915
1243
  let newX = clientX - this.dragOffset.x - containerRect.left;
916
1244
  let newY = clientY - this.dragOffset.y - containerRect.top;
917
- // 限制在容器内
918
- if (container === document.body) {
919
- // 限制在视口内
920
- newX = Math.max(0, Math.min(newX, window.innerWidth - iconWidth));
921
- newY = Math.max(0, Math.min(newY, window.innerHeight - iconHeight));
1245
+ // 允许拖动到容器外(允许负值),但限制在合理范围内(避免拖得太远)
1246
+ // 允许拖动到容器外,最多隐藏到只剩一小部分可见
1247
+ // 使用实际宽度(考虑 max-width 限制)而不是容器宽度
1248
+ const minX = -iconWidth + 5; // 允许向左拖动,最多隐藏到只剩 5px
1249
+ const maxX = container === document.body
1250
+ ? window.innerWidth - 5 // 允许向右拖动,最多隐藏到只剩 5px
1251
+ : actualWidth + iconWidth - 5; // 使用实际宽度(考虑 max-width),允许超出容器,最多隐藏到只剩 5px
1252
+ const minY = -iconHeight + 5; // 允许向上拖动,最多隐藏到只剩 5px
1253
+ const maxY = container === document.body
1254
+ ? window.innerHeight - 5 // 允许向下拖动,最多隐藏到只剩 5px
1255
+ : actualHeight + iconHeight - 5; // 允许超出容器,最多隐藏到只剩 5px
1256
+ newX = Math.max(minX, Math.min(newX, maxX));
1257
+ newY = Math.max(minY, Math.min(newY, maxY));
1258
+ // 区分 PC 模式和手机模式,以及是否在 iframe 上
1259
+ const isPC = window.innerWidth > 768;
1260
+ // 检查鼠标是否在 iframe 上(简单判断)
1261
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
1262
+ // 简化逻辑:直接更新位置,不管是否在 iframe 上
1263
+ if (this.iconElement) {
1264
+ // 重要:保护 dragOffset,确保它在拖动过程中不会被重新计算
1265
+ // 逻辑顺序:
1266
+ // 1. 如果 dragOffset 还没有设置(为 0),且 dragIntent 为 true,先计算 dragOffset
1267
+ // 2. 然后在 iframe 上时,如果 dragIntent 为 true 但 dragStarted 为 false,提前设置 dragStarted
1268
+ // 3. 这样确保 dragOffset 在 dragStarted 设置之前就已经计算好了
1269
+ // 步骤 1:计算 dragOffset(只有在 dragOffset 为 0 且 dragIntent 为 true 时才计算)
1270
+ if (this.dragIntent && (this.dragOffset.x === 0 && this.dragOffset.y === 0)) {
1271
+ const currentRect = this.iconElement.getBoundingClientRect();
1272
+ const oldDragOffset = { x: this.dragOffset.x, y: this.dragOffset.y };
1273
+ this.dragOffset.x = clientX - currentRect.left;
1274
+ this.dragOffset.y = clientY - currentRect.top;
1275
+ if (this.debug && isPC) {
1276
+ console.log('[IconManager] [PC+iframe] Calculated dragOffset', {
1277
+ oldDragOffset,
1278
+ newDragOffset: { x: this.dragOffset.x, y: this.dragOffset.y },
1279
+ clientPosition: { x: clientX, y: clientY },
1280
+ iconRect: currentRect,
1281
+ isOverIframe
1282
+ });
1283
+ }
1284
+ }
1285
+ else if (this.debug && isPC && this.dragStarted && (this.dragOffset.x === 0 && this.dragOffset.y === 0)) {
1286
+ // 警告:dragStarted 为 true 但 dragOffset 为 0,这是不应该发生的
1287
+ console.warn('[IconManager] [PC] dragOffset is 0 but dragStarted is true! This should not happen.', {
1288
+ dragStarted: this.dragStarted,
1289
+ dragIntent: this.dragIntent,
1290
+ isDragging: this.isDragging,
1291
+ isOverIframe
1292
+ });
1293
+ }
1294
+ // 步骤 2:在 iframe 上时,如果 dragIntent 为 true 但 dragStarted 为 false,提前设置 dragStarted
1295
+ // 这样可以确保拖动状态正确
1296
+ if (isOverIframe && this.dragIntent && !this.dragStarted) {
1297
+ this.dragStarted = true;
1298
+ this.isDragging = true;
1299
+ this.hasMoved = true;
1300
+ }
1301
+ // 更新位置
1302
+ this.iconElement.style.left = `${newX}px`;
1303
+ this.iconElement.style.top = `${newY}px`;
1304
+ this.iconElement.style.right = 'auto';
1305
+ this.iconElement.style.bottom = 'auto';
1306
+ // 简化日志:只显示 x, y 坐标(PC 模式下总是输出,方便调试)
1307
+ if (this.debug && isPC) {
1308
+ const calculatedX = clientX - this.dragOffset.x - containerRect.left;
1309
+ const calculatedY = clientY - this.dragOffset.y - containerRect.top;
1310
+ console.log('[IconManager] [PC+iframe] Icon position:', {
1311
+ x: newX,
1312
+ y: newY,
1313
+ isOverIframe,
1314
+ clientX,
1315
+ clientY,
1316
+ dragOffset: { x: this.dragOffset.x, y: this.dragOffset.y },
1317
+ containerLeft: containerRect.left,
1318
+ containerTop: containerRect.top,
1319
+ calculatedX,
1320
+ calculatedY,
1321
+ minX,
1322
+ maxX,
1323
+ actualWidth
1324
+ });
1325
+ // 如果计算出的位置和实际位置不一致,输出警告
1326
+ if (Math.abs(calculatedX - newX) > 1 || Math.abs(calculatedY - newY) > 1) {
1327
+ console.warn('[IconManager] [PC+iframe] Position calculation mismatch!', {
1328
+ calculated: { x: calculatedX, y: calculatedY },
1329
+ actual: { x: newX, y: newY },
1330
+ dragOffset: { x: this.dragOffset.x, y: this.dragOffset.y },
1331
+ containerRect: { left: containerRect.left, top: containerRect.top },
1332
+ clientPosition: { x: clientX, y: clientY }
1333
+ });
1334
+ }
1335
+ }
1336
+ }
1337
+ // 超时逻辑:统一处理
1338
+ if (isPC) {
1339
+ // PC 模式:使用简单的超时逻辑
1340
+ if (isOverIframe) {
1341
+ // PC 模式 + 在 iframe 上:使用较长的超时(因为 mousemove 事件会丢失)
1342
+ this.dragTimeoutId = window.setTimeout(() => {
1343
+ if (this.isDragging && this.dragStarted) {
1344
+ this.stopDrag();
1345
+ }
1346
+ }, 5000); // 5 秒超时,等待 mouseup
1347
+ }
1348
+ else {
1349
+ // PC 模式 + 不在 iframe 上:使用短超时
1350
+ this.dragTimeoutId = window.setTimeout(() => {
1351
+ if (this.isDragging && this.dragStarted) {
1352
+ this.stopDrag();
1353
+ }
1354
+ }, 50); // 50ms 超时
1355
+ }
922
1356
  }
923
1357
  else {
924
- // 限制在容器内
925
- const containerWidth = containerRect.width;
926
- const containerHeight = containerRect.height;
927
- newX = Math.max(0, Math.min(newX, containerWidth - iconWidth));
928
- newY = Math.max(0, Math.min(newY, containerHeight - iconHeight));
929
- }
930
- // 性能优化:使用 requestAnimationFrame 节流位置更新
931
- this.pendingPosition.x = newX;
932
- this.pendingPosition.y = newY;
933
- this.pendingPosition.needsUpdate = true;
934
- if (this.rafId === null) {
935
- this.rafId = requestAnimationFrame(() => {
936
- this.rafId = null;
937
- if (this.pendingPosition.needsUpdate && this.iconElement && this.isDragging) {
938
- this.iconElement.style.left = `${this.pendingPosition.x}px`;
939
- this.iconElement.style.top = `${this.pendingPosition.y}px`;
1358
+ // ========== 手机模式 ==========
1359
+ if (isOverIframe) {
1360
+ // 手机模式 + 在 iframe 上:直接更新 DOM
1361
+ if (this.iconElement) {
1362
+ this.iconElement.style.left = `${newX}px`;
1363
+ this.iconElement.style.top = `${newY}px`;
940
1364
  this.iconElement.style.right = 'auto';
941
1365
  this.iconElement.style.bottom = 'auto';
942
- this.pendingPosition.needsUpdate = false;
943
1366
  }
944
- });
1367
+ }
1368
+ else {
1369
+ // 手机模式 + 不在 iframe 上:使用 requestAnimationFrame 节流
1370
+ this.pendingPosition.x = newX;
1371
+ this.pendingPosition.y = newY;
1372
+ this.pendingPosition.needsUpdate = true;
1373
+ if (this.rafId === null) {
1374
+ this.rafId = requestAnimationFrame(() => {
1375
+ this.rafId = null;
1376
+ if (this.pendingPosition.needsUpdate && this.iconElement && this.isDragging) {
1377
+ this.iconElement.style.left = `${this.pendingPosition.x}px`;
1378
+ this.iconElement.style.top = `${this.pendingPosition.y}px`;
1379
+ this.iconElement.style.right = 'auto';
1380
+ this.iconElement.style.bottom = 'auto';
1381
+ this.pendingPosition.needsUpdate = false;
1382
+ }
1383
+ });
1384
+ }
1385
+ }
945
1386
  }
946
- // 更新最后位置
1387
+ // 更新最后位置(同时更新 lastTouchPosition 和 lastMousePosition)
947
1388
  this.lastTouchPosition.x = clientX;
948
1389
  this.lastTouchPosition.y = clientY;
1390
+ this.lastMousePosition.x = clientX;
1391
+ this.lastMousePosition.y = clientY;
1392
+ this.lastMousePosition.timestamp = Date.now();
949
1393
  }
950
1394
  catch (error) {
951
1395
  if (this.debug) {
@@ -953,6 +1397,88 @@ class IconManager {
953
1397
  }
954
1398
  }
955
1399
  }
1400
+ /**
1401
+ * 从鼠标位置更新图标位置(用于主动检测机制)
1402
+ */
1403
+ updateIconPositionFromMouse(clientX, clientY) {
1404
+ // 这个方法只在 iframe 上时使用,因为 mousemove 事件丢失
1405
+ // 检查拖动状态,只有在真正拖动中或拖动意图存在时才更新
1406
+ if (!this.iconElement) {
1407
+ return;
1408
+ }
1409
+ // 只有在拖动中或拖动意图存在时才更新位置
1410
+ // 但需要确保 dragOffset 已经正确设置(在第一次 mousemove 时设置)
1411
+ if (!this.isDragging && !this.dragIntent && !this.dragStarted) {
1412
+ return;
1413
+ }
1414
+ // 如果 dragOffset 还没有设置(还没有收到过 mousemove 事件),需要先计算它
1415
+ // 因为需要 dragOffset 来计算相对位置
1416
+ if (this.dragOffset.x === 0 && this.dragOffset.y === 0) {
1417
+ // 如果已经拖动开始了,从当前图标位置计算 dragOffset
1418
+ if (this.dragStarted && this.iconElement) {
1419
+ const currentRect = this.iconElement.getBoundingClientRect();
1420
+ this.dragOffset.x = clientX - currentRect.left;
1421
+ this.dragOffset.y = clientY - currentRect.top;
1422
+ }
1423
+ else {
1424
+ // 如果还没有拖动开始,不能更新位置
1425
+ return;
1426
+ }
1427
+ }
1428
+ try {
1429
+ const container = this.getTargetElement();
1430
+ if (!container) {
1431
+ return;
1432
+ }
1433
+ // 获取容器信息
1434
+ const containerRect = container.getBoundingClientRect();
1435
+ const iconWidth = this.iconElement.offsetWidth;
1436
+ const iconHeight = this.iconElement.offsetHeight;
1437
+ // 获取容器的实际渲染宽度和高度
1438
+ const actualWidth = container === document.body
1439
+ ? window.innerWidth
1440
+ : container.offsetWidth;
1441
+ const actualHeight = container === document.body
1442
+ ? window.innerHeight
1443
+ : container.offsetHeight;
1444
+ // 计算新位置(相对于容器的实际内容区域)
1445
+ let newX = clientX - this.dragOffset.x - containerRect.left;
1446
+ let newY = clientY - this.dragOffset.y - containerRect.top;
1447
+ // 允许拖动到容器外(允许负值),但限制在合理范围内
1448
+ const minX = -iconWidth + 5;
1449
+ const maxX = container === document.body
1450
+ ? window.innerWidth - 5
1451
+ : actualWidth + iconWidth - 5;
1452
+ const minY = -iconHeight + 5;
1453
+ const maxY = container === document.body
1454
+ ? window.innerHeight - 5
1455
+ : actualHeight + iconHeight - 5;
1456
+ newX = Math.max(minX, Math.min(newX, maxX));
1457
+ newY = Math.max(minY, Math.min(newY, maxY));
1458
+ // 主动检测机制中,直接更新 DOM 确保流畅(不使用节流)
1459
+ // 因为此时 mousemove 事件已经丢失,需要及时更新位置
1460
+ if (this.iconElement) {
1461
+ this.iconElement.style.left = `${newX}px`;
1462
+ this.iconElement.style.top = `${newY}px`;
1463
+ this.iconElement.style.right = 'auto';
1464
+ this.iconElement.style.bottom = 'auto';
1465
+ if (this.debug) {
1466
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
1467
+ console.log('[IconManager] [PC+iframe] Updated icon position in startActiveDetection', {
1468
+ mousePosition: { x: clientX, y: clientY },
1469
+ iconPosition: { x: newX, y: newY },
1470
+ timeSinceLastMove,
1471
+ lastMousePosition: this.lastMousePosition
1472
+ });
1473
+ }
1474
+ }
1475
+ }
1476
+ catch (error) {
1477
+ if (this.debug) {
1478
+ console.error('[IconManager] Error in updateIconPositionFromMouse:', error);
1479
+ }
1480
+ }
1481
+ }
956
1482
  /**
957
1483
  * 停止拖动
958
1484
  */
@@ -972,6 +1498,36 @@ class IconManager {
972
1498
  return;
973
1499
  }
974
1500
  this.isStoppingDrag = true;
1501
+ // 在 iframe 上时,如果 mouseup 事件存在,从事件中获取鼠标位置并更新图标位置
1502
+ // 这是解决 iframe 上拖动时图标不跟随鼠标的关键
1503
+ if (_e && this.dragStarted && this.iconElement) {
1504
+ const clientX = 'clientX' in _e ? _e.clientX : _e.touches?.[0]?.clientX ?? _e.changedTouches?.[0]?.clientX ?? 0;
1505
+ const clientY = 'clientY' in _e ? _e.clientY : _e.touches?.[0]?.clientY ?? _e.changedTouches?.[0]?.clientY ?? 0;
1506
+ if (clientX > 0 && clientY > 0) {
1507
+ // 检查鼠标是否在 iframe 上
1508
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
1509
+ if (isOverIframe) {
1510
+ // 在 iframe 上时,从 mouseup 事件中获取鼠标位置并更新图标位置
1511
+ // 更新 lastMousePosition,这样后续的逻辑可以使用最新的位置
1512
+ this.lastMousePosition = {
1513
+ x: clientX,
1514
+ y: clientY,
1515
+ timestamp: Date.now()
1516
+ };
1517
+ // 更新图标位置
1518
+ this.updateIconPositionFromMouse(clientX, clientY);
1519
+ if (this.debug) {
1520
+ console.log('[IconManager] [PC+iframe] Updated icon position from mouseup event', {
1521
+ mousePosition: { x: clientX, y: clientY },
1522
+ iconPosition: this.iconElement ? {
1523
+ left: this.iconElement.style.left,
1524
+ top: this.iconElement.style.top
1525
+ } : null
1526
+ });
1527
+ }
1528
+ }
1529
+ }
1530
+ }
975
1531
  const stopReason = this.isDragging ? 'drag_stopped' : 'not_dragging';
976
1532
  const finalPosition = this.iconElement ? {
977
1533
  left: this.iconElement.style.left,
@@ -987,15 +1543,28 @@ class IconManager {
987
1543
  this.hasMoved = false;
988
1544
  this.isDragging = false;
989
1545
  this.dragStarted = false;
1546
+ this.dragIntent = false;
990
1547
  this.isStoppingDrag = false;
991
1548
  return;
992
1549
  }
993
1550
  // 恢复样式
994
1551
  this.iconElement.style.transition = 'transform 0.2s ease';
995
1552
  this.iconElement.style.cursor = 'pointer';
1553
+ // 拖动结束时,恢复 pointer-events: none 让事件穿透到 iframe
1554
+ this.iconElement.style.pointerEvents = 'none';
996
1555
  // 检查是否是点击
1556
+ // 点击的条件:
1557
+ // 1. 没有移动过(hasMoved = false)
1558
+ // 2. 持续时间小于 1000ms
1559
+ // 3. 没有真正开始拖动(dragStarted = false)
1560
+ // 4. 没有拖动意图(dragIntent = false)或者拖动意图存在但持续时间很短(可能是误触)
1561
+ // 5. 没有真正拖动过(isDragging = false)
997
1562
  const touchDuration = Date.now() - this.touchStartTime;
998
- const isValidClick = !this.hasMoved && touchDuration < 1000 && !this.dragStarted;
1563
+ const isValidClick = !this.hasMoved &&
1564
+ touchDuration < 1000 &&
1565
+ !this.dragStarted &&
1566
+ !this.isDragging &&
1567
+ (!this.dragIntent || touchDuration < 200); // 如果 dragIntent 存在,但持续时间很短(< 200ms),可能是误触,也认为是点击
999
1568
  if (this.debug) {
1000
1569
  console.log('[IconManager] Drag end', {
1001
1570
  stopReason,
@@ -1009,26 +1578,37 @@ class IconManager {
1009
1578
  }
1010
1579
  // 先保存点击状态,然后重置拖动状态
1011
1580
  const wasClick = isValidClick;
1012
- // 如果真正拖动过,执行磁性吸附
1013
- if (this.dragStarted && this.magnetic && this.iconElement) {
1014
- this.magneticSnap();
1581
+ // 保存 stoppedByIframe 状态,用于后续判断是否启动自动吸附
1582
+ const wasStoppedByIframe = this.stoppedByIframe;
1583
+ // 如果是因为 iframe 而停止拖动,不执行磁性吸附和自动吸附
1584
+ // 这样可以避免拖动到 iframe 上时 icon 缩回去
1585
+ if (this.stoppedByIframe) {
1586
+ if (this.debug) {
1587
+ console.log('[IconManager] Drag stopped by iframe, skipping magnetic snap and auto attach');
1588
+ }
1015
1589
  }
1016
- // 如果真正拖动过,保存当前位置到 iconPosition
1017
- if (this.dragStarted && this.isDragging && this.iconElement) {
1018
- const computedStyle = window.getComputedStyle(this.iconElement);
1019
- const left = computedStyle.left;
1020
- const top = computedStyle.top;
1021
- // 如果 left/top 是有效的像素值,保存到 iconPosition
1022
- if (left !== 'auto' && top !== 'auto') {
1023
- const leftValue = parseFloat(left);
1024
- const topValue = parseFloat(top);
1025
- if (!isNaN(leftValue) && !isNaN(topValue)) {
1026
- this.iconPosition = {
1027
- x: leftValue,
1028
- y: topValue
1029
- };
1030
- if (this.debug) {
1031
- console.log('Icon position saved:', this.iconPosition);
1590
+ else {
1591
+ // 如果真正拖动过,执行磁性吸附
1592
+ if (this.dragStarted && this.magnetic && this.iconElement) {
1593
+ this.magneticSnap();
1594
+ }
1595
+ // 如果真正拖动过,保存当前位置到 iconPosition
1596
+ if (this.dragStarted && this.isDragging && this.iconElement) {
1597
+ const computedStyle = window.getComputedStyle(this.iconElement);
1598
+ const left = computedStyle.left;
1599
+ const top = computedStyle.top;
1600
+ // 如果 left/top 是有效的像素值,保存到 iconPosition
1601
+ if (left !== 'auto' && top !== 'auto') {
1602
+ const leftValue = parseFloat(left);
1603
+ const topValue = parseFloat(top);
1604
+ if (!isNaN(leftValue) && !isNaN(topValue)) {
1605
+ this.iconPosition = {
1606
+ x: leftValue,
1607
+ y: topValue
1608
+ };
1609
+ if (this.debug) {
1610
+ console.log('Icon position saved:', this.iconPosition);
1611
+ }
1032
1612
  }
1033
1613
  }
1034
1614
  }
@@ -1037,9 +1617,13 @@ class IconManager {
1037
1617
  this.hasMoved = false;
1038
1618
  this.isDragging = false;
1039
1619
  this.dragStarted = false;
1620
+ this.dragIntent = false; // 重置拖动意图
1040
1621
  this.isStoppingDrag = false; // 重置停止标志
1622
+ this.stoppedByIframe = false; // 重置 iframe 停止标志
1623
+ this.hasLoggedIframeWarning = false; // 重置 iframe 警告标志
1041
1624
  // 如果拖动后没有吸附到侧边,启动自动吸附定时器
1042
- if (!this.isAttachedToSide && this.sideAttach && this.autoAttachDelay > 0) {
1625
+ // 但如果是因为 iframe 而停止拖动,不启动自动吸附
1626
+ if (!wasStoppedByIframe && !this.isAttachedToSide && this.sideAttach && this.autoAttachDelay > 0) {
1043
1627
  this.startAutoAttachTimer();
1044
1628
  }
1045
1629
  if (wasClick) {
@@ -1068,72 +1652,88 @@ class IconManager {
1068
1652
  console.log('[IconManager] Starting active detection mechanism');
1069
1653
  }
1070
1654
  const checkMousePosition = () => {
1071
- // 检查是否还在拖动中,如果不在拖动中或正在停止,则停止检测
1072
- if (!this.isDragging || this.isStoppingDrag) {
1655
+ // 检查是否还在拖动中
1656
+ // 注意:即使 isDragging 暂时为 false(因为 mousemove 事件丢失),
1657
+ // 只要 dragIntent 或 dragStarted 为 true,说明用户还在拖动,应该继续检测
1658
+ if (this.isStoppingDrag || (!this.isDragging && !this.dragStarted && !this.dragIntent)) {
1073
1659
  if (this.debug) {
1074
- console.log('[IconManager] Active detection stopped (drag ended or stopping)');
1660
+ console.log('[IconManager] Active detection stopped (drag ended or stopping)', {
1661
+ isDragging: this.isDragging,
1662
+ dragStarted: this.dragStarted,
1663
+ dragIntent: this.dragIntent,
1664
+ isStoppingDrag: this.isStoppingDrag
1665
+ });
1075
1666
  }
1076
1667
  this.activeDetectionRafId = null;
1668
+ if (this.activeDetectionTimeoutId !== null) {
1669
+ window.clearTimeout(this.activeDetectionTimeoutId);
1670
+ this.activeDetectionTimeoutId = null;
1671
+ }
1077
1672
  return;
1078
1673
  }
1079
- // 检查是否长时间没有收到 mousemove 事件(超过 50ms)
1674
+ // 检查是否长时间没有收到 mousemove 事件(超过 100ms 才检测,避免频繁检测)
1080
1675
  const now = Date.now();
1081
1676
  const timeSinceLastMove = now - this.lastMousePosition.timestamp;
1082
- // 如果超过 50ms 没有收到事件,进行检测
1083
- if (timeSinceLastMove > 50) {
1084
- // 长时间没有收到事件,可能鼠标已经进入 iframe
1677
+ // 如果超过 100ms 没有收到事件,可能鼠标已经进入 iframe
1678
+ // iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
1679
+ // 所以只需要保持拖动状态,等待 mouseup 事件,不需要每帧都更新位置
1680
+ if (timeSinceLastMove > 100) {
1085
1681
  // 使用最后记录的鼠标位置进行检测
1086
1682
  const clientX = this.lastMousePosition.x;
1087
1683
  const clientY = this.lastMousePosition.y;
1088
- if (this.debug && timeSinceLastMove > 100) {
1089
- // 只在超过 100ms 时记录警告,避免日志过多
1090
- console.warn(`[IconManager] Active detection: No mousemove event for ${timeSinceLastMove}ms`, {
1091
- lastMousePosition: { x: clientX, y: clientY },
1092
- timestamp: this.lastMousePosition.timestamp,
1093
- isDragging: this.isDragging,
1094
- dragStarted: this.dragStarted
1095
- });
1096
- }
1097
- // 更新 iframe 缓存(更频繁,每帧更新)
1684
+ // 更新 iframe 缓存(不需要每帧更新,只在需要时更新)
1098
1685
  if (this.cachedIframes.length === 0 ||
1099
1686
  now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
1100
1687
  const allIframes = document.querySelectorAll('iframe');
1101
- this.cachedIframes = Array.from(allIframes).map(iframe => ({
1688
+ // 排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe)
1689
+ this.cachedIframes = Array.from(allIframes)
1690
+ .filter(iframe => {
1691
+ // 检查 iframe 是否在 SDK 容器内
1692
+ const container = iframe.closest('.customer-sdk-container');
1693
+ return !container; // 排除 SDK 容器内的 iframe
1694
+ })
1695
+ .map(iframe => ({
1102
1696
  element: iframe,
1103
1697
  rect: iframe.getBoundingClientRect()
1104
1698
  }));
1105
1699
  this.lastIframeUpdateTime = now;
1106
- if (this.debug) {
1107
- console.log(`[IconManager] Active detection: Updated iframe cache (${this.cachedIframes.length} iframes)`);
1108
- }
1109
1700
  }
1110
- // 检查最后记录的鼠标位置是否在任何 iframe
1111
- for (const { rect, element } of this.cachedIframes) {
1112
- if (clientX >= rect.left &&
1113
- clientX <= rect.right &&
1114
- clientY >= rect.top &&
1115
- clientY <= rect.bottom) {
1116
- // 鼠标在 iframe 上,立即停止拖动
1701
+ // 如果鼠标在 iframe 上,需要保持拖动状态
1702
+ // 注意:在 PC 模式下,当鼠标进入 iframe 后,mousemove 事件会完全丢失
1703
+ // 这是浏览器的安全限制,无法获取 iframe 内的实时鼠标位置
1704
+ // 因此,我们无法让图标实时跟随鼠标移动
1705
+ // 解决方案:保持图标在最后已知位置(进入 iframe 前的位置),等待 mouseup 事件更新最终位置
1706
+ if (this.isMouseOverIframe(clientX, clientY)) {
1707
+ // iframe 上时,保持拖动状态,防止被误判为停止
1708
+ // 注意:只有在真正拖动后(dragStarted = true)才保持拖动状态
1709
+ if (!this.isDragging && this.dragStarted) {
1710
+ this.isDragging = true;
1117
1711
  if (this.debug) {
1118
- console.log('[IconManager] Active detection: Mouse over iframe detected, stopping drag immediately', {
1119
- mousePosition: { x: clientX, y: clientY },
1120
- iframeRect: {
1121
- left: rect.left,
1122
- top: rect.top,
1123
- right: rect.right,
1124
- bottom: rect.bottom
1125
- },
1126
- iframeSrc: element.src || 'no src',
1127
- timeSinceLastMove
1128
- });
1712
+ console.log('[IconManager] [PC+iframe] Active detection: Mouse over iframe, keeping drag state active (waiting for mouseup)');
1129
1713
  }
1130
- this.stopDrag();
1131
- return;
1714
+ }
1715
+ // 在 PC 模式下,当鼠标进入 iframe 后,我们无法获取实时鼠标位置
1716
+ // 因此,不进行位置更新,保持图标在最后已知位置
1717
+ // 最终位置会在 mouseup 事件中更新
1718
+ if (this.debug && timeSinceLastMove > 500 && !this.hasLoggedIframeWarning) {
1719
+ console.warn('[IconManager] [PC+iframe] Mouse entered iframe - mousemove events lost. Icon position frozen at last known position. Will update on mouseup.', {
1720
+ timeSinceLastMove,
1721
+ lastMousePosition: { x: this.lastMousePosition.x, y: this.lastMousePosition.y },
1722
+ note: 'This is a browser security limitation. Icon will update to final position when mouse is released (mouseup event).'
1723
+ });
1724
+ this.hasLoggedIframeWarning = true;
1132
1725
  }
1133
1726
  }
1134
1727
  }
1135
1728
  // 继续检测
1136
- this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
1729
+ // 如果在 iframe 上,使用更频繁的检测(每 16ms,约 60fps)以确保位置更新及时
1730
+ // 如果不在 iframe 上,使用较低的频率(每 100ms)以节省性能
1731
+ const isOverIframe = timeSinceLastMove > 100 && this.isMouseOverIframe(this.lastMousePosition.x, this.lastMousePosition.y);
1732
+ const detectionInterval = isOverIframe ? 16 : 100; // iframe 上时更频繁检测
1733
+ this.activeDetectionTimeoutId = window.setTimeout(() => {
1734
+ this.activeDetectionTimeoutId = null;
1735
+ requestAnimationFrame(checkMousePosition);
1736
+ }, detectionInterval);
1137
1737
  };
1138
1738
  this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
1139
1739
  }
@@ -1149,6 +1749,40 @@ class IconManager {
1149
1749
  this.activeDetectionRafId = null;
1150
1750
  }
1151
1751
  }
1752
+ /**
1753
+ * 检查鼠标是否在 iframe 上
1754
+ * 注意:排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe),因为 SDK 的 iframe 不应该影响拖动
1755
+ */
1756
+ isMouseOverIframe(x, y) {
1757
+ // 更新 iframe 缓存(如果需要)
1758
+ const now = Date.now();
1759
+ if (this.cachedIframes.length === 0 ||
1760
+ now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
1761
+ const allIframes = document.querySelectorAll('iframe');
1762
+ // 排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe)
1763
+ this.cachedIframes = Array.from(allIframes)
1764
+ .filter(iframe => {
1765
+ // 检查 iframe 是否在 SDK 容器内
1766
+ const container = iframe.closest('.customer-sdk-container');
1767
+ return !container; // 排除 SDK 容器内的 iframe
1768
+ })
1769
+ .map(iframe => ({
1770
+ element: iframe,
1771
+ rect: iframe.getBoundingClientRect()
1772
+ }));
1773
+ this.lastIframeUpdateTime = now;
1774
+ }
1775
+ // 检查鼠标是否在任何 iframe 上
1776
+ for (const { rect } of this.cachedIframes) {
1777
+ if (x >= rect.left &&
1778
+ x <= rect.right &&
1779
+ y >= rect.top &&
1780
+ y <= rect.bottom) {
1781
+ return true;
1782
+ }
1783
+ }
1784
+ return false;
1785
+ }
1152
1786
  /**
1153
1787
  * 清理拖动事件
1154
1788
  */
@@ -1286,6 +1920,12 @@ class IconManager {
1286
1920
  const containerRect = container.getBoundingClientRect();
1287
1921
  const iconWidth = this.iconElement.offsetWidth || 30;
1288
1922
  const iconHeight = this.iconElement.offsetHeight || 30;
1923
+ // 获取容器的实际宽度(考虑 max-width 限制)
1924
+ const containerComputedStyle = window.getComputedStyle(container);
1925
+ const maxWidth = containerComputedStyle.maxWidth;
1926
+ const actualWidth = maxWidth && maxWidth !== 'none'
1927
+ ? Math.min(containerRect.width, parseFloat(maxWidth) || containerRect.width)
1928
+ : containerRect.width;
1289
1929
  // 获取当前图标位置
1290
1930
  const computedStyle = window.getComputedStyle(this.iconElement);
1291
1931
  const currentLeft = parseFloat(computedStyle.left) || 0;
@@ -1294,7 +1934,7 @@ class IconManager {
1294
1934
  let newY = currentTop;
1295
1935
  // X 轴磁性吸附
1296
1936
  if (this.magneticDirection === 'x' || this.magneticDirection === 'both') {
1297
- const containerWidth = containerRect.width;
1937
+ const containerWidth = actualWidth; // 使用实际宽度(考虑 max-width
1298
1938
  const centerX = containerWidth / 2;
1299
1939
  if (currentLeft < centerX) {
1300
1940
  // 吸附到左边
@@ -1483,6 +2123,7 @@ class IconManager {
1483
2123
  justifyContent: 'center',
1484
2124
  zIndex: '1000001',
1485
2125
  boxShadow: '0 -1px 4px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(0, 0, 0, 0.15)',
2126
+ pointerEvents: 'auto', // 子元素启用指针事件,让 badge 可以接收事件
1486
2127
  ...(count === 0 && text === '' && {
1487
2128
  width: '12px',
1488
2129
  height: '12px',
@@ -1688,6 +2329,8 @@ class IframeManager {
1688
2329
  // 创建包装容器(包含iframe和关闭按钮)
1689
2330
  this.containerElement = document.createElement('div');
1690
2331
  this.containerElement.className = 'customer-sdk-container';
2332
+ // 确保容器不可拖动
2333
+ this.containerElement.draggable = false;
1691
2334
  // 创建iframe元素
1692
2335
  this.iframeElement = document.createElement('iframe');
1693
2336
  this.iframeElement.className = 'customer-sdk-iframe';
@@ -1760,24 +2403,29 @@ class IframeManager {
1760
2403
  opacity: '0',
1761
2404
  display: 'none'
1762
2405
  } : (isPC ? {
1763
- // PC模式:没有 target,使用配置的宽度,高度100%
2406
+ // PC模式:没有 target,固定在右下角(不可拖动)
1764
2407
  width: `${this.config.width || 450}px`,
1765
- height: '100%',
2408
+ height: `${this.config.height || 600}px`,
1766
2409
  maxWidth: '90vw',
1767
- maxHeight: '100%',
2410
+ maxHeight: '90vh',
1768
2411
  backgroundColor: '#ffffff',
1769
- borderRadius: '0',
1770
- boxShadow: 'none',
2412
+ borderRadius: '8px',
2413
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.25)',
1771
2414
  border: 'none',
1772
2415
  position: 'fixed',
1773
2416
  zIndex: '999999',
1774
- // PC模式:水平居中,垂直占满
1775
- top: '0',
1776
- left: '50%',
1777
- bottom: '0',
1778
- right: 'auto',
1779
- transform: 'translateX(-50%)',
2417
+ // PC模式:固定在右下角
2418
+ top: 'auto',
2419
+ left: 'auto',
2420
+ bottom: '20px',
2421
+ right: '20px',
2422
+ transform: 'none',
1780
2423
  overflow: 'hidden',
2424
+ // 防止拖动和选择
2425
+ userSelect: 'none',
2426
+ WebkitUserSelect: 'none',
2427
+ MozUserSelect: 'none',
2428
+ msUserSelect: 'none',
1781
2429
  // 初始隐藏的关键样式
1782
2430
  visibility: 'hidden',
1783
2431
  opacity: '0',
@@ -1817,6 +2465,28 @@ class IframeManager {
1817
2465
  Object.assign(this.iframeElement.style, iframeStyles);
1818
2466
  // 将iframe放入容器
1819
2467
  this.containerElement.appendChild(this.iframeElement);
2468
+ // PC模式下:禁止拖动容器
2469
+ if (isPC && !useTargetWidth) {
2470
+ // 阻止拖动事件
2471
+ this.containerElement.addEventListener('dragstart', (e) => {
2472
+ e.preventDefault();
2473
+ e.stopPropagation();
2474
+ return false;
2475
+ }, false);
2476
+ // 阻止鼠标按下事件(防止可能的拖动行为)
2477
+ this.containerElement.addEventListener('mousedown', (e) => {
2478
+ // 只阻止在容器边缘的拖动,允许点击 iframe 内容
2479
+ const rect = this.containerElement.getBoundingClientRect();
2480
+ const isOnEdge = (e.clientX < rect.left + 10 ||
2481
+ e.clientX > rect.right - 10 ||
2482
+ e.clientY < rect.top + 10 ||
2483
+ e.clientY > rect.bottom - 10);
2484
+ if (isOnEdge) {
2485
+ e.preventDefault();
2486
+ e.stopPropagation();
2487
+ }
2488
+ }, false);
2489
+ }
1820
2490
  // 添加iframe加载事件监听(移动端样式优化)
1821
2491
  this.iframeElement.addEventListener('load', () => {
1822
2492
  // 移动端注入自定义样式
@@ -1974,10 +2644,23 @@ class IframeManager {
1974
2644
  }
1975
2645
  // 创建新的消息处理器并保存引用
1976
2646
  this.messageHandler = (event) => {
1977
- // 验证消息来源(可选的安全检查)
1978
- if (!this.config.src || event.origin === new URL(this.config.src).origin) {
1979
- this.handleIframeMessage(event.data);
2647
+ // 关键:过滤掉自己通过 dispatchEvent 发送的消息,避免无限循环
2648
+ // 自己发送的消息 source window,而 iframe 发送的消息 source 是 iframe.contentWindow
2649
+ if (event.source === window) {
2650
+ if (this.debug) {
2651
+ console.log('[IframeManager] Ignoring self-broadcasted message to prevent infinite loop:', event.data);
2652
+ }
2653
+ return;
2654
+ }
2655
+ // 不验证来源,直接处理所有消息(确保消息能够被接收)
2656
+ if (this.debug) {
2657
+ console.log('[IframeManager] Message received:', {
2658
+ data: event.data,
2659
+ origin: event.origin,
2660
+ source: event.source
2661
+ });
1980
2662
  }
2663
+ this.handleIframeMessage(event.data);
1981
2664
  };
1982
2665
  window.addEventListener('message', this.messageHandler, false);
1983
2666
  }
@@ -1986,7 +2669,7 @@ class IframeManager {
1986
2669
  */
1987
2670
  handleIframeMessage(data) {
1988
2671
  if (this.debug) {
1989
- console.log('Message from iframe:', data);
2672
+ console.log('[IframeManager] Message from iframe received:', data);
1990
2673
  }
1991
2674
  // 判断data是字符串还是对象,兼容两种格式
1992
2675
  let messageType;
@@ -1998,10 +2681,13 @@ class IframeManager {
1998
2681
  }
1999
2682
  else {
2000
2683
  if (this.debug) {
2001
- console.log('Unknown message format:', data);
2684
+ console.log('[IframeManager] Unknown message format:', data);
2002
2685
  }
2003
2686
  return;
2004
2687
  }
2688
+ if (this.debug) {
2689
+ console.log('[IframeManager] Parsed message type:', messageType);
2690
+ }
2005
2691
  // 根据消息类型处理不同的操作
2006
2692
  switch (messageType) {
2007
2693
  case 'iframe_ready':
@@ -2034,62 +2720,94 @@ class IframeManager {
2034
2720
  }
2035
2721
  break;
2036
2722
  case 'goto-login':
2037
- // 登录跳转消息 - 通过 window.postMessage 抛出给外层
2723
+ // 登录跳转消息 - 广播给调用 SDK 的 Vue 页面
2038
2724
  if (this.debug) {
2039
- console.log('Received goto-login message, forwarding to parent window');
2725
+ console.log('Received goto-login message, broadcasting to Vue page');
2040
2726
  }
2041
- this.forwardMessageToParent(data);
2727
+ this.broadcastMessageToPage(data);
2042
2728
  if (this.config.onMessage) {
2043
2729
  this.config.onMessage(messageType, data);
2044
2730
  }
2045
2731
  break;
2046
- default:
2047
- // 处理动态消息:gotoActivityDetailById:xxx
2048
- if (typeof messageType === 'string' && messageType.startsWith('gotoActivityDetailById:')) {
2049
- // 活动详情跳转消息 - 通过 window.postMessage 抛出给外层
2050
- if (this.debug) {
2051
- console.log('Received gotoActivityDetailById message, forwarding to parent window:', messageType);
2052
- }
2053
- this.forwardMessageToParent(data);
2054
- if (this.config.onMessage) {
2055
- this.config.onMessage(messageType, data);
2056
- }
2732
+ case 'DCR_DEPOSIT':
2733
+ // 存款跳转消息 - 广播给调用 SDK 的 Vue 页面
2734
+ // 支持两种存款类型:
2735
+ // 1. 钱包直充:{ type: 'DCR_DEPOSIT', payload: { depositType: 0, walletId: 69382 } }
2736
+ // 2. 线下充值:{ type: 'DCR_DEPOSIT', payload: { depositType: 2, offlineTypeId: 2001, accountId: 35213 } }
2737
+ if (this.debug) {
2738
+ const depositType = data.payload?.depositType;
2739
+ const depositTypeText = depositType === 0 ? '钱包直充' : depositType === 2 ? '线下充值' : '未知类型';
2740
+ console.log(`[IframeManager] Received DCR_DEPOSIT message (${depositTypeText}):`, {
2741
+ type: data.type,
2742
+ payload: data.payload,
2743
+ depositType: depositTypeText
2744
+ });
2057
2745
  }
2058
- else {
2059
- // 其他自定义消息处理
2060
- if (this.debug) {
2061
- console.log('Custom message:', data);
2062
- }
2746
+ this.broadcastMessageToPage(data);
2747
+ if (this.config.onMessage) {
2748
+ this.config.onMessage(messageType, data);
2749
+ }
2750
+ break;
2751
+ case 'DCR_ACTIVITY':
2752
+ // 跳转到优惠页面 - 广播给调用 SDK 的 Vue 页面
2753
+ // 格式:{ type: 'DCR_ACTIVITY', payload: { activityId: 123 } }
2754
+ if (this.debug) {
2755
+ console.log('[IframeManager] Received DCR_ACTIVITY message:', {
2756
+ type: data.type,
2757
+ payload: data.payload,
2758
+ activityId: data.payload?.activityId
2759
+ });
2760
+ }
2761
+ this.broadcastMessageToPage(data);
2762
+ if (this.config.onMessage) {
2763
+ this.config.onMessage(messageType, data);
2764
+ }
2765
+ break;
2766
+ default:
2767
+ // 其他自定义消息处理
2768
+ if (this.debug) {
2769
+ console.log('[IframeManager] Custom message:', data);
2063
2770
  }
2064
2771
  break;
2065
2772
  }
2066
2773
  }
2067
2774
  /**
2068
- * 将消息转发给父窗口(通过 window.postMessage)
2069
- * 这样 Vue 页面中的监听器也能收到消息
2775
+ * 向调用 SDK 的地方(Vue 页面)广播消息
2776
+ * 通过 window.dispatchEvent 触发事件,让 Vue 页面中的 window.addEventListener('message') 可以收到
2777
+ *
2778
+ * 重要说明:
2779
+ * 1. window.dispatchEvent 创建的事件会在同一窗口内触发所有监听器,无论 origin 是什么,Vue 页面都能收到
2780
+ * 2. MessageEvent 的 origin 属性不能是 '*',必须是有效的 origin 字符串(如 'http://localhost:5173')
2781
+ * 3. 我们使用 window.location.origin 来标识消息来源(当前页面的 origin)
2782
+ * 4. 如果 Vue 页面需要检查 origin,应该检查 window.location.origin 而不是 '*'
2070
2783
  */
2071
- forwardMessageToParent(data) {
2784
+ broadcastMessageToPage(data) {
2072
2785
  try {
2073
- // 方式1:使用 window.postMessage 发送消息(跨窗口通信)
2074
- // 如果存在父窗口且不是当前窗口,发送给父窗口
2075
- if (window.parent && window.parent !== window) {
2076
- window.parent.postMessage(data, '*');
2077
- }
2078
- // 方式2:使用 dispatchEvent 创建 MessageEvent(在同一窗口内触发所有监听器)
2079
- // 这样 Vue 页面中的 window.addEventListener('message') 也能收到
2786
+ // 使用 dispatchEvent 创建 MessageEvent,在同一窗口内触发所有监听器
2787
+ // 这样调用 SDK 的 Vue 页面中的 window.addEventListener('message') 也能收到
2788
+ // 注意:MessageEvent origin 属性不能是 '*',必须是有效的 origin 字符串
2789
+ // 我们使用 window.location.origin 来标识消息来源(当前页面的 origin)
2790
+ // Vue 页面可以收到消息,因为 dispatchEvent 会在同一窗口内触发所有监听器
2080
2791
  const messageEvent = new MessageEvent('message', {
2081
2792
  data: data,
2082
- origin: window.location.origin,
2083
- source: window
2793
+ origin: window.location.origin, // 使用当前页面的 origin,不是 '*'(MessageEvent 不支持 '*')
2794
+ source: window,
2795
+ bubbles: true,
2796
+ cancelable: true
2084
2797
  });
2085
2798
  window.dispatchEvent(messageEvent);
2086
2799
  if (this.debug) {
2087
- console.log('Message forwarded via window.postMessage and dispatchEvent:', data);
2800
+ console.log('[IframeManager] Message broadcasted to Vue page via window.dispatchEvent:', {
2801
+ data: data,
2802
+ origin: window.location.origin,
2803
+ type: 'message',
2804
+ note: 'origin is window.location.origin, not "*" (MessageEvent does not support "*")'
2805
+ });
2088
2806
  }
2089
2807
  }
2090
2808
  catch (error) {
2091
2809
  if (this.debug) {
2092
- console.error('Failed to forward message:', error);
2810
+ console.error('[IframeManager] Failed to broadcast message:', error);
2093
2811
  }
2094
2812
  }
2095
2813
  }
@@ -2926,20 +3644,20 @@ function copyInputValue(node, cloned) {
2926
3644
  }
2927
3645
 
2928
3646
  const pseudoClasses = [
2929
- ":before",
2930
- ":after"
2931
- // ':placeholder', TODO
3647
+ "::before",
3648
+ "::after"
3649
+ // '::placeholder', TODO
2932
3650
  ];
2933
3651
  const scrollbarPseudoClasses = [
2934
- ":-webkit-scrollbar",
2935
- ":-webkit-scrollbar-button",
2936
- // ':-webkit-scrollbar:horizontal', TODO
2937
- ":-webkit-scrollbar-thumb",
2938
- ":-webkit-scrollbar-track",
2939
- ":-webkit-scrollbar-track-piece",
2940
- // ':-webkit-scrollbar:vertical', TODO
2941
- ":-webkit-scrollbar-corner",
2942
- ":-webkit-resizer"
3652
+ "::-webkit-scrollbar",
3653
+ "::-webkit-scrollbar-button",
3654
+ // '::-webkit-scrollbar:horizontal', TODO
3655
+ "::-webkit-scrollbar-thumb",
3656
+ "::-webkit-scrollbar-track",
3657
+ "::-webkit-scrollbar-track-piece",
3658
+ // '::-webkit-scrollbar:vertical', TODO
3659
+ "::-webkit-scrollbar-corner",
3660
+ "::-webkit-resizer"
2943
3661
  ];
2944
3662
  function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFamilies) {
2945
3663
  const { ownerWindow, svgStyleElement, svgStyles, currentNodeStyle } = context;
@@ -2983,7 +3701,7 @@ function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFami
2983
3701
  allClasses = [];
2984
3702
  svgStyles.set(cssText, allClasses);
2985
3703
  }
2986
- allClasses.push(`.${klasses[0]}:${pseudoClass}`);
3704
+ allClasses.push(`.${klasses[0]}${pseudoClass}`);
2987
3705
  }
2988
3706
  pseudoClasses.forEach(copyBy);
2989
3707
  if (copyScrollbar)
@@ -21902,7 +22620,7 @@ class CustomerServiceSDK {
21902
22620
  const initResult = {
21903
22621
  deviceId,
21904
22622
  iframeUrl,
21905
- referrer: document.referrer,
22623
+ referrer: config.referrer || document.referrer || document.location.href,
21906
22624
  agent: config.agent,
21907
22625
  timestamp: Date.now()
21908
22626
  };
@@ -22328,7 +23046,7 @@ class CustomerServiceSDK {
22328
23046
  url.searchParams.set('Authorization', config.token);
22329
23047
  }
22330
23048
  url.searchParams.set('DeviceSign', deviceId);
22331
- url.searchParams.set('Referrer', document.referrer);
23049
+ url.searchParams.set('Referrer', config.referrer || document.referrer);
22332
23050
  return url.toString();
22333
23051
  }
22334
23052
  }