customer-chat-sdk 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/IconManager.d.ts +18 -0
- package/dist/core/IconManager.d.ts.map +1 -1
- package/dist/core/IframeManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +826 -126
- package/dist/customer-sdk.esm.js +826 -126
- package/dist/customer-sdk.min.js +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/customer-sdk.cjs.js
CHANGED
|
@@ -16,7 +16,8 @@ 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 }; // 最后触摸位置
|
|
@@ -38,7 +39,9 @@ class IconManager {
|
|
|
38
39
|
this.pendingPosition = { x: 0, y: 0, needsUpdate: false };
|
|
39
40
|
// 主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置(即使没有 mousemove 事件)
|
|
40
41
|
this.activeDetectionRafId = null;
|
|
42
|
+
this.activeDetectionTimeoutId = null; // setTimeout 的 ID
|
|
41
43
|
this.lastMousePosition = { x: 0, y: 0, timestamp: 0 }; // 最后记录的鼠标位置和时间戳
|
|
44
|
+
this.mouseVelocity = { vx: 0, vy: 0 }; // 鼠标移动速度(用于预测位置)
|
|
42
45
|
// 事件处理器引用(用于清理)
|
|
43
46
|
this.onDragHandler = null;
|
|
44
47
|
this.stopDragHandler = null;
|
|
@@ -50,6 +53,7 @@ class IconManager {
|
|
|
50
53
|
this.mouseLeaveTimeout = null; // 鼠标离开防抖定时器
|
|
51
54
|
this.pointerLeaveTimeout = null; // 指针离开防抖定时器
|
|
52
55
|
this.isStoppingDrag = false; // 防止 stopDrag 重复调用
|
|
56
|
+
this.hasLoggedIframeWarning = false; // 防止 iframe 警告日志重复输出
|
|
53
57
|
// 侧边吸附相关状态
|
|
54
58
|
this.sideAttach = true; // 是否启用侧边吸附(默认 true)
|
|
55
59
|
this.sideHideRatio = 0.5; // 侧边吸附时的隐藏比例(默认 0.5,显示一半)
|
|
@@ -208,6 +212,15 @@ class IconManager {
|
|
|
208
212
|
this.iconElement.appendChild(imgContainer);
|
|
209
213
|
// 添加到目标元素(如果 target 是字符串,需要重新查找,因为可能在初始化时元素还不存在)
|
|
210
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
|
+
}
|
|
211
224
|
// 确保目标容器有 overflow-x: hidden,防止图标超出边界时出现横向滚动条
|
|
212
225
|
// 注意:只在启用侧边吸附时才设置,避免影响其他场景
|
|
213
226
|
if (this.sideAttach && targetElement !== document.body) {
|
|
@@ -240,12 +253,20 @@ class IconManager {
|
|
|
240
253
|
}
|
|
241
254
|
if (targetElement) {
|
|
242
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
|
+
}
|
|
243
264
|
}
|
|
244
265
|
else {
|
|
245
266
|
// 如果目标元素不存在,回退到 document.body
|
|
246
267
|
document.body.appendChild(this.iconElement);
|
|
247
268
|
if (this.debug) {
|
|
248
|
-
console.warn('Target element not found, icon added to document.body');
|
|
269
|
+
console.warn('[IconManager] Target element not found, icon added to document.body');
|
|
249
270
|
}
|
|
250
271
|
}
|
|
251
272
|
// 设置拖动事件
|
|
@@ -560,12 +581,70 @@ class IconManager {
|
|
|
560
581
|
this.iconElement.addEventListener('mousedown', this.startDragHandler);
|
|
561
582
|
this.iconElement.addEventListener('touchstart', this.startDragHandler, { passive: false });
|
|
562
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
|
+
}
|
|
563
621
|
/**
|
|
564
622
|
* 开始拖动
|
|
565
623
|
*/
|
|
566
624
|
startDrag(e) {
|
|
567
625
|
if (!this.iconElement || !this.isClickEnabled)
|
|
568
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
|
+
}
|
|
569
648
|
// 清除自动吸附定时器(用户开始拖动)
|
|
570
649
|
this.clearAutoAttachTimer();
|
|
571
650
|
// 如果正在停止拖动,等待完成后再允许新的拖动
|
|
@@ -586,6 +665,8 @@ class IconManager {
|
|
|
586
665
|
}
|
|
587
666
|
return;
|
|
588
667
|
}
|
|
668
|
+
// 重置 iframe 警告标志
|
|
669
|
+
this.hasLoggedIframeWarning = false;
|
|
589
670
|
// 如果发现残留的资源,先清理它们(防御性编程)
|
|
590
671
|
if (this.dragTimeoutId !== null || this.activeDetectionRafId !== null || this.rafId !== null) {
|
|
591
672
|
if (this.debug) {
|
|
@@ -609,8 +690,6 @@ class IconManager {
|
|
|
609
690
|
// 如果不在,让事件穿透到 iframe,不触发拖动
|
|
610
691
|
if (this.iconElement) {
|
|
611
692
|
const iconRect = this.iconElement.getBoundingClientRect();
|
|
612
|
-
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
613
|
-
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
614
693
|
// 计算触摸点相对于 icon 中心的位置
|
|
615
694
|
const iconCenterX = iconRect.left + iconRect.width / 2;
|
|
616
695
|
const iconCenterY = iconRect.top + iconRect.height / 2;
|
|
@@ -641,10 +720,15 @@ class IconManager {
|
|
|
641
720
|
this.hasMoved = false;
|
|
642
721
|
this.dragStarted = false;
|
|
643
722
|
this.isDragging = false;
|
|
723
|
+
this.dragIntent = true; // 设置拖动意图,表示用户已经开始拖动意图(在 mousedown/touchstart 时)
|
|
644
724
|
// 记录开始时间和位置
|
|
645
725
|
this.touchStartTime = Date.now();
|
|
646
|
-
|
|
647
|
-
|
|
726
|
+
// 初始化 lastMousePosition,避免第一次 onDrag 时 timeSinceLastUpdate 过大
|
|
727
|
+
this.lastMousePosition = {
|
|
728
|
+
x: clientX,
|
|
729
|
+
y: clientY,
|
|
730
|
+
timestamp: Date.now()
|
|
731
|
+
};
|
|
648
732
|
this.lastTouchPosition.x = clientX;
|
|
649
733
|
this.lastTouchPosition.y = clientY;
|
|
650
734
|
try {
|
|
@@ -662,14 +746,21 @@ class IconManager {
|
|
|
662
746
|
// 注意:不在这里转换位置,只在真正开始拖动时才转换(在 onDrag 中)
|
|
663
747
|
// 性能优化:在拖动开始时预加载所有 iframe 位置信息
|
|
664
748
|
// 这样可以避免在拖动过程中频繁查询 DOM
|
|
749
|
+
// 注意:排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe),因为 SDK 的 iframe 不应该影响拖动
|
|
665
750
|
const allIframes = document.querySelectorAll('iframe');
|
|
666
|
-
this.cachedIframes = Array.from(allIframes)
|
|
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 => ({
|
|
667
758
|
element: iframe,
|
|
668
759
|
rect: iframe.getBoundingClientRect()
|
|
669
760
|
}));
|
|
670
761
|
this.lastIframeUpdateTime = Date.now();
|
|
671
762
|
if (this.debug) {
|
|
672
|
-
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)`, {
|
|
673
764
|
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
674
765
|
left: rect.left,
|
|
675
766
|
top: rect.top,
|
|
@@ -691,43 +782,54 @@ class IconManager {
|
|
|
691
782
|
}
|
|
692
783
|
// 添加处理 iframe 上事件丢失的机制
|
|
693
784
|
// 1. 监听 mouseleave 和 pointerleave 事件(鼠标离开窗口时停止拖动)
|
|
694
|
-
//
|
|
785
|
+
// 注意:当鼠标移动到 iframe 上时,这些事件可能会误触发,需要更严格的检测
|
|
695
786
|
this.mouseLeaveHandler = (e) => {
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
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)) {
|
|
699
795
|
// 添加防抖,避免重复触发
|
|
700
796
|
if (this.mouseLeaveTimeout) {
|
|
701
797
|
clearTimeout(this.mouseLeaveTimeout);
|
|
702
798
|
}
|
|
703
799
|
this.mouseLeaveTimeout = window.setTimeout(() => {
|
|
704
|
-
|
|
800
|
+
// 再次确认鼠标真的离开了窗口
|
|
801
|
+
if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
|
|
705
802
|
if (this.debug) {
|
|
706
803
|
console.log('[IconManager] Mouse left window, stopping drag');
|
|
707
804
|
}
|
|
708
805
|
this.stopDrag();
|
|
709
806
|
}
|
|
710
807
|
this.mouseLeaveTimeout = null;
|
|
711
|
-
},
|
|
808
|
+
}, 100); // 增加到 100ms 防抖,避免误判
|
|
712
809
|
}
|
|
713
810
|
};
|
|
714
811
|
this.pointerLeaveHandler = (e) => {
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
e.clientX
|
|
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)) {
|
|
718
819
|
// 添加防抖,避免重复触发
|
|
719
820
|
if (this.pointerLeaveTimeout) {
|
|
720
821
|
clearTimeout(this.pointerLeaveTimeout);
|
|
721
822
|
}
|
|
722
823
|
this.pointerLeaveTimeout = window.setTimeout(() => {
|
|
723
|
-
|
|
824
|
+
// 再次确认指针真的离开了窗口
|
|
825
|
+
if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
|
|
724
826
|
if (this.debug) {
|
|
725
827
|
console.log('[IconManager] Pointer left window, stopping drag');
|
|
726
828
|
}
|
|
727
829
|
this.stopDrag();
|
|
728
830
|
}
|
|
729
831
|
this.pointerLeaveTimeout = null;
|
|
730
|
-
},
|
|
832
|
+
}, 100); // 增加到 100ms 防抖,避免误判
|
|
731
833
|
}
|
|
732
834
|
};
|
|
733
835
|
document.addEventListener('mouseleave', this.mouseLeaveHandler);
|
|
@@ -749,15 +851,97 @@ class IconManager {
|
|
|
749
851
|
};
|
|
750
852
|
window.addEventListener('blur', this.blurHandler);
|
|
751
853
|
// 3. 添加超时机制(如果一段时间没有收到 mousemove 事件,自动停止拖动)
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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);
|
|
759
891
|
}
|
|
760
|
-
|
|
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 超时
|
|
904
|
+
}
|
|
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
|
+
}
|
|
761
945
|
// 4. 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
|
|
762
946
|
// 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
|
|
763
947
|
this.startActiveDetection();
|
|
@@ -786,27 +970,72 @@ class IconManager {
|
|
|
786
970
|
onDrag(e) {
|
|
787
971
|
if (!this.iconElement)
|
|
788
972
|
return;
|
|
789
|
-
e.preventDefault();
|
|
790
973
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
791
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();
|
|
792
1005
|
// 检测鼠标是否在任何 iframe 上(通过坐标判断)
|
|
793
|
-
//
|
|
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
|
+
}
|
|
794
1015
|
// 重要:需要检测所有 iframe(包括嵌套的),因为任何 iframe 都会导致事件丢失
|
|
795
|
-
|
|
1016
|
+
// 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
|
|
1017
|
+
// 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
|
|
1018
|
+
if (this.isDragging || this.dragIntent || this.dragStarted) {
|
|
796
1019
|
const now = Date.now();
|
|
797
1020
|
// 性能优化:缓存 iframe 位置信息,避免频繁查询 DOM
|
|
798
1021
|
if (this.cachedIframes.length === 0 ||
|
|
799
1022
|
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
800
|
-
// 更新 iframe
|
|
1023
|
+
// 更新 iframe 缓存(排除 SDK 自己的 iframe)
|
|
801
1024
|
const allIframes = document.querySelectorAll('iframe');
|
|
802
1025
|
const previousCount = this.cachedIframes.length;
|
|
803
|
-
this.cachedIframes = Array.from(allIframes)
|
|
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 => ({
|
|
804
1033
|
element: iframe,
|
|
805
1034
|
rect: iframe.getBoundingClientRect()
|
|
806
1035
|
}));
|
|
807
1036
|
this.lastIframeUpdateTime = now;
|
|
808
1037
|
if (this.debug && this.cachedIframes.length !== previousCount) {
|
|
809
|
-
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)`, {
|
|
810
1039
|
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
811
1040
|
left: rect.left,
|
|
812
1041
|
top: rect.top,
|
|
@@ -822,40 +1051,97 @@ class IconManager {
|
|
|
822
1051
|
// 更新最后记录的鼠标位置和时间戳(用于主动检测)
|
|
823
1052
|
const now = Date.now();
|
|
824
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;
|
|
825
1057
|
this.lastMousePosition = {
|
|
826
1058
|
x: clientX,
|
|
827
1059
|
y: clientY,
|
|
828
1060
|
timestamp: now
|
|
829
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
|
+
}
|
|
830
1072
|
// 如果距离上次更新超过 50ms,记录警告(可能事件丢失)
|
|
831
1073
|
if (this.debug && timeSinceLastUpdate > 50 && this.lastMousePosition.timestamp > 0) {
|
|
832
1074
|
console.warn(`[IconManager] Long gap between mousemove events: ${timeSinceLastUpdate}ms`, {
|
|
833
|
-
lastPosition: { x:
|
|
834
|
-
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 }
|
|
835
1078
|
});
|
|
836
1079
|
}
|
|
837
1080
|
// 重置超时定时器(每次移动都重置,确保只有真正停止移动时才触发超时)
|
|
838
|
-
// 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
839
1081
|
if (this.dragTimeoutId !== null) {
|
|
840
1082
|
window.clearTimeout(this.dragTimeoutId);
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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);
|
|
852
1119
|
}
|
|
853
|
-
|
|
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
|
+
}
|
|
854
1138
|
}
|
|
855
1139
|
// 检查是否有足够的移动距离
|
|
856
1140
|
const deltaX = Math.abs(clientX - this.lastTouchPosition.x);
|
|
857
1141
|
const deltaY = Math.abs(clientY - this.lastTouchPosition.y);
|
|
858
1142
|
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
1143
|
+
// 只有在移动距离超过阈值时,才认为是拖动
|
|
1144
|
+
// 这样可以避免轻微的鼠标抖动被误判为拖动
|
|
859
1145
|
if (totalMovement > this.clickThreshold) {
|
|
860
1146
|
this.hasMoved = true;
|
|
861
1147
|
if (!this.dragStarted) {
|
|
@@ -899,7 +1185,9 @@ class IconManager {
|
|
|
899
1185
|
}
|
|
900
1186
|
}
|
|
901
1187
|
}
|
|
902
|
-
|
|
1188
|
+
// 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
|
|
1189
|
+
// 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
|
|
1190
|
+
if (!this.isDragging && !this.dragIntent && !this.dragStarted) {
|
|
903
1191
|
return;
|
|
904
1192
|
}
|
|
905
1193
|
try {
|
|
@@ -924,40 +1212,184 @@ class IconManager {
|
|
|
924
1212
|
const containerRect = this.cachedContainerRect;
|
|
925
1213
|
const iconWidth = this.cachedIconSize.width;
|
|
926
1214
|
const iconHeight = this.cachedIconSize.height;
|
|
927
|
-
//
|
|
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
|
+
// 计算新位置(相对于容器的实际内容区域)
|
|
928
1243
|
let newX = clientX - this.dragOffset.x - containerRect.left;
|
|
929
1244
|
let newY = clientY - this.dragOffset.y - containerRect.top;
|
|
930
1245
|
// 允许拖动到容器外(允许负值),但限制在合理范围内(避免拖得太远)
|
|
931
1246
|
// 允许拖动到容器外,最多隐藏到只剩一小部分可见
|
|
1247
|
+
// 使用实际宽度(考虑 max-width 限制)而不是容器宽度
|
|
932
1248
|
const minX = -iconWidth + 5; // 允许向左拖动,最多隐藏到只剩 5px
|
|
933
1249
|
const maxX = container === document.body
|
|
934
1250
|
? window.innerWidth - 5 // 允许向右拖动,最多隐藏到只剩 5px
|
|
935
|
-
:
|
|
1251
|
+
: actualWidth + iconWidth - 5; // 使用实际宽度(考虑 max-width),允许超出容器,最多隐藏到只剩 5px
|
|
936
1252
|
const minY = -iconHeight + 5; // 允许向上拖动,最多隐藏到只剩 5px
|
|
937
1253
|
const maxY = container === document.body
|
|
938
1254
|
? window.innerHeight - 5 // 允许向下拖动,最多隐藏到只剩 5px
|
|
939
|
-
:
|
|
1255
|
+
: actualHeight + iconHeight - 5; // 允许超出容器,最多隐藏到只剩 5px
|
|
940
1256
|
newX = Math.max(minX, Math.min(newX, maxX));
|
|
941
1257
|
newY = Math.max(minY, Math.min(newY, maxY));
|
|
942
|
-
//
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
this.
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
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`;
|
|
952
1364
|
this.iconElement.style.right = 'auto';
|
|
953
1365
|
this.iconElement.style.bottom = 'auto';
|
|
954
|
-
this.pendingPosition.needsUpdate = false;
|
|
955
1366
|
}
|
|
956
|
-
}
|
|
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
|
+
}
|
|
957
1386
|
}
|
|
958
|
-
//
|
|
1387
|
+
// 更新最后位置(同时更新 lastTouchPosition 和 lastMousePosition)
|
|
959
1388
|
this.lastTouchPosition.x = clientX;
|
|
960
1389
|
this.lastTouchPosition.y = clientY;
|
|
1390
|
+
this.lastMousePosition.x = clientX;
|
|
1391
|
+
this.lastMousePosition.y = clientY;
|
|
1392
|
+
this.lastMousePosition.timestamp = Date.now();
|
|
961
1393
|
}
|
|
962
1394
|
catch (error) {
|
|
963
1395
|
if (this.debug) {
|
|
@@ -965,6 +1397,88 @@ class IconManager {
|
|
|
965
1397
|
}
|
|
966
1398
|
}
|
|
967
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
|
+
}
|
|
968
1482
|
/**
|
|
969
1483
|
* 停止拖动
|
|
970
1484
|
*/
|
|
@@ -984,6 +1498,36 @@ class IconManager {
|
|
|
984
1498
|
return;
|
|
985
1499
|
}
|
|
986
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
|
+
}
|
|
987
1531
|
const stopReason = this.isDragging ? 'drag_stopped' : 'not_dragging';
|
|
988
1532
|
const finalPosition = this.iconElement ? {
|
|
989
1533
|
left: this.iconElement.style.left,
|
|
@@ -999,6 +1543,7 @@ class IconManager {
|
|
|
999
1543
|
this.hasMoved = false;
|
|
1000
1544
|
this.isDragging = false;
|
|
1001
1545
|
this.dragStarted = false;
|
|
1546
|
+
this.dragIntent = false;
|
|
1002
1547
|
this.isStoppingDrag = false;
|
|
1003
1548
|
return;
|
|
1004
1549
|
}
|
|
@@ -1008,8 +1553,18 @@ class IconManager {
|
|
|
1008
1553
|
// 拖动结束时,恢复 pointer-events: none 让事件穿透到 iframe
|
|
1009
1554
|
this.iconElement.style.pointerEvents = 'none';
|
|
1010
1555
|
// 检查是否是点击
|
|
1556
|
+
// 点击的条件:
|
|
1557
|
+
// 1. 没有移动过(hasMoved = false)
|
|
1558
|
+
// 2. 持续时间小于 1000ms
|
|
1559
|
+
// 3. 没有真正开始拖动(dragStarted = false)
|
|
1560
|
+
// 4. 没有拖动意图(dragIntent = false)或者拖动意图存在但持续时间很短(可能是误触)
|
|
1561
|
+
// 5. 没有真正拖动过(isDragging = false)
|
|
1011
1562
|
const touchDuration = Date.now() - this.touchStartTime;
|
|
1012
|
-
const isValidClick = !this.hasMoved &&
|
|
1563
|
+
const isValidClick = !this.hasMoved &&
|
|
1564
|
+
touchDuration < 1000 &&
|
|
1565
|
+
!this.dragStarted &&
|
|
1566
|
+
!this.isDragging &&
|
|
1567
|
+
(!this.dragIntent || touchDuration < 200); // 如果 dragIntent 存在,但持续时间很短(< 200ms),可能是误触,也认为是点击
|
|
1013
1568
|
if (this.debug) {
|
|
1014
1569
|
console.log('[IconManager] Drag end', {
|
|
1015
1570
|
stopReason,
|
|
@@ -1062,8 +1617,10 @@ class IconManager {
|
|
|
1062
1617
|
this.hasMoved = false;
|
|
1063
1618
|
this.isDragging = false;
|
|
1064
1619
|
this.dragStarted = false;
|
|
1620
|
+
this.dragIntent = false; // 重置拖动意图
|
|
1065
1621
|
this.isStoppingDrag = false; // 重置停止标志
|
|
1066
1622
|
this.stoppedByIframe = false; // 重置 iframe 停止标志
|
|
1623
|
+
this.hasLoggedIframeWarning = false; // 重置 iframe 警告标志
|
|
1067
1624
|
// 如果拖动后没有吸附到侧边,启动自动吸附定时器
|
|
1068
1625
|
// 但如果是因为 iframe 而停止拖动,不启动自动吸附
|
|
1069
1626
|
if (!wasStoppedByIframe && !this.isAttachedToSide && this.sideAttach && this.autoAttachDelay > 0) {
|
|
@@ -1095,50 +1652,88 @@ class IconManager {
|
|
|
1095
1652
|
console.log('[IconManager] Starting active detection mechanism');
|
|
1096
1653
|
}
|
|
1097
1654
|
const checkMousePosition = () => {
|
|
1098
|
-
//
|
|
1099
|
-
|
|
1655
|
+
// 检查是否还在拖动中
|
|
1656
|
+
// 注意:即使 isDragging 暂时为 false(因为 mousemove 事件丢失),
|
|
1657
|
+
// 只要 dragIntent 或 dragStarted 为 true,说明用户还在拖动,应该继续检测
|
|
1658
|
+
if (this.isStoppingDrag || (!this.isDragging && !this.dragStarted && !this.dragIntent)) {
|
|
1100
1659
|
if (this.debug) {
|
|
1101
|
-
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
|
+
});
|
|
1102
1666
|
}
|
|
1103
1667
|
this.activeDetectionRafId = null;
|
|
1668
|
+
if (this.activeDetectionTimeoutId !== null) {
|
|
1669
|
+
window.clearTimeout(this.activeDetectionTimeoutId);
|
|
1670
|
+
this.activeDetectionTimeoutId = null;
|
|
1671
|
+
}
|
|
1104
1672
|
return;
|
|
1105
1673
|
}
|
|
1106
|
-
// 检查是否长时间没有收到 mousemove 事件(超过
|
|
1674
|
+
// 检查是否长时间没有收到 mousemove 事件(超过 100ms 才检测,避免频繁检测)
|
|
1107
1675
|
const now = Date.now();
|
|
1108
1676
|
const timeSinceLastMove = now - this.lastMousePosition.timestamp;
|
|
1109
|
-
// 如果超过
|
|
1110
|
-
|
|
1111
|
-
|
|
1677
|
+
// 如果超过 100ms 没有收到事件,可能鼠标已经进入 iframe
|
|
1678
|
+
// 在 iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
|
|
1679
|
+
// 所以只需要保持拖动状态,等待 mouseup 事件,不需要每帧都更新位置
|
|
1680
|
+
if (timeSinceLastMove > 100) {
|
|
1112
1681
|
// 使用最后记录的鼠标位置进行检测
|
|
1113
1682
|
const clientX = this.lastMousePosition.x;
|
|
1114
1683
|
const clientY = this.lastMousePosition.y;
|
|
1115
|
-
|
|
1116
|
-
// 只在超过 100ms 时记录警告,避免日志过多
|
|
1117
|
-
console.warn(`[IconManager] Active detection: No mousemove event for ${timeSinceLastMove}ms`, {
|
|
1118
|
-
lastMousePosition: { x: clientX, y: clientY },
|
|
1119
|
-
timestamp: this.lastMousePosition.timestamp,
|
|
1120
|
-
isDragging: this.isDragging,
|
|
1121
|
-
dragStarted: this.dragStarted
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
// 更新 iframe 缓存(更频繁,每帧更新)
|
|
1684
|
+
// 更新 iframe 缓存(不需要每帧更新,只在需要时更新)
|
|
1125
1685
|
if (this.cachedIframes.length === 0 ||
|
|
1126
1686
|
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
1127
1687
|
const allIframes = document.querySelectorAll('iframe');
|
|
1128
|
-
|
|
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 => ({
|
|
1129
1696
|
element: iframe,
|
|
1130
1697
|
rect: iframe.getBoundingClientRect()
|
|
1131
1698
|
}));
|
|
1132
1699
|
this.lastIframeUpdateTime = now;
|
|
1133
|
-
|
|
1134
|
-
|
|
1700
|
+
}
|
|
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;
|
|
1711
|
+
if (this.debug) {
|
|
1712
|
+
console.log('[IconManager] [PC+iframe] Active detection: Mouse over iframe, keeping drag state active (waiting for mouseup)');
|
|
1713
|
+
}
|
|
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;
|
|
1135
1725
|
}
|
|
1136
1726
|
}
|
|
1137
|
-
// 移除 iframe 检测停止拖动的逻辑,让拖动可以在 iframe 上顺畅进行
|
|
1138
|
-
// 不再因为检测到 iframe 而停止拖动,保持拖动的流畅性
|
|
1139
1727
|
}
|
|
1140
1728
|
// 继续检测
|
|
1141
|
-
|
|
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);
|
|
1142
1737
|
};
|
|
1143
1738
|
this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
|
|
1144
1739
|
}
|
|
@@ -1154,6 +1749,40 @@ class IconManager {
|
|
|
1154
1749
|
this.activeDetectionRafId = null;
|
|
1155
1750
|
}
|
|
1156
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
|
+
}
|
|
1157
1786
|
/**
|
|
1158
1787
|
* 清理拖动事件
|
|
1159
1788
|
*/
|
|
@@ -1291,6 +1920,12 @@ class IconManager {
|
|
|
1291
1920
|
const containerRect = container.getBoundingClientRect();
|
|
1292
1921
|
const iconWidth = this.iconElement.offsetWidth || 30;
|
|
1293
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;
|
|
1294
1929
|
// 获取当前图标位置
|
|
1295
1930
|
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
1296
1931
|
const currentLeft = parseFloat(computedStyle.left) || 0;
|
|
@@ -1299,7 +1934,7 @@ class IconManager {
|
|
|
1299
1934
|
let newY = currentTop;
|
|
1300
1935
|
// X 轴磁性吸附
|
|
1301
1936
|
if (this.magneticDirection === 'x' || this.magneticDirection === 'both') {
|
|
1302
|
-
const containerWidth =
|
|
1937
|
+
const containerWidth = actualWidth; // 使用实际宽度(考虑 max-width)
|
|
1303
1938
|
const centerX = containerWidth / 2;
|
|
1304
1939
|
if (currentLeft < centerX) {
|
|
1305
1940
|
// 吸附到左边
|
|
@@ -1575,16 +2210,42 @@ class IframeManager {
|
|
|
1575
2210
|
if (this.containerElement) {
|
|
1576
2211
|
// 确保容器在 DOM 中(理论上已经在 init() 时添加,但为了安全)
|
|
1577
2212
|
if (!this.containerElement.parentNode) {
|
|
1578
|
-
// 如果容器不在 DOM
|
|
1579
|
-
document.body
|
|
2213
|
+
// 如果容器不在 DOM 中(异常情况),添加到 target 或 body
|
|
2214
|
+
if (this.targetElement && this.targetElement !== document.body) {
|
|
2215
|
+
// 确保 target 是相对定位
|
|
2216
|
+
const targetStyle = window.getComputedStyle(this.targetElement);
|
|
2217
|
+
if (targetStyle.position === 'static') {
|
|
2218
|
+
this.targetElement.style.position = 'relative';
|
|
2219
|
+
}
|
|
2220
|
+
this.targetElement.appendChild(this.containerElement);
|
|
2221
|
+
if (this.debug) {
|
|
2222
|
+
console.log('Iframe container re-added to target element in show()');
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
else {
|
|
2226
|
+
// 没有 target 或 target 是 body,添加到 body
|
|
2227
|
+
document.body.appendChild(this.containerElement);
|
|
2228
|
+
if (this.debug) {
|
|
2229
|
+
console.log('Iframe container re-added to document.body in show()');
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
1580
2232
|
}
|
|
1581
|
-
//
|
|
1582
|
-
|
|
2233
|
+
// 根据是否有 target 设置正确的定位方式
|
|
2234
|
+
const useTargetWidth = !!this.targetElement && this.targetElement !== document.body;
|
|
2235
|
+
const displayStyles = useTargetWidth ? {
|
|
2236
|
+
// 使用 target 时,保持 absolute 定位(相对于 target)
|
|
2237
|
+
position: 'absolute',
|
|
2238
|
+
visibility: 'visible',
|
|
2239
|
+
opacity: '1',
|
|
2240
|
+
display: 'block'
|
|
2241
|
+
} : {
|
|
2242
|
+
// 没有 target 时,使用 fixed 定位(相对于视口)
|
|
1583
2243
|
position: 'fixed',
|
|
1584
2244
|
visibility: 'visible',
|
|
1585
2245
|
opacity: '1',
|
|
1586
2246
|
display: 'block'
|
|
1587
|
-
}
|
|
2247
|
+
};
|
|
2248
|
+
Object.assign(this.containerElement.style, displayStyles);
|
|
1588
2249
|
// 禁用body滚动,防止出现滚动条
|
|
1589
2250
|
this.preventBodyScroll(true);
|
|
1590
2251
|
}
|
|
@@ -1694,6 +2355,8 @@ class IframeManager {
|
|
|
1694
2355
|
// 创建包装容器(包含iframe和关闭按钮)
|
|
1695
2356
|
this.containerElement = document.createElement('div');
|
|
1696
2357
|
this.containerElement.className = 'customer-sdk-container';
|
|
2358
|
+
// 确保容器不可拖动
|
|
2359
|
+
this.containerElement.draggable = false;
|
|
1697
2360
|
// 创建iframe元素
|
|
1698
2361
|
this.iframeElement = document.createElement('iframe');
|
|
1699
2362
|
this.iframeElement.className = 'customer-sdk-iframe';
|
|
@@ -1766,24 +2429,29 @@ class IframeManager {
|
|
|
1766
2429
|
opacity: '0',
|
|
1767
2430
|
display: 'none'
|
|
1768
2431
|
} : (isPC ? {
|
|
1769
|
-
// PC模式:没有 target
|
|
2432
|
+
// PC模式:没有 target,固定在右下角(不可拖动)
|
|
1770
2433
|
width: `${this.config.width || 450}px`,
|
|
1771
|
-
height:
|
|
2434
|
+
height: `${this.config.height || 600}px`,
|
|
1772
2435
|
maxWidth: '90vw',
|
|
1773
|
-
maxHeight: '
|
|
2436
|
+
maxHeight: '90vh',
|
|
1774
2437
|
backgroundColor: '#ffffff',
|
|
1775
|
-
borderRadius: '
|
|
1776
|
-
boxShadow: '
|
|
2438
|
+
borderRadius: '8px',
|
|
2439
|
+
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.25)',
|
|
1777
2440
|
border: 'none',
|
|
1778
2441
|
position: 'fixed',
|
|
1779
2442
|
zIndex: '999999',
|
|
1780
|
-
// PC
|
|
1781
|
-
top: '
|
|
1782
|
-
left: '
|
|
1783
|
-
bottom: '
|
|
1784
|
-
right: '
|
|
1785
|
-
transform: '
|
|
2443
|
+
// PC模式:固定在右下角
|
|
2444
|
+
top: 'auto',
|
|
2445
|
+
left: 'auto',
|
|
2446
|
+
bottom: '20px',
|
|
2447
|
+
right: '20px',
|
|
2448
|
+
transform: 'none',
|
|
1786
2449
|
overflow: 'hidden',
|
|
2450
|
+
// 防止拖动和选择
|
|
2451
|
+
userSelect: 'none',
|
|
2452
|
+
WebkitUserSelect: 'none',
|
|
2453
|
+
MozUserSelect: 'none',
|
|
2454
|
+
msUserSelect: 'none',
|
|
1787
2455
|
// 初始隐藏的关键样式
|
|
1788
2456
|
visibility: 'hidden',
|
|
1789
2457
|
opacity: '0',
|
|
@@ -1823,6 +2491,28 @@ class IframeManager {
|
|
|
1823
2491
|
Object.assign(this.iframeElement.style, iframeStyles);
|
|
1824
2492
|
// 将iframe放入容器
|
|
1825
2493
|
this.containerElement.appendChild(this.iframeElement);
|
|
2494
|
+
// PC模式下:禁止拖动容器
|
|
2495
|
+
if (isPC && !useTargetWidth) {
|
|
2496
|
+
// 阻止拖动事件
|
|
2497
|
+
this.containerElement.addEventListener('dragstart', (e) => {
|
|
2498
|
+
e.preventDefault();
|
|
2499
|
+
e.stopPropagation();
|
|
2500
|
+
return false;
|
|
2501
|
+
}, false);
|
|
2502
|
+
// 阻止鼠标按下事件(防止可能的拖动行为)
|
|
2503
|
+
this.containerElement.addEventListener('mousedown', (e) => {
|
|
2504
|
+
// 只阻止在容器边缘的拖动,允许点击 iframe 内容
|
|
2505
|
+
const rect = this.containerElement.getBoundingClientRect();
|
|
2506
|
+
const isOnEdge = (e.clientX < rect.left + 10 ||
|
|
2507
|
+
e.clientX > rect.right - 10 ||
|
|
2508
|
+
e.clientY < rect.top + 10 ||
|
|
2509
|
+
e.clientY > rect.bottom - 10);
|
|
2510
|
+
if (isOnEdge) {
|
|
2511
|
+
e.preventDefault();
|
|
2512
|
+
e.stopPropagation();
|
|
2513
|
+
}
|
|
2514
|
+
}, false);
|
|
2515
|
+
}
|
|
1826
2516
|
// 添加iframe加载事件监听(移动端样式优化)
|
|
1827
2517
|
this.iframeElement.addEventListener('load', () => {
|
|
1828
2518
|
// 移动端注入自定义样式
|
|
@@ -2067,32 +2757,42 @@ class IframeManager {
|
|
|
2067
2757
|
break;
|
|
2068
2758
|
case 'DCR_DEPOSIT':
|
|
2069
2759
|
// 存款跳转消息 - 广播给调用 SDK 的 Vue 页面
|
|
2070
|
-
//
|
|
2760
|
+
// 支持两种存款类型:
|
|
2761
|
+
// 1. 钱包直充:{ type: 'DCR_DEPOSIT', payload: { depositType: 0, walletId: 69382 } }
|
|
2762
|
+
// 2. 线下充值:{ type: 'DCR_DEPOSIT', payload: { depositType: 2, offlineTypeId: 2001, accountId: 35213 } }
|
|
2071
2763
|
if (this.debug) {
|
|
2072
|
-
|
|
2764
|
+
const depositType = data.payload?.depositType;
|
|
2765
|
+
const depositTypeText = depositType === 0 ? '钱包直充' : depositType === 2 ? '线下充值' : '未知类型';
|
|
2766
|
+
console.log(`[IframeManager] Received DCR_DEPOSIT message (${depositTypeText}):`, {
|
|
2767
|
+
type: data.type,
|
|
2768
|
+
payload: data.payload,
|
|
2769
|
+
depositType: depositTypeText
|
|
2770
|
+
});
|
|
2073
2771
|
}
|
|
2074
2772
|
this.broadcastMessageToPage(data);
|
|
2075
2773
|
if (this.config.onMessage) {
|
|
2076
2774
|
this.config.onMessage(messageType, data);
|
|
2077
2775
|
}
|
|
2078
2776
|
break;
|
|
2079
|
-
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
this.config.onMessage(messageType, data);
|
|
2089
|
-
}
|
|
2777
|
+
case 'DCR_ACTIVITY':
|
|
2778
|
+
// 跳转到优惠页面 - 广播给调用 SDK 的 Vue 页面
|
|
2779
|
+
// 格式:{ type: 'DCR_ACTIVITY', payload: { activityId: 123 } }
|
|
2780
|
+
if (this.debug) {
|
|
2781
|
+
console.log('[IframeManager] Received DCR_ACTIVITY message:', {
|
|
2782
|
+
type: data.type,
|
|
2783
|
+
payload: data.payload,
|
|
2784
|
+
activityId: data.payload?.activityId
|
|
2785
|
+
});
|
|
2090
2786
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2787
|
+
this.broadcastMessageToPage(data);
|
|
2788
|
+
if (this.config.onMessage) {
|
|
2789
|
+
this.config.onMessage(messageType, data);
|
|
2790
|
+
}
|
|
2791
|
+
break;
|
|
2792
|
+
default:
|
|
2793
|
+
// 其他自定义消息处理
|
|
2794
|
+
if (this.debug) {
|
|
2795
|
+
console.log('[IframeManager] Custom message:', data);
|
|
2096
2796
|
}
|
|
2097
2797
|
break;
|
|
2098
2798
|
}
|
|
@@ -21946,7 +22646,7 @@ class CustomerServiceSDK {
|
|
|
21946
22646
|
const initResult = {
|
|
21947
22647
|
deviceId,
|
|
21948
22648
|
iframeUrl,
|
|
21949
|
-
referrer: document.referrer,
|
|
22649
|
+
referrer: config.referrer || document.referrer || document.location.href,
|
|
21950
22650
|
agent: config.agent,
|
|
21951
22651
|
timestamp: Date.now()
|
|
21952
22652
|
};
|
|
@@ -22372,7 +23072,7 @@ class CustomerServiceSDK {
|
|
|
22372
23072
|
url.searchParams.set('Authorization', config.token);
|
|
22373
23073
|
}
|
|
22374
23074
|
url.searchParams.set('DeviceSign', deviceId);
|
|
22375
|
-
url.searchParams.set('Referrer', document.referrer);
|
|
23075
|
+
url.searchParams.set('Referrer', config.referrer || document.referrer || document.location.href);
|
|
22376
23076
|
return url.toString();
|
|
22377
23077
|
}
|
|
22378
23078
|
}
|