diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx
index 586698487..4ad2de11d 100644
--- a/components/image-annotator.tsx
+++ b/components/image-annotator.tsx
@@ -9,7 +9,7 @@ import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { toast } from "sonner";
import { AngleEditDialog } from "./angle-edit-dialog";
-import { SceneFusionDialog } from "./scene-fusion-dialog";
+import { SceneFusionView } from "./scene-fusion-view";
type Tool = "pen" | "arrow" | "circle" | "text";
@@ -1216,7 +1216,15 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
className="fixed inset-0 z-[9999] flex items-center justify-center bg-background"
onClick={onCancel}
>
- {/* 弹窗容器 - 全屏显示 */}
+ {showSceneFusion ? (
+
e.stopPropagation()}
+ >
+ setShowSceneFusion(false)} />
+
+ ) : (
+ /* 弹窗容器 - 全屏显示 */
e.stopPropagation()}
@@ -1701,6 +1709,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
)}
+ )}
{/* 文字输入弹窗 */}
{textPosition && (
@@ -1739,12 +1748,6 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
onComplete={handleAngleEditComplete}
/>
)}
-
- {/* 场景裂变弹窗 */}
-
);
diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx
new file mode 100644
index 000000000..29a4e4575
--- /dev/null
+++ b/components/scene-fusion-view.tsx
@@ -0,0 +1,563 @@
+"use client";
+
+import React, { useState, useRef, useEffect } from "react";
+import useSWR from "swr";
+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 {
+ Upload,
+ Plus,
+ Trash2,
+ Download,
+ RefreshCw,
+ Image as ImageIcon,
+ Loader2,
+ Layers,
+ Sparkles,
+ ArrowLeft
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { toast } from "sonner";
+
+const fetcher = (url: string) => fetch(url).then((res) => res.json());
+
+// 场景项数据结构
+export interface SceneItem {
+ id: string;
+ image: string; // 场景图 URL 或 Base64
+ customPrompt: string; // 该场景的补充关键词
+ status: 'pending' | 'processing' | 'completed' | 'failed';
+ result?: string; // 生成结果缩略图 URL
+ originalUrl?: string; // 生成结果原图 URL(用于下载)
+ error?: string; // 错误信息
+}
+
+interface SceneFusionViewProps {
+ onBack: () => void;
+}
+
+export function SceneFusionView({ onBack }: SceneFusionViewProps) {
+ // 获取场景裂变配置(使用公开 API 端点)
+ const { data: configData } = useSWR(
+ "/api/scene-fusion/config",
+ fetcher
+ );
+
+ const config = configData as {
+ enabled?: boolean;
+ defaultPrompt?: string;
+ maxScenes?: number;
+ } | undefined;
+
+ const defaultPrompt = config?.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。";
+ const maxScenes = config?.maxScenes || 10;
+
+ // 状态管理
+ const [productImage, setProductImage] = useState(null);
+ const [scenes, setScenes] = useState([]);
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ // 文件上传 Input 引用
+ const productInputRef = useRef(null);
+ const sceneInputRef = useRef(null);
+
+ // 处理产品图上传
+ const handleProductUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ if (file.size > 10 * 1024 * 1024) {
+ toast.error("图片大小不能超过 10MB");
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setProductImage(e.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ // 重置 input,允许重复选择同一文件
+ e.target.value = "";
+ };
+
+ // 处理场景图上传
+ const handleSceneUpload = (e: React.ChangeEvent) => {
+ const files = Array.from(e.target.files || []);
+ if (files.length === 0) return;
+
+ if (scenes.length + files.length > maxScenes) {
+ toast.error(`最多只能添加 ${maxScenes} 张场景参考图`);
+ return;
+ }
+
+ files.forEach(file => {
+ if (file.size > 10 * 1024 * 1024) {
+ toast.error(`图片 ${file.name} 大小不能超过 10MB`);
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const newScene: SceneItem = {
+ id: Math.random().toString(36).substring(7),
+ image: e.target?.result as string,
+ customPrompt: "",
+ status: 'pending'
+ };
+ setScenes(prev => [...prev, newScene]);
+ };
+ reader.readAsDataURL(file);
+ });
+
+ // 重置 input
+ e.target.value = "";
+ };
+
+ // 移除场景
+ const removeScene = (id: string) => {
+ setScenes(prev => prev.filter(scene => scene.id !== id));
+ };
+
+ // 更新场景关键词
+ const updateScenePrompt = (id: string, prompt: string) => {
+ setScenes(prev => prev.map(scene =>
+ scene.id === id ? { ...scene, customPrompt: prompt } : scene
+ ));
+ };
+
+ // 开始裂变
+ const startFusion = async () => {
+ if (config?.enabled === false) {
+ toast.error("场景裂变功能已被管理员禁用");
+ return;
+ }
+ if (!productImage) {
+ toast.error("请先上传产品图");
+ return;
+ }
+ if (scenes.length === 0) {
+ toast.error("请至少添加一张场景参考图");
+ return;
+ }
+
+ setIsProcessing(true);
+
+ // 将所有 pending 或 failed 的状态改为 processing
+ const scenesToProcess = scenes.filter(s => s.status === 'pending' || s.status === 'failed');
+ setScenes(prev => prev.map(s =>
+ (s.status === 'pending' || s.status === 'failed')
+ ? { ...s, status: 'processing' }
+ : s
+ ));
+
+ try {
+ // 调用后端 API
+ const response = await fetch('/api/scene-fusion', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ productImage,
+ scenes: scenesToProcess.map(s => ({
+ id: s.id,
+ image: s.image,
+ customPrompt: s.customPrompt || undefined,
+ })),
+ }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || '处理失败');
+ }
+
+ // 更新场景状态
+ setScenes(prev => prev.map(s => {
+ const result = data.results?.find((r: any) => r.id === s.id);
+ if (result) {
+ if (result.status === 'completed') {
+ return {
+ ...s,
+ status: 'completed',
+ result: result.result,
+ originalUrl: result.originalUrl,
+ };
+ } else {
+ return {
+ ...s,
+ status: 'failed',
+ error: result.error || '生成失败'
+ };
+ }
+ }
+ return s;
+ }));
+
+ const { summary } = data;
+ if (summary.failed > 0) {
+ toast.warning(`批量处理完成:${summary.success} 成功,${summary.failed} 失败`);
+ } else {
+ toast.success(`批量处理完成:${summary.success} 张图片已生成`);
+ }
+ } catch (error) {
+ console.error('[SceneFusion] Error:', error);
+ toast.error(error instanceof Error ? error.message : '处理失败');
+
+ // 将 processing 状态改回 failed
+ setScenes(prev => prev.map(s =>
+ s.status === 'processing' ? { ...s, status: 'failed', error: '处理失败' } : s
+ ));
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ // 重新生成单个场景
+ const regenerateScene = async (id: string) => {
+ if (!productImage) return;
+
+ const scene = scenes.find(s => s.id === id);
+ if (!scene) return;
+
+ setScenes(prev => prev.map(s =>
+ s.id === id ? { ...s, status: 'processing', error: undefined } : s
+ ));
+
+ try {
+ const response = await fetch('/api/scene-fusion', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ productImage,
+ scenes: [{
+ id: scene.id,
+ image: scene.image,
+ customPrompt: scene.customPrompt || undefined,
+ }],
+ }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || '处理失败');
+ }
+
+ const result = data.results?.[0];
+ if (result?.status === 'completed') {
+ setScenes(prev => prev.map(s =>
+ s.id === id ? {
+ ...s,
+ status: 'completed',
+ result: result.result,
+ originalUrl: result.originalUrl,
+ } : s
+ ));
+ toast.success('重新生成成功');
+ } else {
+ throw new Error(result?.error || '生成失败');
+ }
+ } catch (error) {
+ console.error('[SceneFusion] Regenerate error:', error);
+ setScenes(prev => prev.map(s =>
+ s.id === id ? { ...s, status: 'failed', error: error instanceof Error ? error.message : '处理失败' } : s
+ ));
+ toast.error(error instanceof Error ? error.message : '重新生成失败');
+ }
+ };
+
+ // 下载图片
+ const downloadImage = (url: string, filename: string) => {
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ // 全部下载
+ const downloadAll = async () => {
+ const completedScenes = scenes.filter(s => s.status === 'completed' && (s.originalUrl || s.result));
+ if (completedScenes.length === 0) {
+ toast.error("没有可下载的图片");
+ return;
+ }
+
+ toast.info(`正在下载 ${completedScenes.length} 张图片...`);
+
+ for (let i = 0; i < completedScenes.length; i++) {
+ const scene = completedScenes[i];
+ const url = scene.originalUrl || scene.result!;
+ // 延迟下载避免浏览器阻止
+ setTimeout(() => {
+ downloadImage(url, `scene-fusion-${i + 1}.png`);
+ }, i * 300);
+ }
+ };
+
+ return (
+
+ {/* 顶部标题栏 */}
+
+
+
+ 上传1张产品图和多张场景图,AI 自动将产品融合到所有场景中
+
+
{/* 占位,保持标题居中 */}
+
+
+
+ {/* 左侧:输入区域 */}
+
+
+
+ {/* 1. 产品图上传 */}
+
+
+
+ {productImage && (
+
+ )}
+
+
+
!productImage && productInputRef.current?.click()}
+ >
+
+
+ {productImage ? (
+
+

+
+
+
+
+ ) : (
+
+
+ 点击上传产品图
+
+ )}
+
+
+
+ {/* 2. 场景图列表 */}
+
+
+
+
+
+
+
+
+ {scenes.length === 0 && (
+
sceneInputRef.current?.click()}
+ >
+
+
点击添加场景参考图
+
支持批量上传,最多{maxScenes}张
+
+ )}
+
+ {scenes.map((scene, index) => (
+
+
+ {/* 缩略图 */}
+
+

+
+
+ {/* 关键词输入 */}
+
+
+ 场景 {index + 1}
+ {!isProcessing && (
+
+ )}
+
+
updateScenePrompt(scene.id, e.target.value)}
+ className="h-8 text-xs"
+ disabled={isProcessing}
+ />
+
+
+
+ ))}
+
+
+
+
+
+ {/* 默认提示词提示 */}
+
+
+ 默认提示词:
+ {defaultPrompt.length > 60 ? `${defaultPrompt.substring(0, 60)}...` : defaultPrompt}
+
+
+
+
+
+
+
+
+ {/* 右侧:结果展示区域 */}
+
+
+
生成结果
+ {scenes.some(s => s.status === 'completed') && (
+
+ )}
+
+
+
+ {scenes.length === 0 ? (
+
+
+
添加场景图并开始裂变,结果将显示在这里
+
+ ) : (
+
+ {scenes.map((scene, index) => (
+
+
+ 场景 {index + 1}
+
+ {scene.status === 'completed' ? '完成' :
+ scene.status === 'failed' ? '失败' :
+ scene.status === 'processing' ? '生成中' : '等待'}
+
+
+
+
+ {scene.result ? (
+

+ ) : (
+
+ {scene.status === 'processing' ? (
+
+
+ 正在生成...
+
+ ) : scene.status === 'failed' ? (
+
+
{scene.error || "生成失败"}
+
+
+ ) : (
+
+

+
等待处理
+
+ )}
+
+ )}
+
+ {/* 悬停操作层 */}
+ {scene.status === 'completed' && (
+
+
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file