From 309c4c8e2803f9c8f7c264da5ee6a3409609d0cc Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 6 Mar 2026 00:10:10 +0800 Subject: [PATCH] fix(scene-fusion): restore light UI and fix mode select overlay in dialog --- components/scene-fusion-dialog.tsx | 12 +- components/scene-fusion-view.tsx | 1323 ++++++++++------------------ 2 files changed, 451 insertions(+), 884 deletions(-) diff --git a/components/scene-fusion-dialog.tsx b/components/scene-fusion-dialog.tsx index 4c63c3671..38636f733 100644 --- a/components/scene-fusion-dialog.tsx +++ b/components/scene-fusion-dialog.tsx @@ -19,15 +19,17 @@ interface SceneFusionDialogProps { export function SceneFusionDialog({ open, onOpenChange, onBatchAddToCanvas }: SceneFusionDialogProps) { return ( - + 批量处理 批量处理图片与场景裂变。 -
-
-
- onOpenChange(false)} onBatchAddToCanvas={onBatchAddToCanvas} /> +
+
+ onOpenChange(false)} + onBatchAddToCanvas={onBatchAddToCanvas} + />
diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx index 0dec282e6..2867649d5 100644 --- a/components/scene-fusion-view.tsx +++ b/components/scene-fusion-view.tsx @@ -1,38 +1,28 @@ "use client"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import useSWR from "swr"; import { - AlertCircle, - ArrowLeft, - Check, Download, - Eraser, Image as ImageIcon, Layers, Loader2, Plus, RefreshCw, Sparkles, + Trash2, Upload, Wand2, - X, + Eraser, } from "lucide-react"; -import type React from "react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import useSWR from "swr"; +import { requestLongTaskResult } from "@/lib/client/long-task"; +import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { requestLongTaskResult } from "@/lib/client/long-task"; -import { cn } from "@/lib/utils"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; const fetcher = (url: string) => fetch(url).then((res) => res.json()); const MAX_BATCH_ITEMS = 10; @@ -42,16 +32,10 @@ const MULTI_PRODUCT_FAILOVER_RETRY_DELAY_MS = 1500; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -type BatchMode = - | "scene-fission" - | "multi-product-scene" - | "random-bg" - | "rmbg" - | "enhance" - | "watermark"; +type BatchMode = "scene-fission" | "multi-product-scene" | "random-bg" | "rmbg" | "enhance" | "watermark"; type ItemStatus = "pending" | "processing" | "completed" | "failed"; -type ProcessItem = { +interface ProcessItem { id: string; image: string; status: ItemStatus; @@ -60,14 +44,14 @@ type ProcessItem = { error?: string; targetName?: string; customPrompt?: string; -}; +} -type SceneFusionViewProps = { +interface SceneFusionViewProps { onBack: () => void; initialProductImage?: string | null; initialProductName?: string; onBatchAddToCanvas?: (imageUrls: string[]) => void | Promise; -}; +} const aspectRatioOptions = [ { label: "自动", value: "auto" }, @@ -97,48 +81,13 @@ const randomBackgroundStyles = [ "高端商业橱窗", ]; -const modeOptions: Array<{ - id: BatchMode; - label: string; - description: string; - icon: React.ComponentType<{ className?: string }>; -}> = [ - { - id: "scene-fission", - label: "裂变场景", - description: "1张产品图 + 多张参考图", - icon: Layers, - }, - { - id: "multi-product-scene", - label: "多产品替换", - description: "10张产品图 + 1张参考图", - icon: ImageIcon, - }, - { - id: "random-bg", - label: "随机换背景", - description: "1张图随机生成多种背景", - icon: Wand2, - }, - { - id: "rmbg", - label: "批量抠图", - description: "多图同步去背景", - icon: Eraser, - }, - { - id: "enhance", - label: "批量变清晰", - description: "多图同步增强画质", - icon: Sparkles, - }, - { - id: "watermark", - label: "批量去水印", - description: "多图同步去除水印", - icon: RefreshCw, - }, +const modeOptions: Array<{ id: BatchMode; label: string; description: string; icon: React.ComponentType<{ className?: string }> }> = [ + { id: "scene-fission", label: "裂变场景", description: "1张产品图 + 多张参考图", icon: Layers }, + { id: "multi-product-scene", label: "多产品替换", description: "10张产品图 + 1张参考图", icon: ImageIcon }, + { id: "random-bg", label: "随机换背景", description: "1张图随机生成多种背景", icon: Wand2 }, + { id: "rmbg", label: "批量抠图", description: "多图同步去背景", icon: Eraser }, + { id: "enhance", label: "批量变清晰", description: "多图同步增强画质", icon: Sparkles }, + { id: "watermark", label: "批量去水印", description: "多图同步去除水印", icon: RefreshCw }, ]; function getModeActionLabel(mode: BatchMode, count: number) { @@ -155,8 +104,6 @@ function getModeActionLabel(mode: BatchMode, count: number) { return `批量变清晰 (${count} 张)`; case "watermark": return `批量去水印 (${count} 张)`; - default: - return `开始处理 (${count} 张)`; } } @@ -180,18 +127,13 @@ export function SceneFusionView({ onBatchAddToCanvas, }: SceneFusionViewProps) { const { data: configData } = useSWR("/api/scene-fusion/config", fetcher); - const config = configData as - | { - enabled?: boolean; - defaultPrompt?: string; - maxScenes?: number; - } - | undefined; + const config = configData as { + enabled?: boolean; + defaultPrompt?: string; + maxScenes?: number; + } | undefined; - const maxScenes = Math.min( - config?.maxScenes ?? MAX_BATCH_ITEMS, - MAX_BATCH_ITEMS - ); + const maxScenes = Math.min(config?.maxScenes ?? MAX_BATCH_ITEMS, MAX_BATCH_ITEMS); const [mode, setMode] = useState("scene-fission"); const [isProcessing, setIsProcessing] = useState(false); @@ -199,16 +141,12 @@ export function SceneFusionView({ const [imageSize, setImageSize] = useState("2K"); const [productImage, setProductImage] = useState(null); - const [productImagePreview, setProductImagePreview] = useState( - null - ); + const [productImagePreview, setProductImagePreview] = useState(null); const [productName, setProductName] = useState(initialProductName || ""); const [sceneItems, setSceneItems] = useState([]); const [multiProductItems, setMultiProductItems] = useState([]); - const [referenceSceneImage, setReferenceSceneImage] = useState( - null - ); + const [referenceSceneImage, setReferenceSceneImage] = useState(null); const [referenceTargetName, setReferenceTargetName] = useState(""); const [referenceCustomPrompt, setReferenceCustomPrompt] = useState(""); const [randomItems, setRandomItems] = useState([]); @@ -232,56 +170,32 @@ export function SceneFusionView({ }, [batchItems, mode, multiProductItems, randomItems, sceneItems]); const modeCanStart = useMemo(() => { - if (mode === "scene-fission") - return Boolean(productImage) && sceneItems.length > 0; - if (mode === "multi-product-scene") - return multiProductItems.length > 0 && Boolean(referenceSceneImage); + if (mode === "scene-fission") return Boolean(productImage) && sceneItems.length > 0; + if (mode === "multi-product-scene") return multiProductItems.length > 0 && Boolean(referenceSceneImage); if (mode === "random-bg") return Boolean(productImage) && randomCount > 0; return batchItems.length > 0; - }, [ - batchItems.length, - mode, - multiProductItems.length, - productImage, - randomCount, - referenceSceneImage, - sceneItems.length, - ]); + }, [batchItems.length, mode, multiProductItems.length, productImage, randomCount, referenceSceneImage, sceneItems.length]); - const selectedModeOption = useMemo( - () => modeOptions.find((option) => option.id === mode), - [mode] - ); + const selectedModeOption = useMemo(() => modeOptions.find((option) => option.id === mode), [mode]); const completedResultItems = useMemo( - () => - resultItems.filter( - (item) => - item.status === "completed" && (item.originalUrl || item.result) - ), + () => resultItems.filter((item) => item.status === "completed" && (item.originalUrl || item.result)), [resultItems] ); const resultAllFinished = useMemo( - () => - resultItems.length > 0 && - resultItems.every( - (item) => item.status !== "processing" && item.status !== "pending" - ), + () => resultItems.length > 0 && resultItems.every((item) => item.status !== "processing" && item.status !== "pending"), [resultItems] ); - const updateProductPreview = useCallback( - (previewUrl: string | null, isObjectUrl = false) => { - if (productPreviewObjectUrlRef.current) { - URL.revokeObjectURL(productPreviewObjectUrlRef.current); - productPreviewObjectUrlRef.current = null; - } - setProductImagePreview(previewUrl); - if (isObjectUrl && previewUrl) { - productPreviewObjectUrlRef.current = previewUrl; - } - }, - [] - ); + const updateProductPreview = (previewUrl: string | null, isObjectUrl = false) => { + if (productPreviewObjectUrlRef.current) { + URL.revokeObjectURL(productPreviewObjectUrlRef.current); + productPreviewObjectUrlRef.current = null; + } + setProductImagePreview(previewUrl); + if (isObjectUrl && previewUrl) { + productPreviewObjectUrlRef.current = previewUrl; + } + }; const clearProductImage = () => { setProductImage(null); @@ -293,7 +207,7 @@ export function SceneFusionView({ setProductImage(initialProductImage); updateProductPreview(initialProductImage); } - }, [initialProductImage, updateProductPreview]); + }, [initialProductImage]); useEffect(() => { return () => { @@ -331,18 +245,16 @@ export function SceneFusionView({ const handleSceneUpload = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); - if (files.length === 0) { - return; - } + if (files.length === 0) return; if (sceneItems.length + files.length > maxScenes) { toast.error(`单次最多上传 ${maxScenes} 张参考图`); return; } - for (const file of files) { + files.forEach((file) => { if (file.size > 50 * 1024 * 1024) { toast.error(`图片 ${file.name} 大小不能超过 50MB`); - continue; + return; } const reader = new FileReader(); reader.onload = (event) => { @@ -359,25 +271,23 @@ export function SceneFusionView({ ]); }; reader.readAsDataURL(file); - } + }); e.target.value = ""; }; const handleMultiProductUpload = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); - if (files.length === 0) { - return; - } + if (files.length === 0) return; if (multiProductItems.length + files.length > MAX_BATCH_ITEMS) { toast.error(`单次最多上传 ${MAX_BATCH_ITEMS} 张产品图`); return; } - for (const file of files) { + files.forEach((file) => { if (file.size > 50 * 1024 * 1024) { toast.error(`图片 ${file.name} 大小不能超过 50MB`); - continue; + return; } const reader = new FileReader(); reader.onload = (event) => { @@ -392,14 +302,12 @@ export function SceneFusionView({ ]); }; reader.readAsDataURL(file); - } + }); e.target.value = ""; }; - const handleReferenceSceneUpload = ( - e: React.ChangeEvent - ) => { + const handleReferenceSceneUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 50 * 1024 * 1024) { @@ -418,18 +326,16 @@ export function SceneFusionView({ const handleBatchUpload = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); - if (files.length === 0) { - return; - } + if (files.length === 0) return; if (batchItems.length + files.length > MAX_BATCH_ITEMS) { toast.error(`单次最多上传 ${MAX_BATCH_ITEMS} 张图片`); return; } - for (const file of files) { + files.forEach((file) => { if (file.size > 50 * 1024 * 1024) { toast.error(`图片 ${file.name} 大小不能超过 50MB`); - continue; + return; } const reader = new FileReader(); reader.onload = (event) => { @@ -444,15 +350,13 @@ export function SceneFusionView({ ]); }; reader.readAsDataURL(file); - } + }); e.target.value = ""; }; const updateSceneItem = (id: string, patch: Partial) => { - setSceneItems((prev) => - prev.map((item) => (item.id === id ? { ...item, ...patch } : item)) - ); + setSceneItems((prev) => prev.map((item) => (item.id === id ? { ...item, ...patch } : item))); }; const removeSceneItem = (id: string) => { @@ -470,13 +374,7 @@ export function SceneFusionView({ const updateBatchItemsStatus = (ids: string[], status: ItemStatus) => { setBatchItems((prev) => prev.map((item) => - ids.includes(item.id) - ? { - ...item, - status, - error: status === "processing" ? undefined : item.error, - } - : item + ids.includes(item.id) ? { ...item, status, error: status === "processing" ? undefined : item.error } : item ) ); }; @@ -488,24 +386,13 @@ export function SceneFusionView({ ) => { setter((prev) => prev.map((item) => - ids.includes(item.id) - ? { - ...item, - status, - error: status === "processing" ? undefined : item.error, - } - : item + ids.includes(item.id) ? { ...item, status, error: status === "processing" ? undefined : item.error } : item ) ); }; const runSceneFusion = async ( - scenesPayload: Array<{ - id: string; - image: string; - targetName?: string; - customPrompt?: string; - }>, + scenesPayload: Array<{ id: string; image: string; targetName?: string; customPrompt?: string }>, setter: React.Dispatch> ) => { if (!productImage) return; @@ -573,21 +460,14 @@ export function SceneFusionView({ }) => { if (msg.type === "agent_meta" && msg.data) { const detectedProductName = - typeof msg.data.productName === "string" - ? msg.data.productName.trim() - : ""; + typeof msg.data.productName === "string" ? msg.data.productName.trim() : ""; if (!productName.trim() && detectedProductName) { setProductName(detectedProductName); } - if ( - Array.isArray(msg.data.sceneTargets) && - msg.data.sceneTargets.length > 0 - ) { + if (Array.isArray(msg.data.sceneTargets) && msg.data.sceneTargets.length > 0) { setter((prev) => prev.map((item) => { - const detected = msg.data?.sceneTargets?.find( - (entry) => entry.id === item.id - ); + const detected = msg.data?.sceneTargets?.find((entry) => entry.id === item.id); if (!detected?.targetName) return item; const currentTargetName = (item.targetName || "").trim(); if (currentTargetName) return item; @@ -674,10 +554,7 @@ export function SceneFusionView({ }; handleParsedMessage(msg); } catch (parseError) { - console.warn( - "[SceneFusionView] Ignore invalid stream line:", - parseError - ); + console.warn("[SceneFusionView] Ignore invalid stream line:", parseError); } } } @@ -706,23 +583,15 @@ export function SceneFusionView({ }; handleParsedMessage(msg); } catch (parseError) { - console.warn( - "[SceneFusionView] Ignore trailing stream buffer:", - parseError - ); + console.warn("[SceneFusionView] Ignore trailing stream buffer:", parseError); } } if (streamErrorMessage) { setter((prev) => prev.map((item) => - scenesPayload.some((scene) => scene.id === item.id) && - item.status === "processing" - ? { - ...item, - status: "failed", - error: streamErrorMessage || "处理失败", - } + scenesPayload.some((scene) => scene.id === item.id) && item.status === "processing" + ? { ...item, status: "failed", error: streamErrorMessage || "处理失败" } : item ) ); @@ -739,8 +608,7 @@ export function SceneFusionView({ toast.error(message); setter((prev) => prev.map((item) => - scenesPayload.some((scene) => scene.id === item.id) && - item.status === "processing" + scenesPayload.some((scene) => scene.id === item.id) && item.status === "processing" ? { ...item, status: "failed", error: message } : item ) @@ -815,10 +683,7 @@ export function SceneFusionView({ if (msg.data.status === "completed") { return { result: msg.data.result || params.sceneImageInput, - originalUrl: - msg.data.originalUrl || - msg.data.result || - params.sceneImageInput, + originalUrl: msg.data.originalUrl || msg.data.result || params.sceneImageInput, }; } throw new Error(msg.data.error || "生成失败"); @@ -842,11 +707,7 @@ export function SceneFusionView({ }) => { let lastError: Error | null = null; - for ( - let round = 1; - round <= MULTI_PRODUCT_FAILOVER_MAX_ROUNDS; - round += 1 - ) { + for (let round = 1; round <= MULTI_PRODUCT_FAILOVER_MAX_ROUNDS; round += 1) { try { return await runSingleSceneFusionAttempt({ ...params, @@ -859,7 +720,6 @@ export function SceneFusionView({ `[SceneFusionView] 多产品替换第 ${round}/${MULTI_PRODUCT_FAILOVER_MAX_ROUNDS} 轮失败,准备故障迁移`, lastError.message ); - if (!isLastRound) { await sleep(MULTI_PRODUCT_FAILOVER_RETRY_DELAY_MS); } @@ -885,9 +745,7 @@ export function SceneFusionView({ return; } - let itemsToProcess = multiProductItems.filter( - (item) => item.status === "pending" || item.status === "failed" - ); + let itemsToProcess = multiProductItems.filter((item) => item.status === "pending" || item.status === "failed"); if (itemsToProcess.length === 0) { itemsToProcess = multiProductItems; } @@ -967,17 +825,13 @@ export function SceneFusionView({ if (successCount > 0 && failedCount === 0) { toast.success(`多产品替换完成:${successCount} 张`); } else { - toast.warning( - `多产品替换完成:${successCount} 成功,${failedCount} 失败` - ); + toast.warning(`多产品替换完成:${successCount} 成功,${failedCount} 失败`); } }; const buildRandomItems = () => { const count = Math.max(1, Math.min(MAX_BATCH_ITEMS, randomCount)); - const stylePool = [...randomBackgroundStyles].sort( - () => Math.random() - 0.5 - ); + 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}` : ""}`; @@ -1006,8 +860,7 @@ export function SceneFusionView({ } return { result: payload.thumbnail_url || payload.original_url || item.image, - originalUrl: - payload.original_url || payload.thumbnail_url || item.image, + originalUrl: payload.original_url || payload.thumbnail_url || item.image, }; } @@ -1027,8 +880,7 @@ export function SceneFusionView({ } return { result: payload.thumbnail_url || payload.original_url || item.image, - originalUrl: - payload.original_url || payload.thumbnail_url || item.image, + originalUrl: payload.original_url || payload.thumbnail_url || item.image, }; } @@ -1044,8 +896,7 @@ export function SceneFusionView({ } return { result: payload.thumbnail_url || payload.original_url || item.image, - originalUrl: - payload.original_url || payload.thumbnail_url || item.image, + originalUrl: payload.original_url || payload.thumbnail_url || item.image, }; } @@ -1058,9 +909,7 @@ export function SceneFusionView({ return; } - let itemsToProcess = batchItems.filter( - (item) => item.status === "pending" || item.status === "failed" - ); + let itemsToProcess = batchItems.filter((item) => item.status === "pending" || item.status === "failed"); if (itemsToProcess.length === 0) { itemsToProcess = batchItems; } @@ -1144,9 +993,7 @@ export function SceneFusionView({ return; } - let scenesToProcess = sceneItems.filter( - (item) => item.status === "pending" || item.status === "failed" - ); + let scenesToProcess = sceneItems.filter((item) => item.status === "pending" || item.status === "failed"); if (scenesToProcess.length === 0) { scenesToProcess = sceneItems; } @@ -1226,9 +1073,7 @@ export function SceneFusionView({ } setMultiProductItems((prev) => prev.map((productItem) => - productItem.id === item.id - ? { ...productItem, status: "pending", error: undefined } - : productItem + productItem.id === item.id ? { ...productItem, status: "pending", error: undefined } : productItem ) ); await runMultiProductSceneMode(); @@ -1237,9 +1082,7 @@ export function SceneFusionView({ setBatchItems((prev) => prev.map((batchItem) => - batchItem.id === item.id - ? { ...batchItem, status: "pending", error: undefined } - : batchItem + batchItem.id === item.id ? { ...batchItem, status: "pending", error: undefined } : batchItem ) ); await runBatchImageMode(); @@ -1263,19 +1106,14 @@ export function SceneFusionView({ }; const downloadAll = async () => { - const completed = resultItems.filter( - (item) => item.status === "completed" && (item.originalUrl || item.result) - ); + const completed = resultItems.filter((item) => item.status === "completed" && (item.originalUrl || item.result)); if (completed.length === 0) { toast.error("没有可下载的图片"); return; } for (let i = 0; i < completed.length; i += 1) { - const item = completed[i]; - if (!item) continue; - const url = item.originalUrl || item.result; - if (!url) continue; - await downloadImage(url, `batch-result-${i + 1}.png`); + const item = completed[i]!; + await downloadImage(item.originalUrl || item.result!, `batch-result-${i + 1}.png`); await new Promise((resolve) => setTimeout(resolve, 400)); } toast.success(`已下载 ${completed.length} 张图片`); @@ -1286,7 +1124,6 @@ export function SceneFusionView({ const imageUrls = completedResultItems .map((item) => item.originalUrl || item.result) .filter((url): url is string => Boolean(url)); - if (imageUrls.length === 0) { toast.error("没有可添加到画布的图片"); return; @@ -1301,214 +1138,143 @@ export function SceneFusionView({ }; return ( -
+
- {/* Left Sidebar - Settings & Upload */} -
- {/* Header */} -
-
-
- +
+
+
+
+ + 批量处理
- - 批量工作台 - +
-
- {/* Scrollable Content */} -
-
- {/* Mode Selection */} -
- - setMode(value as BatchMode)} disabled={isProcessing}> + - + {modeOptions.map((option) => ( - -
- - {option.label} -
+ + {option.label} ))}
- {selectedModeOption && ( -
-
- - {selectedModeOption.label} -
-
- {selectedModeOption.description} -
+
+
{selectedModeOption.label}
+
{selectedModeOption.description}
)} +

