From 73121f18bac39d555d7bf0dc5aebf90544deed4c Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 28 Jun 2026 17:40:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BE=E7=89=87=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=96=87=E6=9C=AC=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除图片信息弹窗的整段复制按钮 保留选中文本时的浏览器原生 Ctrl+C 放行图片信息弹窗内的浏览器右键菜单 补充文本复制与右键菜单回归测试 --- .../ImageCanvasEditorShellView.test.tsx | 31 +++++++++ .../ImageCanvasEditorShellView.tsx | 10 ++- .../ImageCanvasMetadataModalView.test.tsx | 50 --------------- .../ImageCanvasMetadataModalView.tsx | 64 ------------------- .../useImageCanvasKeyboardShortcuts.test.tsx | 27 ++++++++ .../useImageCanvasKeyboardShortcuts.ts | 8 +++ src/index.css | 8 --- 7 files changed, 75 insertions(+), 123 deletions(-) diff --git a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx index d3c2fd8e7..d99142629 100644 --- a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx @@ -287,6 +287,37 @@ describe('ImageCanvasEditorShellView', () => { expect((preview as HTMLElement).style.top).toBe('48px'); }); + it('keeps the native context menu available inside metadata text', () => { + render( + ()} + uploadInputRef={createRef()} + onUploadInputChange={vi.fn()} + assetDragPreview={null} + sidebarProps={createSidebarProps()} + topbarProps={createTopbarProps()} + stageProps={createStageProps()} + metadataProps={createMetadataProps(createLayer())} + />, + ); + + const editor = screen.getByRole('region', { name: '图片画布编辑器' }); + const metadataText = screen.getByText('上传图片'); + const metadataContextMenuEvent = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + }); + metadataText.dispatchEvent(metadataContextMenuEvent); + expect(metadataContextMenuEvent.defaultPrevented).toBe(false); + + const editorContextMenuEvent = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + }); + editor.dispatchEvent(editorContextMenuEvent); + expect(editorContextMenuEvent.defaultPrevented).toBe(true); + }); + it('renders quick edit selection tools through the stage props', () => { const layer = createLayer(); const handleQuickEditToolChange = vi.fn(); diff --git a/src/components/image-editor/ImageCanvasEditorShellView.tsx b/src/components/image-editor/ImageCanvasEditorShellView.tsx index 1701d3f47..00c1ceb57 100644 --- a/src/components/image-editor/ImageCanvasEditorShellView.tsx +++ b/src/components/image-editor/ImageCanvasEditorShellView.tsx @@ -48,7 +48,15 @@ export function ImageCanvasEditorShellView({ ref={editorRootRef} className="image-canvas-editor" aria-label="图片画布编辑器" - onContextMenu={(event) => event.preventDefault()} + onContextMenu={(event) => { + if ( + event.target instanceof Element && + event.target.closest('.image-canvas-editor__metadata-dialog') + ) { + return; + } + event.preventDefault(); + }} > ({ - copyTextToClipboard: vi.fn(), -})); - function createLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-1', @@ -72,51 +67,6 @@ describe('ImageCanvasMetadataModalView', () => { expect(within(dialog).getByText('generated/object.png')).toBeTruthy(); }); - it('copies the visible metadata snapshot without restoring internal prompt fields', async () => { - vi.mocked(clipboardService.copyTextToClipboard).mockResolvedValueOnce(true); - - render( - , - ); - - const dialog = screen.getByRole('dialog', { name: '图片信息' }); - fireEvent.click(within(dialog).getByRole('button', { name: '复制图片信息' })); - - expect(clipboardService.copyTextToClipboard).toHaveBeenCalledWith( - [ - '图片类型:生成图片', - '生成输入:\n生成提示词:清爽游戏按钮\n参考图:角色立绘 (项目资源 · resource-reference)', - 'Model:gpt-image-2', - 'Resolution:1024 x 768 px', - 'Task:123', - 'Object:generated/object.png', - ].join('\n'), - ); - expect(clipboardService.copyTextToClipboard).not.toHaveBeenCalledWith( - expect.stringContaining('内部组装 Prompt 不应复制'), - ); - expect(await within(dialog).findByText('已复制')).toBeTruthy(); - }); - it('renders nanobanana2 instead of the raw Gemini image model id', () => { render( `${field.title}:${field.value}`), - ...references.map( - (reference) => - `${reference.title}:${reference.label} (${formatReferenceType( - reference.refType, - )} · ${reference.refId})`, - ), - ].join('\n'); -} - -function buildLayerMetadataCopyText(layer: CanvasLayer, mediaLabel: string) { - return [ - `${mediaLabel}类型:${formatLayerImageType(layer)}`, - `生成输入:\n${formatGenerationInputsForCopy(layer)}`, - `Model:${layer.model ? getEditorImageModelDisplayName(layer.model) : '-'}`, - layer.mediaType === 'audio' - ? `时长:${formatLayerDurationForDisplay(layer)}` - : `Resolution:${layer.originalWidth} x ${layer.originalHeight} px`, - `Task:${formatTaskIdForDisplay(layer.taskId)}`, - `Object:${layer.objectKey ?? layer.assetObjectId ?? '-'}`, - ].join('\n'); -} - export function ImageCanvasMetadataModalView({ layer, onClose, }: ImageCanvasMetadataModalViewProps) { - const { copyState, copyText, resetCopyState } = useCopyFeedback(); const mediaLabel = layer?.mediaType === 'audio' ? '音频' @@ -72,19 +35,6 @@ export function ImageCanvasMetadataModalView({ : layer?.mediaType === 'image-sequence' ? '序列帧' : '图片'; - const metadataText = layer ? buildLayerMetadataCopyText(layer, mediaLabel) : ''; - - useEffect(() => { - resetCopyState(); - }, [metadataText, resetCopyState]); - - const copyMetadata = () => { - if (!metadataText) { - return; - } - - void copyText(metadataText); - }; return ( - ) : null - } > {layer ? (
diff --git a/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx b/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx index 22e74f157..5ef9250a2 100644 --- a/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx +++ b/src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx @@ -347,6 +347,33 @@ describe('useImageCanvasKeyboardShortcuts', () => { expect(pasteEvent.defaultPrevented).toBe(false); }); + it('leaves native copy available when text is selected', () => { + const copySelectedCanvasLayers = vi.fn(); + const removeAllRanges = vi.fn(); + const selectedText = vi.fn(() => '清爽游戏按钮'); + vi.spyOn(window, 'getSelection').mockReturnValue({ + toString: selectedText, + removeAllRanges, + } as unknown as Selection); + render( + , + ); + + const copyEvent = new KeyboardEvent('keydown', { + key: 'c', + code: 'KeyC', + ctrlKey: true, + bubbles: true, + cancelable: true, + }); + window.dispatchEvent(copyEvent); + + expect(copySelectedCanvasLayers).not.toHaveBeenCalled(); + expect(copyEvent.defaultPrevented).toBe(false); + }); + it('routes Windows view and layer ordering shortcuts', () => { const fitLayers = vi.fn(); const updateScaleFromCenter = vi.fn(); diff --git a/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts b/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts index 1a728bc2e..1066833c7 100644 --- a/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts +++ b/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts @@ -112,6 +112,11 @@ function isCtrlShortcut(event: KeyboardEvent) { return event.ctrlKey && !event.altKey && !event.metaKey; } +function hasSelectedText() { + const selection = window.getSelection(); + return Boolean(selection?.toString().trim()); +} + function isKey(event: KeyboardEvent, key: string, code?: string) { return event.key.toLowerCase() === key || event.code === code; } @@ -268,6 +273,9 @@ export function useImageCanvasKeyboardShortcuts({ return; } if (event.code === 'KeyC') { + if (hasSelectedText()) { + return; + } event.preventDefault(); copySelectedCanvasLayers?.(); return; diff --git a/src/index.css b/src/index.css index fe850a37e..998530f53 100644 --- a/src/index.css +++ b/src/index.css @@ -8571,14 +8571,6 @@ button.image-canvas-editor__reference-chip:disabled { overflow-wrap: anywhere; } -.image-canvas-editor__metadata-footer { - padding: 0.72rem 0.85rem; -} - -.image-canvas-editor__metadata-copy-button { - justify-content: center; -} - @media (max-width: 760px) { .image-canvas-editor { flex-direction: column;