diff --git a/.memory/worklog.json b/.memory/worklog.json
index c880a01..08797d8 100644
--- a/.memory/worklog.json
+++ b/.memory/worklog.json
@@ -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
}
]
}
diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx
index 6594b50..6ca7935 100644
--- a/web/components/lightbox.tsx
+++ b/web/components/lightbox.tsx
@@ -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(
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)",
+ }}
>
-
-
- {activeIndex > 0 && (
+ {/* 顶部工具栏 — 切换 / 关闭 */}
+
+
+
+
+
+ 分镜 {String(f.index + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
+ ·
+ {f.timestamp.toFixed(2)}s
+
+
- )}
- {activeIndex < frames.length - 1 && (
-
- )}
+
-
e.stopPropagation()} className="flex gap-4 max-w-[92vw] max-h-[92vh] items-start">
+ {/* 主体 — 左大图 + 右识别面板 */}
+
{/* 左侧大图 */}
-
+

-
-
- {String(f.index + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
- ·
- {f.timestamp.toFixed(2)}s
-
-
-
+
{/* 右侧识别面板 */}
{/* 识别到的元素 */}
@@ -246,9 +257,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
-
- ←/→ 切换 · Space 选用 · ESC 关闭
+
+ ←/→ 切换 · Space 选用 · ESC 关闭 · 可拖侧画布看背后
-
+
,
+ document.body,
)
}