From 09e6b070c75384af8282aec7cb193647a98155bc Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Feb 2026 12:22:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=87=E7=AD=BE=E6=82=AC=E5=81=9C?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=85=A8=E8=B2=8C=E5=92=8C=E6=A0=87=E6=B3=A8?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/canvas/CanvasChatPanel.tsx | 182 ++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index b5bef02..39f386c 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -24,6 +24,21 @@ import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils"; import type { Attachment, ChatMessage } from "@/lib/types"; import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar"; +// 标签悬停预览信息 +interface TagPreviewInfo { + url: string; + name: string; + x: number; // 弹窗位置 + y: number; + // 物品标签额外信息 + pickedObject?: { + objectName: string; + objectPosition: string; + clickX: number; + clickY: number; + }; +} + interface CanvasChatPanelProps { chatId: string; initialMessages?: ChatMessage[]; @@ -119,6 +134,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const fileInputRef = useRef(null); const textareaRef = useRef(null); + // 🆕 标签悬停预览状态 + const [tagPreview, setTagPreview] = useState(null); + const previewTimeoutRef = useRef(null); + // 🆕 图片参数状态 const [imageParams, setImageParams] = useState({ aspectRatio: "auto", @@ -347,6 +366,76 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF } }, [pendingPickedObjects, attachments, insertPickedObjectTagInEditor, clearPendingPickedObjects]); + // 🆕 标签悬停预览 - 事件委托处理 + useEffect(() => { + const editableDiv = textareaRef.current as HTMLDivElement | null; + if (!editableDiv) return; + + 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 (previewTimeoutRef.current) { + clearTimeout(previewTimeoutRef.current); + } + }; + }, [attachments]); + // 获取模型列表 const { data: modelsData } = useSWR<{ models: Array<{ @@ -1333,6 +1422,99 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF + + {/* 🆕 标签悬停预览弹窗 */} + {tagPreview && ( +
{ + // 鼠标进入预览弹窗,取消隐藏 + if (previewTimeoutRef.current) { + clearTimeout(previewTimeoutRef.current); + previewTimeoutRef.current = null; + } + }} + onMouseLeave={() => { + // 鼠标离开预览弹窗,隐藏 + setTagPreview(null); + }} + > + {/* 图片全貌 */} +
+ {tagPreview.name} + {/* 如果是物品标签,显示标注位置 */} + {tagPreview.pickedObject && ( + <> + {/* 标注点 */} +
+ {/* 序号标记 */} +
+ {/* 脉冲动画背景 */} +
+ {/* 主标记点 */} +
+ + + +
+
+
+ {/* 物品名称标签 */} +
+ {tagPreview.pickedObject.objectName} +
+ + )} +
+ {/* 底部信息 */} +
+
+ {tagPreview.pickedObject ? tagPreview.pickedObject.objectName : tagPreview.name} +
+ {tagPreview.pickedObject && ( +
+ {tagPreview.pickedObject.objectPosition} +
+ )} +
+ {/* 小三角箭头 */} +
+
+ )}
); }