fix(scene-fusion): strengthen random-bg replacement, UI actions, and gallery persistence
This commit is contained in:
parent
7a32e6ef65
commit
2a40607fc1
@ -436,19 +436,32 @@ async function finalizeEnhanceTaskResult(params: {
|
||||
const uploadResult = await uploadBase64ToS3(enhancedImageBase64, "image/jpeg");
|
||||
const finalPrompt = params.prompt?.trim() || "图片变清晰处理";
|
||||
|
||||
try {
|
||||
await db.insert(generatedImage).values({
|
||||
userId: params.userId,
|
||||
chatId: params.chatId || null,
|
||||
messageId: params.messageId || null,
|
||||
thumbnailUrl: uploadResult.thumbnail,
|
||||
originalUrl: uploadResult.original,
|
||||
imageSize: "2K",
|
||||
prompt: finalPrompt,
|
||||
modelId: "enhance-1.0",
|
||||
});
|
||||
} catch (dbError) {
|
||||
console.error("[Enhance] 保存到相册失败(已忽略):", dbError);
|
||||
let saved = false;
|
||||
let saveError: unknown = null;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
try {
|
||||
await db.insert(generatedImage).values({
|
||||
userId: params.userId,
|
||||
chatId: params.chatId || null,
|
||||
messageId: params.messageId || null,
|
||||
thumbnailUrl: uploadResult.thumbnail,
|
||||
originalUrl: uploadResult.original,
|
||||
imageSize: "2K",
|
||||
prompt: finalPrompt,
|
||||
modelId: "enhance-1.0",
|
||||
});
|
||||
saved = true;
|
||||
break;
|
||||
} catch (dbError) {
|
||||
saveError = dbError;
|
||||
if (attempt < 3) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 120 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!saved) {
|
||||
console.error("[Enhance] 保存到相册失败:", saveError);
|
||||
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
|
||||
}
|
||||
|
||||
const processingTime =
|
||||
@ -714,21 +727,33 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 步骤4: 保存到数据库
|
||||
try {
|
||||
const [savedImage] = await db.insert(generatedImage).values({
|
||||
userId,
|
||||
chatId: chatId || null,
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: `${config.backupScale}x放大`,
|
||||
prompt: prompt || "Real-ESRGAN图片变清晰处理",
|
||||
modelId: `realesrgan-${config.backupModel}`,
|
||||
}).returning();
|
||||
console.log(`[Enhance-RealESRGAN] ✅ 保存到数据库成功,ID: ${savedImage.id}`);
|
||||
} catch (dbError) {
|
||||
console.error("[Enhance-RealESRGAN] ⚠️ 保存到数据库失败:", dbError);
|
||||
// 继续返回成功,图片已处理并上传
|
||||
let saved = false;
|
||||
let saveError: unknown = null;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
try {
|
||||
const [savedImage] = await db.insert(generatedImage).values({
|
||||
userId,
|
||||
chatId: chatId || null,
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: `${config.backupScale}x放大`,
|
||||
prompt: prompt || "Real-ESRGAN图片变清晰处理",
|
||||
modelId: `realesrgan-${config.backupModel}`,
|
||||
}).returning();
|
||||
console.log(`[Enhance-RealESRGAN] ✅ 保存到数据库成功,ID: ${savedImage.id}`);
|
||||
saved = true;
|
||||
break;
|
||||
} catch (dbError) {
|
||||
saveError = dbError;
|
||||
if (attempt < 3) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 120 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!saved) {
|
||||
console.error("[Enhance-RealESRGAN] ❌ 保存到数据库失败:", saveError);
|
||||
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
|
||||
@ -648,20 +648,33 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 5. 保存图片记录到数据库(可选)
|
||||
if (!skipGallerySave) {
|
||||
try {
|
||||
const [savedImage] = await db.insert(generatedImage).values({
|
||||
userId,
|
||||
chatId: chatId || null,
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: "2K",
|
||||
prompt: prompt || "抠图处理",
|
||||
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
||||
}).returning();
|
||||
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
||||
} catch (dbError) {
|
||||
console.error("[RMBG] ❌ 保存到数据库失败:", dbError);
|
||||
let saved = false;
|
||||
let saveError: unknown = null;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
try {
|
||||
const [savedImage] = await db.insert(generatedImage).values({
|
||||
userId,
|
||||
chatId: chatId || null,
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: "2K",
|
||||
prompt: prompt || "抠图处理",
|
||||
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
||||
}).returning();
|
||||
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
||||
saved = true;
|
||||
break;
|
||||
} catch (dbError) {
|
||||
saveError = dbError;
|
||||
if (attempt < 3) {
|
||||
await sleep(120 * attempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!saved) {
|
||||
console.error("[RMBG] ❌ 保存到数据库失败:", saveError);
|
||||
throw new Error("图片处理成功但保存到相册失败,请稍后重试");
|
||||
}
|
||||
} else {
|
||||
console.log("[RMBG] Skip gallery save by request flag");
|
||||
|
||||
@ -34,6 +34,8 @@ const sceneFusionRequestSchema = z.object({
|
||||
})).min(1, "至少需要一个场景图").max(10, "最多支持10个场景图"),
|
||||
});
|
||||
|
||||
type SceneFusionMode = "scene-fission" | "multi-product-scene" | "random-bg";
|
||||
|
||||
// 默认提示词(当数据库未配置时使用)- 使用"参考"的表述方式
|
||||
const FALLBACK_PROMPT = "请参考第一张图片中的产品,将其自然地融合到第二张图片的场景中。要求:1)保持产品的整体外观、颜色、形状和设计风格与第一张图一致;2)调整产品的大小和角度以适应场景;3)注意光影和透视关系使融合自然。";
|
||||
|
||||
@ -47,6 +49,8 @@ const SCENE_FUSION_ECHO_HASH_TIMEOUT_MS = 20_000;
|
||||
const SCENE_FUSION_ECHO_HASH_THRESHOLD = 8;
|
||||
const SCENE_FUSION_STRICT_PROMPT_TAIL =
|
||||
"硬性要求:严禁直接返回第一张参考图(产品图)原图,必须保留第二张图的场景与构图,仅替换目标主体;若识别不确定,请优先保持第二张图背景不变并完成主体替换。";
|
||||
const SCENE_FUSION_RANDOM_BG_PROMPT_TAIL =
|
||||
"硬性要求:背景必须与原图明显不同(颜色、结构、元素至少两项显著变化),禁止保留原背景纹理或直接回传原图;仅保留产品主体并保证主体边缘完整自然。";
|
||||
const sceneFusionModelCooldown = new Map<string, number>();
|
||||
let sceneFusionRoundRobinCursor = 0;
|
||||
|
||||
@ -155,7 +159,11 @@ function getNextSceneFusionRoundRobinOffset(poolSize: number): number {
|
||||
function buildPreRmbgCandidateUrls(request: Request): string[] {
|
||||
const urls: string[] = [];
|
||||
const requestUrl = new URL(request.url);
|
||||
urls.push(new URL("/api/rmbg", request.url).toString());
|
||||
const requestHost = requestUrl.hostname.toLowerCase();
|
||||
const requestProtocol = requestUrl.protocol.replace(":", "");
|
||||
if (!(requestProtocol === "https" && (requestHost === "localhost" || requestHost === "127.0.0.1"))) {
|
||||
urls.push(new URL("/api/rmbg", request.url).toString());
|
||||
}
|
||||
|
||||
const host = request.headers.get("host")?.trim();
|
||||
const protoHint = request.headers
|
||||
@ -166,7 +174,14 @@ function buildPreRmbgCandidateUrls(request: Request): string[] {
|
||||
const protocol = protoHint === "https" || protoHint === "http" ? protoHint : requestUrl.protocol.replace(":", "");
|
||||
|
||||
if (host) {
|
||||
urls.push(`${protocol}://${host}/api/rmbg`);
|
||||
const normalizedHost = host.split(":")[0]?.toLowerCase() || "";
|
||||
const isLocalHost = normalizedHost === "localhost" || normalizedHost === "127.0.0.1";
|
||||
if (!(protocol === "https" && isLocalHost)) {
|
||||
urls.push(`${protocol}://${host}/api/rmbg`);
|
||||
}
|
||||
if (isLocalHost) {
|
||||
urls.push(`http://${host}/api/rmbg`);
|
||||
}
|
||||
}
|
||||
|
||||
const localPort = requestUrl.port || process.env.PORT || "3125";
|
||||
@ -899,6 +914,39 @@ async function downloadAndUploadToS3(
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSceneFusionImageRecord(params: {
|
||||
userId: string;
|
||||
thumbnailUrl: string;
|
||||
originalUrl: string;
|
||||
imageSize: string;
|
||||
prompt: string;
|
||||
modelId: string;
|
||||
}): Promise<void> {
|
||||
let lastError: unknown = null;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
try {
|
||||
await db.insert(generatedImage).values({
|
||||
userId: params.userId,
|
||||
chatId: null,
|
||||
messageId: null,
|
||||
thumbnailUrl: params.thumbnailUrl,
|
||||
originalUrl: params.originalUrl,
|
||||
imageSize: params.imageSize,
|
||||
prompt: params.prompt,
|
||||
modelId: params.modelId,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < 3) {
|
||||
await waitSceneFusion(120 * attempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error("保存到相册失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 aspectRatio 和 imageSize 计算实际尺寸
|
||||
* @param aspectRatio - 宽高比,如 "1:1", "4:3", "16:9", "auto"
|
||||
@ -1063,13 +1111,17 @@ function buildPrompt(
|
||||
defaultPrompt: string,
|
||||
productName?: string,
|
||||
targetName?: string,
|
||||
customPrompt?: string
|
||||
customPrompt?: string,
|
||||
sceneMode: SceneFusionMode = "scene-fission"
|
||||
): string {
|
||||
let prompt = defaultPrompt;
|
||||
|
||||
// 如果用户提供了具体的产品名称和被替换物体名称,构建更精确的提示词
|
||||
// 使用"参考图1"的表述方式,并强调产品的视觉特征
|
||||
if (productName && targetName) {
|
||||
|
||||
if (sceneMode === "random-bg") {
|
||||
const subject = productName || "产品";
|
||||
prompt = `请以第一张图片中的${subject}为唯一主体,对第二张图片进行背景重绘。要求:1)${subject}外观、材质、颜色与细节保持一致;2)允许轻微调整主体位置与角度以匹配新背景;3)背景需要完整重建并与原图明显不同;4)光影透视自然,输出商业摄影质感。`;
|
||||
} else if (productName && targetName) {
|
||||
// 如果用户提供了具体的产品名称和被替换物体名称,构建更精确的提示词
|
||||
// 使用"参考图1"的表述方式,并强调产品的视觉特征
|
||||
prompt = `请参考第一张图片中的${productName},将其自然地融合到第二张图片的场景中,替换掉场景中的${targetName}。要求:1)保持${productName}的整体外观、颜色、形状和设计风格与第一张图一致;2)调整${productName}的大小和角度以适应场景;3)注意光影和透视关系使融合自然。`;
|
||||
} else if (productName) {
|
||||
prompt = `请参考第一张图片中的${productName},将其自然地融合到第二张图片的场景中。要求:1)保持${productName}的整体外观、颜色、形状和设计风格与第一张图一致;2)调整${productName}的大小和角度以适应场景;3)注意光影和透视关系使融合自然。`;
|
||||
@ -1082,7 +1134,11 @@ function buildPrompt(
|
||||
prompt = `${prompt} 补充要求:${customPrompt}`;
|
||||
}
|
||||
|
||||
if (!prompt.includes("严禁直接返回第一张参考图")) {
|
||||
if (sceneMode === "random-bg") {
|
||||
if (!prompt.includes("背景必须与原图明显不同")) {
|
||||
prompt = `${prompt} ${SCENE_FUSION_RANDOM_BG_PROMPT_TAIL}`;
|
||||
}
|
||||
} else if (!prompt.includes("严禁直接返回第一张参考图")) {
|
||||
prompt = `${prompt} ${SCENE_FUSION_STRICT_PROMPT_TAIL}`;
|
||||
}
|
||||
|
||||
@ -1099,13 +1155,14 @@ async function processSceneWithProvider(
|
||||
targetName: string | undefined,
|
||||
customPrompt: string | undefined,
|
||||
defaultPrompt: string,
|
||||
sceneMode: SceneFusionMode,
|
||||
provider: { type: string; baseUrl: string; apiKey: string },
|
||||
modelId: string,
|
||||
aspectRatio?: string,
|
||||
imageSize?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
// 构建智能提示词
|
||||
const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt);
|
||||
const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt, sceneMode);
|
||||
|
||||
console.log(`[SceneFusion] Built prompt: ${prompt}`);
|
||||
console.log(`[SceneFusion] Provider type: ${provider.type}`);
|
||||
@ -1382,13 +1439,14 @@ async function processScene(
|
||||
targetName: string | undefined,
|
||||
customPrompt: string | undefined,
|
||||
defaultPrompt: string,
|
||||
sceneMode: SceneFusionMode,
|
||||
baseURL: string,
|
||||
apiKey: string,
|
||||
modelId: string,
|
||||
size?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
// 构建智能提示词
|
||||
const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt);
|
||||
const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt, sceneMode);
|
||||
|
||||
console.log(`[SceneFusion] Built prompt: ${prompt}`);
|
||||
|
||||
@ -1417,6 +1475,7 @@ async function processSceneWithDiversity(
|
||||
targetName?: string;
|
||||
customPrompt?: string;
|
||||
defaultPrompt: string;
|
||||
sceneMode: SceneFusionMode;
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
sceneIndex: number;
|
||||
@ -1467,6 +1526,7 @@ async function processSceneWithDiversity(
|
||||
params.targetName,
|
||||
params.customPrompt,
|
||||
params.defaultPrompt,
|
||||
params.sceneMode,
|
||||
{
|
||||
type: candidate.provider.type,
|
||||
baseUrl: candidate.provider.baseUrl,
|
||||
@ -1777,6 +1837,7 @@ export async function POST(request: Request) {
|
||||
targetName: resolvedScene.targetName,
|
||||
customPrompt: resolvedScene.customPrompt,
|
||||
defaultPrompt: config.defaultPrompt,
|
||||
sceneMode: resolvedSceneMode,
|
||||
aspectRatio,
|
||||
imageSize,
|
||||
sceneIndex: currentIndex,
|
||||
@ -1788,23 +1849,17 @@ export async function POST(request: Request) {
|
||||
|
||||
const dbImageSize = imageSize || "2K";
|
||||
|
||||
try {
|
||||
await db.insert(generatedImage).values({
|
||||
userId: session.user.id,
|
||||
chatId: null,
|
||||
messageId: null,
|
||||
thumbnailUrl: result.thumbnail,
|
||||
originalUrl: result.original,
|
||||
imageSize: dbImageSize,
|
||||
prompt: `[场景裂变] ${result.prompt}`,
|
||||
modelId: candidate.model.modelId,
|
||||
});
|
||||
console.log(
|
||||
`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`
|
||||
);
|
||||
} catch (dbError) {
|
||||
console.error("[SceneFusion] ❌ Failed to save to gallery:", dbError);
|
||||
}
|
||||
await saveSceneFusionImageRecord({
|
||||
userId: session.user.id,
|
||||
thumbnailUrl: result.thumbnail,
|
||||
originalUrl: result.original,
|
||||
imageSize: dbImageSize,
|
||||
prompt: `[场景裂变] ${result.prompt}`,
|
||||
modelId: candidate.model.modelId,
|
||||
});
|
||||
console.log(
|
||||
`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`
|
||||
);
|
||||
|
||||
successCount += 1;
|
||||
if (!enqueue({
|
||||
|
||||
@ -136,17 +136,30 @@ async function finalizeWatermarkTaskResult(params: {
|
||||
const filename = `watermark-removed-${Date.now()}.png`;
|
||||
const uploadResult = await uploadFile(filename, imageBuffer);
|
||||
|
||||
try {
|
||||
await db.insert(generatedImage).values({
|
||||
userId: params.userId,
|
||||
thumbnailUrl: uploadResult.url,
|
||||
originalUrl: uploadResult.url,
|
||||
imageSize: "2K",
|
||||
prompt: "去水印处理",
|
||||
modelId: "watermark-remove",
|
||||
});
|
||||
} catch (saveError) {
|
||||
let saved = false;
|
||||
let saveError: unknown = null;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
try {
|
||||
await db.insert(generatedImage).values({
|
||||
userId: params.userId,
|
||||
thumbnailUrl: uploadResult.url,
|
||||
originalUrl: uploadResult.url,
|
||||
imageSize: "2K",
|
||||
prompt: "去水印处理",
|
||||
modelId: "watermark-remove",
|
||||
});
|
||||
saved = true;
|
||||
break;
|
||||
} catch (error) {
|
||||
saveError = error;
|
||||
if (attempt < 3) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 120 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!saved) {
|
||||
console.error("[WatermarkRemove] 保存到相册失败:", saveError);
|
||||
throw new Error("去水印成功但保存到相册失败,请稍后重试");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -1374,6 +1374,39 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSceneFusionBatchAddToCanvas = useCallback((imageUrls: string[]) => {
|
||||
const validUrls = imageUrls.filter((url) => typeof url === "string" && url.trim().length > 0);
|
||||
if (validUrls.length === 0) {
|
||||
throw new Error("没有可添加到画布的图片");
|
||||
}
|
||||
|
||||
const maxColumns = 3;
|
||||
const gap = 20;
|
||||
const baseX = node.position.x + node.size.width + gap;
|
||||
const baseY = node.position.y;
|
||||
|
||||
validUrls.forEach((url, index) => {
|
||||
const column = index % maxColumns;
|
||||
const row = Math.floor(index / maxColumns);
|
||||
addNode({
|
||||
type: "image",
|
||||
position: {
|
||||
x: baseX + column * (node.size.width + gap),
|
||||
y: baseY + row * (node.size.height + gap),
|
||||
},
|
||||
size: { width: node.size.width, height: node.size.height },
|
||||
data: {
|
||||
src: url,
|
||||
status: "completed",
|
||||
prompt: "批量处理结果",
|
||||
title: `${displayTitle}-批量${index + 1}`,
|
||||
originalSrc: data.src,
|
||||
},
|
||||
connections: [node.id],
|
||||
});
|
||||
});
|
||||
}, [addNode, data.src, displayTitle, node.id, node.position.x, node.position.y, node.size.height, node.size.width]);
|
||||
|
||||
// 图片标注完成回调
|
||||
const handleAnnotateComplete = async (annotatedImageUrl: string) => {
|
||||
setAnnotatorOpen(false);
|
||||
@ -2010,6 +2043,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
<SceneFusionDialog
|
||||
open={sceneFusionDialogOpen}
|
||||
onOpenChange={setSceneFusionDialogOpen}
|
||||
onBatchAddToCanvas={handleSceneFusionBatchAddToCanvas}
|
||||
/>
|
||||
|
||||
{/* 提示词重绘对话框 */}
|
||||
|
||||
@ -837,7 +837,7 @@ export function SceneFusionView({
|
||||
const stylePool = [...randomBackgroundStyles].sort(() => Math.random() - 0.5);
|
||||
return Array.from({ length: count }).map((_, index) => {
|
||||
const style = stylePool[index % stylePool.length];
|
||||
const customPrompt = `请保持主体和构图尽量不变,仅替换背景为“${style}”风格,确保光影自然。${randomPrompt ? `补充要求:${randomPrompt}` : ""}`;
|
||||
const customPrompt = `请保持产品主体清晰完整,背景必须明显变化为“${style}”风格(不得与原背景相似),光影自然。${randomPrompt ? `补充要求:${randomPrompt}` : ""}`;
|
||||
return {
|
||||
id: `rand-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 6)}`,
|
||||
image: productImage || "",
|
||||
@ -855,6 +855,7 @@ export function SceneFusionView({
|
||||
body: JSON.stringify({
|
||||
imageUrl: item.image,
|
||||
prompt: getModePrompt(mode),
|
||||
skipGallerySave: false,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json();
|
||||
@ -1619,12 +1620,18 @@ export function SceneFusionView({
|
||||
</div>
|
||||
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col bg-white dark:bg-zinc-950">
|
||||
<div className="h-16 px-4 border-b flex items-center justify-between bg-white dark:bg-zinc-950 shrink-0">
|
||||
<div className="h-16 px-4 pr-14 border-b flex items-center justify-between bg-white dark:bg-zinc-950 shrink-0">
|
||||
<h3 className="font-medium">处理结果</h3>
|
||||
{(completedResultItems.length > 0 || (resultAllFinished && onBatchAddToCanvas)) && (
|
||||
{(completedResultItems.length > 0 || onBatchAddToCanvas) && (
|
||||
<div className="flex items-center gap-2">
|
||||
{resultAllFinished && onBatchAddToCanvas && completedResultItems.length > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={handleBatchAddToCanvas}>
|
||||
{onBatchAddToCanvas && completedResultItems.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBatchAddToCanvas}
|
||||
disabled={!resultAllFinished}
|
||||
title={resultAllFinished ? "将处理结果批量添加到画布" : "请等待所有任务处理完成"}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
批量添加到画布
|
||||
</Button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user