customer-chat-sdk 1.1.11 → 1.1.13
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 +47 -1
- package/dist/core/IconManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +705 -24
- package/dist/customer-sdk.esm.js +705 -24
- package/dist/customer-sdk.min.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/customer-sdk.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 直接使用base64字符串,避免打包后路径问题
|
|
2
2
|
const iconImage = 'data:image/png;base64,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';
|
|
3
3
|
class IconManager {
|
|
4
|
-
constructor(position, debug = false, target) {
|
|
4
|
+
constructor(position, debug = false, target, options) {
|
|
5
5
|
this.iconElement = null;
|
|
6
6
|
this.badgeElement = null;
|
|
7
7
|
this.onClickCallback = null;
|
|
@@ -27,10 +27,13 @@ class IconManager {
|
|
|
27
27
|
// 性能优化:缓存所有 iframe 的位置信息(用于检测鼠标是否在 iframe 上)
|
|
28
28
|
this.cachedIframes = [];
|
|
29
29
|
this.lastIframeUpdateTime = 0;
|
|
30
|
-
this.iframeUpdateInterval =
|
|
30
|
+
this.iframeUpdateInterval = 16; // 每 16ms 更新一次 iframe 位置信息(约一帧,更频繁,因为 iframe 可能移动)
|
|
31
31
|
// 性能优化:使用 requestAnimationFrame 节流位置更新
|
|
32
32
|
this.rafId = null;
|
|
33
33
|
this.pendingPosition = { x: 0, y: 0, needsUpdate: false };
|
|
34
|
+
// 主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置(即使没有 mousemove 事件)
|
|
35
|
+
this.activeDetectionRafId = null;
|
|
36
|
+
this.lastMousePosition = { x: 0, y: 0, timestamp: 0 }; // 最后记录的鼠标位置和时间戳
|
|
34
37
|
// 事件处理器引用(用于清理)
|
|
35
38
|
this.onDragHandler = null;
|
|
36
39
|
this.stopDragHandler = null;
|
|
@@ -39,10 +42,32 @@ class IconManager {
|
|
|
39
42
|
this.pointerLeaveHandler = null;
|
|
40
43
|
this.blurHandler = null;
|
|
41
44
|
this.dragTimeoutId = null; // 拖动超时定时器
|
|
45
|
+
this.mouseLeaveTimeout = null; // 鼠标离开防抖定时器
|
|
46
|
+
this.pointerLeaveTimeout = null; // 指针离开防抖定时器
|
|
47
|
+
this.isStoppingDrag = false; // 防止 stopDrag 重复调用
|
|
48
|
+
// 侧边吸附相关状态
|
|
49
|
+
this.sideAttach = true; // 是否启用侧边吸附(默认 true)
|
|
50
|
+
this.sideHideRatio = 0.5; // 侧边吸附时的隐藏比例(默认 0.5,显示一半)
|
|
51
|
+
this.magnetic = true; // 是否启用磁性吸附(默认 true)
|
|
52
|
+
this.magneticDirection = 'x'; // 磁性吸附方向(默认 'x')
|
|
53
|
+
this.margin = 10; // 边距(默认 10px)
|
|
54
|
+
this.isAttachedToSide = false; // 是否吸附到侧边
|
|
55
|
+
this.attachedSide = ''; // 吸附到哪一边
|
|
56
|
+
this.autoAttachDelay = 3000; // 自动吸附延迟时间(默认 3000ms,3秒)
|
|
57
|
+
this.autoAttachTimer = null; // 自动吸附定时器
|
|
42
58
|
this.iconPosition = position || null;
|
|
43
59
|
this.debug = debug;
|
|
44
60
|
// 保存 target(可以是 HTMLElement 或字符串选择器)
|
|
45
61
|
this.target = target || null;
|
|
62
|
+
// 侧边吸附配置
|
|
63
|
+
if (options) {
|
|
64
|
+
this.sideAttach = options.sideAttach !== undefined ? options.sideAttach : true;
|
|
65
|
+
this.sideHideRatio = options.sideHideRatio !== undefined ? options.sideHideRatio : 0.5;
|
|
66
|
+
this.magnetic = options.magnetic !== undefined ? options.magnetic : true;
|
|
67
|
+
this.magneticDirection = options.magneticDirection || 'x';
|
|
68
|
+
this.margin = options.margin !== undefined ? options.margin : 10;
|
|
69
|
+
this.autoAttachDelay = options.autoAttachDelay !== undefined ? options.autoAttachDelay : 3000;
|
|
70
|
+
}
|
|
46
71
|
}
|
|
47
72
|
/**
|
|
48
73
|
* 显示悬浮图标
|
|
@@ -96,8 +121,42 @@ class IconManager {
|
|
|
96
121
|
}
|
|
97
122
|
else {
|
|
98
123
|
// 默认位置:右下角
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
// 如果启用侧边吸附且没有指定位置,初始化时直接应用右边吸附样式
|
|
125
|
+
if (this.sideAttach && this.magnetic) {
|
|
126
|
+
const iconWidth = 30; // 图标宽度
|
|
127
|
+
const hideDistance = iconWidth * this.sideHideRatio; // 隐藏部分(在容器外)
|
|
128
|
+
// 右边吸附:图标右边缘对齐容器右边缘,隐藏部分在容器外
|
|
129
|
+
// right = -hideDistance,让图标向右移动 hideDistance,这样隐藏部分在容器外
|
|
130
|
+
defaultStyle.right = `-${hideDistance}px`; // 负值,让图标超出容器右边缘
|
|
131
|
+
defaultStyle.left = 'auto';
|
|
132
|
+
defaultStyle.bottom = '80px';
|
|
133
|
+
defaultStyle.transform = 'translateX(0)';
|
|
134
|
+
// 标记为已吸附到右边
|
|
135
|
+
this.isAttachedToSide = true;
|
|
136
|
+
this.attachedSide = 'right';
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
defaultStyle.bottom = '80px';
|
|
140
|
+
defaultStyle.right = '20px';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 如果侧边吸附已启用且已吸附,应用侧边吸附样式
|
|
144
|
+
if (this.sideAttach && this.isAttachedToSide && this.attachedSide) {
|
|
145
|
+
const iconWidth = 30; // 图标宽度
|
|
146
|
+
const hideDistance = iconWidth * this.sideHideRatio; // 隐藏部分(在容器外)
|
|
147
|
+
if (this.attachedSide === 'left') {
|
|
148
|
+
// 左边吸附:图标左边缘对齐容器左边缘,隐藏部分在容器外
|
|
149
|
+
defaultStyle.left = `-${hideDistance}px`; // 负值,让图标超出容器左边缘
|
|
150
|
+
defaultStyle.right = 'auto';
|
|
151
|
+
defaultStyle.transform = 'translateX(0)';
|
|
152
|
+
}
|
|
153
|
+
else if (this.attachedSide === 'right') {
|
|
154
|
+
// 右边吸附:图标右边缘对齐容器右边缘,隐藏部分在容器外
|
|
155
|
+
// right = -hideDistance,让图标向右移动 hideDistance,这样隐藏部分在容器外
|
|
156
|
+
defaultStyle.right = `-${hideDistance}px`; // 负值,让图标超出容器右边缘
|
|
157
|
+
defaultStyle.left = 'auto';
|
|
158
|
+
defaultStyle.transform = 'translateX(0)';
|
|
159
|
+
}
|
|
101
160
|
}
|
|
102
161
|
Object.assign(this.iconElement.style, defaultStyle);
|
|
103
162
|
// 添加图标图片(直接使用base64字符串,避免打包后路径问题)
|
|
@@ -153,6 +212,72 @@ class IconManager {
|
|
|
153
212
|
}
|
|
154
213
|
// 设置拖动事件
|
|
155
214
|
this.setupDragEvents();
|
|
215
|
+
// 如果启用磁性吸附且没有指定位置,初始化后自动吸附到右边(默认显示一半)
|
|
216
|
+
if (this.magnetic && !this.iconPosition) {
|
|
217
|
+
// 等待图标渲染完成后再执行吸附
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
if (this.iconElement) {
|
|
220
|
+
// 先转换位置(从 right/bottom 到 left/top)
|
|
221
|
+
const container = this.getTargetElement();
|
|
222
|
+
if (container) {
|
|
223
|
+
const containerRect = container.getBoundingClientRect();
|
|
224
|
+
const iconWidth = this.iconElement.offsetWidth || 30;
|
|
225
|
+
const iconHeight = this.iconElement.offsetHeight || 30;
|
|
226
|
+
// 计算默认右下角位置对应的 left/top
|
|
227
|
+
const defaultRight = 20; // 默认 right: 20px
|
|
228
|
+
const defaultBottom = 80; // 默认 bottom: 80px
|
|
229
|
+
// 转换为 left/top(相对于容器)
|
|
230
|
+
const initialLeft = containerRect.width - iconWidth - defaultRight;
|
|
231
|
+
const initialTop = containerRect.height - iconHeight - defaultBottom;
|
|
232
|
+
// 临时设置位置,用于磁性吸附计算
|
|
233
|
+
this.iconElement.style.left = `${initialLeft}px`;
|
|
234
|
+
this.iconElement.style.top = `${initialTop}px`;
|
|
235
|
+
this.iconElement.style.right = 'auto';
|
|
236
|
+
this.iconElement.style.bottom = 'auto';
|
|
237
|
+
// 如果启用侧边吸附,强制吸附到右边并显示一半
|
|
238
|
+
if (this.sideAttach) {
|
|
239
|
+
this.isAttachedToSide = true;
|
|
240
|
+
this.attachedSide = 'right';
|
|
241
|
+
const hideDistance = iconWidth * this.sideHideRatio; // 隐藏部分(在容器外)
|
|
242
|
+
// right = -hideDistance,让图标向右移动 hideDistance,这样隐藏部分在容器外
|
|
243
|
+
this.iconElement.style.right = `-${hideDistance}px`; // 负值,让图标超出容器右边缘
|
|
244
|
+
this.iconElement.style.left = 'auto';
|
|
245
|
+
this.iconElement.style.bottom = `${defaultBottom}px`; // 保持底部位置
|
|
246
|
+
this.iconElement.style.top = 'auto';
|
|
247
|
+
this.iconElement.style.transform = 'translateX(0)';
|
|
248
|
+
this.iconElement.style.transition = 'all 0.3s ease';
|
|
249
|
+
if (this.debug) {
|
|
250
|
+
console.log('[IconManager] Auto side attach on init', {
|
|
251
|
+
isAttachedToSide: this.isAttachedToSide,
|
|
252
|
+
attachedSide: this.attachedSide,
|
|
253
|
+
hideDistance,
|
|
254
|
+
right: `-${hideDistance}px`
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// 执行磁性吸附(不侧边吸附,只磁性吸附)
|
|
260
|
+
this.magneticSnap();
|
|
261
|
+
if (this.debug) {
|
|
262
|
+
console.log('[IconManager] Auto magnetic snap on init', {
|
|
263
|
+
isAttachedToSide: this.isAttachedToSide,
|
|
264
|
+
attachedSide: this.attachedSide
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}, 200); // 延迟 200ms,确保容器和图标都已渲染完成
|
|
271
|
+
}
|
|
272
|
+
// 启动自动吸附定时器(如果启用侧边吸附且当前未吸附)
|
|
273
|
+
// 注意:初始化时如果已经吸附到侧边,不会启动定时器
|
|
274
|
+
// 但如果图标从隐藏状态恢复显示,且未吸附,应该启动定时器
|
|
275
|
+
if (this.sideAttach && this.autoAttachDelay > 0 && !this.isAttachedToSide) {
|
|
276
|
+
// 延迟启动,确保图标已经完全渲染
|
|
277
|
+
setTimeout(() => {
|
|
278
|
+
this.startAutoAttachTimer();
|
|
279
|
+
}, 300); // 延迟 300ms,确保初始化吸附逻辑已完成
|
|
280
|
+
}
|
|
156
281
|
if (this.debug) {
|
|
157
282
|
console.log('CustomerSDK icon displayed');
|
|
158
283
|
}
|
|
@@ -169,6 +294,8 @@ class IconManager {
|
|
|
169
294
|
hide() {
|
|
170
295
|
// 清理拖动事件
|
|
171
296
|
this.cleanupDragEvents();
|
|
297
|
+
// 清除自动吸附定时器
|
|
298
|
+
this.clearAutoAttachTimer();
|
|
172
299
|
if (this.iconElement) {
|
|
173
300
|
// 在隐藏前保存当前位置(如果图标已经被拖动过)
|
|
174
301
|
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
@@ -230,6 +357,110 @@ class IconManager {
|
|
|
230
357
|
setStyle(style) {
|
|
231
358
|
// 现在样式写死,不做处理
|
|
232
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* 启动自动吸附定时器
|
|
362
|
+
*/
|
|
363
|
+
startAutoAttachTimer() {
|
|
364
|
+
// 清除之前的定时器
|
|
365
|
+
this.clearAutoAttachTimer();
|
|
366
|
+
// 如果已经吸附到侧边,不需要启动定时器
|
|
367
|
+
if (this.isAttachedToSide) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this.autoAttachTimer = window.setTimeout(() => {
|
|
371
|
+
if (this.iconElement && !this.isDragging && !this.dragStarted) {
|
|
372
|
+
// 执行自动吸附到侧边
|
|
373
|
+
this.autoAttachToSide();
|
|
374
|
+
}
|
|
375
|
+
this.autoAttachTimer = null;
|
|
376
|
+
}, this.autoAttachDelay);
|
|
377
|
+
if (this.debug) {
|
|
378
|
+
console.log(`[IconManager] Auto attach timer started, will attach in ${this.autoAttachDelay}ms`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 清除自动吸附定时器
|
|
383
|
+
*/
|
|
384
|
+
clearAutoAttachTimer() {
|
|
385
|
+
if (this.autoAttachTimer !== null) {
|
|
386
|
+
window.clearTimeout(this.autoAttachTimer);
|
|
387
|
+
this.autoAttachTimer = null;
|
|
388
|
+
if (this.debug) {
|
|
389
|
+
console.log('[IconManager] Auto attach timer cleared');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 自动吸附到侧边
|
|
395
|
+
*/
|
|
396
|
+
autoAttachToSide() {
|
|
397
|
+
if (!this.iconElement || !this.sideAttach)
|
|
398
|
+
return;
|
|
399
|
+
try {
|
|
400
|
+
const container = this.getTargetElement();
|
|
401
|
+
if (!container)
|
|
402
|
+
return;
|
|
403
|
+
const containerRect = container.getBoundingClientRect();
|
|
404
|
+
const iconWidth = this.iconElement.offsetWidth || 30;
|
|
405
|
+
// 获取当前图标位置
|
|
406
|
+
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
407
|
+
const currentLeft = parseFloat(computedStyle.left) || 0;
|
|
408
|
+
const currentRight = computedStyle.right;
|
|
409
|
+
// 判断应该吸附到哪一边(根据当前位置)
|
|
410
|
+
const containerWidth = containerRect.width;
|
|
411
|
+
let shouldAttachToRight = true;
|
|
412
|
+
// 如果使用 right 定位,计算实际 left 位置
|
|
413
|
+
if (currentRight !== 'auto' && currentRight !== '') {
|
|
414
|
+
const rightValue = parseFloat(currentRight);
|
|
415
|
+
if (!isNaN(rightValue)) {
|
|
416
|
+
const actualLeft = containerWidth - iconWidth - rightValue;
|
|
417
|
+
shouldAttachToRight = actualLeft >= containerWidth / 2;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
shouldAttachToRight = currentLeft >= containerWidth / 2;
|
|
422
|
+
}
|
|
423
|
+
if (shouldAttachToRight) {
|
|
424
|
+
// 吸附到右边
|
|
425
|
+
this.isAttachedToSide = true;
|
|
426
|
+
this.attachedSide = 'right';
|
|
427
|
+
const hideDistance = iconWidth * this.sideHideRatio;
|
|
428
|
+
this.iconElement.style.right = `-${hideDistance}px`;
|
|
429
|
+
this.iconElement.style.left = 'auto';
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// 吸附到左边
|
|
433
|
+
this.isAttachedToSide = true;
|
|
434
|
+
this.attachedSide = 'left';
|
|
435
|
+
const hideDistance = iconWidth * this.sideHideRatio;
|
|
436
|
+
this.iconElement.style.left = `-${hideDistance}px`;
|
|
437
|
+
this.iconElement.style.right = 'auto';
|
|
438
|
+
}
|
|
439
|
+
// 保持 Y 轴位置不变
|
|
440
|
+
const computedStyleAfter = window.getComputedStyle(this.iconElement);
|
|
441
|
+
if (computedStyleAfter.top !== 'auto' && computedStyleAfter.top !== '') {
|
|
442
|
+
this.iconElement.style.top = computedStyleAfter.top;
|
|
443
|
+
this.iconElement.style.bottom = 'auto';
|
|
444
|
+
}
|
|
445
|
+
else if (computedStyleAfter.bottom !== 'auto' && computedStyleAfter.bottom !== '') {
|
|
446
|
+
this.iconElement.style.bottom = computedStyleAfter.bottom;
|
|
447
|
+
this.iconElement.style.top = 'auto';
|
|
448
|
+
}
|
|
449
|
+
this.iconElement.style.transform = 'translateX(0)';
|
|
450
|
+
this.iconElement.style.transition = 'all 0.3s ease';
|
|
451
|
+
if (this.debug) {
|
|
452
|
+
console.log('[IconManager] Auto attached to side', {
|
|
453
|
+
attachedSide: this.attachedSide,
|
|
454
|
+
hideDistance: iconWidth * this.sideHideRatio
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
if (this.debug) {
|
|
460
|
+
console.error('[IconManager] Error in autoAttachToSide:', error);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
233
464
|
/**
|
|
234
465
|
* 设置点击回调
|
|
235
466
|
*/
|
|
@@ -297,6 +528,37 @@ class IconManager {
|
|
|
297
528
|
startDrag(e) {
|
|
298
529
|
if (!this.iconElement || !this.isClickEnabled)
|
|
299
530
|
return;
|
|
531
|
+
// 清除自动吸附定时器(用户开始拖动)
|
|
532
|
+
this.clearAutoAttachTimer();
|
|
533
|
+
// 如果正在停止拖动,等待完成后再允许新的拖动
|
|
534
|
+
if (this.isStoppingDrag) {
|
|
535
|
+
if (this.debug) {
|
|
536
|
+
console.warn('[IconManager] startDrag called while stopping, ignoring');
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// 如果已经在拖动中,忽略新的拖动开始事件(防止重复调用)
|
|
541
|
+
// 注意:只检查实际的拖动状态,不检查可能残留的资源(这些应该在 cleanup 中清理)
|
|
542
|
+
if (this.isDragging || this.dragStarted) {
|
|
543
|
+
if (this.debug) {
|
|
544
|
+
console.warn('[IconManager] startDrag called but already dragging, ignoring', {
|
|
545
|
+
isDragging: this.isDragging,
|
|
546
|
+
dragStarted: this.dragStarted
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// 如果发现残留的资源,先清理它们(防御性编程)
|
|
552
|
+
if (this.dragTimeoutId !== null || this.activeDetectionRafId !== null || this.rafId !== null) {
|
|
553
|
+
if (this.debug) {
|
|
554
|
+
console.warn('[IconManager] startDrag found leftover resources, cleaning up', {
|
|
555
|
+
hasTimeout: this.dragTimeoutId !== null,
|
|
556
|
+
hasActiveDetection: this.activeDetectionRafId !== null,
|
|
557
|
+
hasRaf: this.rafId !== null
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
this.cleanupDragEvents();
|
|
561
|
+
}
|
|
300
562
|
// 检查事件目标:如果是 iframe 相关元素,不处理
|
|
301
563
|
const target = e.target;
|
|
302
564
|
if (target && (target.tagName === 'IFRAME' ||
|
|
@@ -338,6 +600,18 @@ class IconManager {
|
|
|
338
600
|
rect: iframe.getBoundingClientRect()
|
|
339
601
|
}));
|
|
340
602
|
this.lastIframeUpdateTime = Date.now();
|
|
603
|
+
if (this.debug) {
|
|
604
|
+
console.log(`[IconManager] Drag start - Found ${this.cachedIframes.length} iframe(s)`, {
|
|
605
|
+
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
606
|
+
left: rect.left,
|
|
607
|
+
top: rect.top,
|
|
608
|
+
right: rect.right,
|
|
609
|
+
bottom: rect.bottom,
|
|
610
|
+
width: rect.width,
|
|
611
|
+
height: rect.height
|
|
612
|
+
}))
|
|
613
|
+
});
|
|
614
|
+
}
|
|
341
615
|
// 添加 document 事件监听器
|
|
342
616
|
if (this.onDragHandler) {
|
|
343
617
|
document.addEventListener('mousemove', this.onDragHandler);
|
|
@@ -349,38 +623,65 @@ class IconManager {
|
|
|
349
623
|
}
|
|
350
624
|
// 添加处理 iframe 上事件丢失的机制
|
|
351
625
|
// 1. 监听 mouseleave 和 pointerleave 事件(鼠标离开窗口时停止拖动)
|
|
626
|
+
// 注意:这些事件可能会在拖动过程中多次触发,需要添加防抖机制
|
|
352
627
|
this.mouseLeaveHandler = (e) => {
|
|
353
628
|
// 只有当鼠标真正离开窗口时才停止拖动
|
|
354
629
|
if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
|
|
355
630
|
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
|
|
356
|
-
|
|
357
|
-
|
|
631
|
+
// 添加防抖,避免重复触发
|
|
632
|
+
if (this.mouseLeaveTimeout) {
|
|
633
|
+
clearTimeout(this.mouseLeaveTimeout);
|
|
358
634
|
}
|
|
359
|
-
this.
|
|
635
|
+
this.mouseLeaveTimeout = window.setTimeout(() => {
|
|
636
|
+
if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag) {
|
|
637
|
+
if (this.debug) {
|
|
638
|
+
console.log('[IconManager] Mouse left window, stopping drag');
|
|
639
|
+
}
|
|
640
|
+
this.stopDrag();
|
|
641
|
+
}
|
|
642
|
+
this.mouseLeaveTimeout = null;
|
|
643
|
+
}, 50); // 50ms 防抖
|
|
360
644
|
}
|
|
361
645
|
};
|
|
362
646
|
this.pointerLeaveHandler = (e) => {
|
|
363
647
|
// 只有当指针真正离开窗口时才停止拖动
|
|
364
648
|
if (!e.relatedTarget && (e.clientY <= 0 || e.clientX <= 0 ||
|
|
365
649
|
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight)) {
|
|
366
|
-
|
|
367
|
-
|
|
650
|
+
// 添加防抖,避免重复触发
|
|
651
|
+
if (this.pointerLeaveTimeout) {
|
|
652
|
+
clearTimeout(this.pointerLeaveTimeout);
|
|
368
653
|
}
|
|
369
|
-
this.
|
|
654
|
+
this.pointerLeaveTimeout = window.setTimeout(() => {
|
|
655
|
+
if ((this.isDragging || this.dragStarted) && !this.isStoppingDrag) {
|
|
656
|
+
if (this.debug) {
|
|
657
|
+
console.log('[IconManager] Pointer left window, stopping drag');
|
|
658
|
+
}
|
|
659
|
+
this.stopDrag();
|
|
660
|
+
}
|
|
661
|
+
this.pointerLeaveTimeout = null;
|
|
662
|
+
}, 50); // 50ms 防抖
|
|
370
663
|
}
|
|
371
664
|
};
|
|
372
665
|
document.addEventListener('mouseleave', this.mouseLeaveHandler);
|
|
373
666
|
document.addEventListener('pointerleave', this.pointerLeaveHandler);
|
|
374
667
|
// 2. 监听 blur 事件(窗口失去焦点时停止拖动)
|
|
375
668
|
this.blurHandler = () => {
|
|
669
|
+
// 如果已经在停止拖动,直接返回
|
|
670
|
+
if (this.isStoppingDrag) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// 检查是否真的在拖动中
|
|
674
|
+
if (!this.isDragging && !this.dragStarted) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
376
677
|
if (this.debug) {
|
|
377
|
-
console.log('Window lost focus, stopping drag');
|
|
678
|
+
console.log('[IconManager] Window lost focus, stopping drag');
|
|
378
679
|
}
|
|
379
680
|
this.stopDrag();
|
|
380
681
|
};
|
|
381
682
|
window.addEventListener('blur', this.blurHandler);
|
|
382
683
|
// 3. 添加超时机制(如果一段时间没有收到 mousemove 事件,自动停止拖动)
|
|
383
|
-
// 优化:缩短超时时间到
|
|
684
|
+
// 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
384
685
|
this.dragTimeoutId = window.setTimeout(() => {
|
|
385
686
|
if (this.isDragging) {
|
|
386
687
|
if (this.debug) {
|
|
@@ -388,9 +689,21 @@ class IconManager {
|
|
|
388
689
|
}
|
|
389
690
|
this.stopDrag();
|
|
390
691
|
}
|
|
391
|
-
},
|
|
692
|
+
}, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
693
|
+
// 4. 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
|
|
694
|
+
// 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
|
|
695
|
+
this.startActiveDetection();
|
|
392
696
|
if (this.debug) {
|
|
393
|
-
console.log('Drag start'
|
|
697
|
+
console.log('[IconManager] Drag start', {
|
|
698
|
+
startPosition: { x: clientX, y: clientY },
|
|
699
|
+
iconRect: {
|
|
700
|
+
left: iconRect.left,
|
|
701
|
+
top: iconRect.top,
|
|
702
|
+
width: iconRect.width,
|
|
703
|
+
height: iconRect.height
|
|
704
|
+
},
|
|
705
|
+
dragOffset: this.dragOffset
|
|
706
|
+
});
|
|
394
707
|
}
|
|
395
708
|
}
|
|
396
709
|
catch (error) {
|
|
@@ -418,39 +731,79 @@ class IconManager {
|
|
|
418
731
|
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
419
732
|
// 更新 iframe 缓存
|
|
420
733
|
const allIframes = document.querySelectorAll('iframe');
|
|
734
|
+
const previousCount = this.cachedIframes.length;
|
|
421
735
|
this.cachedIframes = Array.from(allIframes).map(iframe => ({
|
|
422
736
|
element: iframe,
|
|
423
737
|
rect: iframe.getBoundingClientRect()
|
|
424
738
|
}));
|
|
425
739
|
this.lastIframeUpdateTime = now;
|
|
740
|
+
if (this.debug && this.cachedIframes.length !== previousCount) {
|
|
741
|
+
console.log(`[IconManager] Iframe cache updated - Found ${this.cachedIframes.length} iframe(s)`, {
|
|
742
|
+
iframes: this.cachedIframes.map(({ rect }) => ({
|
|
743
|
+
left: rect.left,
|
|
744
|
+
top: rect.top,
|
|
745
|
+
right: rect.right,
|
|
746
|
+
bottom: rect.bottom
|
|
747
|
+
}))
|
|
748
|
+
});
|
|
749
|
+
}
|
|
426
750
|
}
|
|
427
751
|
// 检查鼠标是否在任何 iframe 上
|
|
428
|
-
for (const { rect } of this.cachedIframes) {
|
|
752
|
+
for (const { rect, element } of this.cachedIframes) {
|
|
429
753
|
if (clientX >= rect.left &&
|
|
430
754
|
clientX <= rect.right &&
|
|
431
755
|
clientY >= rect.top &&
|
|
432
756
|
clientY <= rect.bottom) {
|
|
433
757
|
// 鼠标在 iframe 上,立即停止拖动
|
|
434
758
|
if (this.debug) {
|
|
435
|
-
console.log('Mouse over iframe, stopping drag immediately'
|
|
759
|
+
console.log('[IconManager] Mouse over iframe detected, stopping drag immediately', {
|
|
760
|
+
mousePosition: { x: clientX, y: clientY },
|
|
761
|
+
iframeRect: {
|
|
762
|
+
left: rect.left,
|
|
763
|
+
top: rect.top,
|
|
764
|
+
right: rect.right,
|
|
765
|
+
bottom: rect.bottom
|
|
766
|
+
},
|
|
767
|
+
iframeSrc: element.src || 'no src'
|
|
768
|
+
});
|
|
436
769
|
}
|
|
437
770
|
this.stopDrag();
|
|
438
771
|
return;
|
|
439
772
|
}
|
|
440
773
|
}
|
|
441
774
|
}
|
|
775
|
+
// 更新最后记录的鼠标位置和时间戳(用于主动检测)
|
|
776
|
+
const now = Date.now();
|
|
777
|
+
const timeSinceLastUpdate = now - this.lastMousePosition.timestamp;
|
|
778
|
+
this.lastMousePosition = {
|
|
779
|
+
x: clientX,
|
|
780
|
+
y: clientY,
|
|
781
|
+
timestamp: now
|
|
782
|
+
};
|
|
783
|
+
// 如果距离上次更新超过 50ms,记录警告(可能事件丢失)
|
|
784
|
+
if (this.debug && timeSinceLastUpdate > 50 && this.lastMousePosition.timestamp > 0) {
|
|
785
|
+
console.warn(`[IconManager] Long gap between mousemove events: ${timeSinceLastUpdate}ms`, {
|
|
786
|
+
lastPosition: { x: this.lastMousePosition.x, y: this.lastMousePosition.y },
|
|
787
|
+
currentPosition: { x: clientX, y: clientY }
|
|
788
|
+
});
|
|
789
|
+
}
|
|
442
790
|
// 重置超时定时器(每次移动都重置,确保只有真正停止移动时才触发超时)
|
|
443
|
-
// 优化:缩短超时时间到
|
|
791
|
+
// 优化:缩短超时时间到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
444
792
|
if (this.dragTimeoutId !== null) {
|
|
445
793
|
window.clearTimeout(this.dragTimeoutId);
|
|
446
794
|
this.dragTimeoutId = window.setTimeout(() => {
|
|
447
795
|
if (this.isDragging) {
|
|
448
796
|
if (this.debug) {
|
|
449
|
-
|
|
797
|
+
const timeSinceLastMove = Date.now() - this.lastMousePosition.timestamp;
|
|
798
|
+
console.warn('[IconManager] Drag timeout triggered, stopping drag (likely mouse moved over iframe)', {
|
|
799
|
+
timeSinceLastMove,
|
|
800
|
+
lastMousePosition: this.lastMousePosition,
|
|
801
|
+
iframeCount: this.cachedIframes.length
|
|
802
|
+
});
|
|
450
803
|
}
|
|
451
804
|
this.stopDrag();
|
|
452
805
|
}
|
|
453
|
-
},
|
|
806
|
+
}, 50); // 缩短到 50ms,更快检测到事件丢失(特别是嵌套 iframe 场景)
|
|
454
807
|
}
|
|
455
808
|
// 检查是否有足够的移动距离
|
|
456
809
|
const deltaX = Math.abs(clientX - this.lastTouchPosition.x);
|
|
@@ -462,6 +815,14 @@ class IconManager {
|
|
|
462
815
|
// 开始真正的拖拽
|
|
463
816
|
this.dragStarted = true;
|
|
464
817
|
this.isDragging = true;
|
|
818
|
+
// 拖拽开始时重置侧边吸附状态
|
|
819
|
+
if (this.isAttachedToSide) {
|
|
820
|
+
this.isAttachedToSide = false;
|
|
821
|
+
this.attachedSide = '';
|
|
822
|
+
if (this.iconElement) {
|
|
823
|
+
this.iconElement.style.transform = '';
|
|
824
|
+
}
|
|
825
|
+
}
|
|
465
826
|
// 如果是第一次拖动,需要转换位置(从 right/bottom 到 left/top)
|
|
466
827
|
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
467
828
|
if (computedStyle.right !== 'auto' || computedStyle.bottom !== 'auto') {
|
|
@@ -562,9 +923,39 @@ class IconManager {
|
|
|
562
923
|
* 停止拖动
|
|
563
924
|
*/
|
|
564
925
|
stopDrag(_e) {
|
|
926
|
+
// 防止重复调用
|
|
927
|
+
if (this.isStoppingDrag) {
|
|
928
|
+
if (this.debug) {
|
|
929
|
+
console.warn('[IconManager] stopDrag already in progress, ignoring duplicate call');
|
|
930
|
+
}
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
// 如果已经停止且没有相关资源,直接返回
|
|
934
|
+
if (!this.isDragging && !this.dragStarted && !this.dragTimeoutId && !this.activeDetectionRafId) {
|
|
935
|
+
if (this.debug) {
|
|
936
|
+
console.warn('[IconManager] stopDrag called but not dragging, ignoring');
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
this.isStoppingDrag = true;
|
|
941
|
+
const stopReason = this.isDragging ? 'drag_stopped' : 'not_dragging';
|
|
942
|
+
const finalPosition = this.iconElement ? {
|
|
943
|
+
left: this.iconElement.style.left,
|
|
944
|
+
top: this.iconElement.style.top,
|
|
945
|
+
computed: window.getComputedStyle(this.iconElement).left !== 'auto'
|
|
946
|
+
? { left: window.getComputedStyle(this.iconElement).left, top: window.getComputedStyle(this.iconElement).top }
|
|
947
|
+
: null
|
|
948
|
+
} : null;
|
|
949
|
+
// 立即清理事件监听器,防止后续事件触发
|
|
565
950
|
this.cleanupDragEvents();
|
|
566
|
-
if (!this.iconElement)
|
|
951
|
+
if (!this.iconElement) {
|
|
952
|
+
// 即使没有 iconElement,也要重置状态,确保可以再次拖动
|
|
953
|
+
this.hasMoved = false;
|
|
954
|
+
this.isDragging = false;
|
|
955
|
+
this.dragStarted = false;
|
|
956
|
+
this.isStoppingDrag = false;
|
|
567
957
|
return;
|
|
958
|
+
}
|
|
568
959
|
// 恢复样式
|
|
569
960
|
this.iconElement.style.transition = 'transform 0.2s ease';
|
|
570
961
|
this.iconElement.style.cursor = 'pointer';
|
|
@@ -572,15 +963,22 @@ class IconManager {
|
|
|
572
963
|
const touchDuration = Date.now() - this.touchStartTime;
|
|
573
964
|
const isValidClick = !this.hasMoved && touchDuration < 1000 && !this.dragStarted;
|
|
574
965
|
if (this.debug) {
|
|
575
|
-
console.log('Drag end', {
|
|
966
|
+
console.log('[IconManager] Drag end', {
|
|
967
|
+
stopReason,
|
|
576
968
|
hasMoved: this.hasMoved,
|
|
577
969
|
dragStarted: this.dragStarted,
|
|
578
970
|
touchDuration,
|
|
579
|
-
isValidClick
|
|
971
|
+
isValidClick,
|
|
972
|
+
finalPosition,
|
|
973
|
+
lastMousePosition: this.lastMousePosition
|
|
580
974
|
});
|
|
581
975
|
}
|
|
582
976
|
// 先保存点击状态,然后重置拖动状态
|
|
583
977
|
const wasClick = isValidClick;
|
|
978
|
+
// 如果真正拖动过,执行磁性吸附
|
|
979
|
+
if (this.dragStarted && this.magnetic && this.iconElement) {
|
|
980
|
+
this.magneticSnap();
|
|
981
|
+
}
|
|
584
982
|
// 如果真正拖动过,保存当前位置到 iconPosition
|
|
585
983
|
if (this.dragStarted && this.isDragging && this.iconElement) {
|
|
586
984
|
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
@@ -605,6 +1003,11 @@ class IconManager {
|
|
|
605
1003
|
this.hasMoved = false;
|
|
606
1004
|
this.isDragging = false;
|
|
607
1005
|
this.dragStarted = false;
|
|
1006
|
+
this.isStoppingDrag = false; // 重置停止标志
|
|
1007
|
+
// 如果拖动后没有吸附到侧边,启动自动吸附定时器
|
|
1008
|
+
if (!this.isAttachedToSide && this.sideAttach && this.autoAttachDelay > 0) {
|
|
1009
|
+
this.startAutoAttachTimer();
|
|
1010
|
+
}
|
|
608
1011
|
if (wasClick) {
|
|
609
1012
|
// 是点击,触发点击事件
|
|
610
1013
|
// 延迟触发,确保拖动状态已完全重置,并且没有新的拖动开始
|
|
@@ -616,6 +1019,102 @@ class IconManager {
|
|
|
616
1019
|
}, 100); // 增加延迟到 100ms,确保拖动状态完全重置
|
|
617
1020
|
}
|
|
618
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* 启动主动检测机制:使用 requestAnimationFrame 定期检查鼠标位置
|
|
1024
|
+
* 即使没有 mousemove 事件,也能检测到鼠标是否进入 iframe
|
|
1025
|
+
*/
|
|
1026
|
+
startActiveDetection() {
|
|
1027
|
+
if (this.activeDetectionRafId !== null) {
|
|
1028
|
+
if (this.debug) {
|
|
1029
|
+
console.log('[IconManager] Active detection already running');
|
|
1030
|
+
}
|
|
1031
|
+
return; // 已经在运行
|
|
1032
|
+
}
|
|
1033
|
+
if (this.debug) {
|
|
1034
|
+
console.log('[IconManager] Starting active detection mechanism');
|
|
1035
|
+
}
|
|
1036
|
+
const checkMousePosition = () => {
|
|
1037
|
+
// 检查是否还在拖动中,如果不在拖动中或正在停止,则停止检测
|
|
1038
|
+
if (!this.isDragging || this.isStoppingDrag) {
|
|
1039
|
+
if (this.debug) {
|
|
1040
|
+
console.log('[IconManager] Active detection stopped (drag ended or stopping)');
|
|
1041
|
+
}
|
|
1042
|
+
this.activeDetectionRafId = null;
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
// 检查是否长时间没有收到 mousemove 事件(超过 50ms)
|
|
1046
|
+
const now = Date.now();
|
|
1047
|
+
const timeSinceLastMove = now - this.lastMousePosition.timestamp;
|
|
1048
|
+
// 如果超过 50ms 没有收到事件,进行检测
|
|
1049
|
+
if (timeSinceLastMove > 50) {
|
|
1050
|
+
// 长时间没有收到事件,可能鼠标已经进入 iframe
|
|
1051
|
+
// 使用最后记录的鼠标位置进行检测
|
|
1052
|
+
const clientX = this.lastMousePosition.x;
|
|
1053
|
+
const clientY = this.lastMousePosition.y;
|
|
1054
|
+
if (this.debug && timeSinceLastMove > 100) {
|
|
1055
|
+
// 只在超过 100ms 时记录警告,避免日志过多
|
|
1056
|
+
console.warn(`[IconManager] Active detection: No mousemove event for ${timeSinceLastMove}ms`, {
|
|
1057
|
+
lastMousePosition: { x: clientX, y: clientY },
|
|
1058
|
+
timestamp: this.lastMousePosition.timestamp,
|
|
1059
|
+
isDragging: this.isDragging,
|
|
1060
|
+
dragStarted: this.dragStarted
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
// 更新 iframe 缓存(更频繁,每帧更新)
|
|
1064
|
+
if (this.cachedIframes.length === 0 ||
|
|
1065
|
+
now - this.lastIframeUpdateTime > this.iframeUpdateInterval) {
|
|
1066
|
+
const allIframes = document.querySelectorAll('iframe');
|
|
1067
|
+
this.cachedIframes = Array.from(allIframes).map(iframe => ({
|
|
1068
|
+
element: iframe,
|
|
1069
|
+
rect: iframe.getBoundingClientRect()
|
|
1070
|
+
}));
|
|
1071
|
+
this.lastIframeUpdateTime = now;
|
|
1072
|
+
if (this.debug) {
|
|
1073
|
+
console.log(`[IconManager] Active detection: Updated iframe cache (${this.cachedIframes.length} iframes)`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// 检查最后记录的鼠标位置是否在任何 iframe 上
|
|
1077
|
+
for (const { rect, element } of this.cachedIframes) {
|
|
1078
|
+
if (clientX >= rect.left &&
|
|
1079
|
+
clientX <= rect.right &&
|
|
1080
|
+
clientY >= rect.top &&
|
|
1081
|
+
clientY <= rect.bottom) {
|
|
1082
|
+
// 鼠标在 iframe 上,立即停止拖动
|
|
1083
|
+
if (this.debug) {
|
|
1084
|
+
console.log('[IconManager] Active detection: Mouse over iframe detected, stopping drag immediately', {
|
|
1085
|
+
mousePosition: { x: clientX, y: clientY },
|
|
1086
|
+
iframeRect: {
|
|
1087
|
+
left: rect.left,
|
|
1088
|
+
top: rect.top,
|
|
1089
|
+
right: rect.right,
|
|
1090
|
+
bottom: rect.bottom
|
|
1091
|
+
},
|
|
1092
|
+
iframeSrc: element.src || 'no src',
|
|
1093
|
+
timeSinceLastMove
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
this.stopDrag();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// 继续检测
|
|
1102
|
+
this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
|
|
1103
|
+
};
|
|
1104
|
+
this.activeDetectionRafId = requestAnimationFrame(checkMousePosition);
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* 停止主动检测机制
|
|
1108
|
+
*/
|
|
1109
|
+
stopActiveDetection() {
|
|
1110
|
+
if (this.activeDetectionRafId !== null) {
|
|
1111
|
+
if (this.debug) {
|
|
1112
|
+
console.log('[IconManager] Stopping active detection mechanism');
|
|
1113
|
+
}
|
|
1114
|
+
cancelAnimationFrame(this.activeDetectionRafId);
|
|
1115
|
+
this.activeDetectionRafId = null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
619
1118
|
/**
|
|
620
1119
|
* 清理拖动事件
|
|
621
1120
|
*/
|
|
@@ -630,6 +1129,7 @@ class IconManager {
|
|
|
630
1129
|
document.removeEventListener('touchend', this.stopDragHandler);
|
|
631
1130
|
}
|
|
632
1131
|
// 清理处理 iframe 事件丢失的监听器
|
|
1132
|
+
// 注意:必须先移除监听器,再清理防抖定时器,防止事件在清理过程中触发
|
|
633
1133
|
if (this.mouseLeaveHandler) {
|
|
634
1134
|
document.removeEventListener('mouseleave', this.mouseLeaveHandler);
|
|
635
1135
|
this.mouseLeaveHandler = null;
|
|
@@ -642,6 +1142,15 @@ class IconManager {
|
|
|
642
1142
|
window.removeEventListener('blur', this.blurHandler);
|
|
643
1143
|
this.blurHandler = null;
|
|
644
1144
|
}
|
|
1145
|
+
// 清理防抖定时器(在移除监听器之后)
|
|
1146
|
+
if (this.mouseLeaveTimeout !== null) {
|
|
1147
|
+
window.clearTimeout(this.mouseLeaveTimeout);
|
|
1148
|
+
this.mouseLeaveTimeout = null;
|
|
1149
|
+
}
|
|
1150
|
+
if (this.pointerLeaveTimeout !== null) {
|
|
1151
|
+
window.clearTimeout(this.pointerLeaveTimeout);
|
|
1152
|
+
this.pointerLeaveTimeout = null;
|
|
1153
|
+
}
|
|
645
1154
|
// 清理超时定时器
|
|
646
1155
|
if (this.dragTimeoutId !== null) {
|
|
647
1156
|
window.clearTimeout(this.dragTimeoutId);
|
|
@@ -652,11 +1161,31 @@ class IconManager {
|
|
|
652
1161
|
cancelAnimationFrame(this.rafId);
|
|
653
1162
|
this.rafId = null;
|
|
654
1163
|
}
|
|
1164
|
+
// 清理主动检测机制(必须在最后,确保完全清理)
|
|
1165
|
+
this.stopActiveDetection();
|
|
1166
|
+
// 确保 activeDetectionRafId 被清理(双重保险)
|
|
1167
|
+
if (this.activeDetectionRafId !== null) {
|
|
1168
|
+
if (this.debug) {
|
|
1169
|
+
console.warn('[IconManager] Active detection RAF ID still exists, force cleaning');
|
|
1170
|
+
}
|
|
1171
|
+
cancelAnimationFrame(this.activeDetectionRafId);
|
|
1172
|
+
this.activeDetectionRafId = null;
|
|
1173
|
+
}
|
|
655
1174
|
// 清理缓存
|
|
656
1175
|
this.cachedContainer = null;
|
|
657
1176
|
this.cachedContainerRect = null;
|
|
658
1177
|
this.cachedIframes = []; // 清理 iframe 缓存
|
|
659
1178
|
this.pendingPosition.needsUpdate = false;
|
|
1179
|
+
if (this.debug) {
|
|
1180
|
+
console.log('[IconManager] Cleanup completed', {
|
|
1181
|
+
isDragging: this.isDragging,
|
|
1182
|
+
dragStarted: this.dragStarted,
|
|
1183
|
+
isStoppingDrag: this.isStoppingDrag,
|
|
1184
|
+
hasTimeout: this.dragTimeoutId !== null,
|
|
1185
|
+
hasActiveDetection: this.activeDetectionRafId !== null,
|
|
1186
|
+
hasRaf: this.rafId !== null
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
660
1189
|
}
|
|
661
1190
|
/**
|
|
662
1191
|
* 处理点击事件
|
|
@@ -666,6 +1195,8 @@ class IconManager {
|
|
|
666
1195
|
if (!this.isClickEnabled) {
|
|
667
1196
|
return;
|
|
668
1197
|
}
|
|
1198
|
+
// 清除自动吸附定时器(用户有操作)
|
|
1199
|
+
this.clearAutoAttachTimer();
|
|
669
1200
|
if (this.onClickCallback) {
|
|
670
1201
|
this.onClickCallback();
|
|
671
1202
|
}
|
|
@@ -708,6 +1239,142 @@ class IconManager {
|
|
|
708
1239
|
this.iconElement.style.cursor = 'pointer';
|
|
709
1240
|
}
|
|
710
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* 磁性吸附
|
|
1244
|
+
*/
|
|
1245
|
+
magneticSnap() {
|
|
1246
|
+
if (!this.iconElement)
|
|
1247
|
+
return;
|
|
1248
|
+
try {
|
|
1249
|
+
const container = this.getTargetElement();
|
|
1250
|
+
if (!container)
|
|
1251
|
+
return;
|
|
1252
|
+
const containerRect = container.getBoundingClientRect();
|
|
1253
|
+
const iconWidth = this.iconElement.offsetWidth || 30;
|
|
1254
|
+
const iconHeight = this.iconElement.offsetHeight || 30;
|
|
1255
|
+
// 获取当前图标位置
|
|
1256
|
+
const computedStyle = window.getComputedStyle(this.iconElement);
|
|
1257
|
+
const currentLeft = parseFloat(computedStyle.left) || 0;
|
|
1258
|
+
const currentTop = parseFloat(computedStyle.top) || 0;
|
|
1259
|
+
let newX = currentLeft;
|
|
1260
|
+
let newY = currentTop;
|
|
1261
|
+
// X 轴磁性吸附
|
|
1262
|
+
if (this.magneticDirection === 'x' || this.magneticDirection === 'both') {
|
|
1263
|
+
const containerWidth = containerRect.width;
|
|
1264
|
+
const centerX = containerWidth / 2;
|
|
1265
|
+
if (currentLeft < centerX) {
|
|
1266
|
+
// 吸附到左边
|
|
1267
|
+
newX = this.margin;
|
|
1268
|
+
// 侧边吸附逻辑
|
|
1269
|
+
if (this.sideAttach && currentLeft < iconWidth / 2) {
|
|
1270
|
+
this.isAttachedToSide = true;
|
|
1271
|
+
this.attachedSide = 'left';
|
|
1272
|
+
// 计算隐藏部分:图标宽度 * sideHideRatio
|
|
1273
|
+
// 例如:30px * 0.5 = 15px 隐藏,15px 显示
|
|
1274
|
+
const hideDistance = iconWidth * this.sideHideRatio;
|
|
1275
|
+
// 图标左边缘对齐容器左边缘,隐藏部分在容器外
|
|
1276
|
+
newX = -hideDistance; // 负值,让图标超出容器左边缘
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
this.isAttachedToSide = false;
|
|
1280
|
+
this.attachedSide = '';
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1284
|
+
// 吸附到右边
|
|
1285
|
+
newX = containerWidth - iconWidth - this.margin;
|
|
1286
|
+
// 侧边吸附逻辑:如果启用侧边吸附,默认吸附到右边并显示一半
|
|
1287
|
+
if (this.sideAttach) {
|
|
1288
|
+
// 如果靠近右边(距离右边 < 图标宽度的一半),或者从默认位置初始化
|
|
1289
|
+
// 默认位置(右下角)应该自动侧边吸附
|
|
1290
|
+
const distanceFromRight = containerWidth - currentLeft - iconWidth;
|
|
1291
|
+
if (distanceFromRight < iconWidth / 2 ||
|
|
1292
|
+
(isNaN(currentLeft) || currentLeft === 0) ||
|
|
1293
|
+
currentLeft >= containerWidth - iconWidth - this.margin - iconWidth / 2) {
|
|
1294
|
+
this.isAttachedToSide = true;
|
|
1295
|
+
this.attachedSide = 'right';
|
|
1296
|
+
// 计算显示部分:图标宽度 * (1 - sideHideRatio)
|
|
1297
|
+
// 例如:30px * (1 - 0.5) = 15px 显示,15px 隐藏
|
|
1298
|
+
const visibleWidth = iconWidth * (1 - this.sideHideRatio);
|
|
1299
|
+
// 图标右边缘对齐容器右边缘,但只显示 visibleWidth
|
|
1300
|
+
newX = containerWidth - visibleWidth;
|
|
1301
|
+
}
|
|
1302
|
+
else {
|
|
1303
|
+
this.isAttachedToSide = false;
|
|
1304
|
+
this.attachedSide = '';
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
this.isAttachedToSide = false;
|
|
1309
|
+
this.attachedSide = '';
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
// Y 轴磁性吸附
|
|
1314
|
+
if (this.magneticDirection === 'y' || this.magneticDirection === 'both') {
|
|
1315
|
+
const containerHeight = containerRect.height;
|
|
1316
|
+
const centerY = containerHeight / 2;
|
|
1317
|
+
if (currentTop < centerY) {
|
|
1318
|
+
newY = this.margin; // 吸附到上边
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
newY = containerHeight - iconHeight - this.margin; // 吸附到下边
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
// 应用新位置
|
|
1325
|
+
// 如果侧边吸附到右边,使用 right 属性(负值),让图标超出容器右边缘
|
|
1326
|
+
if (this.isAttachedToSide && this.attachedSide === 'right') {
|
|
1327
|
+
const hideDistance = iconWidth * this.sideHideRatio; // 隐藏部分(在容器外)
|
|
1328
|
+
this.iconElement.style.right = `-${hideDistance}px`; // 负值,让图标超出容器右边缘
|
|
1329
|
+
this.iconElement.style.left = 'auto';
|
|
1330
|
+
}
|
|
1331
|
+
else if (this.isAttachedToSide && this.attachedSide === 'left') {
|
|
1332
|
+
// 左边吸附使用 left(负值),让图标超出容器左边缘
|
|
1333
|
+
const hideDistance = iconWidth * this.sideHideRatio;
|
|
1334
|
+
this.iconElement.style.left = `-${hideDistance}px`;
|
|
1335
|
+
this.iconElement.style.right = 'auto';
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
// 正常位置使用 left
|
|
1339
|
+
this.iconElement.style.left = `${newX}px`;
|
|
1340
|
+
this.iconElement.style.right = 'auto';
|
|
1341
|
+
}
|
|
1342
|
+
this.iconElement.style.top = `${newY}px`;
|
|
1343
|
+
this.iconElement.style.bottom = 'auto';
|
|
1344
|
+
// 如果侧边吸附,应用 transform
|
|
1345
|
+
if (this.isAttachedToSide && this.attachedSide) {
|
|
1346
|
+
this.iconElement.style.transform = 'translateX(0)';
|
|
1347
|
+
this.iconElement.style.transition = 'all 0.3s ease';
|
|
1348
|
+
}
|
|
1349
|
+
else {
|
|
1350
|
+
this.iconElement.style.transform = '';
|
|
1351
|
+
}
|
|
1352
|
+
// 保存位置(如果是右边吸附,保存为 right 值,否则保存为 left 值)
|
|
1353
|
+
if (this.isAttachedToSide && this.attachedSide === 'right') {
|
|
1354
|
+
// 右边吸附时,不保存到 iconPosition(因为使用的是 right 属性)
|
|
1355
|
+
// 或者可以保存一个特殊标记
|
|
1356
|
+
this.iconPosition = null; // 清除位置,让下次显示时重新计算
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
this.iconPosition = {
|
|
1360
|
+
x: newX,
|
|
1361
|
+
y: newY
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
if (this.debug) {
|
|
1365
|
+
console.log('[IconManager] Magnetic snap applied', {
|
|
1366
|
+
newPosition: { x: newX, y: newY },
|
|
1367
|
+
isAttachedToSide: this.isAttachedToSide,
|
|
1368
|
+
attachedSide: this.attachedSide
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
if (this.debug) {
|
|
1374
|
+
console.error('[IconManager] Error in magneticSnap:', error);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
711
1378
|
/**
|
|
712
1379
|
* 获取目标元素(支持动态查找)
|
|
713
1380
|
*/
|
|
@@ -21171,7 +21838,14 @@ class CustomerServiceSDK {
|
|
|
21171
21838
|
console.log('Icon config changed, recreating icon manager');
|
|
21172
21839
|
}
|
|
21173
21840
|
}
|
|
21174
|
-
this.iconManager = new IconManager(iconPosition, this.debug, iconTarget
|
|
21841
|
+
this.iconManager = new IconManager(iconPosition, this.debug, iconTarget, {
|
|
21842
|
+
sideAttach: options?.sideAttach !== undefined ? options.sideAttach : true,
|
|
21843
|
+
sideHideRatio: options?.sideHideRatio !== undefined ? options.sideHideRatio : 0.5,
|
|
21844
|
+
magnetic: options?.magnetic !== undefined ? options.magnetic : true,
|
|
21845
|
+
magneticDirection: options?.magneticDirection || 'x',
|
|
21846
|
+
margin: options?.margin !== undefined ? options.margin : 10,
|
|
21847
|
+
autoAttachDelay: options?.autoAttachDelay !== undefined ? options.autoAttachDelay : 3000
|
|
21848
|
+
});
|
|
21175
21849
|
await this.iconManager.show();
|
|
21176
21850
|
// 保存新的配置
|
|
21177
21851
|
this.lastIconConfig = { position: iconPosition, target: iconTarget };
|
|
@@ -21180,7 +21854,14 @@ class CustomerServiceSDK {
|
|
|
21180
21854
|
// 配置没变化,保留图标管理器(避免闪烁)
|
|
21181
21855
|
if (!this.iconManager) {
|
|
21182
21856
|
// 如果不存在,创建新的
|
|
21183
|
-
this.iconManager = new IconManager(iconPosition, this.debug, iconTarget
|
|
21857
|
+
this.iconManager = new IconManager(iconPosition, this.debug, iconTarget, {
|
|
21858
|
+
sideAttach: options?.sideAttach !== undefined ? options.sideAttach : true,
|
|
21859
|
+
sideHideRatio: options?.sideHideRatio !== undefined ? options.sideHideRatio : 0.5,
|
|
21860
|
+
magnetic: options?.magnetic !== undefined ? options.magnetic : true,
|
|
21861
|
+
magneticDirection: options?.magneticDirection || 'x',
|
|
21862
|
+
margin: options?.margin !== undefined ? options.margin : 10,
|
|
21863
|
+
autoAttachDelay: options?.autoAttachDelay !== undefined ? options.autoAttachDelay : 3000
|
|
21864
|
+
});
|
|
21184
21865
|
await this.iconManager.show();
|
|
21185
21866
|
}
|
|
21186
21867
|
else {
|