Files
20260512-skg-tk/web/components/nodes/hover-preview.tsx

81 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { X } from "lucide-react"
/**
* 视觉类节点统一大预览:
* - **在 ReactFlow 节点 DOM 内**作为 absolute 元素,贴 thumb 上方边缘bottom: calc(100% + 10px) + 居中)
* - 跟随 ReactFlow 画布 pan/zoom 一起变化(属于"无限画布"的一部分)
* - 媒体按"自然像素分辨率"渲染 + max-w/max-h 限制,避免占满整个画布
* - 不 pinned 时pointer-events-none依赖 group-hover 显示/隐藏
* - pinned=true强制 visiblepointer-events 开启,可点 × 关闭
* - 用法:父级容器要带 `group` classHoverPreview 直接作为子元素
*/
interface Props {
imgSrc?: string
videoSrc?: string
poster?: string
aspect: string
label?: string
caption?: string
borderClass?: string
maxW?: string // 默认 "min(70vw, 1200px)"
maxH?: string // 默认 "min(70vh, 800px)"
pinned?: boolean
onClose?: () => void
}
export function HoverPreview({
imgSrc, videoSrc, poster, aspect,
label, caption,
borderClass = "border-violet-300/55",
maxW = "min(70vw, 1200px)",
maxH = "min(70vh, 800px)",
pinned = false,
onClose,
}: Props) {
const visibilityCls = pinned
? "opacity-100 scale-100 pointer-events-auto"
: "pointer-events-none opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100"
return (
<div
className={`absolute transition-all duration-150 z-[60] ${visibilityCls}`}
style={{
bottom: "calc(100% + 10px)",
left: "50%",
transform: "translateX(-50%)",
transformOrigin: "bottom center",
}}
>
<div className={`relative rounded-lg overflow-hidden border-2 bg-black shadow-2xl ${pinned ? "ring-2 ring-violet-400/70" : ""} ${borderClass}`}>
{videoSrc ? (
<video src={videoSrc} poster={poster} muted loop playsInline autoPlay
className="block object-contain"
style={{ maxWidth: maxW, maxHeight: maxH }} />
) : imgSrc ? (
<img src={imgSrc} alt="" className="block object-contain"
style={{ maxWidth: maxW, maxHeight: maxH }} />
) : (
<div className="w-40 h-40 bg-black/40" />
)}
{(label || caption) && (
<div className="px-2 py-1 bg-black/80 text-white text-[11px] flex items-center justify-between">
{label && <span>{label}</span>}
{caption && <span className="text-white/60 font-mono">{caption}</span>}
</div>
)}
{pinned && onClose && (
<button
type="button"
onClick={(e) => { e.stopPropagation(); onClose() }}
onMouseDown={(e) => e.stopPropagation()}
title="取消固定预览"
className="absolute right-1 top-1 h-7 w-7 rounded-full bg-black/75 text-white shadow-lg hover:bg-rose-500 inline-flex items-center justify-center z-10"
>
<X className="h-4 w-4" />
</button>
)}
</div>
</div>
)
}