"use client"; import { useState, useCallback, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; import { Loader2, Download, Check, RotateCcw, Move3D, Sparkles, X } from "lucide-react"; import { toast } from "sonner"; import { QwenCameraWidget, type CameraState } from "./qwen-camera-widget"; import { cn } from "@/lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface AngleEditDialogProps { open: boolean; onOpenChange: (open: boolean) => void; imageUrl: string; onComplete?: (resultUrl: string) => void; } export function AngleEditDialog({ open, onOpenChange, imageUrl, onComplete, }: AngleEditDialogProps) { const [isProcessing, setIsProcessing] = useState(false); const [resultUrl, setResultUrl] = useState(null); const [loraScale, setLoraScale] = useState(1.0); // 官方默认值是 1 const [cameraState, setCameraState] = useState({ azimuth: 45, elevation: 0, distance: 5, }); // 预览 Tab 状态:如果生成了结果,默认显示结果;否则显示原图 const [activeTab, setActiveTab] = useState<"original" | "result">("original"); const stopDialogEventPropagation = useCallback((event: React.SyntheticEvent) => { event.stopPropagation(); }, []); const stopDialogEventCapture = useCallback((event: React.SyntheticEvent) => { event.stopPropagation(); }, []); const stopDialogWheelPropagation = useCallback((event: React.WheelEvent) => { event.stopPropagation(); }, []); const stopDialogWheelCapture = useCallback((event: React.WheelEvent) => { event.preventDefault(); event.stopPropagation(); }, []); // 当对话框关闭时重置状态 useEffect(() => { if (!open) { setResultUrl(null); setIsProcessing(false); setActiveTab("original"); } }, [open]); // 当生成结果时,自动切换到结果 Tab useEffect(() => { if (resultUrl) { setActiveTab("result"); } }, [resultUrl]); // 处理对话框关闭(防止意外触发导航) const handleOpenChange = useCallback((newOpen: boolean) => { // 只有在明确关闭时才调用 onOpenChange // 避免在处理过程中意外关闭 if (!newOpen && !isProcessing) { onOpenChange(newOpen); } }, [isProcessing, onOpenChange]); // 处理相机状态变化 const handleCameraStateChange = useCallback((state: CameraState) => { setCameraState(state); // 角度变化时清除之前的结果 if (!isProcessing) { setResultUrl(null); } }, [isProcessing]); // 生成新视角 const handleGenerate = useCallback(async (e?: React.MouseEvent) => { if (e) { e.stopPropagation(); // 阻止事件冒泡,避免触发外层的 onCancel } setIsProcessing(true); setResultUrl(null); setActiveTab("result"); // 切换到结果 Tab 显示加载状态 try { // 使用官方 API 的数值参数格式 const response = await fetch("/api/angle-edit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image_url: imageUrl, horizontal_angle: cameraState.azimuth, // 0-360 度 vertical_angle: cameraState.elevation, // -30 到 90 度 zoom: cameraState.distance, // 0-10 lora_scale: loraScale, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "生成失败"); } const data = await response.json(); setResultUrl(data.image_url); toast.success("视角生成成功,已保存到相册!"); } catch (error) { console.error("角度编辑失败:", error); toast.error(error instanceof Error ? error.message : "生成失败,请重试"); setActiveTab("original"); // 失败后切回原图 } finally { setIsProcessing(false); } }, [imageUrl, cameraState, loraScale]); // 下载结果图片 const handleDownload = useCallback(async () => { if (!resultUrl) return; try { const response = await fetch(resultUrl); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `angle-edit-${cameraState.azimuth}-${cameraState.elevation}-${Date.now()}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success("图片已下载"); } catch { toast.error("下载失败"); } }, [resultUrl, cameraState]); // 使用此图片(直接发送到聊天) const handleUseImage = useCallback((e?: React.MouseEvent) => { if (e) { e.stopPropagation(); // 阻止事件冒泡,避免触发外层的 onCancel e.preventDefault(); // 阻止默认行为 } if (resultUrl) { // 如果提供了 onComplete 回调,调用它 if (onComplete) { onComplete(resultUrl); } // 关闭对话框,图片已经通过 API 保存到数据库 // 使用 setTimeout 确保回调先执行,避免事件冒泡 setTimeout(() => { onOpenChange(false); }, 100); toast.success("图片已添加到对话"); } }, [resultUrl, onOpenChange, onComplete]); // 重置 const handleReset = useCallback(() => { setResultUrl(null); setCameraState({ azimuth: 45, elevation: 0, distance: 5, }); setLoraScale(0.9); setActiveTab("original"); }, []); return ( { // 阻止点击遮罩层关闭对话框(防止意外触发导航) if (isProcessing) { e.preventDefault(); } }} onEscapeKeyDown={(e) => { // 处理过程中阻止 ESC 关闭 if (isProcessing) { e.preventDefault(); } }} > {/* 紧凑标题栏 */}
3D 视角编辑器
{/* 左侧:3D 控制区 (固定宽度 340px) */}
{/* 上部内容区域 - 可伸缩 */}
{/* QwenCameraWidget - 包含 Canvas 和 3个角度选择器 */} {/* 变换强度 - 调整样式使其更紧凑 */}
setLoraScale(v)} min={0.5} max={1.0} step={0.05} className="flex-1 py-1" /> {(loraScale * 100).toFixed(0)}%
{/* 底部按钮区域 - 包含重置和生成 */}
{/* 右侧:预览区 (自适应) */}
setActiveTab(v as any)} className="flex-1 flex flex-col">
原图 生成结果 {resultUrl && } {resultUrl && (
)}
{/* 原图 Tab */}
原图
{/* 结果 Tab */}
{isProcessing ? (

正在渲染新视角...

) : resultUrl ? (
生成结果
) : (

暂无结果

)}
); } export default AngleEditDialog;