feat(canvas): 批量创作表 - 支持全局提示词、参考图编排与连线同步

This commit is contained in:
ddCat
2026-09-18 13:03:35 +08:00
parent a8770c89b0
commit c994566ffe
13 changed files with 714 additions and 116 deletions

View File

@@ -3,7 +3,7 @@ import { App } from "antd";
import { nanoid } from "nanoid";
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
import { batchInputColumns, batchReferenceColumns, createBatchRowsFromColumns, createInheritedBatchRow } from "@/lib/canvas/canvas-batch-table";
import { batchInputColumns, batchPromptForRow, batchReferenceColumns, batchReferenceHandleId, createBatchRowsFromColumns, createInheritedBatchRow, moveBatchReferenceCell, removeLastBatchReferenceColumn, reorderBatchReferenceColumns } from "@/lib/canvas/canvas-batch-table";
import { createCanvasNode } from "@/lib/canvas/canvas-project-domain";
import { buildGenerationConfig, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
import { navigateToSettings } from "@/lib/settings-navigation";
@@ -55,23 +55,65 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
patchTable(nodeId, { referenceColumns: [...columns, { id: `reference-${nanoid()}`, label: `参考图 ${nextIndex}` }] });
}, [message, nodesRef, patchTable]);
const fillRowsFromConnections = useCallback((nodeId: string) => {
const removeReferenceColumn = useCallback((nodeId: string) => {
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
if (!table) return;
const columns = batchReferenceColumns(table);
const nextTable = removeLastBatchReferenceColumn(table);
if (!nextTable) return message.info("至少保留 1 组参考图");
const removed = columns.at(-1);
patchTable(nodeId, nextTable);
if (removed) {
const handleId = batchReferenceHandleId(removed.id);
setConnections((current) => current.filter((connection) => !(connection.toNodeId === nodeId && connection.toHandleId === handleId)));
}
}, [message, nodesRef, patchTable, setConnections]);
const syncRowsFromConnections = useCallback((nodeId: string, silent = false) => {
const node = nodesRef.current.find((item) => item.id === nodeId);
const table = node?.metadata?.batchTable;
if (!node || !table) return;
if (!node || !table) return false;
const nodeById = new Map(nodesRef.current.map((item) => [item.id, item]));
const columns = batchInputColumns(node, connectionsRef.current).map((column) => column.filter((inputNodeId) => {
const input = nodeById.get(inputNodeId);
return input?.type === CanvasNodeType.Image && Boolean(input.metadata?.content || input.metadata?.storageKey);
}));
if (!columns.some((column) => column.length)) return message.warning("请先把图片节点连接到批量创作表");
if (!columns.some((column) => column.length)) {
if (!silent) message.warning("请先把图片节点连接到批量创作表");
return false;
}
const rows = createBatchRowsFromColumns(table.operation, columns, table.rows);
if (!rows.length) return message.warning("批量换装至少需要一张人物图和一张服装图");
if (!rows.length) {
if (!silent) message.warning("批量换装至少需要一张人物图和一张服装图");
return false;
}
if (JSON.stringify(rows) === JSON.stringify(table.rows)) return true;
patchTable(nodeId, { rows });
const added = Math.max(0, rows.length - table.rows.length);
message.success(added ? `已同步连线并新增 ${added} 行,原有任务均已保留` : "已同步最新连线,原有任务均已保留");
if (!silent) {
const added = Math.max(0, rows.length - table.rows.length);
message.success(added ? `已同步连线并新增 ${added} 行,原有任务均已保留` : "已同步最新连线,原有任务均已保留");
}
return true;
}, [connectionsRef, message, nodesRef, patchTable]);
const fillRowsFromConnections = useCallback((nodeId: string) => {
syncRowsFromConnections(nodeId);
}, [syncRowsFromConnections]);
const reorderReferenceColumns = useCallback((nodeId: string, fromColumnId: string, toColumnId: string) => {
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
if (!table) return;
const nextTable = reorderBatchReferenceColumns(table, fromColumnId, toColumnId);
if (nextTable !== table) patchTable(nodeId, nextTable);
}, [nodesRef, patchTable]);
const moveReferenceCell = useCallback((nodeId: string, sourceRowId: string, sourceColumnIndex: number, targetRowId: string, targetColumnIndex: number) => {
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
if (!table) return;
const nextTable = moveBatchReferenceCell(table, sourceRowId, sourceColumnIndex, targetRowId, targetColumnIndex);
if (nextTable !== table) patchTable(nodeId, { rows: nextTable.rows });
}, [nodesRef, patchTable]);
const generateRows = useCallback(async (nodeId: string, requestedRowIds?: string[]) => {
const sourceNode = nodesRef.current.find((item) => item.id === nodeId);
const table = sourceNode?.metadata?.batchTable;
@@ -84,9 +126,10 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
const activeNodeIds = new Set((sourceNode.metadata?.generationBatches || []).filter((batch) => batch.mode === "batch_image").flatMap((batch) => batch.items.filter((item) => ["waiting", "submitting", "queued", "running"].includes(item.status)).map((item) => item.nodeId)));
const requested = requestedRowIds?.length ? new Set(requestedRowIds) : null;
const rows = table.rows.filter((row) => {
if (!row.enabled || (requested && !requested.has(row.id)) || !row.prompt.trim()) return false;
if (table.operation === "try_on" && row.inputNodeIds.length < 2) return false;
if (!row.inputNodeIds.length || row.inputNodeIds.some((id) => !nodesRef.current.some((node) => node.id === id && node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey)))) return false;
const inputNodeIds = row.inputNodeIds.filter(Boolean);
if (!row.enabled || (requested && !requested.has(row.id)) || !batchPromptForRow(table, row).trim()) return false;
if (table.operation === "try_on" && inputNodeIds.length < 2) return false;
if (!inputNodeIds.length || inputNodeIds.some((id) => !nodesRef.current.some((node) => node.id === id && node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey)))) return false;
const output = row.outputNodeId ? nodesRef.current.find((node) => node.id === row.outputNodeId) : undefined;
if (output && activeNodeIds.has(output.id)) return false;
// 操作列的单行生成允许对已完成结果重新生成;顶部批量按钮仍只提交未完成项。
@@ -112,10 +155,11 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
const targets: Array<{ rowId: string; nodeId: string }> = [];
rows.forEach((row, index) => {
const existingIndex = row.outputNodeId ? nextNodes.findIndex((node) => node.id === row.outputNodeId && node.type === CanvasNodeType.Image) : -1;
const prompt = batchPromptForRow(table, row).trim();
const metadata = {
...(existingIndex >= 0 ? resetGenerationTaskMetadata(nextNodes[existingIndex].metadata) : {}),
prompt: row.prompt.trim(),
composerContent: row.prompt.trim(),
prompt,
composerContent: prompt,
model: buildGenerationConfig(effectiveConfig, undefined, "image").model,
generationMode: "image" as const,
generationType: "edit" as const,
@@ -134,7 +178,7 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
if (existingIndex >= 0) nextNodes[existingIndex] = output;
else nextNodes.push(output);
nextConnections = nextConnections.filter((connection) => connection.toNodeId !== output.id);
row.inputNodeIds.forEach((inputNodeId) => nextConnections.push({ id: nanoid(), fromNodeId: inputNodeId, toNodeId: output.id }));
row.inputNodeIds.filter(Boolean).forEach((inputNodeId) => nextConnections.push({ id: nanoid(), fromNodeId: inputNodeId, toNodeId: output.id }));
nextConnections.push({ id: nanoid(), fromNodeId: sourceNode.id, toNodeId: output.id, relation: "batch-output", storyboardRowId: row.id });
outputByRowId.set(row.id, output.id);
targets.push({ rowId: row.id, nodeId: output.id });
@@ -149,5 +193,5 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
if (enqueueGenerationBatch(nodeId, "batch_image", targets, { concurrency: table.concurrency })) message.success(`${targets.length} 个任务已加入并发队列`);
}, [connectionsRef, effectiveConfig, enqueueGenerationBatch, isAiConfigReady, message, modal, nodesRef, setConnections, setNodes, setSelectedNodeIds]);
return { addReferenceColumn, addRow, fillRowsFromConnections, generateRows, patchTable, removeRow, updateRow };
return { addReferenceColumn, addRow, fillRowsFromConnections, generateRows, moveReferenceCell, patchTable, removeReferenceColumn, removeRow, reorderReferenceColumns, syncRowsFromConnections, updateRow };
}