fix: 使用全局事件监听修复标签悬停预览

This commit is contained in:
Your Name 2026-02-02 12:31:07 +08:00
parent ee9ac5521f
commit 84726009ff

View File

@ -366,56 +366,78 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
}
}, [pendingPickedObjects, attachments, insertPickedObjectTagInEditor, clearPendingPickedObjects]);
// 🆕 标签悬停预览处理函数
const handleTagMouseEnter = useCallback((e: React.MouseEvent) => {
const target = e.target as HTMLElement;
const tagSpan = target.closest('[data-attachment-url]') as HTMLElement | null;
// 🆕 标签悬停预览 - 使用全局事件监听
useEffect(() => {
let lastTagElement: HTMLElement | null = null;
if (tagSpan) {
// 清除之前的延迟隐藏
const handleMouseMove = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const tagSpan = target.closest('[data-attachment-url]') as HTMLElement | null;
// 检查是否在预览弹窗内
const inPreview = target.closest('[data-tag-preview]');
if (inPreview) {
// 在预览弹窗内,保持显示
if (previewTimeoutRef.current) {
clearTimeout(previewTimeoutRef.current);
previewTimeoutRef.current = null;
}
return;
}
if (tagSpan && tagSpan !== lastTagElement) {
lastTagElement = 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,
});
}
} else if (!tagSpan && lastTagElement) {
// 离开标签
lastTagElement = null;
previewTimeoutRef.current = setTimeout(() => {
setTagPreview(null);
}, 200);
}
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
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<{
@ -1258,8 +1280,6 @@ 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;