e.stopPropagation()}
- className={`rounded-2xl border border-white/15 overflow-hidden flex flex-col ${embedded ? "" : "fixed z-[100] bg-black/70 backdrop-blur-2xl"}`}
+ className={embedded
+ ? "h-full overflow-hidden flex flex-col"
+ : "fixed z-[100] rounded-2xl border border-white/15 bg-black/70 backdrop-blur-2xl overflow-hidden flex flex-col"}
style={embedded ? {
height: "100%",
background: "transparent",
@@ -311,38 +313,40 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
}}
>
{/* 顶部工具栏 */}
-
-
-
-
-
- 分镜 {String(arrayPos + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
- ·
- {f.timestamp.toFixed(2)}s
-
-
-
-
+
+
+
+
+ 分镜 {String(arrayPos + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
+ ·
+ {f.timestamp.toFixed(2)}s
+
+
+
+
+ )}
{/* 主体 — 左:大图 + 清洗 / 选用;右:识别 + 元素清单 */}
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx
index d51a68b..8909e23 100644
--- a/web/components/nodes/index.tsx
+++ b/web/components/nodes/index.tsx
@@ -9,8 +9,8 @@ import { createPortal } from "react-dom"
import { type NodeProps, useReactFlow } from "@xyflow/react"
import {
Link2, Upload, Download, Scissors, Image as ImageIcon,
- Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin, Maximize2,
- Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom,
+ Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Maximize2,
+ Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom, ChevronLeft, ChevronRight,
} from "lucide-react"
import { toast } from "sonner"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
@@ -33,6 +33,7 @@ export interface NodeData {
expandedFrame: number | null
framePanelScale?: number
framePanelPinned?: boolean
+ framePanelDock?: CanvasPanelDock
videoPanelJobId?: string | null
videoPanelScale?: number
videoPanelDock?: CanvasPanelDock
@@ -44,6 +45,7 @@ export interface NodeData {
onOpenFramePanel?: (idx: number) => void // 打开/找回画布内关键帧详情面板
onFramePanelScaleChange?: (scale: number) => void
onFramePanelPinnedChange?: (pinned: boolean) => void
+ onFramePanelDockChange?: (dock: CanvasPanelDock) => void
onCloseExpandedFrame: () => void
onAddManualFrame: (t: number) => void
onAddManualFrameForJob?: (jobId: string, t: number) => Promise
| void
@@ -1519,20 +1521,15 @@ export function KeyframePanelNode({ data }: any) {
const d: NodeData = data
const { getZoom } = useReactFlow()
const panelRef = useRef(null)
- const [pinRect, setPinRect] = useState<{ left: number; top: number }>({
- left: FLOATING_PANEL_EDGE_INSET,
- top: FLOATING_PANEL_EDGE_INSET,
- })
const scale = d.framePanelScale ?? 1
- const pinned = d.framePanelPinned ?? false
-
- useEffect(() => {
- if (!pinned) return
- setPinRect({ left: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET })
- }, [pinned])
+ const dock = d.framePanelDock ?? (d.framePanelPinned ? "left" : "canvas")
+ const docked = dock !== "canvas"
if (!d.job || d.expandedFrame === null) return null
const active = d.job.frames.find((f) => f.index === d.expandedFrame)
+ const arrayPos = active ? d.job.frames.findIndex((f) => f.index === active.index) : -1
+ const prevFrame = arrayPos > 0 ? d.job.frames[arrayPos - 1] : null
+ const nextFrame = arrayPos >= 0 && arrayPos < d.job.frames.length - 1 ? d.job.frames[arrayPos + 1] : null
const panelWidth = Math.round(760 * scale)
const panelHeight = Math.round(746 * scale)
const bodyHeight = Math.max(520, panelHeight - 27)
@@ -1540,15 +1537,18 @@ export function KeyframePanelNode({ data }: any) {
const clamped = Math.max(0.65, Math.min(1.6, Number(next.toFixed(2))))
d.onFramePanelScaleChange?.(clamped)
}
-
- const togglePinned = () => {
- if (!pinned) {
- const zoom = getZoom()
- setScale(scale * zoom)
- setPinRect({ left: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET })
- }
- d.onFramePanelPinnedChange?.(!pinned)
+ const dockText: Record = {
+ canvas: "画布模式",
+ left: "吸附左侧",
+ right: "吸附右侧",
+ bottom: "吸附底部",
}
+ const dockButtonClass = (value: CanvasPanelDock) =>
+ `nodrag inline-flex h-6 w-6 items-center justify-center rounded transition ${
+ dock === value
+ ? "bg-white text-violet-700 shadow"
+ : "bg-white/10 text-white/75 hover:bg-white/20 hover:text-white"
+ }`
const startResize = (e: React.PointerEvent) => {
e.preventDefault()
@@ -1556,7 +1556,7 @@ export function KeyframePanelNode({ data }: any) {
const startX = e.clientX
const startY = e.clientY
const startScale = scale
- const zoom = pinned ? 1 : getZoom()
+ const zoom = docked ? 1 : getZoom()
const onMove = (ev: PointerEvent) => {
const dx = (ev.clientX - startX) / zoom
const dy = (ev.clientY - startY) / zoom
@@ -1574,37 +1574,72 @@ export function KeyframePanelNode({ data }: any) {
const panel = (
-
+
关键帧详情 · 元素提取
{active ? `分镜 ${active.index + 1} · ${active.timestamp.toFixed(2)}s` : "未选分镜"}
+
+
+
+
+ {arrayPos >= 0 ? `${String(arrayPos + 1).padStart(2, "0")} / ${String(d.job.frames.length).padStart(2, "0")}` : ""}
+
+
- {pinned ? "已钉住左侧 · 不跟画布" : "拖动标题栏移动 · 可钉住"}
+ {dockText[dock]}
-
@@ -1652,7 +1687,7 @@ export function KeyframePanelNode({ data }: any) {
@@ -1660,11 +1695,17 @@ export function KeyframePanelNode({ data }: any) {
)
- if (pinned && typeof document !== "undefined") {
+ if (docked && typeof document !== "undefined") {
+ const fixedStyle =
+ dock === "left"
+ ? { left: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET }
+ : dock === "right"
+ ? { right: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET }
+ : { left: "50%", bottom: FLOATING_PANEL_EDGE_INSET, transform: "translateX(-50%)" }
return createPortal(
{panel}
,