fix: 修复标签预览弹窗位置,固定显示在标签上方并防止超出边界

This commit is contained in:
Your Name 2026-02-02 12:36:12 +08:00
parent e314edb283
commit 976e8de4c7

View File

@ -1425,98 +1425,107 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
</div>
</div>
{/* 🆕 标签悬停预览弹窗 */}
{tagPreview && (
<div
data-tag-preview="true"
className="fixed z-[9999] bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-border/50 overflow-hidden animate-in fade-in zoom-in-95 duration-150"
style={{
left: tagPreview.x,
top: tagPreview.y - 8,
transform: 'translate(-50%, -100%)',
maxWidth: '280px',
}}
onMouseEnter={() => {
// 鼠标进入预览弹窗,取消隐藏
if (previewTimeoutRef.current) {
clearTimeout(previewTimeoutRef.current);
previewTimeoutRef.current = null;
}
}}
onMouseLeave={() => {
// 鼠标离开预览弹窗,隐藏
setTagPreview(null);
}}
>
{/* 图片全貌 */}
<div className="relative">
<img
src={tagPreview.url}
alt={tagPreview.name}
className="w-full h-auto max-h-[200px] object-contain bg-zinc-100 dark:bg-zinc-800"
/>
{/* 如果是物品标签,显示标注位置 */}
{tagPreview.pickedObject && (
<>
{/* 标注点 */}
<div
className="absolute pointer-events-none"
style={{
left: `${tagPreview.pickedObject.clickX * 100}%`,
top: `${tagPreview.pickedObject.clickY * 100}%`,
transform: 'translate(-50%, -50%)',
}}
>
{/* 序号标记 */}
<div className="relative flex items-center justify-center">
{/* 脉冲动画背景 */}
<div className="absolute w-8 h-8 rounded-full bg-blue-500/30 animate-ping" />
{/* 主标记点 */}
<div className="relative w-6 h-6 rounded-full bg-blue-500 border-2 border-white shadow-lg flex items-center justify-center">
<svg className="w-3 h-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
<path d="M12 2v20M2 12h20" />
</svg>
{/* 🆕 标签悬停预览弹窗 - 固定显示在标签上方 */}
{tagPreview && (() => {
// 计算弹窗位置,确保不超出视口
const previewWidth = 240;
const previewHeight = 280; // 预估高度
const padding = 16;
// 水平位置:居中对齐标签,但确保不超出左右边界
let left = tagPreview.x - previewWidth / 2;
if (left < padding) left = padding;
if (left + previewWidth > window.innerWidth - padding) {
left = window.innerWidth - previewWidth - padding;
}
// 垂直位置:显示在标签上方
let top = tagPreview.y - previewHeight - 12;
if (top < padding) {
// 如果上方空间不够,显示在标签下方
top = tagPreview.y + 30;
}
return (
<div
data-tag-preview="true"
className="fixed z-[9999] bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-border/50 overflow-hidden animate-in fade-in zoom-in-95 duration-150"
style={{
left: left,
top: top,
width: previewWidth,
}}
onMouseEnter={() => {
// 鼠标进入预览弹窗,取消隐藏
if (previewTimeoutRef.current) {
clearTimeout(previewTimeoutRef.current);
previewTimeoutRef.current = null;
}
}}
onMouseLeave={() => {
// 鼠标离开预览弹窗,隐藏
setTagPreview(null);
}}
>
{/* 图片全貌 */}
<div className="relative">
<img
src={tagPreview.url}
alt={tagPreview.name}
className="w-full h-auto max-h-[180px] object-contain bg-zinc-100 dark:bg-zinc-800"
/>
{/* 如果是物品标签,显示标注位置 */}
{tagPreview.pickedObject && (
<>
{/* 标注点 */}
<div
className="absolute pointer-events-none"
style={{
left: `${tagPreview.pickedObject.clickX * 100}%`,
top: `${tagPreview.pickedObject.clickY * 100}%`,
transform: 'translate(-50%, -50%)',
}}
>
{/* 序号标记 */}
<div className="relative flex items-center justify-center">
{/* 脉冲动画背景 */}
<div className="absolute w-6 h-6 rounded-full bg-blue-500/30 animate-ping" />
{/* 主标记点 */}
<div className="relative w-5 h-5 rounded-full bg-blue-500 border-2 border-white shadow-lg flex items-center justify-center">
<svg className="w-2.5 h-2.5 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
<path d="M12 2v20M2 12h20" />
</svg>
</div>
</div>
</div>
</div>
{/* 物品名称标签 */}
<div
className="absolute bg-blue-500 text-white text-xs px-2 py-0.5 rounded shadow-lg whitespace-nowrap"
style={{
left: `${tagPreview.pickedObject.clickX * 100}%`,
top: `${tagPreview.pickedObject.clickY * 100}%`,
transform: 'translate(-50%, -150%)',
}}
>
{tagPreview.pickedObject.objectName}
</div>
</>
)}
</div>
{/* 底部信息 */}
<div className="px-3 py-2 bg-zinc-50 dark:bg-zinc-800/50 border-t border-border/30">
<div className="text-xs font-medium text-foreground/80 truncate">
{tagPreview.pickedObject ? tagPreview.pickedObject.objectName : tagPreview.name}
{/* 物品名称标签 */}
<div
className="absolute bg-blue-500 text-white text-xs px-1.5 py-0.5 rounded shadow-lg whitespace-nowrap"
style={{
left: `${tagPreview.pickedObject.clickX * 100}%`,
top: `${tagPreview.pickedObject.clickY * 100}%`,
transform: 'translate(-50%, -140%)',
}}
>
{tagPreview.pickedObject.objectName}
</div>
</>
)}
</div>
{tagPreview.pickedObject && (
<div className="text-xs text-muted-foreground">
{tagPreview.pickedObject.objectPosition}
{/* 底部信息 */}
<div className="px-3 py-2 bg-zinc-50 dark:bg-zinc-800/50 border-t border-border/30">
<div className="text-xs font-medium text-foreground/80 truncate">
{tagPreview.pickedObject ? tagPreview.pickedObject.objectName : tagPreview.name}
</div>
)}
{tagPreview.pickedObject && (
<div className="text-xs text-muted-foreground">
{tagPreview.pickedObject.objectPosition}
</div>
)}
</div>
</div>
{/* 小三角箭头 */}
<div
className="absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-full"
style={{
width: 0,
height: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderTop: '8px solid rgb(250 250 250)',
}}
/>
</div>
)}
);
})()}
</div>
);
}