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 91557dced..7939a74c0 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 @@ -588,6 +588,21 @@ export function PreviewWorkspace({ event.currentTarget.releasePointerCapture(event.pointerId); }; + /** 浏览器发起的指针取消不是正常抬起:只清拖拽状态与捕获,不写回树偏移。 */ + const handleTreePointerCancel = ( + event: ReactPointerEvent, + ) => { + const drag = treeDragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + treeDragRef.current = null; + setPreviewTreeOffsets((current) => { + const next = new Map(current); + next.delete(drag.treeId); + return next; + }); + releasePointerCapture(event.currentTarget, event.pointerId); + }; + const handleWheel = (event: React.WheelEvent) => { event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); @@ -702,7 +717,7 @@ export function PreviewWorkspace({ } onPointerMove={handleTreePointerMove} onPointerUp={handleTreePointerUp} - onPointerCancel={handleTreePointerUp} + onPointerCancel={handleTreePointerCancel} > {showOriginImage && previewUrls[item.src_ui_design] ? ( { vi.restoreAllMocks(); }); +describe('PreviewWorkspace tree drag', () => { + function renderTreeDrag() { + const rendered = renderPreview(); + const treeLayer = rendered.container.querySelector( + '[data-tree-id]', + ) as HTMLDivElement; + const rootNode = treeLayer.querySelector( + '[data-node-id="root"]', + ) as HTMLDivElement; + return { ...rendered, rootNode, treeLayer }; + } + + it('commits the tree offset on pointerup but not on pointercancel', () => { + const committed = renderTreeDrag(); + dispatchPointer(committed.rootNode, 'pointerdown', { + button: 0, + clientX: 10, + clientY: 10, + pointerId: 11, + }); + // 指针捕获在树容器上,后续指针事件由浏览器派发给捕获元素。 + dispatchPointer(committed.treeLayer, 'pointermove', { + clientX: 40, + clientY: 50, + pointerId: 11, + }); + dispatchPointer(committed.treeLayer, 'pointerup', { + button: 0, + clientX: 40, + clientY: 50, + pointerId: 11, + }); + expect(committed.canvas.setTreeOffset).toHaveBeenCalledTimes(1); + expect(committed.canvas.setTreeOffset.mock.calls[0]?.[0]).toBe('page-1'); + cleanup(); + + const cancelled = renderTreeDrag(); + dispatchPointer(cancelled.rootNode, 'pointerdown', { + button: 0, + clientX: 10, + clientY: 10, + pointerId: 12, + }); + dispatchPointer(cancelled.treeLayer, 'pointermove', { + clientX: 40, + clientY: 50, + pointerId: 12, + }); + dispatchPointer(cancelled.treeLayer, 'pointercancel', { + button: 0, + clientX: 40, + clientY: 50, + pointerId: 12, + }); + expect(cancelled.canvas.setTreeOffset).not.toHaveBeenCalled(); + }); +}); + describe('PreviewWorkspace right-button pan', () => { it('pans the viewport on a right drag and never opens the node menu', () => { const { canvas, child, container, preview } = renderPreview();