auto-save 2026-05-13 15:39 (~2)

This commit is contained in:
2026-05-13 15:39:25 +08:00
parent 944e7e50dc
commit 5c3da231e0
2 changed files with 54 additions and 86 deletions

View File

@@ -1795,6 +1795,19 @@
"message": "auto-save 2026-05-13 15:28 (~4)",
"hash": "ad895f9",
"files_changed": 4
},
{
"ts": "2026-05-13T15:33:53+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 15:33 (~4)",
"hash": "944e7e5",
"files_changed": 4
},
{
"ts": "2026-05-13T07:37:40Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 2 项未提交变更 · 最近提交auto-save 2026-05-13 15:33 (~4)",
"files_changed": 2
}
]
}

View File

@@ -347,9 +347,7 @@ export function KeyframeNode({ data, selected }: any) {
const st = keyframeStatus(d.job)
const frames = d.job?.frames ?? []
const jobId = d.job?.id
const [hover, setHover] = useState<{ idx: number; rect: DOMRect } | null>(null)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
const aspectStr = d.job && d.job.height > 0 ? `${d.job.width}/${d.job.height}` : "9/16"
return (
<div className="relative" style={{ width: KEYFRAME_WIDTH }}>
@@ -377,8 +375,6 @@ export function KeyframeNode({ data, selected }: any) {
>
<button
onClick={(e) => { e.stopPropagation(); d.onExpandFrame(f.index) }}
onMouseEnter={(e) => setHover({ idx: f.index, rect: (e.currentTarget as HTMLElement).getBoundingClientRect() })}
onMouseLeave={() => setHover(null)}
title={`${f.index + 1} 张 · ${f.timestamp.toFixed(1)}s · hover 看大图 · 点击精细调整`}
className="absolute inset-0 w-full h-full"
>
@@ -441,6 +437,26 @@ export function KeyframeNode({ data, selected }: any) {
<X className="h-3 w-3" />
</button>
)}
{/* hover 预览 — absolute 浮在缩略图上方,跟着 ReactFlow 画布缩放平移 */}
<div
className="pointer-events-none absolute opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100 transition-all duration-150 z-[60]"
style={{
bottom: "calc(100% + 10px)",
left: "50%",
transform: "translateX(-50%)",
transformOrigin: "bottom center",
}}
>
<div className="rounded-lg overflow-hidden border-2 border-orange-300/50 bg-black shadow-2xl" style={{ width: 280 }}>
<div style={{ aspectRatio: aspectStr }}>
<img src={effectiveFrameUrl(jobId, f)} alt="" className="w-full h-full object-cover" />
</div>
<div className="px-2 py-1 bg-black/80 text-white text-[10.5px] flex items-center justify-between">
<span> {f.index + 1}</span>
<span className="text-white/60 font-mono">{f.timestamp.toFixed(2)}s</span>
</div>
</div>
</div>
</div>
)
})}
@@ -479,43 +495,6 @@ export function KeyframeNode({ data, selected }: any) {
)}
</NodeShell>
{/* Portal hover 预览 — 缩略图正上方展开DAG 画布上方空旷) */}
{mounted && hover && jobId && (() => {
const hf = frames.find((x) => x.index === hover.idx)
if (!hf) return null
const vidAspect = d.job && d.job.height > 0 ? d.job.height / d.job.width : 16 / 9
const gap = 12
// 高度优先:不超过缩略图上方可用空间,也不超过视口 80vh
const maxH = Math.max(160, Math.min(window.innerHeight * 0.8, hover.rect.top - gap - 12))
const maxW = Math.min(window.innerWidth * 0.45, 480)
let h = maxH, w = h / vidAspect
if (w > maxW) { w = maxW; h = w * vidAspect }
const centerX = hover.rect.left + hover.rect.width / 2
const left = Math.max(12, Math.min(window.innerWidth - w - 12, centerX - w / 2))
const top = hover.rect.top - h - gap
return createPortal(
<div
className="fixed z-[120] pointer-events-none"
style={{
left, top,
animation: "drawer-in 0.18s cubic-bezier(0.32, 0.72, 0, 1)",
}}
>
<div className="rounded-2xl overflow-hidden border border-orange-300/40 bg-black" style={{ boxShadow: "0 30px 80px -10px rgba(0,0,0,0.85), 0 0 0 1px rgba(255,255,255,0.06)" }}>
<img
src={effectiveFrameUrl(jobId, hf)}
alt={`preview ${hf.index}`}
className="block"
style={{ width: w, height: h, objectFit: "contain" }}
/>
<div className="flex items-center justify-between px-3 py-1.5 bg-black/70 backdrop-blur-md">
<span className="text-white text-[12px] font-medium"> {hf.index + 1} · {hf.timestamp.toFixed(2)}s</span>
</div>
</div>
</div>,
document.body,
)
})()}
</div>
)
}
@@ -619,9 +598,6 @@ const IMAGEGEN_WIDTH = 360
export function ImageGenNode({ data, selected }: any) {
const d: NodeData = data
const job = d?.job
const [hover, setHover] = useState<{ key: string; rect: DOMRect } | null>(null)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// 上方浮条 = 所有 frame 的 elements 已提取图("分镜头编排"的输入素材)
type ElPreview = { frameIdx: number; elementId: string; name: string; src: string; cid: string }
@@ -662,8 +638,6 @@ export function ImageGenNode({ data, selected }: any) {
>
<button
onClick={(e) => { e.stopPropagation(); d.onOpenStoryboard?.(p.frameIdx) }}
onMouseEnter={(e) => setHover({ key, rect: (e.currentTarget as HTMLElement).getBoundingClientRect() })}
onMouseLeave={() => setHover(null)}
title={`${p.name} · 来自分镜 ${p.frameIdx + 1} · hover 看大图`}
className="absolute inset-0 w-full h-full"
>
@@ -692,6 +666,26 @@ export function ImageGenNode({ data, selected }: any) {
</button>
)}
{/* hover 预览 — absolute 浮在缩略图上方 */}
<div
className="pointer-events-none absolute opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100 transition-all duration-150 z-[60]"
style={{
bottom: "calc(100% + 10px)",
left: "50%",
transform: "translateX(-50%)",
transformOrigin: "bottom center",
}}
>
<div className="rounded-lg overflow-hidden border-2 border-violet-300/50 bg-white shadow-2xl" style={{ width: 280 }}>
<div style={{ aspectRatio: aspect }}>
<img src={p.src} alt="" className="w-full h-full object-contain" />
</div>
<div className="px-2 py-1 bg-black/80 text-white text-[10.5px] flex items-center justify-between">
<span className="truncate">{p.name}</span>
<span className="text-white/60 font-mono shrink-0 ml-2"> {p.frameIdx + 1}</span>
</div>
</div>
</div>
</div>
)
})}
@@ -723,45 +717,6 @@ export function ImageGenNode({ data, selected }: any) {
)}
</NodeShell>
{/* Portal hover 预览 — 缩略图正上方展开 */}
{mounted && hover && job && (() => {
const [fi, ei] = hover.key.split("_")
const frameIdx = parseInt(fi, 10)
const p = elementCrops.find((x) => x.frameIdx === frameIdx && x.elementId === ei)
if (!p) return null
const vidAspect = job.height > 0 ? job.height / job.width : 16 / 9
const gap = 12
const maxH = Math.max(160, Math.min(window.innerHeight * 0.8, hover.rect.top - gap - 12))
const maxW = Math.min(window.innerWidth * 0.45, 480)
let h = maxH, w = h / vidAspect
if (w > maxW) { w = maxW; h = w * vidAspect }
const centerX = hover.rect.left + hover.rect.width / 2
const left = Math.max(12, Math.min(window.innerWidth - w - 12, centerX - w / 2))
const top = hover.rect.top - h - gap
return createPortal(
<div
className="fixed z-[120] pointer-events-none"
style={{
left, top,
animation: "drawer-in 0.18s cubic-bezier(0.32, 0.72, 0, 1)",
}}
>
<div className="rounded-2xl overflow-hidden border border-violet-300/40 bg-white" style={{ boxShadow: "0 30px 80px -10px rgba(0,0,0,0.85), 0 0 0 1px rgba(255,255,255,0.06)" }}>
<img
src={p.src}
alt={`preview ${p.elementId}`}
className="block"
style={{ width: w, height: h, objectFit: "contain" }}
/>
<div className="flex items-center justify-between px-3 py-1.5 bg-black/70 backdrop-blur-md">
<span className="text-white text-[12px] font-medium">{p.name}</span>
<span className="text-white/60 text-[11px] font-mono"> {p.frameIdx + 1}</span>
</div>
</div>
</div>,
document.body,
)
})()}
</div>
)
}