feat: 优化画布图片选中工具栏

1. 选中图片时在顶部显示工具栏(放大、移除背景、场景、角度、扩展、调整)
2. 工具栏使用圆角胶囊样式,类似竞品设计
3. 显示图片标题和尺寸信息
4. 添加更多操作下拉菜单(下载、删除)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name 2026-02-01 15:21:29 +08:00
parent 049795e264
commit 8f1f924421
2 changed files with 93 additions and 58 deletions

View File

@ -247,8 +247,8 @@ export function CanvasWorkspace() {
}}
/>
{/* 节点层 - 使用虚拟化渲染 */}
<div className="absolute inset-0">
{/* 节点层 - 使用虚拟化渲染,允许工具栏溢出 */}
<div className="absolute inset-0" style={{ overflow: 'visible' }}>
{visibleNodes.map(renderNode)}
</div>

View File

@ -322,10 +322,21 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
return (
<>
{/* 选中时在图片上方显示标题和尺寸 */}
{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="flex items-center gap-1">
<ImagePlus className="h-3 w-3" />
<span className="truncate max-w-[150px]">{data.prompt || '图片'}</span>
</div>
<span>{node.size.width} × {node.size.height}</span>
</div>
)}
<div
className={cn(
"relative w-full h-full bg-card rounded-lg overflow-hidden border shadow-sm",
isSelected && "border-primary"
"relative w-full h-full bg-card rounded-lg overflow-hidden border-2 shadow-sm transition-colors",
isSelected ? "border-primary" : "border-transparent"
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@ -381,91 +392,115 @@ 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-1 bg-background/95 backdrop-blur-sm border rounded-lg shadow-lg p-1.5 z-10"
className="absolute top-2 left-1/2 -translate-x-1/2 flex items-center gap-0.5 bg-white/95 backdrop-blur-sm border border-gray-200 rounded-full shadow-xl px-2 py-1.5 z-50"
style={{
pointerEvents: 'auto',
minWidth: 'max-content',
}}
onClick={(e) => e.stopPropagation()}
>
{/* 放大/高清 */}
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs gap-1"
onClick={() => handleAction('rmbg')}
title="抠图"
>
<Scissors className="h-3.5 w-3.5" />
<span className="hidden sm:inline">抠图</span>
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs gap-1"
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
onClick={() => handleAction('enhance')}
title="变清晰"
title="高清放大"
>
<Sparkles className="h-3.5 w-3.5" />
<span className="hidden sm:inline">变清晰</span>
<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-8 px-2 text-xs gap-1"
onClick={() => handleAction('extend')}
title="扩展"
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
onClick={() => handleAction('rmbg')}
title="移除背景"
>
<Expand className="h-3.5 w-3.5" />
<span className="hidden sm:inline">扩展</span>
<Scissors className="h-4 w-4" />
<span>移除背景</span>
</Button>
{/* 场景融合 */}
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs gap-1"
onClick={() => handleAction('resize')}
title="调整"
>
<Maximize2 className="h-3.5 w-3.5" />
<span className="hidden sm:inline">调整</span>
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs gap-1"
onClick={() => handleAction('angle')}
title="角度"
>
<Move3D className="h-3.5 w-3.5" />
<span className="hidden sm:inline">角度</span>
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs gap-1"
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
onClick={() => handleAction('scene-fusion')}
title="场景"
title="场景融合"
>
<Layers className="h-3.5 w-3.5" />
<span className="hidden sm:inline">场景</span>
<Layers className="h-4 w-4" />
<span>场景</span>
</Button>
<div className="w-px h-5 bg-border mx-0.5" />
{/* 角度编辑 */}
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs gap-1"
onClick={handleDownload}
title="下载"
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
onClick={() => handleAction('angle')}
title="角度编辑"
>
<Download className="h-3.5 w-3.5" />
<Move3D className="h-4 w-4" />
<span>角度</span>
</Button>
{/* 扩展 */}
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs gap-1 text-destructive hover:text-destructive"
onClick={() => deleteNode(node.id)}
title="删除"
className="h-9 px-3 text-xs gap-1.5 rounded-full hover:bg-gray-100"
onClick={() => handleAction('extend')}
title="图片扩展"
>
<Trash2 className="h-3.5 w-3.5" />
<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>
)}