diff --git a/web/src/pages/projects/detail/workflow-production-workbench.tsx b/web/src/pages/projects/detail/workflow-production-workbench.tsx index c33d7a823..d0c771633 100644 --- a/web/src/pages/projects/detail/workflow-production-workbench.tsx +++ b/web/src/pages/projects/detail/workflow-production-workbench.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { App, Button, Empty, Form, Image, Input, InputNumber, Segmented, Select, Tag } from "antd"; -import { Box, ChevronDown, ChevronLeft, ChevronRight, Download, Film, Image as ImageIcon, Layers3, List, Play, Plus, RefreshCcw, Save, Search, SlidersHorizontal, Trash2, UsersRound, WandSparkles, X } from "lucide-react"; +import { App, Button, Empty, Form, Image, Input, InputNumber, Modal, Segmented, Select, Tag } from "antd"; +import { Box, ChevronDown, ChevronLeft, ChevronRight, Download, Film, Image as ImageIcon, Layers3, List, Maximize2, Play, Plus, RefreshCcw, Save, Search, SlidersHorizontal, Trash2, UsersRound, WandSparkles, X } from "lucide-react"; import { Link, useNavigate } from "react-router"; import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea"; @@ -13,6 +13,7 @@ import { modelQuoteRequest } from "@/lib/model-pricing"; import { customShotTitle, formatShotOrdinal, normalizeDefaultShotTitle } from "@/lib/shot-label"; import { modelCompatibilityError, resolveCompatibleModel, resolveModelVideoBooleanOptions, type ModelRequirements } from "@/lib/model-selection"; import { formatVideoResolutionLabel } from "@/lib/video-generation-options"; +import { captureVideoPoster } from "@/lib/video-poster"; import { submitBackendGenerationTask } from "@/services/api/generation-task"; import { quoteLogicalModel } from "@/services/api/logical-models"; import { type GenerationTask } from "@/services/api/task-center"; @@ -85,6 +86,7 @@ export default function WorkflowProductionWorkbench(props: Props) { const [leftTab, setLeftTab] = useState<"assets" | "episodes" | "shots">("episodes"); const [previewTab, setPreviewTab] = useState<"latest" | "history">("latest"); const [previewArtifactId, setPreviewArtifactId] = useState(""); + const [imagePreviewArtifact, setImagePreviewArtifact] = useState(null); const [editorDirty, setEditorDirty] = useState(false); const [submittingShotIds, setSubmittingShotIds] = useState>(() => new Set()); const [taskClock, setTaskClock] = useState(() => Date.now()); @@ -242,6 +244,7 @@ export default function WorkflowProductionWorkbench(props: Props) { continuityNotes: revision?.continuityNotes || "", }); setPreviewArtifactId(""); + setImagePreviewArtifact(null); setEditorDirty(!revision || videoPrompt !== revision.videoPrompt); }, [effectiveConfig, form, generationCapability, initialModel, revision?.id, selectedShot?.id, shotAssetReferenceContext.mentionReferences]); @@ -567,7 +570,7 @@ export default function WorkflowProductionWorkbench(props: Props) { />
- {previewTab === "latest" ? : { setPreviewArtifactId(artifact.id); setPreviewTab("latest"); }} />} + {previewTab === "latest" ? : { setPreviewArtifactId(artifact.id); setPreviewTab("latest"); }} />}
当前产物
{newestArtifact ? `${formatDuration(selectedShot.durationMs)} · ${resolution}p · v${newestArtifact.version}` : "当前镜头还没有生成产物"}
setPreviewArtifactId(artifact.id)} compact /> @@ -575,6 +578,19 @@ export default function WorkflowProductionWorkbench(props: Props) {
+ setImagePreviewArtifact(null)} + styles={{ body: { padding: 0 } }} + > + {imagePreviewArtifact?.resourceId ? {imagePreviewArtifact.type : null} + + ); @@ -669,11 +685,43 @@ function BoundAssets({ detail, shotId, changing, onUnlink }: { detail: ProjectDe ); } -function LatestPreview({ artifact, emptyText }: { artifact?: ShotArtifact; emptyText: string }) { +function LatestPreview({ artifact, emptyText, onPreviewImage }: { artifact?: ShotArtifact; emptyText: string; onPreviewImage: (artifact: ShotArtifact) => void }) { if (!artifact?.resourceId) return

{emptyText}

; const src = resourceFileUrl(artifact.resourceId); - if (artifact.type === "video") return