单次处理硬限制:最多 {MAX_BATCH_ITEMS} 张图片。

- {/* Main Image Upload (Product) */} {(mode === "scene-fission" || mode === "random-bg") && ( -
+
- + {(productImagePreview || productImage) && ( - + 清除 + )}
{ - if (!(productImagePreview || productImage)) { - productInputRef.current?.click(); - } - }} - onKeyDown={(e) => { - if ( - (e.key === "Enter" || e.key === " ") && - !(productImagePreview || productImage) - ) { - productInputRef.current?.click(); - } - }} - role="button" - tabIndex={0} + onClick={() => !(productImagePreview || productImage) && productInputRef.current?.click()} > {productImagePreview || productImage ? ( -
-
+
主图 -
- 主图 -
) : ( -
-
- -
-
-

点击上传主图

-

- 支持 PNG, JPG, WEBP -

-
+
+ + 点击上传主图
)}
{(productImagePreview || productImage) && ( -
- - setProductName(e.target.value)} - placeholder="例如:真皮沙发、陶瓷花瓶..." - value={productName} - /> -
+ setProductName(e.target.value)} + className="h-9 text-sm" + disabled={isProcessing} + /> )}
)} - {/* Output Settings */} - {(mode === "scene-fission" || - mode === "random-bg" || - mode === "multi-product-scene") && ( -
- - -
- -
+ {(mode === "scene-fission" || mode === "random-bg" || mode === "multi-product-scene") && ( +
+ +
+ +
{aspectRatioOptions.map((option) => ( ))}
- -
- -
+
+ +
{imageSizeOptions.map((option) => ( @@ -1518,101 +1284,75 @@ export function SceneFusionView({
)} - {/* Mode: Scene Fission - Reference Images */} {mode === "scene-fission" && ( -
+
-
{sceneItems.length === 0 && (
sceneInputRef.current?.click()} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - sceneInputRef.current?.click(); - } - }} - role="button" - tabIndex={0} > -
- -
-

- 点击添加场景参考图 -

+ +

点击添加场景参考图

+

最多 {maxScenes} 张

)} {sceneItems.map((item, index) => ( -
+
-
- {`场景 +
+ {`场景
-
+
- - SCENE {index + 1} - + 场景 {index + 1} {!isProcessing && ( - + + )}
- updateSceneItem(item.id, { - targetName: e.target.value, - }) - } - placeholder="被替换物体(如:椅子)" + placeholder="被替换物体(如:椅子、桌子)" value={item.targetName || ""} + onChange={(e) => updateSceneItem(item.id, { targetName: e.target.value })} + className="h-7 text-xs" + disabled={isProcessing} /> - updateSceneItem(item.id, { - customPrompt: e.target.value, - }) - } placeholder="修改要求(可选)" value={item.customPrompt || ""} + onChange={(e) => updateSceneItem(item.id, { customPrompt: e.target.value })} + className="h-7 text-xs" + disabled={isProcessing} />
@@ -1622,330 +1362,226 @@ export function SceneFusionView({
)} - {/* Mode: Multi Product */} {mode === "multi-product-scene" && ( -
-
+
+
- + -
- -
- {multiProductItems.map((item, index) => ( -
-
- {`产品 -
-
-
- - 产品 {index + 1} - - - {item.status === "completed" - ? "完成" - : item.status === "failed" - ? "失败" - : item.status === "processing" - ? "处理中" - : "等待"} - -
- {item.error && ( -

- {item.error} -

- )} -
- {!isProcessing && ( - - )} -
- ))} - {multiProductItems.length === 0 && ( -
- 暂无产品图 -
- )} -
-
- -
-
- - {referenceSceneImage && ( - - )} -
- -
{ - if (!referenceSceneImage) { - referenceSceneInputRef.current?.click(); - } - }} - onKeyDown={(e) => { - if ( - (e.key === "Enter" || e.key === " ") && - !referenceSceneImage - ) { - referenceSceneInputRef.current?.click(); - } - }} - role="button" - tabIndex={0} - > - - {referenceSceneImage ? ( -
- 参考图 -
- ) : ( -
- - 点击上传参考图 -
- )} -
- -
- setReferenceTargetName(e.target.value)} - placeholder="被替换物体(可选,如:椅子)" - value={referenceTargetName} - /> - - setReferenceCustomPrompt(e.target.value) - } - placeholder="补充要求(可选)" - value={referenceCustomPrompt} + onChange={handleMultiProductUpload} />
-
-
- )} - - {/* Mode: Random BG */} - {mode === "random-bg" && ( -
- -
-
- - - setRandomCount( - Math.max( - 1, - Math.min( - MAX_BATCH_ITEMS, - Number(e.target.value) || 1 - ) - ) - ) - } - type="number" - value={randomCount} - /> -
-
- - setRandomPrompt(e.target.value)} - placeholder="例如:赛博朋克、极简主义..." - value={randomPrompt} - /> -
-
-
- )} - - {/* Mode: Batch Image (RMBG, Enhance, Watermark) */} - {(mode === "rmbg" || - mode === "enhance" || - mode === "watermark") && ( -
-
- - - + {multiProductItems.length === 0 ? ( +
multiProductInputRef.current?.click()} + > + +

点击上传产品图(可多张)

+

最多 {MAX_BATCH_ITEMS} 张

+
+ ) : ( +
+ {multiProductItems.map((item, index) => ( +
+
+
+ {`产品 +
+
+
产品 {index + 1}
+
+ 状态:{item.status === "pending" ? "等待" : item.status === "processing" ? "处理中" : item.status === "completed" ? "完成" : "失败"} +
+ {item.error &&
{item.error}
} +
+ {!isProcessing && ( + + )} +
+
+ ))} +
+ )}
+
+ + {referenceSceneImage && ( + + )} +
+
!referenceSceneImage && referenceSceneInputRef.current?.click()} + > + + {referenceSceneImage ? ( +
+ 参考图 +
+ ) : ( +
+ + 点击上传参考图 +
+ )} +
+ + setReferenceTargetName(e.target.value)} + className="h-8 text-xs" + disabled={isProcessing} + /> + setReferenceCustomPrompt(e.target.value)} + className="h-8 text-xs" + disabled={isProcessing} + /> +
+
+ )} + + {mode === "random-bg" && ( +
+ +
+ + setRandomCount(Math.max(1, Math.min(MAX_BATCH_ITEMS, Number(e.target.value) || 1)))} + className="h-9 text-sm" + /> +
+
+ + setRandomPrompt(e.target.value)} + disabled={isProcessing} + placeholder="例如:暖色调、真实摄影感、商业海报风" + className="h-9 text-sm" + /> +
+

+ 使用与“裂变场景”同一套生图模型池,自动随机背景风格生成。 +

+
+ )} + + {(mode === "rmbg" || mode === "enhance" || mode === "watermark") && ( +
+
+ + + +
+ +
{batchItems.length === 0 && (
batchInputRef.current?.click()} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - batchInputRef.current?.click(); - } - }} - role="button" - tabIndex={0} > - -

点击上传图片

+ +

点击上传待处理图片

+

最多 {MAX_BATCH_ITEMS} 张

)} {batchItems.map((item, index) => ( -
-
- {`图片 -
-
-
- - 图片 {index + 1} - - - {item.status === "completed" - ? "完成" - : item.status === "failed" - ? "失败" - : item.status === "processing" - ? "处理中" - : "等待"} - +
+
+
+ {`图片
- {item.error && ( -

- {item.error} -

+
+
图片 {index + 1}
+
+ 状态:{item.status === "pending" ? "等待" : item.status === "processing" ? "处理中" : item.status === "completed" ? "完成" : "失败"} +
+ {item.error &&
{item.error}
} +
+ {!isProcessing && ( + )}
- {!isProcessing && ( - - )}
))}
@@ -1954,60 +1590,37 @@ export function SceneFusionView({
- {/* Start Button */} -
-
- {/* Right Content - Results */} -
-
-

处理结果

- {(completedResultItems.length > 0 || - (resultAllFinished && onBatchAddToCanvas)) && ( +
+
+

处理结果

+ {(completedResultItems.length > 0 || (resultAllFinished && onBatchAddToCanvas)) && (
- {resultAllFinished && - onBatchAddToCanvas && - completedResultItems.length > 0 && ( - - )} + {resultAllFinished && onBatchAddToCanvas && completedResultItems.length > 0 && ( + + )} {completedResultItems.length > 0 && ( - )} @@ -2015,123 +1628,75 @@ export function SceneFusionView({ )}
- -
+ +
{resultItems.length === 0 ? ( -
-
- -
-

准备就绪

-

- 在左侧选择模式并上传图片,结果将显示在这里 -

+
+ +

选择模式并上传图片后,结果会显示在这里

) : ( -
+
{resultItems.map((item, index) => ( -
-
- - RESULT {index + 1} +
+
+ 任务 {index + 1} + + {item.status === "completed" + ? "完成" + : item.status === "failed" + ? "失败" + : item.status === "processing" + ? "处理中" + : "等待"} -
- {item.status === "completed" && ( - - )} - {item.status === "failed" && ( - - )} - {item.status === "processing" && ( - - )} - - {item.status === "completed" - ? "完成" - : item.status === "failed" - ? "失败" - : item.status === "processing" - ? "处理中" - : "等待"} - -
-
+
{item.result ? ( - 处理结果 + 处理结果 ) : ( -
- 原图 -
- +
+ 原图 {item.status === "processing" ? ( -
-
- -
- - AI 处理中... - +
+ + 处理中...
) : item.status === "failed" ? ( -
-
- -
-

+

+

{item.error || "处理失败"}

) : ( - - 等待队列 + + 等待处理 )}
)} {item.status === "completed" && ( -
+
-
)}