From 064c26d7c99cb92bc8a0989e710ff31119bd805d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 24 Sep 2026 16:05:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E5=8F=B3=E9=94=AE=E6=8C=89=E4=B8=8B?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E5=91=BD=E4=B8=AD=E7=9B=AE=E6=A0=87=E5=86=B3?= =?UTF-8?q?=E5=AE=9A=E6=98=AF=E5=90=A6=E5=BC=B9=E8=8A=82=E7=82=B9=E8=8F=9C?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PreviewWorkspace.tsx 在右键按下时记住命中元素,抬起时改成按它解析节点与树:指针被视口捕获后 pointerup 的 target 是捕获元素,原先按抬起目标找不到 [data-node-id],节点菜单永远打不开 指针取消时同步清掉记录的目标 tests/previewRightPanDrag.test.tsx 补「抬起被重定向到捕获元素仍弹菜单」用例 --- .../components/preview/PreviewWorkspace.tsx | 20 +++++++++++++---- .../tests/previewRightPanDrag.test.tsx | 22 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/PreviewWorkspace.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/PreviewWorkspace.tsx index 48d8a0bb9..91557dced 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/PreviewWorkspace.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/PreviewWorkspace.tsx @@ -118,6 +118,9 @@ export function PreviewWorkspace({ const didInitialFitRef = useRef(false); const panRef = useRef | null>(null); const rightPanRef = useRef(null); + // 右键按下时的命中元素:指针被视口捕获后,pointerup 的 target 会变成捕获元素, + // 必须在按下时把真正的节点/树目标记下来。 + const rightPanTargetRef = useRef(null); const rightPressSequenceRef = useRef(false); const treeDragRef = useRef<{ treeId: string; @@ -422,6 +425,7 @@ export function PreviewWorkspace({ event.preventDefault(); event.currentTarget.focus({ preventScroll: true }); event.currentTarget.setPointerCapture(event.pointerId); + rightPanTargetRef.current = event.target; rightPanRef.current = createPreviewRightPanGesture({ pointerId: event.pointerId, pointer: { x: event.clientX, y: event.clientY }, @@ -468,8 +472,11 @@ export function PreviewWorkspace({ ); }; - const openNodeContextMenuAt = (event: ReactPointerEvent) => { - const target = resolveRightClickMenuTarget(event.target); + const openNodeContextMenuAt = ( + event: ReactPointerEvent, + pressedTarget: EventTarget | null, + ) => { + const target = resolveRightClickMenuTarget(pressedTarget); if (!target) return; const layout = treeLayouts.find( (item) => item.tree.src_ui_design === target.treeId, @@ -501,7 +508,9 @@ export function PreviewWorkspace({ ), }); if (release.viewport) setViewport(release.viewport); - if (release.openNodeMenu) openNodeContextMenuAt(event); + const pressedTarget = rightPanTargetRef.current; + rightPanTargetRef.current = null; + if (release.openNodeMenu) openNodeContextMenuAt(event, pressedTarget); return; } if (panRef.current?.pointerId !== event.pointerId) return; @@ -516,7 +525,10 @@ export function PreviewWorkspace({ const cancelsRightPan = rightPan !== null && rightPan.pointerId === event.pointerId; const cancelsPan = pan !== null && pan.pointerId === event.pointerId; - if (cancelsRightPan) rightPanRef.current = null; + if (cancelsRightPan) { + rightPanRef.current = null; + rightPanTargetRef.current = null; + } if (cancelsPan) panRef.current = null; if (cancelsRightPan || cancelsPan) { setIsPanning(false); diff --git a/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx b/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx index 7b5cb0c6d..466cd8bac 100644 --- a/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx +++ b/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx @@ -292,6 +292,28 @@ describe('PreviewWorkspace right-button pan', () => { expect(screen.getByRole('menuitem', { name: '新增同级节点' })).toBeTruthy(); }); + it('opens the node menu when pointerup is retargeted to the capture element', () => { + const { canvas, child, preview } = renderPreview(); + + dispatchPointer(child, 'pointerdown', { + button: 2, + clientX: 100, + clientY: 100, + pointerId: 7, + }); + // 指针被视口捕获后,浏览器把后续指针事件派发给捕获元素:抬起目标不再是节点, + // 菜单必须按按下时的命中目标裁决。 + dispatchPointer(preview, 'pointerup', { + button: 2, + clientX: 101, + clientY: 100, + pointerId: 7, + }); + + expect(canvas.selectNode).toHaveBeenCalledWith('child'); + expect(screen.getByRole('menu')).toBeTruthy(); + }); + it('keeps an empty-grid right click silent, unlike the left button', () => { const { canvas, preview } = renderPreview();