auto-save 2026-05-14 01:57 (+1, ~4)

This commit is contained in:
2026-05-14 01:57:34 +08:00
parent 82a721b5ac
commit 11de581068
5 changed files with 2168 additions and 2082 deletions

View File

@@ -3,21 +3,23 @@ import { X } from "lucide-react"
/**
* 视觉类节点统一大预览:
* - **在 ReactFlow 节点 DOM 内**作为 absolute 元素,贴 thumb 上方边缘bottom: calc(100% + 10px) + 居中)
* - **在 ReactFlow 节点 DOM 内**作为 absolute 元素,贴节点卡片上边缘
* - 跟随 ReactFlow 画布 pan/zoom 一起变化(属于"无限画布"的一部分)
* - 媒体按"自然像素分辨率"渲染 + max-w/max-h 限制,避免占满整个画布
* - 不 pinned 时pointer-events-none依赖 group-hover 显示/隐藏
* - 媒体按"自然像素分辨率"渲染,不做 max 尺寸限制
* - 不 pinned 时pointer-events-none依赖调用方传入 visible
* - pinned=true强制 visiblepointer-events 开启,可点 × 关闭
* - 用法:父级容器要带 `group` classHoverPreview 直接作为子元素
* - 用法:渲染在节点根层,不要放进 overflow-x-auto 缩略图滚动条里
*/
interface Props {
imgSrc?: string
videoSrc?: string
poster?: string
aspect: string
aspect?: string
label?: string
caption?: string
borderClass?: string
visible?: boolean
anchorX?: number
pinned?: boolean
onClose?: () => void
}
@@ -26,19 +28,22 @@ export function HoverPreview({
imgSrc, videoSrc, poster, aspect,
label, caption,
borderClass = "border-violet-300/55",
visible = false,
anchorX,
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"
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-[60] ${visibilityCls}`}
className={`absolute transition-all duration-150 z-[120] ${visibilityCls}`}
style={{
bottom: "calc(100% + 10px)",
left: "50%",
transform: "translateX(-50%)",
bottom: "calc(100% + 8px)",
left: typeof anchorX === "number" ? `${anchorX}px` : "50%",
transform: `translateX(-50%) scale(${shown ? 1 : 0.96})`,
transformOrigin: "bottom center",
}}
>
@@ -54,14 +59,19 @@ export function HoverPreview({
autoPlay
preload="auto"
className="block"
style={{ maxWidth: "none" }}
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={{ maxWidth: "none" }} />
<img
src={imgSrc}
alt=""
className="block"
style={{ width: "auto", height: "auto", maxWidth: "none", maxHeight: "none" }}
/>
) : (
<div className="w-40 h-40 bg-black/40" />
<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">