diff --git a/apps/viewer/src/pages/embed/EmbedStream.vue b/apps/viewer/src/pages/embed/EmbedStream.vue index 6f3d8f8..0d82400 100644 --- a/apps/viewer/src/pages/embed/EmbedStream.vue +++ b/apps/viewer/src/pages/embed/EmbedStream.vue @@ -155,10 +155,6 @@ function simulateBurst(photoId: string, delta: number): void { } defineExpose({ simulateBurst }); -// 临时探针(卡死排查,随后移除) -if (typeof window !== 'undefined') { - (window as unknown as Record).__embedDebug = { reasons: pauseReasons, userPaused }; -} // 收起/隐藏/换绑/减少动效 → 清粒子(§5.3)。 watch(activeRef, (value) => { @@ -205,6 +201,8 @@ let pointerMoved = 0; let lockedTarget: string | null = null; /** 拖动抑制一次 click。 */ let suppressClick = false; +/** 最近 pointercancel 时刻(enter 噪音过滤)。 */ +let lastPointerCancelTs = 0; /** 按住看门狗(滚动卡死修复):up 丢失(窗外释放)时 2s 强制恢复滚动。 */ const POINTER_WATCHDOG_MS = 2000; let pointerWatchdog: ReturnType | null = null; @@ -273,19 +271,32 @@ function onPointerUp(event: PointerEvent): void { // 滚动停死到刷新;滚轮/键盘路径各有 500ms 定时清除,拖动漏了)。 setReason('manual', false); setReason('pointer', false); - // 窗外结束时(拖出窗外 Chrome 派发 pointercancel 而非 pointerleave——leave - // 不会再来)hover 原因同样会永久残留,这里按坐标判定一并清除;窗内正常 - // 释放不清(点击后悬停继续暂停是合理行为)。 - const rect = viewportEl.value?.getBoundingClientRect(); - if ( - rect !== undefined && - (event.clientX < rect.left || - event.clientX > rect.right || - event.clientY < rect.top || - event.clientY > rect.bottom) - ) { + // 窗外结束时 hover 原因同样会永久残留(拖出窗外 Chrome 派发 pointercancel + // 而非 pointerleave;其合成坐标恒为 0,0——恰落在铺满 iframe 的 viewport + // rect 内,坐标判定不可用)。pointercancel 本身即手势被打断(拖出/系统 + // 抢占),鼠标大概率已不在窗上:无条件清 hover。真实 pointerup 坐标可信, + // 窗内释放不清(点击后悬停继续暂停是合理行为)。 + if (event.type === 'pointercancel') { + lastPointerCancelTs = Date.now(); hoverPointerIsMouse = false; setReason('hover', false); + } else { + const rect = viewportEl.value?.getBoundingClientRect(); + const outside = + rect !== undefined && + (event.clientX < rect.left || + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom); + if (outside) { + hoverPointerIsMouse = false; + setReason('hover', false); + } else if (event.pointerType === 'mouse') { + // 窗内真实释放:按住期间的 enter 被忽略后在此补设悬停(释放后鼠标 + // 仍在窗上 → 悬停暂停继续成立,与直接移入行为一致)。 + hoverPointerIsMouse = true; + setReason('hover', true); + } } scheduleResume(); } @@ -351,6 +362,13 @@ function onVisibilityChange(): void { } function onPointerEnter(event: PointerEvent): void { // 悬停仅鼠标(效果示例 e.pointerType==='mouse'——触摸不粘滞)。 + // 按住期间 / pointercancel 后短窗内的 enter 是边界移交噪音(滚动卡死末层 + // 根因 2026-09-23:拖出窗外时 Chrome 先派发 pointercancel 清掉按住标志, + // 再补发一个 cancel 前排队的 pointerenter——此后 iframe 不再收到任何 + // boundary 事件,此 enter 设置的 hover 会永久卡住滚动)。真实悬停意图只在 + // 无按住且非 cancel 余波时成立;释放后鼠标仍在窗上由 onPointerUp 补设。 + if (pointerDownY !== null) return; + if (Date.now() - lastPointerCancelTs < 500) return; if (event.pointerType === 'mouse') { hoverPointerIsMouse = true; if (resumeTimer !== null) {