fix: 使用 Portal 渲染工具栏避免被裁剪
1. 工具栏使用 createPortal 渲染到 body 级别 2. 根据图片位置动态计算工具栏定位 3. z-index 设为 9999 确保最顶层显示 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
58b9c1cbd5
commit
dbdd188c9e
@ -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<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(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 && (
|
||||
<div className="absolute -top-7 left-0 right-0 flex items-center justify-between text-xs text-muted-foreground px-1">
|
||||
<div className="absolute -top-7 left-0 right-0 flex items-center justify-between text-xs text-muted-foreground px-1 z-10">
|
||||
<div className="flex items-center gap-1">
|
||||
<ImagePlus className="h-3 w-3" />
|
||||
<span className="truncate max-w-[150px]">{data.prompt || '图片'}</span>
|
||||
@ -334,6 +350,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative w-full h-full bg-card rounded-lg overflow-hidden border-2 shadow-sm transition-colors",
|
||||
isSelected ? "border-primary" : "border-transparent"
|
||||
@ -392,117 +409,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 选中时显示的顶部工具栏 - 显示在图片外部上方 */}
|
||||
{isSelected && data.src && !isProcessing && (
|
||||
<div
|
||||
className="absolute -top-12 left-1/2 -translate-x-1/2 flex items-center gap-0.5 bg-white border border-gray-200 rounded-full shadow-xl px-3 py-1.5 z-[100]"
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
minWidth: 'max-content',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 放大/高清 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('enhance')}
|
||||
title="高清放大"
|
||||
>
|
||||
<span className="text-[10px] font-medium text-gray-500 border border-gray-300 rounded px-1">HD</span>
|
||||
<span>放大</span>
|
||||
</Button>
|
||||
|
||||
{/* 移除背景/抠图 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('rmbg')}
|
||||
title="移除背景"
|
||||
>
|
||||
<Scissors className="h-4 w-4" />
|
||||
<span>移除背景</span>
|
||||
</Button>
|
||||
|
||||
{/* 场景融合 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('scene-fusion')}
|
||||
title="场景融合"
|
||||
>
|
||||
<Layers className="h-4 w-4" />
|
||||
<span>场景</span>
|
||||
</Button>
|
||||
|
||||
{/* 角度编辑 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('angle')}
|
||||
title="角度编辑"
|
||||
>
|
||||
<Move3D className="h-4 w-4" />
|
||||
<span>角度</span>
|
||||
</Button>
|
||||
|
||||
{/* 扩展 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('extend')}
|
||||
title="图片扩展"
|
||||
>
|
||||
<Expand className="h-4 w-4" />
|
||||
<span>扩展</span>
|
||||
</Button>
|
||||
|
||||
{/* 尺寸调整 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('resize')}
|
||||
title="尺寸调整"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
<span>调整</span>
|
||||
</Button>
|
||||
|
||||
{/* 更多操作 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-2 rounded-full hover:bg-gray-100"
|
||||
title="更多"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleDownload}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
下载图片
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => deleteNode(node.id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
|
||||
|
||||
{/* 悬浮时的更多操作按钮 (右上角) */}
|
||||
{isHovered && data.src && !isProcessing && !isSelected && (
|
||||
@ -627,6 +534,121 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
|
||||
{isSelected && data.src && !isProcessing && toolbarPosition && typeof document !== 'undefined' && createPortal(
|
||||
<div
|
||||
className="fixed flex items-center gap-0.5 bg-white border border-gray-200 rounded-full shadow-xl px-3 py-1.5 z-[9999]"
|
||||
style={{
|
||||
top: toolbarPosition.top,
|
||||
left: toolbarPosition.left,
|
||||
transform: 'translateX(-50%)',
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 放大/高清 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('enhance')}
|
||||
title="高清放大"
|
||||
>
|
||||
<span className="text-[10px] font-medium text-gray-500 border border-gray-300 rounded px-1">HD</span>
|
||||
<span>放大</span>
|
||||
</Button>
|
||||
|
||||
{/* 移除背景/抠图 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('rmbg')}
|
||||
title="移除背景"
|
||||
>
|
||||
<Scissors className="h-4 w-4" />
|
||||
<span>移除背景</span>
|
||||
</Button>
|
||||
|
||||
{/* 场景融合 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('scene-fusion')}
|
||||
title="场景融合"
|
||||
>
|
||||
<Layers className="h-4 w-4" />
|
||||
<span>场景</span>
|
||||
</Button>
|
||||
|
||||
{/* 角度编辑 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('angle')}
|
||||
title="角度编辑"
|
||||
>
|
||||
<Move3D className="h-4 w-4" />
|
||||
<span>角度</span>
|
||||
</Button>
|
||||
|
||||
{/* 扩展 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('extend')}
|
||||
title="图片扩展"
|
||||
>
|
||||
<Expand className="h-4 w-4" />
|
||||
<span>扩展</span>
|
||||
</Button>
|
||||
|
||||
{/* 尺寸调整 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
|
||||
onClick={() => handleAction('resize')}
|
||||
title="尺寸调整"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
<span>调整</span>
|
||||
</Button>
|
||||
|
||||
{/* 更多操作 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 px-2 rounded-full hover:bg-gray-100"
|
||||
title="更多"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleDownload}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
下载图片
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => deleteNode(node.id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user