From e8d8dee7674c9f378e7322788e6bdf4c7a59790f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 20 Jul 2026 19:52:49 +0800 Subject: [PATCH] fix interactive error --- .../【编辑器】画布Agent对话面板-2026-07-03.md | 1 + .../MessageBubble.test.tsx | 88 +++++++++++++++++-- .../MessageBubbleRightClickMenu.tsx | 8 ++ .../useRightClickMenu.ts | 14 +-- 4 files changed, 98 insertions(+), 13 deletions(-) diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 0bde7f9b9..daa156bcf 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -116,6 +116,7 @@ 6. 错误气泡(失败/余额不足,带原因); 7. 普通消息请求等待期间禁用发送按钮,不提供客户端停止操作;前端持续等待后端响应,避免后端已持久化消息但前端中断请求后产生会话状态错位。 8. 桌面端右键消息正文可复制该条可见文本;右键消息附件或生成结果可下载素材,图片额外支持复制图片本体和“引用”到当前输入区。引用复用附件去重、9 张上限和发送链路;右键菜单只保留已有的 `objectKey` 与素材 `source`,引用时由输入区优先按 `objectKey`、缺失时按 `source` 从当前画布和素材库选项重新查询并构造 `EditorAgentAttachmentRef`,同时命中时画布优先。 +9. 消息右键菜单遵循 Canva 式单实例交互:任一菜单已打开时,下一次右键必须先关闭旧菜单;新落点是消息正文或素材时再在新位置打开对应菜单,新落点没有右键动作时仅收起旧菜单,不允许多个消息菜单并存。复制、引用或下载成功后自动关闭菜单;失败时保留菜单和失败状态,避免错误无提示消失。 不做(明确排除,防止后人补齐): diff --git a/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx b/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx index 6b02603ff..6aec36f33 100644 --- a/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx +++ b/src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx @@ -95,9 +95,74 @@ describe('MessageBubble', () => { await waitFor(() => expect(copyTextToClipboard).toHaveBeenCalledWith('第一行\n第二行'), ); - expect( - await screen.findByRole('menuitem', { name: '已复制' }), - ).toBeTruthy(); + await waitFor(() => + expect( + screen.queryByRole('menu', { name: '消息右键菜单' }), + ).toBeNull(), + ); + }); + + it('closes the previous menu before another right click opens a new one', () => { + render( + <> + + + , + ); + + const messages = screen.getAllByLabelText('Agent消息'); + fireEvent.contextMenu(messages[0]!, { clientX: 30, clientY: 40 }); + fireEvent.contextMenu(messages[1]!, { clientX: 70, clientY: 80 }); + + const menus = screen.getAllByRole('menu', { name: '消息右键菜单' }); + expect(menus).toHaveLength(1); + expect(menus[0]?.style.left).toBe('70px'); + expect(menus[0]?.style.top).toBe('80px'); + }); + + it('closes an open menu when the next right click has no menu target', () => { + renderMessage({ + id: 42, + role: 'assistant', + text: '右键后应可收起', + attachments: [], + toolCall: null, + createdAt: '2026-07-20T00:00:00Z', + }); + + fireEvent.contextMenu(screen.getByLabelText('Agent消息'), { + clientX: 30, + clientY: 40, + }); + expect(screen.getByRole('menu', { name: '消息右键菜单' })).toBeTruthy(); + + fireEvent.contextMenu(document.body, { clientX: 70, clientY: 80 }); + + expect(screen.queryByRole('menu', { name: '消息右键菜单' })).toBeNull(); }); it('copies an attached WebP as PNG without opening the message text menu', async () => { @@ -182,7 +247,11 @@ describe('MessageBubble', () => { .fn() .mockResolvedValue(new Blob(['webp'], { type: 'image/webp' })), } as unknown as Response); - await screen.findByRole('menuitem', { name: '已复制' }); + await waitFor(() => + expect( + screen.queryByRole('menu', { name: '消息素材右键菜单' }), + ).toBeNull(), + ); expect(bitmapClose).toHaveBeenCalledTimes(1); }); @@ -219,9 +288,11 @@ describe('MessageBubble', () => { objectKey: 'editor/reference.png', }), ); - expect( - await screen.findByRole('menuitem', { name: '已引用' }), - ).toBeTruthy(); + await waitFor(() => + expect( + screen.queryByRole('menu', { name: '消息素材右键菜单' }), + ).toBeNull(), + ); }); it('offers reference for generated images using their media lookup fields', async () => { @@ -335,6 +406,9 @@ describe('MessageBubble', () => { fireEvent.click(screen.getByRole('menuitem', { name: '下载视频' })); await waitFor(() => expect(anchorClick).toHaveBeenCalledTimes(1)); + expect( + screen.queryByRole('menu', { name: '消息素材右键菜单' }), + ).toBeNull(); expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob)); expect(revokeObjectURL).toHaveBeenCalledWith('blob:agent-video'); }); diff --git a/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx b/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx index 689cf9663..0bbb3c367 100644 --- a/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx +++ b/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx @@ -115,17 +115,25 @@ export function MessageBubbleRightClickMenu({ onClose(); } }; + const handleContextMenu = (event: MouseEvent) => { + if (menuRef.current?.contains(event.target as Node)) { + event.preventDefault(); + } + onClose(); + }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onClose(); } }; window.addEventListener('pointerdown', handlePointerDown); + window.addEventListener('contextmenu', handleContextMenu, true); window.addEventListener('keydown', handleKeyDown); window.addEventListener('scroll', onClose, true); window.addEventListener('resize', onClose); return () => { window.removeEventListener('pointerdown', handlePointerDown); + window.removeEventListener('contextmenu', handleContextMenu, true); window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('scroll', onClose, true); window.removeEventListener('resize', onClose); diff --git a/src/components/image-editor/EditorAgentConversation/useRightClickMenu.ts b/src/components/image-editor/EditorAgentConversation/useRightClickMenu.ts index fa6c5e84e..b69dd09de 100644 --- a/src/components/image-editor/EditorAgentConversation/useRightClickMenu.ts +++ b/src/components/image-editor/EditorAgentConversation/useRightClickMenu.ts @@ -197,12 +197,14 @@ export function useRightClickMenu({ : false; setRightClickMenu((current) => current?.target === target && current.pendingAction === action - ? { - ...current, - pendingAction: null, - resultAction: action, - result: succeeded ? 'success' : 'error', - } + ? succeeded + ? null + : { + ...current, + pendingAction: null, + resultAction: action, + result: 'error', + } : current, ); },