auto-save 2026-05-13 13:48 (~3)
This commit is contained in:
@@ -1589,6 +1589,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 13:37 (~1)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T13:43:08+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-13 13:42 (+1, ~4)",
|
||||
"hash": "7e55b9b",
|
||||
"files_changed": 5
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T05:47:39Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 3 项未提交变更 · 最近提交:auto-save 2026-05-13 13:42 (+1, ~4)",
|
||||
"files_changed": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -618,8 +618,8 @@ export function ImageGenNode({ data, selected }: any) {
|
||||
style={{ aspectRatio: aspect }}
|
||||
>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); d.onOpenPanel?.("imagegen") }}
|
||||
title={`${p.name} · 来自分镜 ${p.frameIdx + 1}`}
|
||||
onClick={(e) => { e.stopPropagation(); d.onExpandFrame(p.frameIdx) }}
|
||||
title={`${p.name} · 来自分镜 ${p.frameIdx + 1} · 点击进入该分镜精细调整`}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { LayoutGrid, ChevronDown, ChevronUp, Sparkle } from "lucide-react"
|
||||
import { type Job, effectiveFrameUrl } from "@/lib/api"
|
||||
import { type Job, type KeyFrame, effectiveFrameUrl } from "@/lib/api"
|
||||
|
||||
interface Props {
|
||||
job: Job | null
|
||||
@@ -11,6 +12,12 @@ interface Props {
|
||||
|
||||
export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
// hover preview state — portal 渲染到 body 避免被父级 overflow-x-auto clip
|
||||
const [hover, setHover] = useState<{ frame: KeyFrame; seq: number; rect: DOMRect } | null>(null)
|
||||
const btnRefs = useRef<Record<number, HTMLButtonElement | null>>({})
|
||||
|
||||
if (!job) return null
|
||||
|
||||
// 按时间序排已选用的分镜
|
||||
@@ -63,28 +70,31 @@ export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
|
||||
return (
|
||||
<button
|
||||
key={f.index}
|
||||
ref={(el) => { btnRefs.current[f.index] = el }}
|
||||
onClick={() => onExpandFrame(f.index)}
|
||||
title={`分镜 ${i + 1} · ${f.timestamp.toFixed(2)}s${cleaned ? " · 已清洗" : ""} · ${elementCount}/${totalElCount} 元素 · 点击编辑`}
|
||||
className="group relative shrink-0 rounded-md border border-white/15 hover:border-violet-300/60 overflow-hidden transition shadow-lg hover:-translate-y-0.5"
|
||||
onMouseEnter={() => {
|
||||
const el = btnRefs.current[f.index]
|
||||
if (el) setHover({ frame: f, seq: i + 1, rect: el.getBoundingClientRect() })
|
||||
}}
|
||||
onMouseLeave={() => setHover(null)}
|
||||
title={`分镜 ${i + 1} · ${f.timestamp.toFixed(2)}s${cleaned ? " · 已清洗" : ""} · ${elementCount}/${totalElCount} 元素 · 点击进入精细调整`}
|
||||
className="relative shrink-0 rounded-md border border-white/15 hover:border-violet-300/60 transition shadow-lg hover:-translate-y-0.5"
|
||||
style={{ width: 88, aspectRatio: aspect }}
|
||||
>
|
||||
<img
|
||||
src={effectiveFrameUrl(job.id, f)}
|
||||
alt={`frame ${f.index}`}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
{/* 左上:序号 */}
|
||||
<div className="absolute top-1 left-1 text-[9.5px] font-bold text-white bg-violet-500/85 backdrop-blur px-1.5 py-0.5 rounded">
|
||||
#{i + 1}
|
||||
</div>
|
||||
{/* 右上:清洗标记 */}
|
||||
{cleaned && (
|
||||
<div className="absolute top-1 right-1 text-[9px] text-white bg-cyan-500/85 backdrop-blur px-1 py-0.5 rounded font-bold" title="已清洗">
|
||||
✨
|
||||
</div>
|
||||
)}
|
||||
{/* 底部:时间 + 元素数 */}
|
||||
<div className="absolute bottom-0 right-0 left-0 px-1.5 py-0.5 text-[9px] font-mono text-white bg-gradient-to-t from-black/85 to-transparent flex items-center justify-between">
|
||||
<div className="absolute bottom-0 right-0 left-0 px-1.5 py-0.5 text-[9px] font-mono text-white bg-gradient-to-t from-black/85 to-transparent flex items-center justify-between rounded-b-md">
|
||||
<span>{f.timestamp.toFixed(1)}s</span>
|
||||
{totalElCount > 0 && (
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
@@ -99,6 +109,38 @@ export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Hover 大图预览 · portal 到 body 避免父容器 overflow clip */}
|
||||
{mounted && hover && createPortal(
|
||||
<div
|
||||
className="fixed z-[120] pointer-events-none"
|
||||
style={{
|
||||
top: hover.rect.bottom + 8,
|
||||
left: Math.max(12, Math.min(window.innerWidth - 380, hover.rect.left + hover.rect.width / 2 - 180)),
|
||||
animation: "drawer-in 0.18s cubic-bezier(0.32, 0.72, 0, 1)",
|
||||
}}
|
||||
>
|
||||
<div className="rounded-2xl overflow-hidden border border-white/25 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(job.id, hover.frame)}
|
||||
alt={`preview ${hover.frame.index}`}
|
||||
className="block"
|
||||
style={{
|
||||
width: 360,
|
||||
maxWidth: "min(560px, 70vw)",
|
||||
height: "auto",
|
||||
maxHeight: "70vh",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-black/70 backdrop-blur-md">
|
||||
<span className="text-white text-[12.5px] font-medium">分镜 {hover.seq} · {hover.frame.timestamp.toFixed(2)}s</span>
|
||||
<span className="text-white/60 text-[11px] font-mono">点击进入精细调整</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user