auto-save 2026-05-13 16:23 (~6)
This commit is contained in:
@@ -327,6 +327,7 @@ export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({
|
||||
data.onCloseExpandedFrame()
|
||||
setExpanded(new Set([key]))
|
||||
}}
|
||||
onCopyImage={data.onCopyImage}
|
||||
/>
|
||||
) : (
|
||||
renderSection(t.key)
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
frameUrl, cleanedFrameUrl, cutoutUrl,
|
||||
describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, deleteElement, cutoutElement, deleteCutout,
|
||||
pushStoryboardImage,
|
||||
type KeyFrame, type Job,
|
||||
type KeyFrame, type Job, type ImageRef,
|
||||
} from "@/lib/api"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -20,10 +20,11 @@ interface Props {
|
||||
onToggleSelect: (idx: number) => void
|
||||
onJobUpdate?: (job: Job) => void
|
||||
onSwitchPanel?: (key: string) => void
|
||||
onCopyImage?: (ref: ImageRef) => void
|
||||
embedded?: boolean
|
||||
}
|
||||
|
||||
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, onSwitchPanel, embedded = false }: Props) {
|
||||
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, onSwitchPanel, onCopyImage, embedded = false }: Props) {
|
||||
const [describing, setDescribing] = useState(false)
|
||||
const [cleaning, setCleaning] = useState(false)
|
||||
const [applying, setApplying] = useState(false)
|
||||
@@ -714,18 +715,26 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
<div className="absolute bottom-0 left-0 text-[8.5px] font-mono text-white bg-black/70 backdrop-blur px-1 rounded-tr">
|
||||
#{ci + 1}
|
||||
</div>
|
||||
{/* 上推按钮:常驻可见 */}
|
||||
<button
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault(); ev.stopPropagation()
|
||||
const isLegacy = !(e.cutouts && e.cutouts.length > 0)
|
||||
handlePushCutout(e.id, cid, `${e.name_zh} #${ci + 1}`, isLegacy)
|
||||
}}
|
||||
className="absolute left-0.5 top-0.5 h-4 w-4 rounded-sm bg-violet-500/90 text-white shadow hover:bg-violet-400 inline-flex items-center justify-center transition text-[11px] leading-none font-bold"
|
||||
title="⬆ 上推到分镜头编排"
|
||||
>
|
||||
⬆
|
||||
</button>
|
||||
{/* 复制按钮:常驻可见 */}
|
||||
{onCopyImage && (
|
||||
<button
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault(); ev.stopPropagation()
|
||||
const isLegacy = !(e.cutouts && e.cutouts.length > 0)
|
||||
onCopyImage({
|
||||
kind: "cutout",
|
||||
frame_idx: f.index,
|
||||
element_id: e.id,
|
||||
cutout_id: isLegacy ? e.id : cid,
|
||||
label: `${e.name_zh} #${ci + 1}`,
|
||||
})
|
||||
}}
|
||||
className="absolute left-0.5 top-0.5 h-4 w-4 rounded-sm bg-violet-500/90 text-white shadow hover:bg-violet-400 inline-flex items-center justify-center transition text-[9px] leading-none"
|
||||
title="📋 复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
)}
|
||||
{/* 删除该张 — 仅 v2 多图支持,老 fallback 不显示 */}
|
||||
{e.cutouts && e.cutouts.length > 0 && (
|
||||
<button
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, X, LayoutGrid,
|
||||
} from "lucide-react"
|
||||
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
|
||||
import { type Job, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl, cutoutUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
|
||||
import { type Job, type ImageRef, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl, cutoutUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
|
||||
|
||||
export interface NodeData {
|
||||
job: Job | null // 当前 active job
|
||||
@@ -32,6 +32,7 @@ export interface NodeData {
|
||||
onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图
|
||||
onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板
|
||||
onPushToStoryboard?: (payload: { kind: "keyframe" | "cutout"; frameIdx: number; elementId?: string; cutoutId?: string; label?: string }) => void
|
||||
onCopyImage?: (ref: ImageRef) => void // 复制图片到全局剪贴板(粘贴到分镜头编排工作台插槽)
|
||||
}
|
||||
|
||||
/* ---- 状态映射工具 ---- */
|
||||
@@ -405,21 +406,21 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
{f.timestamp.toFixed(1)}s
|
||||
</div>
|
||||
</button>
|
||||
{/* 上推按钮:常驻可见 — 推送关键帧本身到分镜头编排 */}
|
||||
{d.onPushToStoryboard && (
|
||||
{/* 复制按钮:常驻可见 — 复制该关键帧到剪贴板 */}
|
||||
{d.onCopyImage && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
d.onPushToStoryboard?.({
|
||||
d.onCopyImage?.({
|
||||
kind: "keyframe",
|
||||
frameIdx: f.index,
|
||||
frame_idx: f.index,
|
||||
label: `分镜 ${f.index + 1} 关键帧`,
|
||||
})
|
||||
}}
|
||||
title="⬆ 上推到分镜头编排"
|
||||
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[12px] leading-none font-bold"
|
||||
title="📋 复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[10px] leading-none"
|
||||
>
|
||||
⬆
|
||||
📋
|
||||
</button>
|
||||
)}
|
||||
{/* 删除按钮:hover 时右上角浮出 */}
|
||||
@@ -647,23 +648,23 @@ export function ImageGenNode({ data, selected }: any) {
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
/>
|
||||
</button>
|
||||
{/* 上推按钮:常驻可见 */}
|
||||
{d.onPushToStoryboard && (
|
||||
{/* 复制按钮:常驻可见 — 复制元素提取图到剪贴板 */}
|
||||
{d.onCopyImage && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
d.onPushToStoryboard?.({
|
||||
d.onCopyImage?.({
|
||||
kind: "cutout",
|
||||
frameIdx: p.frameIdx,
|
||||
elementId: p.elementId,
|
||||
cutoutId: p.cid,
|
||||
frame_idx: p.frameIdx,
|
||||
element_id: p.elementId,
|
||||
cutout_id: p.cid,
|
||||
label: p.name,
|
||||
})
|
||||
}}
|
||||
title="⬆ 上推到分镜头编排"
|
||||
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[12px] leading-none font-bold"
|
||||
title="📋 复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[10px] leading-none"
|
||||
>
|
||||
⬆
|
||||
📋
|
||||
</button>
|
||||
)}
|
||||
{/* hover 预览 — absolute 浮在缩略图上方 */}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useEffect, useState, useRef, type ReactNode } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { X, LayoutGrid, Loader2, Check, Sparkle, Wand2 } from "lucide-react"
|
||||
import {
|
||||
type Job, type StoryboardScene,
|
||||
effectiveFrameUrl, cutoutUrl, updateStoryboard,
|
||||
type Job, type StoryboardScene, type ImageRef,
|
||||
effectiveFrameUrl, updateStoryboard, resolveImageRefUrl,
|
||||
} from "@/lib/api"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -14,13 +14,14 @@ interface Props {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onJobUpdate?: (j: Job) => void
|
||||
clipboard: ImageRef | null // 全局剪贴板(page.tsx 提供)
|
||||
}
|
||||
|
||||
const emptyScene = (): StoryboardScene => ({
|
||||
subject: "", product: "", scene: "", action: "", duration: 0, reference_ids: [],
|
||||
})
|
||||
|
||||
export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate }: Props) {
|
||||
export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate, clipboard }: Props) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user