diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index b562927d7..7a3f7113f 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -28,8 +28,15 @@ import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-p interface TagPreviewInfo { url: string; name: string; - x: number; // 弹窗位置 - y: number; + x: number; // 标签中心x位置 + y: number; // 标签顶部y位置 + // 聊天面板容器边界 + containerBounds?: { + left: number; + right: number; + top: number; + bottom: number; + }; // 物品标签额外信息 pickedObject?: { objectName: string; @@ -406,11 +413,25 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF (isPickedObject ? a.pickedObject?.objectName === objectName : true) ); + // 获取聊天面板容器边界(向上查找最近的面板容器) + const panelContainer = tagSpan.closest('.flex.flex-col.h-full') as HTMLElement | null; + let containerBounds: TagPreviewInfo['containerBounds'] = undefined; + if (panelContainer) { + const panelRect = panelContainer.getBoundingClientRect(); + containerBounds = { + left: panelRect.left, + right: panelRect.right, + top: panelRect.top, + bottom: panelRect.bottom, + }; + } + setTagPreview({ url, name: objectName || tagSpan.textContent || '图片', x: rect.left + rect.width / 2, y: rect.top, + containerBounds, pickedObject: attachment?.pickedObject ? { objectName: attachment.pickedObject.objectName, objectPosition: attachment.pickedObject.objectPosition, @@ -1427,49 +1448,60 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF {/* 🆕 标签悬停预览弹窗 - 固定显示在标签上方 */} {tagPreview && (() => { - // 计算弹窗位置,确保不超出视口 + // 计算弹窗位置,确保不超出容器边界 const previewWidth = 240; const previewHeight = 280; // 预估高度 const padding = 16; - // 获取聊天面板的边界(弹窗应该在面板内显示) - const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 800; - const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 600; + // 使用容器边界(如果有),否则使用视口 + const bounds = tagPreview.containerBounds || { + left: 0, + right: typeof window !== 'undefined' ? window.innerWidth : 800, + top: 0, + bottom: typeof window !== 'undefined' ? window.innerHeight : 600, + }; - // 水平位置:居中对齐标签,但确保不超出左右边界 + const containerWidth = bounds.right - bounds.left; + const containerHeight = bounds.bottom - bounds.top; + + // 水平位置:居中对齐标签,但确保不超出容器边界 let left = tagPreview.x - previewWidth / 2; // 确保不超出左边界 - if (left < padding) left = padding; - // 确保不超出右边界(考虑弹窗宽度) - if (left + previewWidth > viewportWidth - padding) { - left = viewportWidth - previewWidth - padding; + if (left < bounds.left + padding) { + left = bounds.left + padding; + } + // 确保不超出右边界 + if (left + previewWidth > bounds.right - padding) { + left = bounds.right - previewWidth - padding; } // 垂直位置:优先显示在标签上方 let top = tagPreview.y - previewHeight - 12; // 如果上方空间不够(弹窗会超出顶部),显示在标签下方 - if (top < padding) { + if (top < bounds.top + padding) { top = tagPreview.y + 30; } // 再次检查:如果下方也会超出底部,则尽量靠近底部显示 - if (top + previewHeight > viewportHeight - padding) { - top = viewportHeight - previewHeight - padding; + if (top + previewHeight > bounds.bottom - padding) { + top = bounds.bottom - previewHeight - padding; } - // 最终确保 top 不为负数 - if (top < padding) top = padding; + // 最终确保 top 不超出顶部 + if (top < bounds.top + padding) { + top = bounds.top + padding; + } return (
{ // 鼠标进入预览弹窗,取消隐藏