auto-save 2026-05-13 19:28 (~4)

This commit is contained in:
2026-05-13 19:28:47 +08:00
parent 1f9c0947a8
commit 4da7fa8f2f
4 changed files with 117 additions and 20 deletions

View File

@@ -67,7 +67,8 @@ export default function Home() {
const [analyzing, setAnalyzing] = useState(false)
const [selectedFrames, setSelectedFrames] = useState<Set<number>>(new Set())
const [expandedFrame, setExpandedFrame] = useState<number | null>(null)
const [framePanelResetNonce, setFramePanelResetNonce] = useState(0)
const [framePanelScale, setFramePanelScale] = useState(1)
const [framePanelPinned, setFramePanelPinned] = useState(false)
const [videoLightboxOpen, setVideoLightboxOpen] = useState(false)
const [storyboardFrame, setStoryboardFrame] = useState<number | null>(null)
const [workbenchOpen, setWorkbenchOpen] = useState(false)
@@ -170,7 +171,10 @@ export default function Home() {
const handleOpenFramePanel = useCallback((idx: number) => {
setExpandedFrame(idx)
setFramePanelResetNonce((n) => n + 1)
}, [])
const handleFramePanelScaleChange = useCallback((scale: number) => {
setFramePanelScale(Math.max(0.75, Math.min(1.35, Number(scale.toFixed(2)))))
}, [])
const handleDeleteFrame = useCallback(async (idx: number) => {
@@ -281,12 +285,16 @@ export default function Home() {
analyzing,
selectedFrames,
expandedFrame,
framePanelScale,
framePanelPinned,
onSubmitUrl: handleSubmit,
onUploadFile: handleUpload,
onAnalyze: handleAnalyze,
onToggleFrame: handleToggleFrame,
onExpandFrame: setExpandedFrame,
onOpenFramePanel: handleOpenFramePanel,
onFramePanelScaleChange: handleFramePanelScaleChange,
onFramePanelPinnedChange: setFramePanelPinned,
onCloseExpandedFrame: () => setExpandedFrame(null),
onAddManualFrame: handleAddManualFrame,
onOpenVideoLightbox: () => setVideoLightboxOpen(true),
@@ -300,7 +308,7 @@ export default function Home() {
setWorkbenchOpen(true)
},
onCopyImage: handleCopyImage,
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleCopyImage])
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleCopyImage])
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag
const [nodes, setNodes, onNodesChange] = useNodesState<Node>(
@@ -324,16 +332,14 @@ export default function Home() {
}, [nodeData, setNodes])
// 关键帧详情面板是独立 ReactFlow 节点:可拖动、跟随画布缩放。
// 再次点击任意关键帧缩略图时,重新放回关键帧节点左侧附近,避免拖丢后找不到
const panelResetHandledRef = useRef(0)
// 已打开时点击其他关键帧只切换内容,不移动用户拖好的面板位置
useEffect(() => {
if (!job || expandedFrame === null) {
setNodes((prev) => prev.filter((n) => n.id !== KEYFRAME_PANEL_ID))
return
}
const shouldReset = panelResetHandledRef.current !== framePanelResetNonce
panelResetHandledRef.current = framePanelResetNonce
let shouldFocusNewPanel = false
setNodes((prev) => {
const keyframeNode = prev.find((n) => n.id === "keyframe")
const inputNode = prev.find((n) => n.id === "input")
@@ -347,13 +353,13 @@ export default function Home() {
? {
...n,
data: nodeData,
position: shouldReset ? defaultPosition : n.position,
draggable: true,
dragHandle: ".keyframe-panel-drag",
draggable: !framePanelPinned,
dragHandle: framePanelPinned ? undefined : ".keyframe-panel-drag",
}
: n,
)
}
shouldFocusNewPanel = true
return [
...prev,
{
@@ -361,13 +367,13 @@ export default function Home() {
type: "keyframePanel",
position: defaultPosition,
data: nodeData,
draggable: true,
dragHandle: ".keyframe-panel-drag",
draggable: !framePanelPinned,
dragHandle: framePanelPinned ? undefined : ".keyframe-panel-drag",
selectable: true,
},
]
})
if (shouldReset) {
if (shouldFocusNewPanel) {
window.setTimeout(() => {
flowRef.current?.fitView?.({
nodes: [{ id: KEYFRAME_PANEL_ID }, { id: "keyframe" }],
@@ -376,7 +382,7 @@ export default function Home() {
})
}, 0)
}
}, [job?.id, expandedFrame, framePanelResetNonce, nodeData, setNodes])
}, [job?.id, expandedFrame, framePanelPinned, nodeData, setNodes])
// 边的 animated 状态跟 Job 进度联动
useEffect(() => {

View File

@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"
import { type NodeProps } from "@xyflow/react"
import {
Link2, Upload, Download, Scissors, Image as ImageIcon,
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid,
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin,
} from "lucide-react"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
import { type Job, type ImageRef, effectiveFrameUrl, videoUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
@@ -17,12 +17,16 @@ export interface NodeData {
analyzing: boolean
selectedFrames: Set<number>
expandedFrame: number | null
framePanelScale?: number
framePanelPinned?: boolean
onSubmitUrl: (url: string) => void
onUploadFile: (file: File) => void
onAnalyze: () => void
onToggleFrame: (idx: number) => void
onExpandFrame: (idx: number) => void
onOpenFramePanel?: (idx: number) => void // 打开/找回画布内关键帧详情面板
onFramePanelScaleChange?: (scale: number) => void
onFramePanelPinnedChange?: (pinned: boolean) => void
onCloseExpandedFrame: () => void
onAddManualFrame: (t: number) => void
onOpenVideoLightbox: () => void
@@ -511,12 +515,21 @@ export function KeyframePanelNode({ data }: any) {
const d: NodeData = data
if (!d.job || d.expandedFrame === null) return null
const active = d.job.frames.find((f) => f.index === d.expandedFrame)
const scale = d.framePanelScale ?? 1
const pinned = d.framePanelPinned ?? false
const panelWidth = Math.round(760 * scale)
const panelHeight = Math.round(746 * scale)
const bodyHeight = Math.max(520, panelHeight - 27)
const setScale = (next: number) => {
const clamped = Math.max(0.75, Math.min(1.35, Number(next.toFixed(2))))
d.onFramePanelScaleChange?.(clamped)
}
return (
<div
className="rounded-2xl border border-white/15 bg-black/70 shadow-2xl overflow-hidden"
style={{ width: 760, height: 746, boxShadow: "0 30px 80px -20px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.05)" }}
style={{ width: panelWidth, height: panelHeight, boxShadow: "0 30px 80px -20px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.05)" }}
>
<div className="keyframe-panel-drag flex h-7 cursor-move items-center justify-between bg-gradient-to-r from-orange-500 to-red-500 px-3 text-white">
<div className={`keyframe-panel-drag flex h-7 items-center justify-between bg-gradient-to-r from-orange-500 to-red-500 px-3 text-white ${pinned ? "cursor-default" : "cursor-move"}`}>
<div className="flex min-w-0 items-center gap-2">
<ImageIcon className="h-3.5 w-3.5 shrink-0" />
<span className="truncate text-[12px] font-semibold"> · </span>
@@ -524,8 +537,46 @@ export function KeyframePanelNode({ data }: any) {
{active ? `分镜 ${active.index + 1} · ${active.timestamp.toFixed(2)}s` : "未选分镜"}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] text-white/60"> · </span>
<div className="flex items-center gap-1.5">
<span className="mr-1 text-[10px] text-white/60">
{pinned ? "已钉住 · 切图不移动" : "拖动标题栏移动 · 可钉住"}
</span>
<button
type="button"
onClick={(e) => { e.stopPropagation(); d.onFramePanelPinnedChange?.(!pinned) }}
className={`nodrag h-5 w-5 rounded inline-flex items-center justify-center transition ${
pinned
? "bg-white text-orange-600 shadow"
: "bg-white/10 text-white/85 hover:bg-white/20 hover:text-white"
}`}
title={pinned ? "取消钉住,允许拖动" : "钉住面板,防止被拖动"}
>
<Pin className="h-3 w-3" />
</button>
<button
type="button"
onClick={(e) => { e.stopPropagation(); setScale(scale - 0.1) }}
className="nodrag h-5 w-5 rounded bg-white/10 text-white/85 hover:bg-white/20 hover:text-white inline-flex items-center justify-center text-[13px] leading-none"
title="缩小面板"
>
-
</button>
<button
type="button"
onClick={(e) => { e.stopPropagation(); setScale(1) }}
className="nodrag h-5 min-w-9 rounded bg-white/10 px-1.5 text-[10px] font-mono text-white/80 hover:bg-white/20 hover:text-white inline-flex items-center justify-center"
title="重置为 100%"
>
{Math.round(scale * 100)}%
</button>
<button
type="button"
onClick={(e) => { e.stopPropagation(); setScale(scale + 0.1) }}
className="nodrag h-5 w-5 rounded bg-white/10 text-white/85 hover:bg-white/20 hover:text-white inline-flex items-center justify-center text-[13px] leading-none"
title="放大面板"
>
+
</button>
<button
type="button"
onClick={(e) => { e.stopPropagation(); d.onCloseExpandedFrame() }}
@@ -536,7 +587,7 @@ export function KeyframePanelNode({ data }: any) {
</button>
</div>
</div>
<div className="nodrag nowheel h-[719px]" onWheel={(e) => e.stopPropagation()}>
<div className="nodrag nowheel" style={{ height: bodyHeight }} onWheel={(e) => e.stopPropagation()}>
<FrameLightbox
embedded
jobId={d.job.id}