From 2722a231bccdd04e5876842451579c32eede34c8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Feb 2026 12:24:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E6=82=AC=E5=81=9C=E9=A2=84=E8=A7=88=E4=BA=8B=E4=BB=B6=E7=BB=91?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/canvas/CanvasChatPanel.tsx | 110 +++++++++++--------------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 39f386c2f..6e45fc34f 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -366,76 +366,56 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF } }, [pendingPickedObjects, attachments, insertPickedObjectTagInEditor, clearPendingPickedObjects]); - // 🆕 标签悬停预览 - 事件委托处理 - useEffect(() => { - const editableDiv = textareaRef.current as HTMLDivElement | null; - if (!editableDiv) return; + // 🆕 标签悬停预览处理函数 + const handleTagMouseEnter = useCallback((e: React.MouseEvent) => { + const target = e.target as HTMLElement; + const tagSpan = target.closest('[data-attachment-url]') as HTMLElement | null; - const handleMouseOver = (e: MouseEvent) => { - const target = e.target as HTMLElement; - // 检查是否悬停在标签上 - const tagSpan = target.closest('[data-attachment-url]') as HTMLElement | null; - if (tagSpan) { - // 清除之前的延迟隐藏 - if (previewTimeoutRef.current) { - clearTimeout(previewTimeoutRef.current); - previewTimeoutRef.current = null; - } - - const url = tagSpan.getAttribute('data-attachment-url'); - const isPickedObject = tagSpan.getAttribute('data-picked-object') === 'true'; - const objectName = tagSpan.getAttribute('data-object-name'); - - if (url) { - const rect = tagSpan.getBoundingClientRect(); - // 从 attachments 中获取物品信息 - const attachment = attachments.find(a => - a.url === url && - (isPickedObject ? a.pickedObject?.objectName === objectName : true) - ); - - setTagPreview({ - url, - name: objectName || tagSpan.textContent || '图片', - x: rect.left + rect.width / 2, - y: rect.top, - pickedObject: attachment?.pickedObject ? { - objectName: attachment.pickedObject.objectName, - objectPosition: attachment.pickedObject.objectPosition, - clickX: attachment.pickedObject.clickX, - clickY: attachment.pickedObject.clickY, - } : undefined, - }); - } - } - }; - - const handleMouseOut = (e: MouseEvent) => { - const target = e.target as HTMLElement; - const relatedTarget = e.relatedTarget as HTMLElement | null; - const tagSpan = target.closest('[data-attachment-url]'); - - // 如果离开标签且不是进入预览弹窗 - if (tagSpan && !relatedTarget?.closest('[data-tag-preview]')) { - // 延迟隐藏,给用户时间移动到预览弹窗 - previewTimeoutRef.current = setTimeout(() => { - setTagPreview(null); - }, 150); - } - }; - - editableDiv.addEventListener('mouseover', handleMouseOver); - editableDiv.addEventListener('mouseout', handleMouseOut); - - return () => { - editableDiv.removeEventListener('mouseover', handleMouseOver); - editableDiv.removeEventListener('mouseout', handleMouseOut); + if (tagSpan) { + // 清除之前的延迟隐藏 if (previewTimeoutRef.current) { clearTimeout(previewTimeoutRef.current); + previewTimeoutRef.current = null; } - }; + + const url = tagSpan.getAttribute('data-attachment-url'); + const isPickedObject = tagSpan.getAttribute('data-picked-object') === 'true'; + const objectName = tagSpan.getAttribute('data-object-name'); + + if (url) { + const rect = tagSpan.getBoundingClientRect(); + // 从 attachments 中获取物品信息 + const attachment = attachments.find(a => + a.url === url && + (isPickedObject ? a.pickedObject?.objectName === objectName : true) + ); + + setTagPreview({ + url, + name: objectName || tagSpan.textContent || '图片', + x: rect.left + rect.width / 2, + y: rect.top, + pickedObject: attachment?.pickedObject ? { + objectName: attachment.pickedObject.objectName, + objectPosition: attachment.pickedObject.objectPosition, + clickX: attachment.pickedObject.clickX, + clickY: attachment.pickedObject.clickY, + } : undefined, + }); + } + } }, [attachments]); + const handleTagMouseLeave = useCallback((e: React.MouseEvent) => { + const relatedTarget = e.relatedTarget as HTMLElement | null; + // 如果不是进入预览弹窗,则延迟隐藏 + if (!relatedTarget?.closest('[data-tag-preview]')) { + previewTimeoutRef.current = setTimeout(() => { + setTagPreview(null); + }, 150); + } + }, []); + // 获取模型列表 const { data: modelsData } = useSWR<{ models: Array<{ @@ -1278,6 +1258,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF suppressContentEditableWarning className="min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/50" data-placeholder="请输入你的设计需求,例如:绿色椅子 替换 Image" + onMouseOver={handleTagMouseEnter} + onMouseLeave={handleTagMouseLeave} onInput={(e) => { // 从 contenteditable 中提取纯文本(排除图片标签的 data-url) const target = e.target as HTMLDivElement;