From 2524ec551b2b920b4d948b32724ba9e4a9fcd1a4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Feb 2026 15:35:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=94=BB=E5=B8=83?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E9=87=8D=E5=A4=8D=E6=B7=BB=E5=8A=A0=E5=92=8C?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=A0=8F=E4=BD=8D=E7=BD=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 首次加载时不将历史消息中的图片添加到画布(避免重复) 2. 检查画布中是否已存在相同 URL 的图片 3. 工具栏位置在渲染时实时计算(跟随画布平移缩放) Co-authored-by: Cursor --- components/canvas/CanvasChatPanel.tsx | 33 +++++++++++++++++++++++---- components/canvas/nodes/ImageNode.tsx | 26 ++++++++++----------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index ecb3d380e..2b3adb440 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -286,9 +286,24 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF }, [historyData, setMessages]); // 监听消息变化,提取图片并添加到画布 + // 只处理新生成的图片,不处理历史消息中的图片 const processedMessageIds = useRef>(new Set()); + const isInitialLoad = useRef(true); useEffect(() => { + // 首次加载历史消息时,只记录已处理的消息 ID,不添加图片到画布 + // (画布数据应该从 canvasData 中加载,而不是从消息中提取) + if (isInitialLoad.current) { + messages.forEach(message => { + if (message.role === 'assistant') { + processedMessageIds.current.add(message.id); + } + }); + isInitialLoad.current = false; + return; + } + + // 只处理新消息 messages.forEach(message => { if (message.role === 'assistant' && !processedMessageIds.current.has(message.id)) { const content = message.parts?.map((p: any) => p.text || '').join('') || ''; @@ -297,8 +312,16 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF if (imageUrls.length > 0) { processedMessageIds.current.add(message.id); - // 为每张图片创建画布节点 - imageUrls.forEach((url, index) => { + // 检查画布中是否已存在相同 URL 的图片 + const existingUrls = new Set( + nodes.filter(n => n.type === 'image' && (n.data as any)?.src) + .map(n => (n.data as any).src) + ); + + // 只添加不存在的图片 + const newUrls = imageUrls.filter(url => !existingUrls.has(url)); + + newUrls.forEach((url, index) => { // 计算位置(避免重叠) const existingNodes = nodes.length; const x = 100 + ((existingNodes + index) % 4) * 320; @@ -317,13 +340,13 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF }); }); - if (imageUrls.length > 0) { - toast.success(`已将 ${imageUrls.length} 张图片添加到画布`); + if (newUrls.length > 0) { + toast.success(`已将 ${newUrls.length} 张图片添加到画布`); } } } }); - }, [messages, addNode, nodes.length]); + }, [messages, addNode, nodes]); // 滚动到底部 useEffect(() => { diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 71da15e41..c595f8e09 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -43,7 +43,6 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const [resizeWidth, setResizeWidth] = useState(1024); const [resizeHeight, setResizeHeight] = useState(1024); const [isUploading, setIsUploading] = useState(false); - const [toolbarPosition, setToolbarPosition] = useState<{ top: number; left: number } | null>(null); // 文件输入的 ref const fileInputRef = useRef(null); @@ -53,18 +52,17 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const deleteNode = useCanvasStore((state) => state.deleteNode); const addNode = useCanvasStore((state) => state.addNode); - // 计算工具栏位置 - useEffect(() => { - if (isSelected && containerRef.current) { + // 获取工具栏位置 - 在渲染时直接计算 + const getToolbarPosition = () => { + if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); - setToolbarPosition({ - top: rect.top - 50, // 工具栏在图片上方 50px - left: rect.left + rect.width / 2, // 居中 - }); - } else { - setToolbarPosition(null); + return { + top: rect.top - 50, + left: rect.left + rect.width / 2, + }; } - }, [isSelected, node.position, node.size]); + return { top: 0, left: 0 }; + }; // 触发文件选择 const handleSelectFile = (e: React.MouseEvent) => { @@ -536,12 +534,12 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} - {isSelected && data.src && !isProcessing && toolbarPosition && typeof document !== 'undefined' && createPortal( + {isSelected && data.src && !isProcessing && typeof document !== 'undefined' && createPortal(