auto-save 2026-05-12 23:49 (~2)
This commit is contained in:
@@ -482,6 +482,13 @@
|
||||
"message": "auto-save 2026-05-12 23:38 (~5)",
|
||||
"hash": "447f116",
|
||||
"files_changed": 5
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-12T23:44:18+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-12 23:44 (~2)",
|
||||
"hash": "494d990",
|
||||
"files_changed": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
import { useEffect, useState } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Copy } from "lucide-react"
|
||||
import { frameUrl, describeFrame, type KeyFrame, type Job } from "@/lib/api"
|
||||
import { toast } from "sonner"
|
||||
@@ -18,6 +19,8 @@ interface Props {
|
||||
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate }: Props) {
|
||||
const [extractPrompt, setExtractPrompt] = useState("")
|
||||
const [describing, setDescribing] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex === null) return
|
||||
@@ -37,7 +40,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
return () => window.removeEventListener("keydown", onKey)
|
||||
}, [activeIndex, frames.length, onClose, onChange, onToggleSelect])
|
||||
|
||||
if (activeIndex === null || !frames[activeIndex]) return null
|
||||
if (activeIndex === null || !frames[activeIndex] || !mounted) return null
|
||||
const f = frames[activeIndex]
|
||||
const isSelected = selected.has(f.index)
|
||||
const desc = f.description
|
||||
@@ -59,68 +62,76 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
navigator.clipboard.writeText(text).then(() => toast.success("已复制"))
|
||||
}
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[100] bg-black/85 backdrop-blur-sm flex items-center justify-center"
|
||||
onClick={onClose}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="fixed z-[100] rounded-2xl border border-white/15 bg-black/70 backdrop-blur-2xl overflow-hidden flex flex-col"
|
||||
style={{
|
||||
top: 80,
|
||||
right: 16,
|
||||
width: 740,
|
||||
maxHeight: "calc(100vh - 96px)",
|
||||
boxShadow: "0 40px 100px -20px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05)",
|
||||
animation: "drawer-in 0.24s cubic-bezier(0.32, 0.72, 0, 1)",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onClose() }}
|
||||
className="absolute top-5 right-5 h-10 w-10 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center z-10"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{activeIndex > 0 && (
|
||||
{/* 顶部工具栏 — 切换 / 关闭 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-white/10 bg-white/[0.03]">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (activeIndex > 0) onChange(activeIndex - 1) }}
|
||||
disabled={activeIndex === 0}
|
||||
className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (activeIndex < frames.length - 1) onChange(activeIndex + 1) }}
|
||||
disabled={activeIndex >= frames.length - 1}
|
||||
className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="text-[11px] font-mono text-white/70 ml-1">
|
||||
分镜 {String(f.index + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
|
||||
<span className="mx-1.5 text-white/30">·</span>
|
||||
<span className="text-white/60">{f.timestamp.toFixed(2)}s</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onChange(activeIndex - 1) }}
|
||||
className="absolute left-5 top-1/2 -translate-y-1/2 h-12 w-12 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center z-10"
|
||||
onClick={onClose}
|
||||
className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{activeIndex < frames.length - 1 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onChange(activeIndex + 1) }}
|
||||
className="absolute right-5 top-1/2 -translate-y-1/2 h-12 w-12 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center z-10"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div onClick={(e) => e.stopPropagation()} className="flex gap-4 max-w-[92vw] max-h-[92vh] items-start">
|
||||
{/* 主体 — 左大图 + 右识别面板 */}
|
||||
<div className="flex gap-3 p-3 overflow-hidden flex-1 min-h-0">
|
||||
{/* 左侧大图 */}
|
||||
<div className="flex flex-col items-center gap-3 flex-shrink-0">
|
||||
<div className="flex flex-col items-center gap-2 flex-shrink-0">
|
||||
<img
|
||||
src={frameUrl(jobId, f.index)}
|
||||
alt={`frame ${f.index}`}
|
||||
className="rounded-xl shadow-2xl object-contain"
|
||||
style={{ maxWidth: "60vw", maxHeight: "80vh" }}
|
||||
className="rounded-lg object-contain"
|
||||
style={{ maxWidth: 320, maxHeight: "70vh" }}
|
||||
/>
|
||||
<div className="flex items-center gap-3 text-white">
|
||||
<div className="font-mono text-sm tabular-nums text-white/80">
|
||||
{String(f.index + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
|
||||
<span className="mx-2 text-white/40">·</span>
|
||||
<span className="text-white/60">{f.timestamp.toFixed(2)}s</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onToggleSelect(f.index)}
|
||||
className={`px-4 py-1.5 rounded-lg text-[13px] font-medium inline-flex items-center gap-2 transition ${
|
||||
isSelected
|
||||
? "bg-emerald-500 text-white hover:bg-emerald-400"
|
||||
: "bg-white/10 text-white hover:bg-white/20"
|
||||
}`}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
{isSelected ? "已选用" : "选用此帧"}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onToggleSelect(f.index)}
|
||||
className={`w-full px-3 py-1.5 rounded-md text-[12px] font-medium inline-flex items-center justify-center gap-1.5 transition ${
|
||||
isSelected
|
||||
? "bg-emerald-500 text-white hover:bg-emerald-400"
|
||||
: "bg-white/10 text-white hover:bg-white/20"
|
||||
}`}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
{isSelected ? "已选用" : "选用此帧"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 右侧识别面板 */}
|
||||
<div
|
||||
className="flex flex-col gap-3 overflow-y-auto rounded-2xl border border-white/15 bg-black/40 backdrop-blur-xl p-4"
|
||||
style={{ width: 360, maxHeight: "80vh" }}
|
||||
className="flex flex-col gap-2.5 overflow-y-auto flex-1 min-h-0"
|
||||
>
|
||||
{/* 识别到的元素 */}
|
||||
<section>
|
||||
@@ -246,9 +257,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 text-[10.5px] text-white/40 font-mono">
|
||||
←/→ 切换 · Space 选用 · ESC 关闭
|
||||
<div className="px-4 py-1.5 text-[10px] text-white/40 font-mono text-center border-t border-white/5 bg-white/[0.02]">
|
||||
←/→ 切换 · Space 选用 · ESC 关闭 · 可拖侧画布看背后
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user