feat(canvas): 视频节点 - 接入 Vidstack 播放器并收敛任务查询范围

This commit is contained in:
ddCat
2026-07-31 20:19:12 +08:00
parent 8de9b42c00
commit 53483b3d39
8 changed files with 144 additions and 12 deletions

View File

@@ -3,6 +3,7 @@ import { Button, Image, Modal } from "antd";
import { TaskDetailItem, taskStatusText } from "./canvas-project-feedback";
import type { GenerationTask, TaskLog } from "@/services/api/task-center";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import { VideoPlayer } from "@/components/video-player";
type CanvasProjectStatusDialogsProps = {
theme: { node: { stroke: string; panel: string; muted: string; fill: string } };
@@ -48,7 +49,7 @@ export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading,
</Modal>
<Modal title="视频预览" open={Boolean(previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Video)} centered onCancel={onClosePreview} footer={null} width="min(1200px, calc(100vw - 32px))" styles={{ body: { padding: 0, display: "flex", justifyContent: "center", alignItems: "center", maxHeight: "84vh", overflow: "hidden", background: "#090909" } }}>
{previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Video ? <video src={previewNode.metadata.content} controls className="max-h-[84vh] max-w-full bg-black object-contain" /> : null}
{previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Video ? <VideoPlayer src={previewNode.metadata.content} mimeType={previewNode.metadata.mimeType} title={previewNode.title || "视频预览"} className="max-h-[84vh] max-w-full bg-black" /> : null}
</Modal>
{previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Image ? (

View File

@@ -140,15 +140,26 @@ export function useCanvasGenerationBatches({ projectId, projectLoaded, nodes, no
});
}, [projectId, setNodes]);
// 调度只补齐后台返回的用户级活跃任务空位,最终并发仍由后端原子校验。
// 仅查询当前画布的活跃任务来安排批次,跨画布并发仍由后端创建任务时原子校验。
const scheduleWaitingItems = useCallback(async () => {
if (!projectLoaded || schedulingRef.current) return;
schedulingRef.current = true;
try {
const tasks = await listGenerationTasks(100).catch(() => null);
const currentNodes = nodesRef.current;
// 没有当前画布的等待项时无需查询任务中心,避免空画布持续轮询。
const hasWaitingItems = currentNodes.some((sourceNode) =>
(sourceNode.metadata?.generationBatches || []).some((batch) =>
batch.projectId === projectId &&
batch.status !== "completed" &&
batch.status !== "cancelled" &&
batch.items.some((item) => item.status === "waiting"),
),
);
if (!hasWaitingItems) return;
const tasks = await listGenerationTasks(100, { projectId, activeOnly: true }).catch(() => null);
if (!tasks) return;
const activeTaskCount = tasks.filter((task) => task.status === "queued" || task.status === "running").length;
const currentNodes = nodesRef.current;
const nodeById = new Map(currentNodes.map((node) => [node.id, node]));
const pendingReservations = [...controllersRef.current.keys()].filter((key) => {
const [, itemId] = key.split(":");

View File

@@ -165,7 +165,7 @@ export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded,
const taskIds = Array.from(new Set(recoveryNodes.map((node) => node.metadata?.taskId).filter((id): id is string => Boolean(id))));
const tasks = (await Promise.all(taskIds.map((id) => queryGenerationTask(id).catch(() => undefined)))).filter((task): task is GenerationTask => Boolean(task));
if (recoveryNodes.some((node) => !node.metadata?.taskId)) {
const recentTasks = await listGenerationTasks(30).catch(() => []);
const recentTasks = await listGenerationTasks(30, { projectId }).catch(() => []);
tasks.push(...recentTasks.filter((task) => !tasks.some((item) => item.id === task.id)));
}
const projectTasks = tasks.filter((task) => task.projectId === projectId && (task.type.startsWith("canvas_") || task.type === "agent_storyboard_rows"));