"use client"; import React, { useState } from "react"; import { AlertCircle, Box, Copy, Download, ExternalLink, Loader2, MoreVertical, Trash2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { toast } from "sonner"; import { useCanvasStore } from "@/store/canvas"; import type { CanvasNode, Model3DNodeData } from "@/types/canvas"; import { cn } from "@/lib/utils"; interface Model3DNodeProps { node: CanvasNode; isSelected: boolean; } function inferModelFileName(data: Model3DNodeData, nodeId: string) { const raw = String(data.src || "").trim(); if (!raw) return `model3d-${nodeId.slice(0, 6)}.glb`; try { const url = new URL(raw); const filename = url.pathname.split("/").filter(Boolean).pop(); if (filename) return decodeURIComponent(filename); } catch {} return raw.split("/").pop() || `model3d-${nodeId.slice(0, 6)}.glb`; } function inferModelFormat(data: Model3DNodeData) { if (data.format) return data.format.toUpperCase(); const src = String(data.src || "").toLowerCase(); if (src.endsWith(".gltf")) return "GLTF"; if (src.endsWith(".glb")) return "GLB"; if (src.endsWith(".usdz")) return "USDZ"; if (src.endsWith(".fbx")) return "FBX"; if (src.endsWith(".obj")) return "OBJ"; if (src.endsWith(".stl")) return "STL"; if (src.endsWith(".3mf")) return "3MF"; return "3D"; } export function Model3DNode({ node, isSelected }: Model3DNodeProps) { const data = node.data as Model3DNodeData; const [isHovered, setIsHovered] = useState(false); const deleteNode = useCanvasStore((state) => state.deleteNode); const providerLabel = data.metadata?.provider === "worldlabs-3d" ? "World Labs 结果" : "Tripo3D 结果"; const handleOpen = () => { if (!data.src) { toast.error("暂无可打开的 3D 地址"); return; } window.open(data.src, "_blank", "noopener,noreferrer"); }; const handleDownload = () => { if (!data.src) { toast.error("暂无可下载的 3D 文件"); return; } const link = document.createElement("a"); link.href = data.src; link.download = inferModelFileName(data, node.id); link.click(); }; const handleCopy = async () => { if (!data.src) { toast.error("暂无可复制的地址"); return; } try { await navigator.clipboard.writeText(data.src); toast.success("已复制 3D 链接"); } catch { toast.error("复制失败"); } }; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {data.status === "generating" ? (
3D 生成中...
) : data.status === "failed" ? (
3D 生成失败
) : (
{data.previewImage ? ( {data.prompt ) : (
)}
{inferModelFormat(data)}

{data.prompt || inferModelFileName(data, node.id)}

{data.modelVersion ? `模型: ${data.modelVersion}` : providerLabel}

)} {isHovered && data.status === "completed" && (
打开 3D 下载 复制链接 deleteNode(node.id)}> 删除
)}
); }