customer-chat-sdk 1.1.16 → 1.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/IconManager.d.ts +19 -0
- package/dist/core/IconManager.d.ts.map +1 -1
- package/dist/core/IframeManager.d.ts +10 -4
- package/dist/core/IframeManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +943 -225
- package/dist/customer-sdk.esm.js +943 -225
- 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 +3 -2
package/dist/customer-sdk.esm.js
CHANGED
|
@@ -12,12 +12,14 @@ 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 }; // 最后触摸位置
|
|
19
20
|
this.touchStartTime = 0; // 触摸开始时间
|
|
20
21
|
this.clickThreshold = 15; // 点击阈值(像素)
|
|
22
|
+
this.stoppedByIframe = false; // 是否因为 iframe 而停止拖动(用于避免触发磁性吸附)
|
|
21
23
|
// 性能优化:缓存容器信息,避免频繁查询 DOM
|
|
22
24
|
this.cachedContainer = null;
|
|
23
25
|
this.cachedContainerRect = null;
|
|
@@ -33,7 +35,9 @@ class IconManager {
|
|
|
33
35
|
this.pendingPosition = { x: 0, y: 0, needsUpdate: false };
|
|
34
36
|
// 主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置(即使没有 mousemove 事件)
|
|
35
37
|
this.activeDetectionRafId = null;
|
|
38
|
+
this.activeDetectionTimeoutId = null; // setTimeout 的 ID
|
|
36
39
|
this.lastMousePosition = { x: 0, y: 0, timestamp: 0 }; // 最后记录的鼠标位置和时间戳
|
|
40
|
+
this.mouseVelocity = { vx: 0, vy: 0 }; // 鼠标移动速度(用于预测位置)
|
|
37
41
|
// 事件处理器引用(用于清理)
|
|
38
42
|
this.onDragHandler = null;
|
|
39
43
|
this.stopDragHandler = null;
|
|
@@ -45,6 +49,7 @@ class IconManager {
|
|
|
45
49
|
this.mouseLeaveTimeout = null; // 鼠标离开防抖定时器
|
|
46
50
|
this.pointerLeaveTimeout = null; // 指针离开防抖定时器
|
|
47
51
|
this.isStoppingDrag = false; // 防止 stopDrag 重复调用
|
|
52
|
+
this.hasLoggedIframeWarning = false; // 防止 iframe 警告日志重复输出
|
|
48
53
|
// 侧边吸附相关状态
|
|
49
54
|
this.sideAttach = true; // 是否启用侧边吸附(默认 true)
|
|
50
55
|
this.sideHideRatio = 0.5; // 侧边吸附时的隐藏比例(默认 0.5,显示一半)
|
|
@@ -80,6 +85,7 @@ class IconManager {
|
|
|
80
85
|
this.iconElement = document.createElement('div');
|
|
81
86
|
this.iconElement.className = 'customer-sdk-icon';
|
|
82
87
|
// 直接设置样式 - 图标容器
|
|
88
|
+
// 关键优化:使用 pointer-events: none 让事件穿透到 iframe,子元素使用 pointer-events: auto 保持可点击
|
|
83
89
|
const defaultStyle = {
|
|
84
90
|
position: 'absolute',
|
|
85
91
|
width: '30px',
|
|
@@ -96,7 +102,8 @@ class IconManager {
|
|
|
96
102
|
transition: 'transform 0.2s ease',
|
|
97
103
|
border: 'none',
|
|
98
104
|
outline: 'none',
|
|
99
|
-
overflow: 'visible' // 允许红点显示在图标外部
|
|
105
|
+
overflow: 'visible', // 允许红点显示在图标外部
|
|
106
|
+
pointerEvents: 'none' // 让事件穿透到 iframe,只有子元素可以接收事件
|
|
100
107
|
};
|
|
101
108
|
// 如果指定了位置,使用left/top;否则使用默认的bottom/right
|
|
102
109
|
if (this.iconPosition) {
|
|
@@ -194,12 +201,22 @@ class IconManager {
|
|
|
194
201
|
height: '100%',
|
|
195
202
|
borderRadius: '50%',
|
|
196
203
|
overflow: 'hidden', // 限制图片溢出圆形边界
|
|
197
|
-
position: 'relative'
|
|
204
|
+
position: 'relative',
|
|
205
|
+
pointerEvents: 'auto' // 子元素启用指针事件,让 icon 可以点击和拖动
|
|
198
206
|
});
|
|
199
207
|
imgContainer.appendChild(iconImg);
|
|
200
208
|
this.iconElement.appendChild(imgContainer);
|
|
201
209
|
// 添加到目标元素(如果 target 是字符串,需要重新查找,因为可能在初始化时元素还不存在)
|
|
202
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
|
+
}
|
|
203
220
|
// 确保目标容器有 overflow-x: hidden,防止图标超出边界时出现横向滚动条
|
|
204
221
|
// 注意:只在启用侧边吸附时才设置,避免影响其他场景
|
|
205
222
|
if (this.sideAttach && targetElement !== document.body) {
|
|
@@ -232,12 +249,20 @@ class IconManager {
|
|
|
232
249
|
}
|
|
233
250
|
if (targetElement) {
|
|
234
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
|
+
}
|
|
235
260
|
}
|
|
236
261
|
else {
|
|
237
262
|
// 如果目标元素不存在,回退到 document.body
|
|
238
263
|
document.body.appendChild(this.iconElement);
|
|
239
264
|
if (this.debug) {
|
|
240
|
-
console.warn('Target element not found, icon added to document.body');
|
|
265
|
+
console.warn('[IconManager] Target element not found, icon added to document.body');
|
|
241
266
|
}
|
|
242
267
|
}
|
|
243
268
|
// 设置拖动事件
|
|
@@ -552,12 +577,70 @@ class IconManager {
|
|
|
552
577
|
this.iconElement.addEventListener('mousedown', this.startDragHandler);
|
|
553
578
|
this.iconElement.addEventListener('touchstart', this.startDragHandler, { passive: false });
|
|
554
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
|
+
}
|
|
555
617
|
/**
|
|
556
618
|
* 开始拖动
|
|
557
619
|
*/
|
|
558
620
|
startDrag(e) {
|
|
559
621
|
if (!this.iconElement || !this.isClickEnabled)
|
|
560
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
|
+
}
|
|
561
644
|
// 清除自动吸附定时器(用户开始拖动)
|
|
562
645
|
this.clearAutoAttachTimer();
|
|
563
646
|
// 如果正在停止拖动,等待完成后再允许新的拖动
|
|
@@ -578,6 +661,8 @@ class IconManager {
|
|
|
578
661
|
}
|
|
579
662
|
return;
|
|
580
663
|
}
|
|
664
|
+
// 重置 iframe 警告标志
|
|
665
|
+
this.hasLoggedIframeWarning = false;
|
|
581
666
|
// 如果发现残留的资源,先清理它们(防御性编程)
|
|
582
667
|
if (this.dragTimeoutId !== null || this.activeDetectionRafId !== null || this.rafId !== null) {
|
|
583
668
|
if (this.debug) {
|
|
@@ -597,16 +682,49 @@ class IconManager {
|
|
|
597
682
|
target.closest('.customer-sdk-overlay'))) {
|
|
598
683
|
return;
|
|
599
684
|
}
|
|
685
|
+
// 关键优化:检查触摸点是否真的在 icon 的圆形区域内
|
|
686
|
+
// 如果不在,让事件穿透到 iframe,不触发拖动
|
|
687
|
+
if (this.iconElement) {
|
|
688
|
+
const iconRect = this.iconElement.getBoundingClientRect();
|
|
689
|
+
// 计算触摸点相对于 icon 中心的位置
|
|
690
|
+
const iconCenterX = iconRect.left + iconRect.width / 2;
|
|
691
|
+
const iconCenterY = iconRect.top + iconRect.height / 2;
|
|
692
|
+
const iconRadius = Math.min(iconRect.width, iconRect.height) / 2;
|
|
693
|
+
const distanceFromCenter = Math.sqrt(Math.pow(clientX - iconCenterX, 2) + Math.pow(clientY - iconCenterY, 2));
|
|
694
|
+
// 如果触摸点在 icon 圆形区域外(留一点边距,比如 10px),不处理拖动
|
|
695
|
+
// 这样可以让 iframe 内的滑动正常工作
|
|
696
|
+
// 但边距不能太大,否则无法拖动
|
|
697
|
+
if (distanceFromCenter > iconRadius + 10) {
|
|
698
|
+
if (this.debug) {
|
|
699
|
+
console.log('[IconManager] Touch point outside icon circle, allowing event to pass through', {
|
|
700
|
+
distance: distanceFromCenter,
|
|
701
|
+
radius: iconRadius,
|
|
702
|
+
touchPoint: { x: clientX, y: clientY },
|
|
703
|
+
iconCenter: { x: iconCenterX, y: iconCenterY }
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
600
709
|
e.preventDefault();
|
|
601
710
|
e.stopPropagation();
|
|
711
|
+
// 拖动开始时,临时启用 pointer-events 确保拖动事件能正常接收
|
|
712
|
+
if (this.iconElement) {
|
|
713
|
+
this.iconElement.style.pointerEvents = 'auto';
|
|
714
|
+
}
|
|
602
715
|
// 重置状态
|
|
603
716
|
this.hasMoved = false;
|
|
604
717
|
this.dragStarted = false;
|
|
605
718
|
this.isDragging = false;
|
|
719
|
+
this.dragIntent = true; // 设置拖动意图,表示用户已经开始拖动意图(在 mousedown/touchstart 时)
|
|
606
720
|
// 记录开始时间和位置
|
|
607
721
|
this.touchStartTime = Date.now();
|
|
608
|
-
|
|
609
|
-
|
|
722
|
+
// 初始化 lastMousePosition,避免第一次 onDrag 时 timeSinceLastUpdate 过大
|
|
723
|
+
this.lastMousePosition = {
|
|
724
|
+
x: clientX,
|
|
725
|
+
y: clientY,
|
|
726
|
+
timestamp: Date.now()
|
|
727
|
+
};
|
|
610
728
|
this.lastTouchPosition.x = clientX;
|
|
611
729
|
this.lastTouchPosition.y = clientY;
|
|
612
730
|
try {
|
|
@@ -624,14 +742,21 @@ class IconManager {
|
|
|
624
742
|
// 注意:不在这里转换位置,只在真正开始拖动时才转换(在 onDrag 中)
|
|
625
743
|
// 性能优化:在拖动开始时预加载所有 iframe 位置信息
|
|
626
744
|
// 这样可以避免在拖动过程中频繁查询 DOM
|
|
745
|
+
// 注意:排除 SDK 自己的 iframe(customer-sdk-container 内的 iframe),因为 SDK 的 iframe 不应该影响拖动
|
|
627
746
|
const allIframes = document.querySelectorAll('iframe');
|
|
628
|
-
this.cachedIframes = Array.from(allIframes)
|
|
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 => ({
|
|
629
754
|
element: iframe,
|
|
630
755
|
rect: iframe.getBoundingClientRect()
|
|
631
756
|
}));
|
|
632
757
|
this.lastIframeUpdateTime = Date.now();
|
|
633
758
|
if (this.debug) {
|
|
634
|
-
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)`, {
|
|
635
760
|
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
636
761
|
left: rect.left,
|
|
637
762
|
top: rect.top,
|
|
@@ -653,43 +778,54 @@ class IconManager {
|
|
|
653
778
|
}
|
|
654
779
|
// 添加处理 iframe 上事件丢失的机制
|
|
655
780
|
// 1. 监听 mouseleave 和 pointerleave 事件(鼠标离开窗口时停止拖动)
|
|
656
|
-
//
|
|
781
|
+
// 注意:当鼠标移动到 iframe 上时,这些事件可能会误触发,需要更严格的检测
|
|
657
782
|
this.mouseLeaveHandler = (e) => {
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
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)) {
|
|
661
791
|
// 添加防抖,避免重复触发
|
|
662
792
|
if (this.mouseLeaveTimeout) {
|
|
663
793
|
clearTimeout(this.mouseLeaveTimeout);
|
|
664
794
|
}
|
|
665
795
|
this.mouseLeaveTimeout = window.setTimeout(() => {
|
|
666
|
-
|
|
796
|
+
// 再次确认鼠标真的离开了窗口
|
|
797
|
+
if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
|
|
667
798
|
if (this.debug) {
|
|
668
799
|
console.log('[IconManager] Mouse left window, stopping drag');
|
|
669
800
|
}
|
|
670
801
|
this.stopDrag();
|
|
671
802
|
}
|
|
672
803
|
this.mouseLeaveTimeout = null;
|
|
673
|
-
},
|
|
804
|
+
}, 100); // 增加到 100ms 防抖,避免误判
|
|
674
805
|
}
|
|
675
806
|
};
|
|
676
807
|
this.pointerLeaveHandler = (e) => {
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
e.clientX
|
|
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)) {
|
|
680
815
|
// 添加防抖,避免重复触发
|
|
681
816
|
if (this.pointerLeaveTimeout) {
|
|
682
817
|
clearTimeout(this.pointerLeaveTimeout);
|
|
683
818
|
}
|
|
684
819
|
this.pointerLeaveTimeout = window.setTimeout(() => {
|
|
685
|
-
|
|
820
|
+
// 再次确认指针真的离开了窗口
|
|
821
|
+
if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag && !this.isMouseOverIframe(e.clientX, e.clientY)) {
|
|
686
822
|
if (this.debug) {
|
|
687
823
|
console.log('[IconManager] Pointer left window, stopping drag');
|
|
688
824
|
}
|
|
689
825
|
this.stopDrag();
|
|
690
826
|
}
|
|
691
827
|
this.pointerLeaveTimeout = null;
|
|
692
|
-
},
|
|
828
|
+
}, 100); // 增加到 100ms 防抖,避免误判
|
|
693
829
|
}
|
|
694
830
|
};
|
|
695
831
|
document.addEventListener('mouseleave', this.mouseLeaveHandler);
|
|
@@ -711,15 +847,97 @@ class IconManager {
|
|
|
711
847
|
};
|
|
712
848
|
window.addEventListener('blur', this.blurHandler);
|
|
713
849
|
// 3. 添加超时机制(如果一段时间没有收到 mousemove 事件,自动停止拖动)
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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 超时
|
|
721
900
|
}
|
|
722
|
-
}
|
|
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 超时
|
|
939
|
+
}
|
|
940
|
+
}
|
|
723
941
|
// 4. 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
|
|
724
942
|
// 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
|
|
725
943
|
this.startActiveDetection();
|
|
@@ -748,27 +966,72 @@ class IconManager {
|
|
|
748
966
|
onDrag(e) {
|
|
749
967
|
if (!this.iconElement)
|
|
750
968
|
return;
|
|
751
|
-
e.preventDefault();
|
|
752
969
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
753
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();
|
|
754
1001
|
// 检测鼠标是否在任何 iframe 上(通过坐标判断)
|
|
755
|
-
//
|
|
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
|
+
}
|
|
756
1011
|
// 重要:需要检测所有 iframe(包括嵌套的),因为任何 iframe 都会导致事件丢失
|
|
757
|
-
|
|
1012
|
+
// 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
|
|
1013
|
+
// 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
|
|
1014
|
+
if (this.isDragging || this.dragIntent || this.dragStarted) {
|
|
758
1015
|
const now = Date.now();
|
|
759
1016
|
// 性能优化:缓存 iframe 位置信息,避免频繁查询 DOM
|
|
760
1017
|
if (this.cachedIframes.length === 0 ||
|
|
761
1018
|
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
762
|
-
// 更新 iframe
|
|
1019
|
+
// 更新 iframe 缓存(排除 SDK 自己的 iframe)
|
|
763
1020
|
const allIframes = document.querySelectorAll('iframe');
|
|
764
1021
|
const previousCount = this.cachedIframes.length;
|
|
765
|
-
this.cachedIframes = Array.from(allIframes)
|
|
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 => ({
|
|
766
1029
|
element: iframe,
|
|
767
1030
|
rect: iframe.getBoundingClientRect()
|
|
768
1031
|
}));
|
|
769
1032
|
this.lastIframeUpdateTime = now;
|
|
770
1033
|
if (this.debug && this.cachedIframes.length !== previousCount) {
|
|
771
|
-
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)`, {
|
|
772
1035
|
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
773
1036
|
left: rect.left,
|
|
774
1037
|
top: rect.top,
|
|
@@ -778,67 +1041,103 @@ class IconManager {
|
|
|
778
1041
|
});
|
|
779
1042
|
}
|
|
780
1043
|
}
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
if (clientX >= rect.left &&
|
|
784
|
-
clientX <= rect.right &&
|
|
785
|
-
clientY >= rect.top &&
|
|
786
|
-
clientY <= rect.bottom) {
|
|
787
|
-
// 鼠标在 iframe 上,立即停止拖动
|
|
788
|
-
if (this.debug) {
|
|
789
|
-
console.log('[IconManager] Mouse over iframe detected, stopping drag immediately', {
|
|
790
|
-
mousePosition: { x: clientX, y: clientY },
|
|
791
|
-
iframeRect: {
|
|
792
|
-
left: rect.left,
|
|
793
|
-
top: rect.top,
|
|
794
|
-
right: rect.right,
|
|
795
|
-
bottom: rect.bottom
|
|
796
|
-
},
|
|
797
|
-
iframeSrc: element.src || 'no src'
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
this.stopDrag();
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
1044
|
+
// 移除 iframe 检测停止拖动的逻辑,让拖动可以在 iframe 上顺畅进行
|
|
1045
|
+
// 不再因为检测到 iframe 而停止拖动,保持拖动的流畅性
|
|
804
1046
|
}
|
|
805
1047
|
// 更新最后记录的鼠标位置和时间戳(用于主动检测)
|
|
806
1048
|
const now = Date.now();
|
|
807
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;
|
|
808
1053
|
this.lastMousePosition = {
|
|
809
1054
|
x: clientX,
|
|
810
1055
|
y: clientY,
|
|
811
1056
|
timestamp: now
|
|
812
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
|
+
}
|
|
813
1068
|
// 如果距离上次更新超过 50ms,记录警告(可能事件丢失)
|
|
814
1069
|
if (this.debug && timeSinceLastUpdate > 50 && this.lastMousePosition.timestamp > 0) {
|
|
815
1070
|
console.warn(`[IconManager] Long gap between mousemove events: ${timeSinceLastUpdate}ms`, {
|
|
816
|
-
lastPosition: { x:
|
|
817
|
-
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 }
|
|
818
1074
|
});
|
|
819
1075
|
}
|
|
820
1076
|
// 重置超时定时器(每次移动都重置,确保只有真正停止移动时才触发超时)
|
|
821
|
-
// 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
822
1077
|
if (this.dragTimeoutId !== null) {
|
|
823
1078
|
window.clearTimeout(this.dragTimeoutId);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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);
|
|
835
1115
|
}
|
|
836
|
-
|
|
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
|
+
}
|
|
837
1134
|
}
|
|
838
1135
|
// 检查是否有足够的移动距离
|
|
839
1136
|
const deltaX = Math.abs(clientX - this.lastTouchPosition.x);
|
|
840
1137
|
const deltaY = Math.abs(clientY - this.lastTouchPosition.y);
|
|
841
1138
|
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
1139
|
+
// 只有在移动距离超过阈值时,才认为是拖动
|
|
1140
|
+
// 这样可以避免轻微的鼠标抖动被误判为拖动
|
|
842
1141
|
if (totalMovement > this.clickThreshold) {
|
|
843
1142
|
this.hasMoved = true;
|
|
844
1143
|
if (!this.dragStarted) {
|
|
@@ -882,7 +1181,9 @@ class IconManager {
|
|
|
882
1181
|
}
|
|
883
1182
|
}
|
|
884
1183
|
}
|
|
885
|
-
|
|
1184
|
+
// 注意:即使 isDragging 为 false,只要 dragIntent 或 dragStarted 为 true,也应该继续处理
|
|
1185
|
+
// 因为在 iframe 上时,isDragging 可能会因为 mousemove 事件丢失而暂时为 false
|
|
1186
|
+
if (!this.isDragging && !this.dragIntent && !this.dragStarted) {
|
|
886
1187
|
return;
|
|
887
1188
|
}
|
|
888
1189
|
try {
|
|
@@ -907,41 +1208,184 @@ class IconManager {
|
|
|
907
1208
|
const containerRect = this.cachedContainerRect;
|
|
908
1209
|
const iconWidth = this.cachedIconSize.width;
|
|
909
1210
|
const iconHeight = this.cachedIconSize.height;
|
|
910
|
-
//
|
|
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
|
+
// 计算新位置(相对于容器的实际内容区域)
|
|
911
1239
|
let newX = clientX - this.dragOffset.x - containerRect.left;
|
|
912
1240
|
let newY = clientY - this.dragOffset.y - containerRect.top;
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1241
|
+
// 允许拖动到容器外(允许负值),但限制在合理范围内(避免拖得太远)
|
|
1242
|
+
// 允许拖动到容器外,最多隐藏到只剩一小部分可见
|
|
1243
|
+
// 使用实际宽度(考虑 max-width 限制)而不是容器宽度
|
|
1244
|
+
const minX = -iconWidth + 5; // 允许向左拖动,最多隐藏到只剩 5px
|
|
1245
|
+
const maxX = container === document.body
|
|
1246
|
+
? window.innerWidth - 5 // 允许向右拖动,最多隐藏到只剩 5px
|
|
1247
|
+
: actualWidth + iconWidth - 5; // 使用实际宽度(考虑 max-width),允许超出容器,最多隐藏到只剩 5px
|
|
1248
|
+
const minY = -iconHeight + 5; // 允许向上拖动,最多隐藏到只剩 5px
|
|
1249
|
+
const maxY = container === document.body
|
|
1250
|
+
? window.innerHeight - 5 // 允许向下拖动,最多隐藏到只剩 5px
|
|
1251
|
+
: actualHeight + iconHeight - 5; // 允许超出容器,最多隐藏到只剩 5px
|
|
1252
|
+
newX = Math.max(minX, Math.min(newX, maxX));
|
|
1253
|
+
newY = Math.max(minY, Math.min(newY, maxY));
|
|
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
|
+
}
|
|
918
1352
|
}
|
|
919
1353
|
else {
|
|
920
|
-
//
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
// 性能优化:使用 requestAnimationFrame 节流位置更新
|
|
927
|
-
this.pendingPosition.x = newX;
|
|
928
|
-
this.pendingPosition.y = newY;
|
|
929
|
-
this.pendingPosition.needsUpdate = true;
|
|
930
|
-
if (this.rafId === null) {
|
|
931
|
-
this.rafId = requestAnimationFrame(() => {
|
|
932
|
-
this.rafId = null;
|
|
933
|
-
if (this.pendingPosition.needsUpdate && this.iconElement && this.isDragging) {
|
|
934
|
-
this.iconElement.style.left = `${this.pendingPosition.x}px`;
|
|
935
|
-
this.iconElement.style.top = `${this.pendingPosition.y}px`;
|
|
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`;
|
|
936
1360
|
this.iconElement.style.right = 'auto';
|
|
937
1361
|
this.iconElement.style.bottom = 'auto';
|
|
938
|
-
this.pendingPosition.needsUpdate = false;
|
|
939
1362
|
}
|
|
940
|
-
}
|
|
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
|
+
}
|
|
941
1382
|
}
|
|
942
|
-
//
|
|
1383
|
+
// 更新最后位置(同时更新 lastTouchPosition 和 lastMousePosition)
|
|
943
1384
|
this.lastTouchPosition.x = clientX;
|
|
944
1385
|
this.lastTouchPosition.y = clientY;
|
|
1386
|
+
this.lastMousePosition.x = clientX;
|
|
1387
|
+
this.lastMousePosition.y = clientY;
|
|
1388
|
+
this.lastMousePosition.timestamp = Date.now();
|
|
945
1389
|
}
|
|
946
1390
|
catch (error) {
|
|
947
1391
|
if (this.debug) {
|
|
@@ -949,6 +1393,88 @@ class IconManager {
|
|
|
949
1393
|
}
|
|
950
1394
|
}
|
|
951
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
|
+
}
|
|
952
1478
|
/**
|
|
953
1479
|
* 停止拖动
|
|
954
1480
|
*/
|
|
@@ -968,6 +1494,36 @@ class IconManager {
|
|
|
968
1494
|
return;
|
|
969
1495
|
}
|
|
970
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
|
+
}
|
|
971
1527
|
const stopReason = this.isDragging ? 'drag_stopped' : 'not_dragging';
|
|
972
1528
|
const finalPosition = this.iconElement ? {
|
|
973
1529
|
left: this.iconElement.style.left,
|
|
@@ -983,15 +1539,28 @@ class IconManager {
|
|
|
983
1539
|
this.hasMoved = false;
|
|
984
1540
|
this.isDragging = false;
|
|
985
1541
|
this.dragStarted = false;
|
|
1542
|
+
this.dragIntent = false;
|
|
986
1543
|
this.isStoppingDrag = false;
|
|
987
1544
|
return;
|
|
988
1545
|
}
|
|
989
1546
|
// 恢复样式
|
|
990
1547
|
this.iconElement.style.transition = 'transform 0.2s ease';
|
|
991
1548
|
this.iconElement.style.cursor = 'pointer';
|
|
1549
|
+
// 拖动结束时,恢复 pointer-events: none 让事件穿透到 iframe
|
|
1550
|
+
this.iconElement.style.pointerEvents = 'none';
|
|
992
1551
|
// 检查是否是点击
|
|
1552
|
+
// 点击的条件:
|
|
1553
|
+
// 1. 没有移动过(hasMoved = false)
|
|
1554
|
+
// 2. 持续时间小于 1000ms
|
|
1555
|
+
// 3. 没有真正开始拖动(dragStarted = false)
|
|
1556
|
+
// 4. 没有拖动意图(dragIntent = false)或者拖动意图存在但持续时间很短(可能是误触)
|
|
1557
|
+
// 5. 没有真正拖动过(isDragging = false)
|
|
993
1558
|
const touchDuration = Date.now() - this.touchStartTime;
|
|
994
|
-
const isValidClick = !this.hasMoved &&
|
|
1559
|
+
const isValidClick = !this.hasMoved &&
|
|
1560
|
+
touchDuration < 1000 &&
|
|
1561
|
+
!this.dragStarted &&
|
|
1562
|
+
!this.isDragging &&
|
|
1563
|
+
(!this.dragIntent || touchDuration < 200); // 如果 dragIntent 存在,但持续时间很短(< 200ms),可能是误触,也认为是点击
|
|
995
1564
|
if (this.debug) {
|
|
996
1565
|
console.log('[IconManager] Drag end', {
|
|
997
1566
|
stopReason,
|
|
@@ -1005,26 +1574,37 @@ class IconManager {
|
|
|
1005
1574
|
}
|
|
1006
1575
|
// 先保存点击状态,然后重置拖动状态
|
|
1007
1576
|
const wasClick = isValidClick;
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1577
|
+
// 保存 stoppedByIframe 状态,用于后续判断是否启动自动吸附
|
|
1578
|
+
const wasStoppedByIframe = this.stoppedByIframe;
|
|
1579
|
+
// 如果是因为 iframe 而停止拖动,不执行磁性吸附和自动吸附
|
|
1580
|
+
// 这样可以避免拖动到 iframe 上时 icon 缩回去
|
|
1581
|
+
if (this.stoppedByIframe) {
|
|
1582
|
+
if (this.debug) {
|
|
1583
|
+
console.log('[IconManager] Drag stopped by iframe, skipping magnetic snap and auto attach');
|
|
1584
|
+
}
|
|
1011
1585
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
//
|
|
1018
|
-
if (
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1586
|
+
else {
|
|
1587
|
+
// 如果真正拖动过,执行磁性吸附
|
|
1588
|
+
if (this.dragStarted && this.magnetic && this.iconElement) {
|
|
1589
|
+
this.magneticSnap();
|
|
1590
|
+
}
|
|
1591
|
+
// 如果真正拖动过,保存当前位置到 iconPosition
|
|
1592
|
+
if (this.dragStarted && this.isDragging && this.iconElement) {
|
|
1593
|
+
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
1594
|
+
const left = computedStyle.left;
|
|
1595
|
+
const top = computedStyle.top;
|
|
1596
|
+
// 如果 left/top 是有效的像素值,保存到 iconPosition
|
|
1597
|
+
if (left !== 'auto' && top !== 'auto') {
|
|
1598
|
+
const leftValue = parseFloat(left);
|
|
1599
|
+
const topValue = parseFloat(top);
|
|
1600
|
+
if (!isNaN(leftValue) && !isNaN(topValue)) {
|
|
1601
|
+
this.iconPosition = {
|
|
1602
|
+
x: leftValue,
|
|
1603
|
+
y: topValue
|
|
1604
|
+
};
|
|
1605
|
+
if (this.debug) {
|
|
1606
|
+
console.log('Icon position saved:', this.iconPosition);
|
|
1607
|
+
}
|
|
1028
1608
|
}
|
|
1029
1609
|
}
|
|
1030
1610
|
}
|
|
@@ -1033,9 +1613,13 @@ class IconManager {
|
|
|
1033
1613
|
this.hasMoved = false;
|
|
1034
1614
|
this.isDragging = false;
|
|
1035
1615
|
this.dragStarted = false;
|
|
1616
|
+
this.dragIntent = false; // 重置拖动意图
|
|
1036
1617
|
this.isStoppingDrag = false; // 重置停止标志
|
|
1618
|
+
this.stoppedByIframe = false; // 重置 iframe 停止标志
|
|
1619
|
+
this.hasLoggedIframeWarning = false; // 重置 iframe 警告标志
|
|
1037
1620
|
// 如果拖动后没有吸附到侧边,启动自动吸附定时器
|
|
1038
|
-
|
|
1621
|
+
// 但如果是因为 iframe 而停止拖动,不启动自动吸附
|
|
1622
|
+
if (!wasStoppedByIframe && !this.isAttachedToSide && this.sideAttach && this.autoAttachDelay > 0) {
|
|
1039
1623
|
this.startAutoAttachTimer();
|
|
1040
1624
|
}
|
|
1041
1625
|
if (wasClick) {
|
|
@@ -1064,72 +1648,88 @@ class IconManager {
|
|
|
1064
1648
|
console.log('[IconManager] Starting active detection mechanism');
|
|
1065
1649
|
}
|
|
1066
1650
|
const checkMousePosition = () => {
|
|
1067
|
-
//
|
|
1068
|
-
|
|
1651
|
+
// 检查是否还在拖动中
|
|
1652
|
+
// 注意:即使 isDragging 暂时为 false(因为 mousemove 事件丢失),
|
|
1653
|
+
// 只要 dragIntent 或 dragStarted 为 true,说明用户还在拖动,应该继续检测
|
|
1654
|
+
if (this.isStoppingDrag || (!this.isDragging && !this.dragStarted && !this.dragIntent)) {
|
|
1069
1655
|
if (this.debug) {
|
|
1070
|
-
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
|
+
});
|
|
1071
1662
|
}
|
|
1072
1663
|
this.activeDetectionRafId = null;
|
|
1664
|
+
if (this.activeDetectionTimeoutId !== null) {
|
|
1665
|
+
window.clearTimeout(this.activeDetectionTimeoutId);
|
|
1666
|
+
this.activeDetectionTimeoutId = null;
|
|
1667
|
+
}
|
|
1073
1668
|
return;
|
|
1074
1669
|
}
|
|
1075
|
-
// 检查是否长时间没有收到 mousemove 事件(超过
|
|
1670
|
+
// 检查是否长时间没有收到 mousemove 事件(超过 100ms 才检测,避免频繁检测)
|
|
1076
1671
|
const now = Date.now();
|
|
1077
1672
|
const timeSinceLastMove = now - this.lastMousePosition.timestamp;
|
|
1078
|
-
// 如果超过
|
|
1079
|
-
|
|
1080
|
-
|
|
1673
|
+
// 如果超过 100ms 没有收到事件,可能鼠标已经进入 iframe
|
|
1674
|
+
// 在 iframe 上时,mousemove 事件会丢失,但 mouseup 事件仍然能收到
|
|
1675
|
+
// 所以只需要保持拖动状态,等待 mouseup 事件,不需要每帧都更新位置
|
|
1676
|
+
if (timeSinceLastMove > 100) {
|
|
1081
1677
|
// 使用最后记录的鼠标位置进行检测
|
|
1082
1678
|
const clientX = this.lastMousePosition.x;
|
|
1083
1679
|
const clientY = this.lastMousePosition.y;
|
|
1084
|
-
|
|
1085
|
-
// 只在超过 100ms 时记录警告,避免日志过多
|
|
1086
|
-
console.warn(`[IconManager] Active detection: No mousemove event for ${timeSinceLastMove}ms`, {
|
|
1087
|
-
lastMousePosition: { x: clientX, y: clientY },
|
|
1088
|
-
timestamp: this.lastMousePosition.timestamp,
|
|
1089
|
-
isDragging: this.isDragging,
|
|
1090
|
-
dragStarted: this.dragStarted
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
// 更新 iframe 缓存(更频繁,每帧更新)
|
|
1680
|
+
// 更新 iframe 缓存(不需要每帧更新,只在需要时更新)
|
|
1094
1681
|
if (this.cachedIframes.length === 0 ||
|
|
1095
1682
|
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
1096
1683
|
const allIframes = document.querySelectorAll('iframe');
|
|
1097
|
-
|
|
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 => ({
|
|
1098
1692
|
element: iframe,
|
|
1099
1693
|
rect: iframe.getBoundingClientRect()
|
|
1100
1694
|
}));
|
|
1101
1695
|
this.lastIframeUpdateTime = now;
|
|
1102
|
-
if (this.debug) {
|
|
1103
|
-
console.log(`[IconManager] Active detection: Updated iframe cache (${this.cachedIframes.length} iframes)`);
|
|
1104
|
-
}
|
|
1105
1696
|
}
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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;
|
|
1113
1707
|
if (this.debug) {
|
|
1114
|
-
console.log('[IconManager] Active detection: Mouse over iframe
|
|
1115
|
-
mousePosition: { x: clientX, y: clientY },
|
|
1116
|
-
iframeRect: {
|
|
1117
|
-
left: rect.left,
|
|
1118
|
-
top: rect.top,
|
|
1119
|
-
right: rect.right,
|
|
1120
|
-
bottom: rect.bottom
|
|
1121
|
-
},
|
|
1122
|
-
iframeSrc: element.src || 'no src',
|
|
1123
|
-
timeSinceLastMove
|
|
1124
|
-
});
|
|
1708
|
+
console.log('[IconManager] [PC+iframe] Active detection: Mouse over iframe, keeping drag state active (waiting for mouseup)');
|
|
1125
1709
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
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;
|
|
1128
1721
|
}
|
|
1129
1722
|
}
|
|
1130
1723
|
}
|
|
1131
1724
|
// 继续检测
|
|
1132
|
-
|
|
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);
|
|
1133
1733
|
};
|
|
1134
1734
|
this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
|
|
1135
1735
|
}
|
|
@@ -1145,6 +1745,40 @@ class IconManager {
|
|
|
1145
1745
|
this.activeDetectionRafId = null;
|
|
1146
1746
|
}
|
|
1147
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
|
+
}
|
|
1148
1782
|
/**
|
|
1149
1783
|
* 清理拖动事件
|
|
1150
1784
|
*/
|
|
@@ -1282,6 +1916,12 @@ class IconManager {
|
|
|
1282
1916
|
const containerRect = container.getBoundingClientRect();
|
|
1283
1917
|
const iconWidth = this.iconElement.offsetWidth || 30;
|
|
1284
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;
|
|
1285
1925
|
// 获取当前图标位置
|
|
1286
1926
|
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
1287
1927
|
const currentLeft = parseFloat(computedStyle.left) || 0;
|
|
@@ -1290,7 +1930,7 @@ class IconManager {
|
|
|
1290
1930
|
let newY = currentTop;
|
|
1291
1931
|
// X 轴磁性吸附
|
|
1292
1932
|
if (this.magneticDirection === 'x' || this.magneticDirection === 'both') {
|
|
1293
|
-
const containerWidth =
|
|
1933
|
+
const containerWidth = actualWidth; // 使用实际宽度(考虑 max-width)
|
|
1294
1934
|
const centerX = containerWidth / 2;
|
|
1295
1935
|
if (currentLeft < centerX) {
|
|
1296
1936
|
// 吸附到左边
|
|
@@ -1479,6 +2119,7 @@ class IconManager {
|
|
|
1479
2119
|
justifyContent: 'center',
|
|
1480
2120
|
zIndex: '1000001',
|
|
1481
2121
|
boxShadow: '0 -1px 4px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
2122
|
+
pointerEvents: 'auto', // 子元素启用指针事件,让 badge 可以接收事件
|
|
1482
2123
|
...(count === 0 && text === '' && {
|
|
1483
2124
|
width: '12px',
|
|
1484
2125
|
height: '12px',
|
|
@@ -1684,6 +2325,8 @@ class IframeManager {
|
|
|
1684
2325
|
// 创建包装容器(包含iframe和关闭按钮)
|
|
1685
2326
|
this.containerElement = document.createElement('div');
|
|
1686
2327
|
this.containerElement.className = 'customer-sdk-container';
|
|
2328
|
+
// 确保容器不可拖动
|
|
2329
|
+
this.containerElement.draggable = false;
|
|
1687
2330
|
// 创建iframe元素
|
|
1688
2331
|
this.iframeElement = document.createElement('iframe');
|
|
1689
2332
|
this.iframeElement.className = 'customer-sdk-iframe';
|
|
@@ -1756,24 +2399,29 @@ class IframeManager {
|
|
|
1756
2399
|
opacity: '0',
|
|
1757
2400
|
display: 'none'
|
|
1758
2401
|
} : (isPC ? {
|
|
1759
|
-
// PC模式:没有 target
|
|
2402
|
+
// PC模式:没有 target,固定在右下角(不可拖动)
|
|
1760
2403
|
width: `${this.config.width || 450}px`,
|
|
1761
|
-
height:
|
|
2404
|
+
height: `${this.config.height || 600}px`,
|
|
1762
2405
|
maxWidth: '90vw',
|
|
1763
|
-
maxHeight: '
|
|
2406
|
+
maxHeight: '90vh',
|
|
1764
2407
|
backgroundColor: '#ffffff',
|
|
1765
|
-
borderRadius: '
|
|
1766
|
-
boxShadow: '
|
|
2408
|
+
borderRadius: '8px',
|
|
2409
|
+
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.25)',
|
|
1767
2410
|
border: 'none',
|
|
1768
2411
|
position: 'fixed',
|
|
1769
2412
|
zIndex: '999999',
|
|
1770
|
-
// PC
|
|
1771
|
-
top: '
|
|
1772
|
-
left: '
|
|
1773
|
-
bottom: '
|
|
1774
|
-
right: '
|
|
1775
|
-
transform: '
|
|
2413
|
+
// PC模式:固定在右下角
|
|
2414
|
+
top: 'auto',
|
|
2415
|
+
left: 'auto',
|
|
2416
|
+
bottom: '20px',
|
|
2417
|
+
right: '20px',
|
|
2418
|
+
transform: 'none',
|
|
1776
2419
|
overflow: 'hidden',
|
|
2420
|
+
// 防止拖动和选择
|
|
2421
|
+
userSelect: 'none',
|
|
2422
|
+
WebkitUserSelect: 'none',
|
|
2423
|
+
MozUserSelect: 'none',
|
|
2424
|
+
msUserSelect: 'none',
|
|
1777
2425
|
// 初始隐藏的关键样式
|
|
1778
2426
|
visibility: 'hidden',
|
|
1779
2427
|
opacity: '0',
|
|
@@ -1813,6 +2461,28 @@ class IframeManager {
|
|
|
1813
2461
|
Object.assign(this.iframeElement.style, iframeStyles);
|
|
1814
2462
|
// 将iframe放入容器
|
|
1815
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
|
+
}
|
|
1816
2486
|
// 添加iframe加载事件监听(移动端样式优化)
|
|
1817
2487
|
this.iframeElement.addEventListener('load', () => {
|
|
1818
2488
|
// 移动端注入自定义样式
|
|
@@ -1970,10 +2640,23 @@ class IframeManager {
|
|
|
1970
2640
|
}
|
|
1971
2641
|
// 创建新的消息处理器并保存引用
|
|
1972
2642
|
this.messageHandler = (event) => {
|
|
1973
|
-
//
|
|
1974
|
-
|
|
1975
|
-
|
|
2643
|
+
// 关键:过滤掉自己通过 dispatchEvent 发送的消息,避免无限循环
|
|
2644
|
+
// 自己发送的消息 source 是 window,而 iframe 发送的消息 source 是 iframe.contentWindow
|
|
2645
|
+
if (event.source === window) {
|
|
2646
|
+
if (this.debug) {
|
|
2647
|
+
console.log('[IframeManager] Ignoring self-broadcasted message to prevent infinite loop:', event.data);
|
|
2648
|
+
}
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
// 不验证来源,直接处理所有消息(确保消息能够被接收)
|
|
2652
|
+
if (this.debug) {
|
|
2653
|
+
console.log('[IframeManager] Message received:', {
|
|
2654
|
+
data: event.data,
|
|
2655
|
+
origin: event.origin,
|
|
2656
|
+
source: event.source
|
|
2657
|
+
});
|
|
1976
2658
|
}
|
|
2659
|
+
this.handleIframeMessage(event.data);
|
|
1977
2660
|
};
|
|
1978
2661
|
window.addEventListener('message', this.messageHandler, false);
|
|
1979
2662
|
}
|
|
@@ -1982,7 +2665,7 @@ class IframeManager {
|
|
|
1982
2665
|
*/
|
|
1983
2666
|
handleIframeMessage(data) {
|
|
1984
2667
|
if (this.debug) {
|
|
1985
|
-
console.log('Message from iframe:', data);
|
|
2668
|
+
console.log('[IframeManager] Message from iframe received:', data);
|
|
1986
2669
|
}
|
|
1987
2670
|
// 判断data是字符串还是对象,兼容两种格式
|
|
1988
2671
|
let messageType;
|
|
@@ -1994,10 +2677,13 @@ class IframeManager {
|
|
|
1994
2677
|
}
|
|
1995
2678
|
else {
|
|
1996
2679
|
if (this.debug) {
|
|
1997
|
-
console.log('Unknown message format:', data);
|
|
2680
|
+
console.log('[IframeManager] Unknown message format:', data);
|
|
1998
2681
|
}
|
|
1999
2682
|
return;
|
|
2000
2683
|
}
|
|
2684
|
+
if (this.debug) {
|
|
2685
|
+
console.log('[IframeManager] Parsed message type:', messageType);
|
|
2686
|
+
}
|
|
2001
2687
|
// 根据消息类型处理不同的操作
|
|
2002
2688
|
switch (messageType) {
|
|
2003
2689
|
case 'iframe_ready':
|
|
@@ -2030,62 +2716,94 @@ class IframeManager {
|
|
|
2030
2716
|
}
|
|
2031
2717
|
break;
|
|
2032
2718
|
case 'goto-login':
|
|
2033
|
-
// 登录跳转消息 -
|
|
2719
|
+
// 登录跳转消息 - 广播给调用 SDK 的 Vue 页面
|
|
2034
2720
|
if (this.debug) {
|
|
2035
|
-
console.log('Received goto-login message,
|
|
2721
|
+
console.log('Received goto-login message, broadcasting to Vue page');
|
|
2036
2722
|
}
|
|
2037
|
-
this.
|
|
2723
|
+
this.broadcastMessageToPage(data);
|
|
2038
2724
|
if (this.config.onMessage) {
|
|
2039
2725
|
this.config.onMessage(messageType, data);
|
|
2040
2726
|
}
|
|
2041
2727
|
break;
|
|
2042
|
-
|
|
2043
|
-
//
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2728
|
+
case 'DCR_DEPOSIT':
|
|
2729
|
+
// 存款跳转消息 - 广播给调用 SDK 的 Vue 页面
|
|
2730
|
+
// 支持两种存款类型:
|
|
2731
|
+
// 1. 钱包直充:{ type: 'DCR_DEPOSIT', payload: { depositType: 0, walletId: 69382 } }
|
|
2732
|
+
// 2. 线下充值:{ type: 'DCR_DEPOSIT', payload: { depositType: 2, offlineTypeId: 2001, accountId: 35213 } }
|
|
2733
|
+
if (this.debug) {
|
|
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
|
+
});
|
|
2053
2741
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2742
|
+
this.broadcastMessageToPage(data);
|
|
2743
|
+
if (this.config.onMessage) {
|
|
2744
|
+
this.config.onMessage(messageType, data);
|
|
2745
|
+
}
|
|
2746
|
+
break;
|
|
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
|
+
});
|
|
2756
|
+
}
|
|
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);
|
|
2059
2766
|
}
|
|
2060
2767
|
break;
|
|
2061
2768
|
}
|
|
2062
2769
|
}
|
|
2063
2770
|
/**
|
|
2064
|
-
*
|
|
2065
|
-
*
|
|
2771
|
+
* 向调用 SDK 的地方(Vue 页面)广播消息
|
|
2772
|
+
* 通过 window.dispatchEvent 触发事件,让 Vue 页面中的 window.addEventListener('message') 可以收到
|
|
2773
|
+
*
|
|
2774
|
+
* 重要说明:
|
|
2775
|
+
* 1. window.dispatchEvent 创建的事件会在同一窗口内触发所有监听器,无论 origin 是什么,Vue 页面都能收到
|
|
2776
|
+
* 2. MessageEvent 的 origin 属性不能是 '*',必须是有效的 origin 字符串(如 'http://localhost:5173')
|
|
2777
|
+
* 3. 我们使用 window.location.origin 来标识消息来源(当前页面的 origin)
|
|
2778
|
+
* 4. 如果 Vue 页面需要检查 origin,应该检查 window.location.origin 而不是 '*'
|
|
2066
2779
|
*/
|
|
2067
|
-
|
|
2780
|
+
broadcastMessageToPage(data) {
|
|
2068
2781
|
try {
|
|
2069
|
-
//
|
|
2070
|
-
//
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
// 方式2:使用 dispatchEvent 创建 MessageEvent(在同一窗口内触发所有监听器)
|
|
2075
|
-
// 这样 Vue 页面中的 window.addEventListener('message') 也能收到
|
|
2782
|
+
// 使用 dispatchEvent 创建 MessageEvent,在同一窗口内触发所有监听器
|
|
2783
|
+
// 这样调用 SDK 的 Vue 页面中的 window.addEventListener('message') 也能收到
|
|
2784
|
+
// 注意:MessageEvent 的 origin 属性不能是 '*',必须是有效的 origin 字符串
|
|
2785
|
+
// 我们使用 window.location.origin 来标识消息来源(当前页面的 origin)
|
|
2786
|
+
// Vue 页面可以收到消息,因为 dispatchEvent 会在同一窗口内触发所有监听器
|
|
2076
2787
|
const messageEvent = new MessageEvent('message', {
|
|
2077
2788
|
data: data,
|
|
2078
|
-
origin: window.location.origin,
|
|
2079
|
-
source: window
|
|
2789
|
+
origin: window.location.origin, // 使用当前页面的 origin,不是 '*'(MessageEvent 不支持 '*')
|
|
2790
|
+
source: window,
|
|
2791
|
+
bubbles: true,
|
|
2792
|
+
cancelable: true
|
|
2080
2793
|
});
|
|
2081
2794
|
window.dispatchEvent(messageEvent);
|
|
2082
2795
|
if (this.debug) {
|
|
2083
|
-
console.log('Message
|
|
2796
|
+
console.log('[IframeManager] Message broadcasted to Vue page via window.dispatchEvent:', {
|
|
2797
|
+
data: data,
|
|
2798
|
+
origin: window.location.origin,
|
|
2799
|
+
type: 'message',
|
|
2800
|
+
note: 'origin is window.location.origin, not "*" (MessageEvent does not support "*")'
|
|
2801
|
+
});
|
|
2084
2802
|
}
|
|
2085
2803
|
}
|
|
2086
2804
|
catch (error) {
|
|
2087
2805
|
if (this.debug) {
|
|
2088
|
-
console.error('Failed to
|
|
2806
|
+
console.error('[IframeManager] Failed to broadcast message:', error);
|
|
2089
2807
|
}
|
|
2090
2808
|
}
|
|
2091
2809
|
}
|
|
@@ -2922,20 +3640,20 @@ function copyInputValue(node, cloned) {
|
|
|
2922
3640
|
}
|
|
2923
3641
|
|
|
2924
3642
|
const pseudoClasses = [
|
|
2925
|
-
"
|
|
2926
|
-
"
|
|
2927
|
-
// '
|
|
3643
|
+
"::before",
|
|
3644
|
+
"::after"
|
|
3645
|
+
// '::placeholder', TODO
|
|
2928
3646
|
];
|
|
2929
3647
|
const scrollbarPseudoClasses = [
|
|
2930
|
-
"
|
|
2931
|
-
"
|
|
2932
|
-
// '
|
|
2933
|
-
"
|
|
2934
|
-
"
|
|
2935
|
-
"
|
|
2936
|
-
// '
|
|
2937
|
-
"
|
|
2938
|
-
"
|
|
3648
|
+
"::-webkit-scrollbar",
|
|
3649
|
+
"::-webkit-scrollbar-button",
|
|
3650
|
+
// '::-webkit-scrollbar:horizontal', TODO
|
|
3651
|
+
"::-webkit-scrollbar-thumb",
|
|
3652
|
+
"::-webkit-scrollbar-track",
|
|
3653
|
+
"::-webkit-scrollbar-track-piece",
|
|
3654
|
+
// '::-webkit-scrollbar:vertical', TODO
|
|
3655
|
+
"::-webkit-scrollbar-corner",
|
|
3656
|
+
"::-webkit-resizer"
|
|
2939
3657
|
];
|
|
2940
3658
|
function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFamilies) {
|
|
2941
3659
|
const { ownerWindow, svgStyleElement, svgStyles, currentNodeStyle } = context;
|
|
@@ -2979,7 +3697,7 @@ function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFami
|
|
|
2979
3697
|
allClasses = [];
|
|
2980
3698
|
svgStyles.set(cssText, allClasses);
|
|
2981
3699
|
}
|
|
2982
|
-
allClasses.push(`.${klasses[0]}
|
|
3700
|
+
allClasses.push(`.${klasses[0]}${pseudoClass}`);
|
|
2983
3701
|
}
|
|
2984
3702
|
pseudoClasses.forEach(copyBy);
|
|
2985
3703
|
if (copyScrollbar)
|
|
@@ -21898,7 +22616,7 @@ class CustomerServiceSDK {
|
|
|
21898
22616
|
const initResult = {
|
|
21899
22617
|
deviceId,
|
|
21900
22618
|
iframeUrl,
|
|
21901
|
-
referrer: document.referrer,
|
|
22619
|
+
referrer: config.referrer || document.referrer || document.location.href,
|
|
21902
22620
|
agent: config.agent,
|
|
21903
22621
|
timestamp: Date.now()
|
|
21904
22622
|
};
|
|
@@ -22324,7 +23042,7 @@ class CustomerServiceSDK {
|
|
|
22324
23042
|
url.searchParams.set('Authorization', config.token);
|
|
22325
23043
|
}
|
|
22326
23044
|
url.searchParams.set('DeviceSign', deviceId);
|
|
22327
|
-
url.searchParams.set('Referrer', document.referrer);
|
|
23045
|
+
url.searchParams.set('Referrer', config.referrer || document.referrer);
|
|
22328
23046
|
return url.toString();
|
|
22329
23047
|
}
|
|
22330
23048
|
}
|