From dbdd188c9e29957a9dd67e2f9cb19ec454a5948a Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Feb 2026 15:29:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=20Portal=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=B7=A5=E5=85=B7=E6=A0=8F=E9=81=BF=E5=85=8D=E8=A2=AB?= =?UTF-8?q?=E8=A3=81=E5=89=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 工具栏使用 createPortal 渲染到 body 级别 2. 根据图片位置动态计算工具栏定位 3. z-index 设为 9999 确保最顶层显示 Co-authored-by: Cursor --- components/canvas/nodes/ImageNode.tsx | 248 ++++++++++++++------------ 1 file changed, 135 insertions(+), 113 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index b59eef7e6..71da15e41 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; +import { createPortal } from "react-dom"; import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -42,13 +43,28 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const [resizeWidth, setResizeWidth] = useState(1024); const [resizeHeight, setResizeHeight] = useState(1024); const [isUploading, setIsUploading] = useState(false); + const [toolbarPosition, setToolbarPosition] = useState<{ top: number; left: number } | null>(null); // 文件输入的 ref const fileInputRef = useRef(null); + const containerRef = useRef(null); const updateNode = useCanvasStore((state) => state.updateNode); const deleteNode = useCanvasStore((state) => state.deleteNode); const addNode = useCanvasStore((state) => state.addNode); + + // 计算工具栏位置 + useEffect(() => { + if (isSelected && containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setToolbarPosition({ + top: rect.top - 50, // 工具栏在图片上方 50px + left: rect.left + rect.width / 2, // 居中 + }); + } else { + setToolbarPosition(null); + } + }, [isSelected, node.position, node.size]); // 触发文件选择 const handleSelectFile = (e: React.MouseEvent) => { @@ -324,7 +340,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { <> {/* 选中时在图片上方显示标题和尺寸 */} {isSelected && data.src && ( -
+
{data.prompt || '图片'} @@ -334,6 +350,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { )}
)} - {/* 选中时显示的顶部工具栏 - 显示在图片外部上方 */} - {isSelected && data.src && !isProcessing && ( -
e.stopPropagation()} - > - {/* 放大/高清 */} - - - {/* 移除背景/抠图 */} - - - {/* 场景融合 */} - - - {/* 角度编辑 */} - - - {/* 扩展 */} - - - {/* 尺寸调整 */} - - - {/* 更多操作 */} - - - - - - - - 下载图片 - - - deleteNode(node.id)} - className="text-destructive focus:text-destructive" - > - - 删除 - - - -
- )} + {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} {/* 悬浮时的更多操作按钮 (右上角) */} {isHovered && data.src && !isProcessing && !isSelected && ( @@ -627,6 +534,121 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { + + {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} + {isSelected && data.src && !isProcessing && toolbarPosition && typeof document !== 'undefined' && createPortal( +
e.stopPropagation()} + > + {/* 放大/高清 */} + + + {/* 移除背景/抠图 */} + + + {/* 场景融合 */} + + + {/* 角度编辑 */} + + + {/* 扩展 */} + + + {/* 尺寸调整 */} + + + {/* 更多操作 */} + + + + + + + + 下载图片 + + + deleteNode(node.id)} + className="text-destructive focus:text-destructive" + > + + 删除 + + + +
, + document.body + )} ); }