From bb1667881df1abdf84ff4308c03a1b081a368e98 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 3 Feb 2026 15:51:30 +0800 Subject: [PATCH] fix: improve canvas image toolbar and tags Add watermark removal, improve dark mode toolbar styling, and sync tag deletion with attachments. --- components/canvas/CanvasChatPanel.tsx | 56 ++++++++++++++++++++++++--- components/canvas/nodes/ImageNode.tsx | 53 ++++++++++++++++++------- services/canvas/index.ts | 19 +++++++++ 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index aa523aa..29f5b25 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -268,6 +268,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF tagSpan.setAttribute('data-attachment-url', url); tagSpan.setAttribute('data-picked-object', 'true'); tagSpan.setAttribute('data-object-name', objectName); + tagSpan.setAttribute('data-object-position', objectPosition); // 样式与普通图片标签一致 tagSpan.className = 'inline-flex items-center gap-1 bg-muted/60 hover:bg-muted rounded-full pl-1 pr-2 py-0.5 mx-0.5 align-middle cursor-default select-none'; // 使用图片缩略图,不使用 emoji 或图标 @@ -1001,11 +1002,54 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF e.target.value = ''; }; - // 移除附件(通过 URL 删除,更可靠) - const removeAttachmentByUrl = (url: string) => { - setAttachments(prev => prev.filter(a => a.url !== url)); - console.log('[CanvasChat] 已移除附件:', url.substring(0, 50)); + // 移除附件(通过标签元素删除,支持同图多物品) + const removeAttachmentByTagElement = (tagEl: HTMLElement) => { + const url = tagEl.getAttribute('data-attachment-url') || ''; + const isPicked = tagEl.getAttribute('data-picked-object') === 'true'; + const objectName = tagEl.getAttribute('data-object-name') || ''; + const objectPosition = tagEl.getAttribute('data-object-position') || ''; + setAttachments(prev => prev.filter(a => { + if (!url) return true; + if (isPicked) { + return !( + a.url === url && + a.pickedObject?.objectName === objectName && + a.pickedObject?.objectPosition === objectPosition + ); + } + return a.url !== url; + })); + if (url) { + console.log('[CanvasChat] 已移除附件:', url.substring(0, 50)); + } }; + + // 从编辑器 DOM 同步附件列表,避免删除标签后仍发送 + const syncAttachmentsWithEditor = useCallback((editableDiv?: HTMLDivElement | null) => { + const container = editableDiv || (textareaRef.current as HTMLDivElement | null); + if (!container) return; + const tagElements = Array.from(container.querySelectorAll('[data-attachment-url]')) as HTMLElement[]; + const tagKeys = new Set(); + tagElements.forEach((el) => { + const url = el.getAttribute('data-attachment-url') || ''; + if (!url) return; + const isPicked = el.getAttribute('data-picked-object') === 'true'; + if (isPicked) { + const objectName = el.getAttribute('data-object-name') || ''; + const objectPosition = el.getAttribute('data-object-position') || ''; + tagKeys.add(`picked|${url}|${objectName}|${objectPosition}`); + } else { + tagKeys.add(`image|${url}`); + } + }); + setAttachments(prev => prev.filter((att) => { + if (att.pickedObject) { + const key = `picked|${att.url}|${att.pickedObject.objectName}|${att.pickedObject.objectPosition}`; + return tagKeys.has(key); + } + return tagKeys.has(`image|${att.url}`); + })); + }, []); // 移除附件(通过索引删除,保留兼容性) const removeAttachment = (index: number) => { @@ -1318,6 +1362,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF textContent += node.textContent; } setInput(textContent.trim()); + syncAttachmentsWithEditor(target); }} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -1336,8 +1381,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF e.preventDefault(); e.stopPropagation(); // 阻止事件冒泡,防止触发全局删除 // 使用 URL 来删除附件,更可靠 - const url = prevSibling.getAttribute('data-attachment-url') || ''; - removeAttachmentByUrl(url); + removeAttachmentByTagElement(prevSibling); prevSibling.remove(); } } diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 3e1ff4e..2c040ed 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; -import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X } from "lucide-react"; +import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Eraser, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -320,6 +320,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const getActionName = (action: string): string => { const names: Record = { 'rmbg': '抠图', + 'watermark': '去水印', 'enhance': '变清晰', 'extend': '图片扩展', 'resize': '尺寸调整', @@ -381,6 +382,9 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { case 'rmbg': result = await imageGenerationService.removeBackground(data.src); break; + case 'watermark': + result = await imageGenerationService.removeWatermark(data.src); + break; case 'enhance': result = await imageGenerationService.enhanceImage(data.src); break; @@ -742,6 +746,10 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 抠图 + handleAction('watermark')}> + + 去水印 + handleAction('enhance')}> 变清晰 @@ -856,7 +864,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { {isSelected && data.src && !isProcessing && typeof document !== 'undefined' && createPortal(
{ addPendingChatAttachment({ url: data.src!, @@ -893,7 +901,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 高清放大 @@ -929,7 +937,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { + + 去水印 + {/* 场景融合 */} @@ -944,7 +967,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {