customer-chat-sdk 1.1.17 → 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.
@@ -12,7 +12,8 @@ class IconManager {
12
12
  this.target = null; // 图标传送目标元素(可以是 HTMLElement 或选择器字符串)
13
13
  // 拖动相关状态
14
14
  this.isDragging = false;
15
- this.dragStarted = false; // 是否真正开始了拖拽
15
+ this.dragStarted = false; // 是否真正开始了拖拽(移动超过阈值)
16
+ this.dragIntent = false; // 拖动意图(mousedown/touchstart 时设置为 true,表示用户想要拖动)
16
17
  this.hasMoved = false; // 是否移动过
17
18
  this.dragOffset = { x: 0, y: 0 }; // 拖动偏移量
18
19
  this.lastTouchPosition = { x: 0, y: 0 }; // 最后触摸位置
@@ -34,7 +35,9 @@ class IconManager {
34
35
  this.pendingPosition = { x: 0, y: 0, needsUpdate: false };
35
36
  // 主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置(即使没有 mousemove 事件)
36
37
  this.activeDetectionRafId = null;
38
+ this.activeDetectionTimeoutId = null; // setTimeout 的 ID
37
39
  this.lastMousePosition = { x: 0, y: 0, timestamp: 0 }; // 最后记录的鼠标位置和时间戳
40
+ this.mouseVelocity = { vx: 0, vy: 0 }; // 鼠标移动速度(用于预测位置)
38
41
  // 事件处理器引用(用于清理)
39
42
  this.onDragHandler = null;
40
43
  this.stopDragHandler = null;
@@ -46,6 +49,7 @@ class IconManager {
46
49
  this.mouseLeaveTimeout = null; // 鼠标离开防抖定时器
47
50
  this.pointerLeaveTimeout = null; // 指针离开防抖定时器
48
51
  this.isStoppingDrag = false; // 防止 stopDrag 重复调用
52
+ this.hasLoggedIframeWarning = false; // 防止 iframe 警告日志重复输出
49
53
  // 侧边吸附相关状态
50
54
  this.sideAttach = true; // 是否启用侧边吸附(默认 true)
51
55
  this.sideHideRatio = 0.5; // 侧边吸附时的隐藏比例(默认 0.5,显示一半)
@@ -204,6 +208,15 @@ class IconManager {
204
208
  this.iconElement.appendChild(imgContainer);
205
209
  // 添加到目标元素(如果 target 是字符串,需要重新查找,因为可能在初始化时元素还不存在)
206
210
  const targetElement = this.getTargetElement();
211
+ if (this.debug) {
212
+ console.log('[IconManager] Target element for icon:', {
213
+ target: this.target,
214
+ targetElement: targetElement,
215
+ targetElementId: targetElement.id,
216
+ targetElementClass: targetElement.className,
217
+ isBody: targetElement === document.body
218
+ });
219
+ }
207
220
  // 确保目标容器有 overflow-x: hidden,防止图标超出边界时出现横向滚动条
208
221
  // 注意:只在启用侧边吸附时才设置,避免影响其他场景
209
222
  if (this.sideAttach && targetElement !== document.body) {
@@ -236,12 +249,20 @@ class IconManager {
236
249
  }
237
250
  if (targetElement) {
238
251
  targetElement.appendChild(this.iconElement);
252
+ if (this.debug) {
253
+ console.log('[IconManager] Icon added to target element:', {
254
+ target: this.target,
255
+ targetElement: targetElement,
256
+ targetElementId: targetElement.id,
257
+ targetElementClass: targetElement.className
258
+ });
259
+ }
239
260
  }
240
261
  else {
241
262
  // 如果目标元素不存在,回退到 document.body
242
263
  document.body.appendChild(this.iconElement);
243
264
  if (this.debug) {
244
- console.warn('Target element not found, icon added to document.body');
265
+ console.warn('[IconManager] Target element not found, icon added to document.body');
245
266
  }
246
267
  }
247
268
  // 设置拖动事件
@@ -556,12 +577,70 @@ class IconManager {
556
577
  this.iconElement.addEventListener('mousedown', this.startDragHandler);
557
578
  this.iconElement.addEventListener('touchstart', this.startDragHandler, { passive: false });
558
579
  }
580
+ /**
581
+ * 检查 SDK iframe 是否打开(PC 模式)
582
+ * PC 模式下,如果 SDK iframe 打开,禁止拖动图标
583
+ */
584
+ isSDKIframeOpen() {
585
+ const isPC = window.innerWidth > 768;
586
+ if (!isPC)
587
+ return false; // 移动端不受影响
588
+ // 检查 SDK iframe 容器
589
+ const iframeContainer = document.querySelector('.customer-sdk-container');
590
+ if (!iframeContainer) {
591
+ if (this.debug) {
592
+ console.log('[IconManager] [PC+iframe] SDK iframe container not found');
593
+ }
594
+ return false;
595
+ }
596
+ // 检查 iframe 容器是否可见
597
+ // 注意:IframeManager.show() 会设置 visibility: 'visible', opacity: '1', display: 'block'
598
+ const style = window.getComputedStyle(iframeContainer);
599
+ const visibility = style.visibility;
600
+ const opacity = style.opacity;
601
+ const display = style.display;
602
+ // 更严格的检查:必须是 visible、opacity > 0、display 不是 none
603
+ const isVisible = (visibility === 'visible' &&
604
+ parseFloat(opacity) > 0 &&
605
+ display !== 'none');
606
+ if (this.debug) {
607
+ console.log('[IconManager] [PC+iframe] Checking SDK iframe visibility:', {
608
+ visibility,
609
+ opacity,
610
+ display,
611
+ isVisible,
612
+ parsedOpacity: parseFloat(opacity)
613
+ });
614
+ }
615
+ return isVisible;
616
+ }
559
617
  /**
560
618
  * 开始拖动
561
619
  */
562
620
  startDrag(e) {
563
621
  if (!this.iconElement || !this.isClickEnabled)
564
622
  return;
623
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
624
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
625
+ // PC 模式下,如果 SDK iframe 打开,禁止拖动图标
626
+ if (this.isSDKIframeOpen()) {
627
+ if (this.debug) {
628
+ console.log('[IconManager] [PC+iframe] SDK iframe is open, icon dragging disabled');
629
+ }
630
+ return;
631
+ }
632
+ // PC 模式下,如果鼠标在任何 iframe 上(排除 SDK 自己的 iframe),禁止拖动图标
633
+ const isPC = window.innerWidth > 768;
634
+ if (isPC && this.isMouseOverIframe(clientX, clientY)) {
635
+ if (this.debug) {
636
+ console.log('[IconManager] [PC+iframe] Mouse is over iframe, icon dragging disabled', {
637
+ clientX,
638
+ clientY,
639
+ isPC
640
+ });
641
+ }
642
+ return;
643
+ }
565
644
  // 清除自动吸附定时器(用户开始拖动)
566
645
  this.clearAutoAttachTimer();
567
646
  // 如果正在停止拖动,等待完成后再允许新的拖动
@@ -582,6 +661,8 @@ class IconManager {
582
661
  }
583
662
  return;
584
663
  }
664
+ // 重置 iframe 警告标志
665
+ this.hasLoggedIframeWarning = false;
585
666
  // 如果发现残留的资源,先清理它们(防御性编程)
586
667
  if (this.dragTimeoutId !== null || this.activeDetectionRafId !== null || this.rafId !== null) {
587
668
  if (this.debug) {
@@ -605,8 +686,6 @@ class IconManager {
605
686
  // 如果不在,让事件穿透到 iframe,不触发拖动
606
687
  if (this.iconElement) {
607
688
  const iconRect = this.iconElement.getBoundingClientRect();
608
- const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
609
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
610
689
  // 计算触摸点相对于 icon 中心的位置
611
690
  const iconCenterX = iconRect.left + iconRect.width / 2;
612
691
  const iconCenterY = iconRect.top + iconRect.height / 2;
@@ -637,10 +716,15 @@ class IconManager {
637
716
  this.hasMoved = false;
638
717
  this.dragStarted = false;
639
718
  this.isDragging = false;
719
+ this.dragIntent = true; // 设置拖动意图,表示用户已经开始拖动意图(在 mousedown/touchstart 时)
640
720
  // 记录开始时间和位置
641
721
  this.touchStartTime = Date.now();
642
- const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
643
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
722
+ // 初始化 lastMousePosition,避免第一次 onDrag timeSinceLastUpdate 过大
723
+ this.lastMousePosition = {
724
+ x: clientX,
725
+ y: clientY,
726
+ timestamp: Date.now()
727
+ };
644
728
  this.lastTouchPosition.x = clientX;
645
729
  this.lastTouchPosition.y = clientY;
646
730
  try {
@@ -658,14 +742,21 @@ class IconManager {
658
742
  // 注意:不在这里转换位置,只在真正开始拖动时才转换(在 onDrag 中)
659
743
  // 性能优化:在拖动开始时预加载所有 iframe 位置信息
660
744
  // 这样可以避免在拖动过程中频繁查询 DOM
745
+ // 注意:排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe),因为 SDK 的 iframe 不应该影响拖动
661
746
  const allIframes = document.querySelectorAll('iframe');
662
- this.cachedIframes = Array.from(allIframes).map(iframe => ({
747
+ this.cachedIframes = Array.from(allIframes)
748
+ .filter(iframe => {
749
+ // 检查 iframe 是否在 SDK 容器内
750
+ const container = iframe.closest('.customer-sdk-container');
751
+ return !container; // 排除 SDK 容器内的 iframe
752
+ })
753
+ .map(iframe => ({
663
754
  element: iframe,
664
755
  rect: iframe.getBoundingClientRect()
665
756
  }));
666
757
  this.lastIframeUpdateTime = Date.now();
667
758
  if (this.debug) {
668
- console.log(`[IconManager] Drag start - Found ${this.cachedIframes.length} iframe(s)`, {
759
+ console.log(`[IconManager] Drag start - Found ${this.cachedIframes.length} iframe(s) (excluding SDK iframes)`, {
669
760
  iframes: this.cachedIframes.map(({ rect }) => ({
670
761
  left: rect.left,
671
762
  top: rect.top,
@@ -687,43 +778,54 @@ class IconManager {
687
778
  }
688
779
  // 添加处理 iframe 上事件丢失的机制
689
780
  // 1. 监听 mouseleave 和 pointerleave 事件(鼠标离开窗口时停止拖动)
690
- // 注意:这些事件可能会在拖动过程中多次触发,需要添加防抖机制
781
+ // 注意:当鼠标移动到 iframe 上时,这些事件可能会误触发,需要更严格的检测
691
782
  this.mouseLeaveHandler = (e) => {
692
- // 只有当鼠标真正离开窗口时才停止拖动
693
- if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
694
- e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
783
+ // 检查鼠标是否真的离开了窗口(而不是移动到 iframe 上)
784
+ // 只有当鼠标完全离开窗口边界时才停止拖动
785
+ const isReallyLeaving = (e.clientY < -10 ||
786
+ e.clientX < -10 ||
787
+ e.clientX > window.innerWidth + 10 ||
788
+ e.clientY > window.innerHeight + 10);
789
+ // 还要检查是否在 iframe 区域内(如果在 iframe 上,不应该停止拖动)
790
+ if (isReallyLeaving && !this.isMouseOverIframe(e.clientX, e.clientY)) {
695
791
  // 添加防抖,避免重复触发
696
792
  if (this.mouseLeaveTimeout) {
697
793
  clearTimeout(this.mouseLeaveTimeout);
698
794
  }
699
795
  this.mouseLeaveTimeout = window.setTimeout(() => {
700
- if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag) {
796
+ // 再次确认鼠标真的离开了窗口
797
+ if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
701
798
  if (this.debug) {
702
799
  console.log('[IconManager] Mouse left window, stopping drag');
703
800
  }
704
801
  this.stopDrag();
705
802
  }
706
803
  this.mouseLeaveTimeout = null;
707
- }, 50); // 50ms 防抖
804
+ }, 100); // 增加到 100ms 防抖,避免误判
708
805
  }
709
806
  };
710
807
  this.pointerLeaveHandler = (e) => {
711
- // 只有当指针真正离开窗口时才停止拖动
712
- if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
713
- e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
808
+ // 检查指针是否真的离开了窗口(而不是移动到 iframe 上)
809
+ const isReallyLeaving = (e.clientY < -10 ||
810
+ e.clientX < -10 ||
811
+ e.clientX > window.innerWidth + 10 ||
812
+ e.clientY > window.innerHeight + 10);
813
+ // 还要检查是否在 iframe 区域内(如果在 iframe 上,不应该停止拖动)
814
+ if (isReallyLeaving && !this.isMouseOverIframe(e.clientX, e.clientY)) {
714
815
  // 添加防抖,避免重复触发
715
816
  if (this.pointerLeaveTimeout) {
716
817
  clearTimeout(this.pointerLeaveTimeout);
717
818
  }
718
819
  this.pointerLeaveTimeout = window.setTimeout(() => {
719
- if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag) {
820
+ // 再次确认指针真的离开了窗口
821
+ if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
720
822
  if (this.debug) {
721
823
  console.log('[IconManager] Pointer left window, stopping drag');
722
824
  }
723
825
  this.stopDrag();
724
826
  }
725
827
  this.pointerLeaveTimeout = null;
726
- }, 50); // 50ms 防抖
828
+ }, 100); // 增加到 100ms 防抖,避免误判
727
829
  }
728
830
  };
729
831
  document.addEventListener('mouseleave', this.mouseLeaveHandler);
@@ -745,15 +847,97 @@ class IconManager {
745
847
  };
746
848
  window.addEventListener('blur', this.blurHandler);
747
849
  // 3. 添加超时机制(如果一段时间没有收到 mousemove 事件,自动停止拖动)
748
- // 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
749
- this.dragTimeoutId = window.setTimeout(() => {
750
- if (this.isDragging) {
751
- if (this.debug) {
752
- console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
753
- }
754
- this.stopDrag();
850
+ // 注意:在 startDrag 时,检查鼠标是否在 iframe
851
+ // 如果在 iframe 上,使用更长的超时时间(5秒),因为 mousemove 事件会丢失
852
+ // 如果不在 iframe 上,使用简单的超时逻辑(50ms)
853
+ const isPC = window.innerWidth > 768;
854
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
855
+ if (isPC) {
856
+ // ========== PC 模式 ==========
857
+ if (isOverIframe) {
858
+ // PC 模式 + 在 iframe 上:特殊处理逻辑
859
+ // 在 iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
860
+ // 所以使用更长的超时时间(5秒),等待 mouseup
861
+ const checkDragTimeoutForPCIframe = () => {
862
+ const lastPos = this.lastMousePosition;
863
+ const stillOverIframe = lastPos && this.isMouseOverIframe(lastPos.x, lastPos.y);
864
+ // 如果还在 iframe 上且还有拖动意图,继续等待 mouseup
865
+ if (stillOverIframe && (this.dragIntent || this.dragStarted || this.isDragging)) {
866
+ if (this.debug) {
867
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
868
+ console.log('[IconManager] [PC+iframe] Drag timeout but mouse is over iframe, keeping drag active (waiting for mouseup)', {
869
+ timeSinceLastMove,
870
+ lastMousePosition: this.lastMousePosition
871
+ });
872
+ }
873
+ // PC 模式 iframe 上:使用 5 秒超时,等待 mouseup
874
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForPCIframe, 5000);
875
+ return;
876
+ }
877
+ // 不在 iframe 上了,或者拖动意图已消失,停止拖动
878
+ if ((this.isDragging || this.dragStarted) && this.dragStarted) {
879
+ if (this.debug) {
880
+ console.warn('[IconManager] [PC+iframe] Drag timeout triggered, stopping drag');
881
+ }
882
+ this.stopDrag();
883
+ }
884
+ };
885
+ // PC 模式 iframe 上:使用 5 秒超时
886
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForPCIframe, 5000);
887
+ }
888
+ else {
889
+ // PC 模式 + 不在 iframe 上:还原到原来的简单逻辑
890
+ // 原来的逻辑:简单的超时,50ms(和之前的代码一致)
891
+ // 只有在真正拖动后(dragStarted = true)才停止拖动
892
+ this.dragTimeoutId = window.setTimeout(() => {
893
+ if (this.isDragging && this.dragStarted) {
894
+ if (this.debug) {
895
+ console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
896
+ }
897
+ this.stopDrag();
898
+ }
899
+ }, 50); // PC 模式非 iframe:使用 50ms 超时
900
+ }
901
+ }
902
+ else {
903
+ // ========== 手机模式 ==========
904
+ if (isOverIframe) {
905
+ // 手机模式 + 在 iframe 上:特殊处理逻辑
906
+ const checkDragTimeoutForMobileIframe = () => {
907
+ const lastPos = this.lastMousePosition;
908
+ const stillOverIframe = lastPos && this.isMouseOverIframe(lastPos.x, lastPos.y);
909
+ if (stillOverIframe && (this.dragIntent || this.dragStarted || this.isDragging)) {
910
+ if (this.debug) {
911
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
912
+ console.log('[IconManager] [Mobile+iframe] Drag timeout but mouse is over iframe, keeping drag active (waiting for mouseup)', {
913
+ timeSinceLastMove,
914
+ lastMousePosition: this.lastMousePosition
915
+ });
916
+ }
917
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
918
+ return;
919
+ }
920
+ if ((this.isDragging || this.dragStarted) && this.dragStarted) {
921
+ if (this.debug) {
922
+ console.warn('[IconManager] [Mobile+iframe] Drag timeout triggered, stopping drag');
923
+ }
924
+ this.stopDrag();
925
+ }
926
+ };
927
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
928
+ }
929
+ else {
930
+ // 手机模式 + 不在 iframe 上:使用简单的超时逻辑
931
+ this.dragTimeoutId = window.setTimeout(() => {
932
+ if (this.isDragging && this.dragStarted) {
933
+ if (this.debug) {
934
+ console.log('Drag timeout, stopping drag (likely mouse moved over iframe)');
935
+ }
936
+ this.stopDrag();
937
+ }
938
+ }, 50); // 手机模式:使用 50ms 超时
755
939
  }
756
- }, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
940
+ }
757
941
  // 4. 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
758
942
  // 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
759
943
  this.startActiveDetection();
@@ -782,27 +966,72 @@ class IconManager {
782
966
  onDrag(e) {
783
967
  if (!this.iconElement)
784
968
  return;
785
- e.preventDefault();
786
969
  const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
787
970
  const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
971
+ // PC 模式下,如果 SDK iframe 打开,直接 return,禁止拖动
972
+ const isSDKOpen = this.isSDKIframeOpen();
973
+ if (this.debug) {
974
+ console.log('[IconManager] [PC+iframe] Checking SDK iframe status:', {
975
+ isSDKOpen,
976
+ windowWidth: window.innerWidth,
977
+ isPC: window.innerWidth > 768
978
+ });
979
+ }
980
+ if (isSDKOpen) {
981
+ if (this.debug) {
982
+ console.log('[IconManager] [PC+iframe] SDK iframe is open, blocking drag - returning immediately');
983
+ }
984
+ // 直接 return,不更新位置,不调用 stopDrag(让图标保持当前位置)
985
+ return;
986
+ }
987
+ // PC 模式下,如果鼠标在任何 iframe 上(排除 SDK 自己的 iframe),禁止拖动
988
+ const isPC = window.innerWidth > 768;
989
+ if (isPC && this.isMouseOverIframe(clientX, clientY)) {
990
+ if (this.debug) {
991
+ console.log('[IconManager] [PC+iframe] Mouse is over iframe, blocking drag - returning immediately', {
992
+ clientX,
993
+ clientY,
994
+ isPC
995
+ });
996
+ }
997
+ // 直接 return,不更新位置,不调用 stopDrag(让图标保持当前位置)
998
+ return;
999
+ }
1000
+ e.preventDefault();
788
1001
  // 检测鼠标是否在任何 iframe 上(通过坐标判断)
789
- // 如果检测到鼠标在 iframe 区域,立即停止拖动
1002
+ // 注意:在 PC 模式下,当鼠标进入 iframe 后,mousemove 事件会丢失
1003
+ // 但在此之前,我们仍然可以正常更新位置
1004
+ if (this.debug) {
1005
+ console.log('[IconManager] onDrag called', {
1006
+ clientPosition: { x: clientX, y: clientY },
1007
+ isDragging: this.isDragging,
1008
+ dragStarted: this.dragStarted
1009
+ });
1010
+ }
790
1011
  // 重要:需要检测所有 iframe(包括嵌套的),因为任何 iframe 都会导致事件丢失
791
- if (this.isDragging) {
1012
+ // 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
1013
+ // 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
1014
+ if (this.isDragging || this.dragIntent || this.dragStarted) {
792
1015
  const now = Date.now();
793
1016
  // 性能优化:缓存 iframe 位置信息,避免频繁查询 DOM
794
1017
  if (this.cachedIframes.length === 0 ||
795
1018
  now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
796
- // 更新 iframe 缓存
1019
+ // 更新 iframe 缓存(排除 SDK 自己的 iframe)
797
1020
  const allIframes = document.querySelectorAll('iframe');
798
1021
  const previousCount = this.cachedIframes.length;
799
- this.cachedIframes = Array.from(allIframes).map(iframe => ({
1022
+ this.cachedIframes = Array.from(allIframes)
1023
+ .filter(iframe => {
1024
+ // 检查 iframe 是否在 SDK 容器内
1025
+ const container = iframe.closest('.customer-sdk-container');
1026
+ return !container; // 排除 SDK 容器内的 iframe
1027
+ })
1028
+ .map(iframe => ({
800
1029
  element: iframe,
801
1030
  rect: iframe.getBoundingClientRect()
802
1031
  }));
803
1032
  this.lastIframeUpdateTime = now;
804
1033
  if (this.debug && this.cachedIframes.length !== previousCount) {
805
- console.log(`[IconManager] Iframe cache updated - Found ${this.cachedIframes.length} iframe(s)`, {
1034
+ console.log(`[IconManager] Iframe cache updated - Found ${this.cachedIframes.length} iframe(s) (excluding SDK iframes)`, {
806
1035
  iframes: this.cachedIframes.map(({ rect }) => ({
807
1036
  left: rect.left,
808
1037
  top: rect.top,
@@ -818,40 +1047,97 @@ class IconManager {
818
1047
  // 更新最后记录的鼠标位置和时间戳(用于主动检测)
819
1048
  const now = Date.now();
820
1049
  const timeSinceLastUpdate = now - this.lastMousePosition.timestamp;
1050
+ const lastX = this.lastMousePosition.x;
1051
+ const lastY = this.lastMousePosition.y;
1052
+ const lastTimestamp = this.lastMousePosition.timestamp;
821
1053
  this.lastMousePosition = {
822
1054
  x: clientX,
823
1055
  y: clientY,
824
1056
  timestamp: now
825
1057
  };
1058
+ // 计算鼠标移动速度(用于在 iframe 上时预测位置)
1059
+ if (lastTimestamp > 0 && timeSinceLastUpdate > 0 && timeSinceLastUpdate < 100) {
1060
+ // 只在时间间隔合理时计算速度(避免时间间隔过大导致速度不准确)
1061
+ const dt = timeSinceLastUpdate / 1000; // 转换为秒
1062
+ this.mouseVelocity.vx = (clientX - lastX) / dt;
1063
+ this.mouseVelocity.vy = (clientY - lastY) / dt;
1064
+ // 限制速度范围,避免异常值
1065
+ this.mouseVelocity.vx = Math.max(-2e3, Math.min(2000, this.mouseVelocity.vx));
1066
+ this.mouseVelocity.vy = Math.max(-2e3, Math.min(2000, this.mouseVelocity.vy));
1067
+ }
826
1068
  // 如果距离上次更新超过 50ms,记录警告(可能事件丢失)
827
1069
  if (this.debug && timeSinceLastUpdate > 50 && this.lastMousePosition.timestamp > 0) {
828
1070
  console.warn(`[IconManager] Long gap between mousemove events: ${timeSinceLastUpdate}ms`, {
829
- lastPosition: { x: this.lastMousePosition.x, y: this.lastMousePosition.y },
830
- currentPosition: { x: clientX, y: clientY }
1071
+ lastPosition: { x: lastX, y: lastY },
1072
+ currentPosition: { x: clientX, y: clientY },
1073
+ velocity: { vx: this.mouseVelocity.vx, vy: this.mouseVelocity.vy }
831
1074
  });
832
1075
  }
833
1076
  // 重置超时定时器(每次移动都重置,确保只有真正停止移动时才触发超时)
834
- // 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
835
1077
  if (this.dragTimeoutId !== null) {
836
1078
  window.clearTimeout(this.dragTimeoutId);
837
- this.dragTimeoutId = window.setTimeout(() => {
838
- if (this.isDragging) {
839
- if (this.debug) {
840
- const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
841
- console.warn('[IconManager] Drag timeout triggered, stopping drag (likely mouse moved over iframe)', {
842
- timeSinceLastMove,
843
- lastMousePosition: this.lastMousePosition,
844
- iframeCount: this.cachedIframes.length
845
- });
846
- }
847
- this.stopDrag();
1079
+ // 区分 PC 模式和手机模式,以及是否在 iframe 上
1080
+ const isPC = window.innerWidth > 768;
1081
+ // 检查鼠标是否在 iframe 上(简单判断)
1082
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
1083
+ if (isPC) ;
1084
+ else {
1085
+ // ========== 手机模式 ==========
1086
+ if (isOverIframe) {
1087
+ // 手机模式 + 在 iframe 上:特殊处理逻辑
1088
+ // 在 iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
1089
+ const checkDragTimeoutForMobileIframe = () => {
1090
+ const lastPos = this.lastMousePosition;
1091
+ const stillOverIframe = lastPos && this.isMouseOverIframe(lastPos.x, lastPos.y);
1092
+ // 如果还在 iframe 上且还有拖动意图,继续等待 mouseup
1093
+ if (stillOverIframe && (this.dragIntent || this.dragStarted || this.isDragging)) {
1094
+ if (this.debug) {
1095
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
1096
+ console.log('[IconManager] [Mobile+iframe] Drag timeout but mouse is over iframe, keeping drag active (waiting for mouseup)', {
1097
+ timeSinceLastMove,
1098
+ lastMousePosition: this.lastMousePosition
1099
+ });
1100
+ }
1101
+ // 手机模式 iframe 上:使用 3 秒超时(比 PC 模式稍短),等待 mouseup
1102
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
1103
+ return;
1104
+ }
1105
+ // 不在 iframe 上了,或者拖动意图已消失,停止拖动
1106
+ if (this.isDragging || this.dragStarted) {
1107
+ if (this.debug) {
1108
+ console.warn('[IconManager] [Mobile+iframe] Drag timeout triggered, stopping drag');
1109
+ }
1110
+ this.stopDrag();
1111
+ }
1112
+ };
1113
+ // 手机模式 iframe 上:使用 3 秒超时
1114
+ this.dragTimeoutId = window.setTimeout(checkDragTimeoutForMobileIframe, 3000);
848
1115
  }
849
- }, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
1116
+ else {
1117
+ // 手机模式 + 不在 iframe 上:正常逻辑
1118
+ // 手机模式不在 iframe 上时,使用 200ms 超时
1119
+ this.dragTimeoutId = window.setTimeout(() => {
1120
+ if (this.isDragging) {
1121
+ if (this.debug) {
1122
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
1123
+ console.warn('[IconManager] [Mobile] Drag timeout triggered, stopping drag', {
1124
+ timeSinceLastMove,
1125
+ lastMousePosition: this.lastMousePosition,
1126
+ iframeCount: this.cachedIframes.length
1127
+ });
1128
+ }
1129
+ this.stopDrag();
1130
+ }
1131
+ }, 200);
1132
+ }
1133
+ }
850
1134
  }
851
1135
  // 检查是否有足够的移动距离
852
1136
  const deltaX = Math.abs(clientX - this.lastTouchPosition.x);
853
1137
  const deltaY = Math.abs(clientY - this.lastTouchPosition.y);
854
1138
  const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1139
+ // 只有在移动距离超过阈值时,才认为是拖动
1140
+ // 这样可以避免轻微的鼠标抖动被误判为拖动
855
1141
  if (totalMovement > this.clickThreshold) {
856
1142
  this.hasMoved = true;
857
1143
  if (!this.dragStarted) {
@@ -895,7 +1181,9 @@ class IconManager {
895
1181
  }
896
1182
  }
897
1183
  }
898
- if (!this.isDragging) {
1184
+ // 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
1185
+ // 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
1186
+ if (!this.isDragging && !this.dragIntent && !this.dragStarted) {
899
1187
  return;
900
1188
  }
901
1189
  try {
@@ -920,40 +1208,184 @@ class IconManager {
920
1208
  const containerRect = this.cachedContainerRect;
921
1209
  const iconWidth = this.cachedIconSize.width;
922
1210
  const iconHeight = this.cachedIconSize.height;
923
- // 计算新位置
1211
+ // 获取容器的实际宽度(考虑 max-width 限制)
1212
+ // 使用 offsetWidth 或 clientWidth,它们会考虑 max-width 限制
1213
+ const computedStyle = window.getComputedStyle(container);
1214
+ const maxWidth = computedStyle.maxWidth;
1215
+ const actualWidth = maxWidth && maxWidth !== 'none'
1216
+ ? Math.min(containerRect.width, parseFloat(maxWidth) || containerRect.width)
1217
+ : containerRect.width;
1218
+ const actualHeight = containerRect.height;
1219
+ // 确保 dragOffset 已经正确设置
1220
+ // 如果 dragOffset 还没有设置(在 startDrag 中设置,但可能在某些情况下未设置),在这里计算
1221
+ // 重要:只有在 dragOffset 真的为 0 且还没有开始拖动时才计算,避免在拖动过程中被重置
1222
+ if ((this.dragOffset.x === 0 && this.dragOffset.y === 0) && (this.dragIntent || this.dragStarted) && this.iconElement && !this.dragStarted) {
1223
+ // 只有在 dragIntent 为 true 但 dragStarted 为 false 时才计算 dragOffset
1224
+ // 如果 dragStarted 已经为 true,说明 dragOffset 应该已经在 startDrag 中设置了,不应该重新计算
1225
+ const currentRect = this.iconElement.getBoundingClientRect();
1226
+ this.dragOffset.x = clientX - currentRect.left;
1227
+ this.dragOffset.y = clientY - currentRect.top;
1228
+ if (this.debug) {
1229
+ console.log('[IconManager] Calculated dragOffset in onDrag (fallback)', {
1230
+ dragOffset: this.dragOffset,
1231
+ clientPosition: { x: clientX, y: clientY },
1232
+ iconRect: currentRect,
1233
+ dragStarted: this.dragStarted,
1234
+ dragIntent: this.dragIntent
1235
+ });
1236
+ }
1237
+ }
1238
+ // 计算新位置(相对于容器的实际内容区域)
924
1239
  let newX = clientX - this.dragOffset.x - containerRect.left;
925
1240
  let newY = clientY - this.dragOffset.y - containerRect.top;
926
1241
  // 允许拖动到容器外(允许负值),但限制在合理范围内(避免拖得太远)
927
1242
  // 允许拖动到容器外,最多隐藏到只剩一小部分可见
1243
+ // 使用实际宽度(考虑 max-width 限制)而不是容器宽度
928
1244
  const minX = -iconWidth + 5; // 允许向左拖动,最多隐藏到只剩 5px
929
1245
  const maxX = container === document.body
930
1246
  ? window.innerWidth - 5 // 允许向右拖动,最多隐藏到只剩 5px
931
- : containerRect.width + iconWidth - 5; // 允许超出容器,最多隐藏到只剩 5px
1247
+ : actualWidth + iconWidth - 5; // 使用实际宽度(考虑 max-width),允许超出容器,最多隐藏到只剩 5px
932
1248
  const minY = -iconHeight + 5; // 允许向上拖动,最多隐藏到只剩 5px
933
1249
  const maxY = container === document.body
934
1250
  ? window.innerHeight - 5 // 允许向下拖动,最多隐藏到只剩 5px
935
- : containerRect.height + iconHeight - 5; // 允许超出容器,最多隐藏到只剩 5px
1251
+ : actualHeight + iconHeight - 5; // 允许超出容器,最多隐藏到只剩 5px
936
1252
  newX = Math.max(minX, Math.min(newX, maxX));
937
1253
  newY = Math.max(minY, Math.min(newY, maxY));
938
- // 性能优化:使用 requestAnimationFrame 节流位置更新
939
- this.pendingPosition.x = newX;
940
- this.pendingPosition.y = newY;
941
- this.pendingPosition.needsUpdate = true;
942
- if (this.rafId === null) {
943
- this.rafId = requestAnimationFrame(() => {
944
- this.rafId = null;
945
- if (this.pendingPosition.needsUpdate && this.iconElement && this.isDragging) {
946
- this.iconElement.style.left = `${this.pendingPosition.x}px`;
947
- this.iconElement.style.top = `${this.pendingPosition.y}px`;
1254
+ // 区分 PC 模式和手机模式,以及是否在 iframe 上
1255
+ const isPC = window.innerWidth > 768;
1256
+ // 检查鼠标是否在 iframe 上(简单判断)
1257
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
1258
+ // 简化逻辑:直接更新位置,不管是否在 iframe
1259
+ if (this.iconElement) {
1260
+ // 重要:保护 dragOffset,确保它在拖动过程中不会被重新计算
1261
+ // 逻辑顺序:
1262
+ // 1. 如果 dragOffset 还没有设置(为 0),且 dragIntent 为 true,先计算 dragOffset
1263
+ // 2. 然后在 iframe 上时,如果 dragIntent 为 true 但 dragStarted 为 false,提前设置 dragStarted
1264
+ // 3. 这样确保 dragOffset 在 dragStarted 设置之前就已经计算好了
1265
+ // 步骤 1:计算 dragOffset(只有在 dragOffset 为 0 且 dragIntent 为 true 时才计算)
1266
+ if (this.dragIntent && (this.dragOffset.x === 0 && this.dragOffset.y === 0)) {
1267
+ const currentRect = this.iconElement.getBoundingClientRect();
1268
+ const oldDragOffset = { x: this.dragOffset.x, y: this.dragOffset.y };
1269
+ this.dragOffset.x = clientX - currentRect.left;
1270
+ this.dragOffset.y = clientY - currentRect.top;
1271
+ if (this.debug && isPC) {
1272
+ console.log('[IconManager] [PC+iframe] Calculated dragOffset', {
1273
+ oldDragOffset,
1274
+ newDragOffset: { x: this.dragOffset.x, y: this.dragOffset.y },
1275
+ clientPosition: { x: clientX, y: clientY },
1276
+ iconRect: currentRect,
1277
+ isOverIframe
1278
+ });
1279
+ }
1280
+ }
1281
+ else if (this.debug && isPC && this.dragStarted && (this.dragOffset.x === 0 && this.dragOffset.y === 0)) {
1282
+ // 警告:dragStarted 为 true 但 dragOffset 为 0,这是不应该发生的
1283
+ console.warn('[IconManager] [PC] dragOffset is 0 but dragStarted is true! This should not happen.', {
1284
+ dragStarted: this.dragStarted,
1285
+ dragIntent: this.dragIntent,
1286
+ isDragging: this.isDragging,
1287
+ isOverIframe
1288
+ });
1289
+ }
1290
+ // 步骤 2:在 iframe 上时,如果 dragIntent 为 true 但 dragStarted 为 false,提前设置 dragStarted
1291
+ // 这样可以确保拖动状态正确
1292
+ if (isOverIframe && this.dragIntent && !this.dragStarted) {
1293
+ this.dragStarted = true;
1294
+ this.isDragging = true;
1295
+ this.hasMoved = true;
1296
+ }
1297
+ // 更新位置
1298
+ this.iconElement.style.left = `${newX}px`;
1299
+ this.iconElement.style.top = `${newY}px`;
1300
+ this.iconElement.style.right = 'auto';
1301
+ this.iconElement.style.bottom = 'auto';
1302
+ // 简化日志:只显示 x, y 坐标(PC 模式下总是输出,方便调试)
1303
+ if (this.debug && isPC) {
1304
+ const calculatedX = clientX - this.dragOffset.x - containerRect.left;
1305
+ const calculatedY = clientY - this.dragOffset.y - containerRect.top;
1306
+ console.log('[IconManager] [PC+iframe] Icon position:', {
1307
+ x: newX,
1308
+ y: newY,
1309
+ isOverIframe,
1310
+ clientX,
1311
+ clientY,
1312
+ dragOffset: { x: this.dragOffset.x, y: this.dragOffset.y },
1313
+ containerLeft: containerRect.left,
1314
+ containerTop: containerRect.top,
1315
+ calculatedX,
1316
+ calculatedY,
1317
+ minX,
1318
+ maxX,
1319
+ actualWidth
1320
+ });
1321
+ // 如果计算出的位置和实际位置不一致,输出警告
1322
+ if (Math.abs(calculatedX - newX) > 1 || Math.abs(calculatedY - newY) > 1) {
1323
+ console.warn('[IconManager] [PC+iframe] Position calculation mismatch!', {
1324
+ calculated: { x: calculatedX, y: calculatedY },
1325
+ actual: { x: newX, y: newY },
1326
+ dragOffset: { x: this.dragOffset.x, y: this.dragOffset.y },
1327
+ containerRect: { left: containerRect.left, top: containerRect.top },
1328
+ clientPosition: { x: clientX, y: clientY }
1329
+ });
1330
+ }
1331
+ }
1332
+ }
1333
+ // 超时逻辑:统一处理
1334
+ if (isPC) {
1335
+ // PC 模式:使用简单的超时逻辑
1336
+ if (isOverIframe) {
1337
+ // PC 模式 + 在 iframe 上:使用较长的超时(因为 mousemove 事件会丢失)
1338
+ this.dragTimeoutId = window.setTimeout(() => {
1339
+ if (this.isDragging && this.dragStarted) {
1340
+ this.stopDrag();
1341
+ }
1342
+ }, 5000); // 5 秒超时,等待 mouseup
1343
+ }
1344
+ else {
1345
+ // PC 模式 + 不在 iframe 上:使用短超时
1346
+ this.dragTimeoutId = window.setTimeout(() => {
1347
+ if (this.isDragging && this.dragStarted) {
1348
+ this.stopDrag();
1349
+ }
1350
+ }, 50); // 50ms 超时
1351
+ }
1352
+ }
1353
+ else {
1354
+ // ========== 手机模式 ==========
1355
+ if (isOverIframe) {
1356
+ // 手机模式 + 在 iframe 上:直接更新 DOM
1357
+ if (this.iconElement) {
1358
+ this.iconElement.style.left = `${newX}px`;
1359
+ this.iconElement.style.top = `${newY}px`;
948
1360
  this.iconElement.style.right = 'auto';
949
1361
  this.iconElement.style.bottom = 'auto';
950
- this.pendingPosition.needsUpdate = false;
951
1362
  }
952
- });
1363
+ }
1364
+ else {
1365
+ // 手机模式 + 不在 iframe 上:使用 requestAnimationFrame 节流
1366
+ this.pendingPosition.x = newX;
1367
+ this.pendingPosition.y = newY;
1368
+ this.pendingPosition.needsUpdate = true;
1369
+ if (this.rafId === null) {
1370
+ this.rafId = requestAnimationFrame(() => {
1371
+ this.rafId = null;
1372
+ if (this.pendingPosition.needsUpdate && this.iconElement && this.isDragging) {
1373
+ this.iconElement.style.left = `${this.pendingPosition.x}px`;
1374
+ this.iconElement.style.top = `${this.pendingPosition.y}px`;
1375
+ this.iconElement.style.right = 'auto';
1376
+ this.iconElement.style.bottom = 'auto';
1377
+ this.pendingPosition.needsUpdate = false;
1378
+ }
1379
+ });
1380
+ }
1381
+ }
953
1382
  }
954
- // 更新最后位置
1383
+ // 更新最后位置(同时更新 lastTouchPosition 和 lastMousePosition)
955
1384
  this.lastTouchPosition.x = clientX;
956
1385
  this.lastTouchPosition.y = clientY;
1386
+ this.lastMousePosition.x = clientX;
1387
+ this.lastMousePosition.y = clientY;
1388
+ this.lastMousePosition.timestamp = Date.now();
957
1389
  }
958
1390
  catch (error) {
959
1391
  if (this.debug) {
@@ -961,6 +1393,88 @@ class IconManager {
961
1393
  }
962
1394
  }
963
1395
  }
1396
+ /**
1397
+ * 从鼠标位置更新图标位置(用于主动检测机制)
1398
+ */
1399
+ updateIconPositionFromMouse(clientX, clientY) {
1400
+ // 这个方法只在 iframe 上时使用,因为 mousemove 事件丢失
1401
+ // 检查拖动状态,只有在真正拖动中或拖动意图存在时才更新
1402
+ if (!this.iconElement) {
1403
+ return;
1404
+ }
1405
+ // 只有在拖动中或拖动意图存在时才更新位置
1406
+ // 但需要确保 dragOffset 已经正确设置(在第一次 mousemove 时设置)
1407
+ if (!this.isDragging && !this.dragIntent && !this.dragStarted) {
1408
+ return;
1409
+ }
1410
+ // 如果 dragOffset 还没有设置(还没有收到过 mousemove 事件),需要先计算它
1411
+ // 因为需要 dragOffset 来计算相对位置
1412
+ if (this.dragOffset.x === 0 && this.dragOffset.y === 0) {
1413
+ // 如果已经拖动开始了,从当前图标位置计算 dragOffset
1414
+ if (this.dragStarted && this.iconElement) {
1415
+ const currentRect = this.iconElement.getBoundingClientRect();
1416
+ this.dragOffset.x = clientX - currentRect.left;
1417
+ this.dragOffset.y = clientY - currentRect.top;
1418
+ }
1419
+ else {
1420
+ // 如果还没有拖动开始,不能更新位置
1421
+ return;
1422
+ }
1423
+ }
1424
+ try {
1425
+ const container = this.getTargetElement();
1426
+ if (!container) {
1427
+ return;
1428
+ }
1429
+ // 获取容器信息
1430
+ const containerRect = container.getBoundingClientRect();
1431
+ const iconWidth = this.iconElement.offsetWidth;
1432
+ const iconHeight = this.iconElement.offsetHeight;
1433
+ // 获取容器的实际渲染宽度和高度
1434
+ const actualWidth = container === document.body
1435
+ ? window.innerWidth
1436
+ : container.offsetWidth;
1437
+ const actualHeight = container === document.body
1438
+ ? window.innerHeight
1439
+ : container.offsetHeight;
1440
+ // 计算新位置(相对于容器的实际内容区域)
1441
+ let newX = clientX - this.dragOffset.x - containerRect.left;
1442
+ let newY = clientY - this.dragOffset.y - containerRect.top;
1443
+ // 允许拖动到容器外(允许负值),但限制在合理范围内
1444
+ const minX = -iconWidth + 5;
1445
+ const maxX = container === document.body
1446
+ ? window.innerWidth - 5
1447
+ : actualWidth + iconWidth - 5;
1448
+ const minY = -iconHeight + 5;
1449
+ const maxY = container === document.body
1450
+ ? window.innerHeight - 5
1451
+ : actualHeight + iconHeight - 5;
1452
+ newX = Math.max(minX, Math.min(newX, maxX));
1453
+ newY = Math.max(minY, Math.min(newY, maxY));
1454
+ // 主动检测机制中,直接更新 DOM 确保流畅(不使用节流)
1455
+ // 因为此时 mousemove 事件已经丢失,需要及时更新位置
1456
+ if (this.iconElement) {
1457
+ this.iconElement.style.left = `${newX}px`;
1458
+ this.iconElement.style.top = `${newY}px`;
1459
+ this.iconElement.style.right = 'auto';
1460
+ this.iconElement.style.bottom = 'auto';
1461
+ if (this.debug) {
1462
+ const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
1463
+ console.log('[IconManager] [PC+iframe] Updated icon position in startActiveDetection', {
1464
+ mousePosition: { x: clientX, y: clientY },
1465
+ iconPosition: { x: newX, y: newY },
1466
+ timeSinceLastMove,
1467
+ lastMousePosition: this.lastMousePosition
1468
+ });
1469
+ }
1470
+ }
1471
+ }
1472
+ catch (error) {
1473
+ if (this.debug) {
1474
+ console.error('[IconManager] Error in updateIconPositionFromMouse:', error);
1475
+ }
1476
+ }
1477
+ }
964
1478
  /**
965
1479
  * 停止拖动
966
1480
  */
@@ -980,6 +1494,36 @@ class IconManager {
980
1494
  return;
981
1495
  }
982
1496
  this.isStoppingDrag = true;
1497
+ // 在 iframe 上时,如果 mouseup 事件存在,从事件中获取鼠标位置并更新图标位置
1498
+ // 这是解决 iframe 上拖动时图标不跟随鼠标的关键
1499
+ if (_e && this.dragStarted && this.iconElement) {
1500
+ const clientX = 'clientX' in _e ? _e.clientX : _e.touches?.[0]?.clientX ?? _e.changedTouches?.[0]?.clientX ?? 0;
1501
+ const clientY = 'clientY' in _e ? _e.clientY : _e.touches?.[0]?.clientY ?? _e.changedTouches?.[0]?.clientY ?? 0;
1502
+ if (clientX > 0 && clientY > 0) {
1503
+ // 检查鼠标是否在 iframe 上
1504
+ const isOverIframe = this.isMouseOverIframe(clientX, clientY);
1505
+ if (isOverIframe) {
1506
+ // 在 iframe 上时,从 mouseup 事件中获取鼠标位置并更新图标位置
1507
+ // 更新 lastMousePosition,这样后续的逻辑可以使用最新的位置
1508
+ this.lastMousePosition = {
1509
+ x: clientX,
1510
+ y: clientY,
1511
+ timestamp: Date.now()
1512
+ };
1513
+ // 更新图标位置
1514
+ this.updateIconPositionFromMouse(clientX, clientY);
1515
+ if (this.debug) {
1516
+ console.log('[IconManager] [PC+iframe] Updated icon position from mouseup event', {
1517
+ mousePosition: { x: clientX, y: clientY },
1518
+ iconPosition: this.iconElement ? {
1519
+ left: this.iconElement.style.left,
1520
+ top: this.iconElement.style.top
1521
+ } : null
1522
+ });
1523
+ }
1524
+ }
1525
+ }
1526
+ }
983
1527
  const stopReason = this.isDragging ? 'drag_stopped' : 'not_dragging';
984
1528
  const finalPosition = this.iconElement ? {
985
1529
  left: this.iconElement.style.left,
@@ -995,6 +1539,7 @@ class IconManager {
995
1539
  this.hasMoved = false;
996
1540
  this.isDragging = false;
997
1541
  this.dragStarted = false;
1542
+ this.dragIntent = false;
998
1543
  this.isStoppingDrag = false;
999
1544
  return;
1000
1545
  }
@@ -1004,8 +1549,18 @@ class IconManager {
1004
1549
  // 拖动结束时,恢复 pointer-events: none 让事件穿透到 iframe
1005
1550
  this.iconElement.style.pointerEvents = 'none';
1006
1551
  // 检查是否是点击
1552
+ // 点击的条件:
1553
+ // 1. 没有移动过(hasMoved = false)
1554
+ // 2. 持续时间小于 1000ms
1555
+ // 3. 没有真正开始拖动(dragStarted = false)
1556
+ // 4. 没有拖动意图(dragIntent = false)或者拖动意图存在但持续时间很短(可能是误触)
1557
+ // 5. 没有真正拖动过(isDragging = false)
1007
1558
  const touchDuration = Date.now() - this.touchStartTime;
1008
- const isValidClick = !this.hasMoved && touchDuration < 1000 && !this.dragStarted;
1559
+ const isValidClick = !this.hasMoved &&
1560
+ touchDuration < 1000 &&
1561
+ !this.dragStarted &&
1562
+ !this.isDragging &&
1563
+ (!this.dragIntent || touchDuration < 200); // 如果 dragIntent 存在,但持续时间很短(< 200ms),可能是误触,也认为是点击
1009
1564
  if (this.debug) {
1010
1565
  console.log('[IconManager] Drag end', {
1011
1566
  stopReason,
@@ -1058,8 +1613,10 @@ class IconManager {
1058
1613
  this.hasMoved = false;
1059
1614
  this.isDragging = false;
1060
1615
  this.dragStarted = false;
1616
+ this.dragIntent = false; // 重置拖动意图
1061
1617
  this.isStoppingDrag = false; // 重置停止标志
1062
1618
  this.stoppedByIframe = false; // 重置 iframe 停止标志
1619
+ this.hasLoggedIframeWarning = false; // 重置 iframe 警告标志
1063
1620
  // 如果拖动后没有吸附到侧边,启动自动吸附定时器
1064
1621
  // 但如果是因为 iframe 而停止拖动,不启动自动吸附
1065
1622
  if (!wasStoppedByIframe && !this.isAttachedToSide && this.sideAttach && this.autoAttachDelay > 0) {
@@ -1091,50 +1648,88 @@ class IconManager {
1091
1648
  console.log('[IconManager] Starting active detection mechanism');
1092
1649
  }
1093
1650
  const checkMousePosition = () => {
1094
- // 检查是否还在拖动中,如果不在拖动中或正在停止,则停止检测
1095
- if (!this.isDragging || this.isStoppingDrag) {
1651
+ // 检查是否还在拖动中
1652
+ // 注意:即使 isDragging 暂时为 false(因为 mousemove 事件丢失),
1653
+ // 只要 dragIntent 或 dragStarted 为 true,说明用户还在拖动,应该继续检测
1654
+ if (this.isStoppingDrag || (!this.isDragging && !this.dragStarted && !this.dragIntent)) {
1096
1655
  if (this.debug) {
1097
- console.log('[IconManager] Active detection stopped (drag ended or stopping)');
1656
+ console.log('[IconManager] Active detection stopped (drag ended or stopping)', {
1657
+ isDragging: this.isDragging,
1658
+ dragStarted: this.dragStarted,
1659
+ dragIntent: this.dragIntent,
1660
+ isStoppingDrag: this.isStoppingDrag
1661
+ });
1098
1662
  }
1099
1663
  this.activeDetectionRafId = null;
1664
+ if (this.activeDetectionTimeoutId !== null) {
1665
+ window.clearTimeout(this.activeDetectionTimeoutId);
1666
+ this.activeDetectionTimeoutId = null;
1667
+ }
1100
1668
  return;
1101
1669
  }
1102
- // 检查是否长时间没有收到 mousemove 事件(超过 50ms)
1670
+ // 检查是否长时间没有收到 mousemove 事件(超过 100ms 才检测,避免频繁检测)
1103
1671
  const now = Date.now();
1104
1672
  const timeSinceLastMove = now - this.lastMousePosition.timestamp;
1105
- // 如果超过 50ms 没有收到事件,进行检测
1106
- if (timeSinceLastMove > 50) {
1107
- // 长时间没有收到事件,可能鼠标已经进入 iframe
1673
+ // 如果超过 100ms 没有收到事件,可能鼠标已经进入 iframe
1674
+ // iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
1675
+ // 所以只需要保持拖动状态,等待 mouseup 事件,不需要每帧都更新位置
1676
+ if (timeSinceLastMove > 100) {
1108
1677
  // 使用最后记录的鼠标位置进行检测
1109
1678
  const clientX = this.lastMousePosition.x;
1110
1679
  const clientY = this.lastMousePosition.y;
1111
- if (this.debug && timeSinceLastMove > 100) {
1112
- // 只在超过 100ms 时记录警告,避免日志过多
1113
- console.warn(`[IconManager] Active detection: No mousemove event for ${timeSinceLastMove}ms`, {
1114
- lastMousePosition: { x: clientX, y: clientY },
1115
- timestamp: this.lastMousePosition.timestamp,
1116
- isDragging: this.isDragging,
1117
- dragStarted: this.dragStarted
1118
- });
1119
- }
1120
- // 更新 iframe 缓存(更频繁,每帧更新)
1680
+ // 更新 iframe 缓存(不需要每帧更新,只在需要时更新)
1121
1681
  if (this.cachedIframes.length === 0 ||
1122
1682
  now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
1123
1683
  const allIframes = document.querySelectorAll('iframe');
1124
- this.cachedIframes = Array.from(allIframes).map(iframe => ({
1684
+ // 排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe)
1685
+ this.cachedIframes = Array.from(allIframes)
1686
+ .filter(iframe => {
1687
+ // 检查 iframe 是否在 SDK 容器内
1688
+ const container = iframe.closest('.customer-sdk-container');
1689
+ return !container; // 排除 SDK 容器内的 iframe
1690
+ })
1691
+ .map(iframe => ({
1125
1692
  element: iframe,
1126
1693
  rect: iframe.getBoundingClientRect()
1127
1694
  }));
1128
1695
  this.lastIframeUpdateTime = now;
1129
- if (this.debug) {
1130
- console.log(`[IconManager] Active detection: Updated iframe cache (${this.cachedIframes.length} iframes)`);
1696
+ }
1697
+ // 如果鼠标在 iframe 上,需要保持拖动状态
1698
+ // 注意:在 PC 模式下,当鼠标进入 iframe 后,mousemove 事件会完全丢失
1699
+ // 这是浏览器的安全限制,无法获取 iframe 内的实时鼠标位置
1700
+ // 因此,我们无法让图标实时跟随鼠标移动
1701
+ // 解决方案:保持图标在最后已知位置(进入 iframe 前的位置),等待 mouseup 事件更新最终位置
1702
+ if (this.isMouseOverIframe(clientX, clientY)) {
1703
+ // 在 iframe 上时,保持拖动状态,防止被误判为停止
1704
+ // 注意:只有在真正拖动后(dragStarted = true)才保持拖动状态
1705
+ if (!this.isDragging && this.dragStarted) {
1706
+ this.isDragging = true;
1707
+ if (this.debug) {
1708
+ console.log('[IconManager] [PC+iframe] Active detection: Mouse over iframe, keeping drag state active (waiting for mouseup)');
1709
+ }
1710
+ }
1711
+ // 在 PC 模式下,当鼠标进入 iframe 后,我们无法获取实时鼠标位置
1712
+ // 因此,不进行位置更新,保持图标在最后已知位置
1713
+ // 最终位置会在 mouseup 事件中更新
1714
+ if (this.debug && timeSinceLastMove > 500 && !this.hasLoggedIframeWarning) {
1715
+ console.warn('[IconManager] [PC+iframe] Mouse entered iframe - mousemove events lost. Icon position frozen at last known position. Will update on mouseup.', {
1716
+ timeSinceLastMove,
1717
+ lastMousePosition: { x: this.lastMousePosition.x, y: this.lastMousePosition.y },
1718
+ note: 'This is a browser security limitation. Icon will update to final position when mouse is released (mouseup event).'
1719
+ });
1720
+ this.hasLoggedIframeWarning = true;
1131
1721
  }
1132
1722
  }
1133
- // 移除 iframe 检测停止拖动的逻辑,让拖动可以在 iframe 上顺畅进行
1134
- // 不再因为检测到 iframe 而停止拖动,保持拖动的流畅性
1135
1723
  }
1136
1724
  // 继续检测
1137
- this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
1725
+ // 如果在 iframe 上,使用更频繁的检测(每 16ms,约 60fps)以确保位置更新及时
1726
+ // 如果不在 iframe 上,使用较低的频率(每 100ms)以节省性能
1727
+ const isOverIframe = timeSinceLastMove > 100 && this.isMouseOverIframe(this.lastMousePosition.x, this.lastMousePosition.y);
1728
+ const detectionInterval = isOverIframe ? 16 : 100; // iframe 上时更频繁检测
1729
+ this.activeDetectionTimeoutId = window.setTimeout(() => {
1730
+ this.activeDetectionTimeoutId = null;
1731
+ requestAnimationFrame(checkMousePosition);
1732
+ }, detectionInterval);
1138
1733
  };
1139
1734
  this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
1140
1735
  }
@@ -1150,6 +1745,40 @@ class IconManager {
1150
1745
  this.activeDetectionRafId = null;
1151
1746
  }
1152
1747
  }
1748
+ /**
1749
+ * 检查鼠标是否在 iframe 上
1750
+ * 注意:排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe),因为 SDK 的 iframe 不应该影响拖动
1751
+ */
1752
+ isMouseOverIframe(x, y) {
1753
+ // 更新 iframe 缓存(如果需要)
1754
+ const now = Date.now();
1755
+ if (this.cachedIframes.length === 0 ||
1756
+ now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
1757
+ const allIframes = document.querySelectorAll('iframe');
1758
+ // 排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe)
1759
+ this.cachedIframes = Array.from(allIframes)
1760
+ .filter(iframe => {
1761
+ // 检查 iframe 是否在 SDK 容器内
1762
+ const container = iframe.closest('.customer-sdk-container');
1763
+ return !container; // 排除 SDK 容器内的 iframe
1764
+ })
1765
+ .map(iframe => ({
1766
+ element: iframe,
1767
+ rect: iframe.getBoundingClientRect()
1768
+ }));
1769
+ this.lastIframeUpdateTime = now;
1770
+ }
1771
+ // 检查鼠标是否在任何 iframe 上
1772
+ for (const { rect } of this.cachedIframes) {
1773
+ if (x >= rect.left &&
1774
+ x <= rect.right &&
1775
+ y >= rect.top &&
1776
+ y <= rect.bottom) {
1777
+ return true;
1778
+ }
1779
+ }
1780
+ return false;
1781
+ }
1153
1782
  /**
1154
1783
  * 清理拖动事件
1155
1784
  */
@@ -1287,6 +1916,12 @@ class IconManager {
1287
1916
  const containerRect = container.getBoundingClientRect();
1288
1917
  const iconWidth = this.iconElement.offsetWidth || 30;
1289
1918
  const iconHeight = this.iconElement.offsetHeight || 30;
1919
+ // 获取容器的实际宽度(考虑 max-width 限制)
1920
+ const containerComputedStyle = window.getComputedStyle(container);
1921
+ const maxWidth = containerComputedStyle.maxWidth;
1922
+ const actualWidth = maxWidth && maxWidth !== 'none'
1923
+ ? Math.min(containerRect.width, parseFloat(maxWidth) || containerRect.width)
1924
+ : containerRect.width;
1290
1925
  // 获取当前图标位置
1291
1926
  const computedStyle = window.getComputedStyle(this.iconElement);
1292
1927
  const currentLeft = parseFloat(computedStyle.left) || 0;
@@ -1295,7 +1930,7 @@ class IconManager {
1295
1930
  let newY = currentTop;
1296
1931
  // X 轴磁性吸附
1297
1932
  if (this.magneticDirection === 'x' || this.magneticDirection === 'both') {
1298
- const containerWidth = containerRect.width;
1933
+ const containerWidth = actualWidth; // 使用实际宽度(考虑 max-width
1299
1934
  const centerX = containerWidth / 2;
1300
1935
  if (currentLeft < centerX) {
1301
1936
  // 吸附到左边
@@ -1690,6 +2325,8 @@ class IframeManager {
1690
2325
  // 创建包装容器(包含iframe和关闭按钮)
1691
2326
  this.containerElement = document.createElement('div');
1692
2327
  this.containerElement.className = 'customer-sdk-container';
2328
+ // 确保容器不可拖动
2329
+ this.containerElement.draggable = false;
1693
2330
  // 创建iframe元素
1694
2331
  this.iframeElement = document.createElement('iframe');
1695
2332
  this.iframeElement.className = 'customer-sdk-iframe';
@@ -1762,24 +2399,29 @@ class IframeManager {
1762
2399
  opacity: '0',
1763
2400
  display: 'none'
1764
2401
  } : (isPC ? {
1765
- // PC模式:没有 target,使用配置的宽度,高度100%
2402
+ // PC模式:没有 target,固定在右下角(不可拖动)
1766
2403
  width: `${this.config.width || 450}px`,
1767
- height: '100%',
2404
+ height: `${this.config.height || 600}px`,
1768
2405
  maxWidth: '90vw',
1769
- maxHeight: '100%',
2406
+ maxHeight: '90vh',
1770
2407
  backgroundColor: '#ffffff',
1771
- borderRadius: '0',
1772
- boxShadow: 'none',
2408
+ borderRadius: '8px',
2409
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.25)',
1773
2410
  border: 'none',
1774
2411
  position: 'fixed',
1775
2412
  zIndex: '999999',
1776
- // PC模式:水平居中,垂直占满
1777
- top: '0',
1778
- left: '50%',
1779
- bottom: '0',
1780
- right: 'auto',
1781
- transform: 'translateX(-50%)',
2413
+ // PC模式:固定在右下角
2414
+ top: 'auto',
2415
+ left: 'auto',
2416
+ bottom: '20px',
2417
+ right: '20px',
2418
+ transform: 'none',
1782
2419
  overflow: 'hidden',
2420
+ // 防止拖动和选择
2421
+ userSelect: 'none',
2422
+ WebkitUserSelect: 'none',
2423
+ MozUserSelect: 'none',
2424
+ msUserSelect: 'none',
1783
2425
  // 初始隐藏的关键样式
1784
2426
  visibility: 'hidden',
1785
2427
  opacity: '0',
@@ -1819,6 +2461,28 @@ class IframeManager {
1819
2461
  Object.assign(this.iframeElement.style, iframeStyles);
1820
2462
  // 将iframe放入容器
1821
2463
  this.containerElement.appendChild(this.iframeElement);
2464
+ // PC模式下:禁止拖动容器
2465
+ if (isPC && !useTargetWidth) {
2466
+ // 阻止拖动事件
2467
+ this.containerElement.addEventListener('dragstart', (e) => {
2468
+ e.preventDefault();
2469
+ e.stopPropagation();
2470
+ return false;
2471
+ }, false);
2472
+ // 阻止鼠标按下事件(防止可能的拖动行为)
2473
+ this.containerElement.addEventListener('mousedown', (e) => {
2474
+ // 只阻止在容器边缘的拖动,允许点击 iframe 内容
2475
+ const rect = this.containerElement.getBoundingClientRect();
2476
+ const isOnEdge = (e.clientX < rect.left + 10 ||
2477
+ e.clientX > rect.right - 10 ||
2478
+ e.clientY < rect.top + 10 ||
2479
+ e.clientY > rect.bottom - 10);
2480
+ if (isOnEdge) {
2481
+ e.preventDefault();
2482
+ e.stopPropagation();
2483
+ }
2484
+ }, false);
2485
+ }
1822
2486
  // 添加iframe加载事件监听(移动端样式优化)
1823
2487
  this.iframeElement.addEventListener('load', () => {
1824
2488
  // 移动端注入自定义样式
@@ -2063,32 +2727,42 @@ class IframeManager {
2063
2727
  break;
2064
2728
  case 'DCR_DEPOSIT':
2065
2729
  // 存款跳转消息 - 广播给调用 SDK 的 Vue 页面
2066
- // payload.path 是动态的,例如: "?depositType=0&walletId=69382"
2730
+ // 支持两种存款类型:
2731
+ // 1. 钱包直充:{ type: 'DCR_DEPOSIT', payload: { depositType: 0, walletId: 69382 } }
2732
+ // 2. 线下充值:{ type: 'DCR_DEPOSIT', payload: { depositType: 2, offlineTypeId: 2001, accountId: 35213 } }
2067
2733
  if (this.debug) {
2068
- console.log('Received DCR_DEPOSIT message, broadcasting to Vue page:', data);
2734
+ const depositType = data.payload?.depositType;
2735
+ const depositTypeText = depositType === 0 ? '钱包直充' : depositType === 2 ? '线下充值' : '未知类型';
2736
+ console.log(`[IframeManager] Received DCR_DEPOSIT message (${depositTypeText}):`, {
2737
+ type: data.type,
2738
+ payload: data.payload,
2739
+ depositType: depositTypeText
2740
+ });
2069
2741
  }
2070
2742
  this.broadcastMessageToPage(data);
2071
2743
  if (this.config.onMessage) {
2072
2744
  this.config.onMessage(messageType, data);
2073
2745
  }
2074
2746
  break;
2075
- default:
2076
- // 处理动态消息:gotoActivityDetailById:xxx
2077
- if (typeof messageType === 'string' && messageType.startsWith('gotoActivityDetailById:')) {
2078
- // 活动详情跳转消息 - 广播给调用 SDK 的 Vue 页面
2079
- if (this.debug) {
2080
- console.log('Received gotoActivityDetailById message, broadcasting to Vue page:', messageType);
2081
- }
2082
- this.broadcastMessageToPage(data);
2083
- if (this.config.onMessage) {
2084
- this.config.onMessage(messageType, data);
2085
- }
2747
+ case 'DCR_ACTIVITY':
2748
+ // 跳转到优惠页面 - 广播给调用 SDK 的 Vue 页面
2749
+ // 格式:{ type: 'DCR_ACTIVITY', payload: { activityId: 123 } }
2750
+ if (this.debug) {
2751
+ console.log('[IframeManager] Received DCR_ACTIVITY message:', {
2752
+ type: data.type,
2753
+ payload: data.payload,
2754
+ activityId: data.payload?.activityId
2755
+ });
2086
2756
  }
2087
- else {
2088
- // 其他自定义消息处理
2089
- if (this.debug) {
2090
- console.log('Custom message:', data);
2091
- }
2757
+ this.broadcastMessageToPage(data);
2758
+ if (this.config.onMessage) {
2759
+ this.config.onMessage(messageType, data);
2760
+ }
2761
+ break;
2762
+ default:
2763
+ // 其他自定义消息处理
2764
+ if (this.debug) {
2765
+ console.log('[IframeManager] Custom message:', data);
2092
2766
  }
2093
2767
  break;
2094
2768
  }
@@ -21942,7 +22616,7 @@ class CustomerServiceSDK {
21942
22616
  const initResult = {
21943
22617
  deviceId,
21944
22618
  iframeUrl,
21945
- referrer: document.referrer,
22619
+ referrer: config.referrer || document.referrer || document.location.href,
21946
22620
  agent: config.agent,
21947
22621
  timestamp: Date.now()
21948
22622
  };
@@ -22368,7 +23042,7 @@ class CustomerServiceSDK {
22368
23042
  url.searchParams.set('Authorization', config.token);
22369
23043
  }
22370
23044
  url.searchParams.set('DeviceSign', deviceId);
22371
- url.searchParams.set('Referrer', document.referrer);
23045
+ url.searchParams.set('Referrer', config.referrer || document.referrer);
22372
23046
  return url.toString();
22373
23047
  }
22374
23048
  }