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

100 lines
3.5 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 元素,底边贴当前缩略图上方边缘
* - 跟随 ReactFlow 画布 pan/zoom 一起变化(属于"无限画布"的一部分)
* - 媒体按"自然像素分辨率"渲染,不做 max 尺寸限制
* - 不 pinned 时pointer-events-none依赖调用方传入 visible
* - pinned=true强制 visiblepointer-events 开启,可点 × 关闭
* - 用法:渲染在节点根层,不要放进 overflow-x-auto 缩略图滚动条里
*/
interface Props {
imgSrc?: string
videoSrc?: string
poster?: string
aspect?: string
label?: string
caption?: string
borderClass?: string
visible?: boolean
anchorX?: number
anchorY?: number
pinned?: boolean
onClose?: () => void
}
export function HoverPreview({
imgSrc, videoSrc, poster, aspect,
label, caption,
borderClass = "border-violet-300/55",
visible = false,
anchorX,
anchorY,
pinned = false,
onClose,
}: Props) {
const shown = pinned || visible
const visibilityCls = shown
? pinned ? "opacity-100 pointer-events-auto" : "opacity-100 pointer-events-none"
: "pointer-events-none opacity-0"
return (
<div
className={`absolute transition-all duration-150 z-[120] ${visibilityCls}`}
style={{
top: typeof anchorY === "number" ? `${anchorY - 8}px` : undefined,
bottom: typeof anchorY === "number" ? undefined : "calc(100% + 8px)",
left: typeof anchorX === "number" ? `${anchorX}px` : "50%",
transform: `${typeof anchorY === "number" ? "translate(-50%, -100%)" : "translateX(-50%)"} scale(${shown ? 1 : 0.96})`,
transformOrigin: "bottom center",
}}
>
<div className={`relative rounded-lg overflow-hidden border-2 bg-black shadow-2xl ${pinned ? "ring-2 ring-violet-400/70" : ""} ${borderClass}`}
style={{ width: "max-content" }}>
{videoSrc ? (
<video
src={videoSrc}
poster={poster}
muted
loop
playsInline
autoPlay
preload="auto"
className="block"
style={{ width: "auto", height: "auto", maxWidth: "none", maxHeight: "none" }}
onLoadedMetadata={(e) => { e.currentTarget.play().catch(() => {}) }}
onCanPlay={(e) => { e.currentTarget.play().catch(() => {}) }}
/>
) : imgSrc ? (
<img
src={imgSrc}
alt=""
className="block"
style={{ width: "auto", height: "auto", maxWidth: "none", maxHeight: "none" }}
/>
) : (
<div className="w-40 bg-black/40" style={{ aspectRatio: aspect ?? "1/1" }} />
)}
{(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>
)
}