From b414b3666ecbb7dfb4aec2919fda8979e78c6dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Wed, 23 Sep 2026 17:04:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(embed):=20pointer=20pause=20watchdog=20?= =?UTF-8?q?=E2=80=94=20capture=20does=20not=20cross=20iframe=20boundary,?= =?UTF-8?q?=20outside-release=20now=20auto-resumes=20in=202s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier capture+window-listener fix is insufficient: releasing outside the 195x280 window dispatches pointerup in the host document, which no in-iframe listener can observe. Deterministic fallback: 2s watchdog on pointerdown force-clears the pause reason (in-frame releases still cancel it immediately). Co-Authored-By: Claude Code --- apps/viewer/src/pages/embed/EmbedStream.vue | 22 ++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/viewer/src/pages/embed/EmbedStream.vue b/apps/viewer/src/pages/embed/EmbedStream.vue index 60f6dab..32f4988 100644 --- a/apps/viewer/src/pages/embed/EmbedStream.vue +++ b/apps/viewer/src/pages/embed/EmbedStream.vue @@ -201,6 +201,9 @@ let pointerMoved = 0; let lockedTarget: string | null = null; /** 拖动抑制一次 click。 */ let suppressClick = false; +/** 按住看门狗(滚动卡死修复):up 丢失(窗外释放)时 2s 强制恢复滚动。 */ +const POINTER_WATCHDOG_MS = 2000; +let pointerWatchdog: ReturnType | null = null; let hoverPointerIsMouse = false; function scheduleResume(): void { @@ -219,14 +222,22 @@ function onPointerDown(event: PointerEvent): void { const anchor = (event.target as HTMLElement).closest('a.stream-photo'); lockedTarget = anchor === null ? null : (anchor.dataset.photoId ?? null); setReason('pointer', true); - // 指针捕获(滚动卡死修复 2026-09-23):按下后拖出窗外松开时 viewport 收不到 - // pointerup,'pointer' 暂停原因会永久残留 → 滚动停死。捕获后 up/cancel 必回 - // 本元素;window 级兜底监听(onMounted)双保险。 + // 指针捕获 + 看门狗(滚动卡死修复 2026-09-23):按下后拖出窗外松开时, + // pointerup 发生在宿主文档——iframe 内任何监听都收不到(capture 不跨文档), + // 'pointer' 暂停原因会永久残留 → 滚动停死。看门狗 2s 强制恢复是确定性兜底; + // capture/window 监听让 iframe 内的释放更快恢复。 try { viewportEl.value?.setPointerCapture(event.pointerId); } catch { /* 指针已释放等:忽略 */ } + if (pointerWatchdog !== null) clearTimeout(pointerWatchdog); + pointerWatchdog = setTimeout(() => { + pointerWatchdog = null; + pointerDownY = null; + setReason('pointer', false); + scheduleResume(); + }, POINTER_WATCHDOG_MS); } function onPointerMove(event: PointerEvent): void { @@ -244,6 +255,10 @@ function onPointerMove(event: PointerEvent): void { function onPointerUp(): void { pointerDownY = null; + if (pointerWatchdog !== null) { + clearTimeout(pointerWatchdog); + pointerWatchdog = null; + } setReason('pointer', false); scheduleResume(); } @@ -373,6 +388,7 @@ onMounted(() => { onBeforeUnmount(() => { setReason('destroyed', true); + if (pointerWatchdog !== null) clearTimeout(pointerWatchdog); layerEl.value?.clear(); document.removeEventListener('visibilitychange', onVisibilityChange); window.removeEventListener('pointerup', onWindowPointerEnd);