feat: show processing state for image operations in canvas

- Add toolbar above selected image for quick access to editing tools
- Create new node with 'generating' status before processing
- Update node status to 'completed' or 'failed' after processing
- Allow overflow visible for toolbar to display outside node bounds
This commit is contained in:
Your Name 2026-01-24 21:15:28 +08:00
parent 14c8679ac6
commit aa4fbdd158
2 changed files with 160 additions and 24 deletions

View File

@ -182,6 +182,7 @@ export function CanvasWorkspace() {
"absolute",
isSelected && "ring-2 ring-primary ring-offset-2"
)}
style={{ overflow: 'visible' }} // 允许工具栏显示在节点外部
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
selectNode(node.id, e.shiftKey);
@ -191,6 +192,7 @@ export function CanvasWorkspace() {
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full h-full"
style={{ overflow: 'visible' }} // 允许工具栏显示在节点外部
>
{node.type === 'image' && (
<ImageNode node={node} isSelected={isSelected} />

View File

@ -101,6 +101,19 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
e.target.value = '';
};
// 获取操作的中文名称
const getActionName = (action: string): string => {
const names: Record<string, string> = {
'rmbg': '抠图',
'enhance': '变清晰',
'extend': '图片扩展',
'resize': '尺寸调整',
'angle': '角度编辑',
'scene-fusion': '场景裂变',
};
return names[action] || action;
};
// 处理图片编辑操作 - 集成我们现有的 API
const handleAction = async (action: string) => {
if (!data.src) {
@ -122,7 +135,25 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
return;
}
setIsProcessing(true);
const actionName = getActionName(action);
// 先创建一个"处理中"状态的节点,显示在原图右侧
const newNodeId = addNode({
type: 'image',
position: {
x: node.position.x + node.size.width + 20,
y: node.position.y
},
size: { width: node.size.width, height: node.size.height },
data: {
src: '', // 暂时没有图片
status: 'generating',
prompt: `${actionName}处理中...`,
originalSrc: data.src,
},
connections: [node.id],
});
let result;
try {
@ -137,37 +168,52 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
result = await imageGenerationService.extendImage(data.src);
break;
default:
setIsProcessing(false);
// 删除创建的节点
if (newNodeId) deleteNode(newNodeId);
return;
}
if (result?.success && result.data?.url) {
// 创建新的图片节点显示处理结果
addNode({
type: 'image',
position: {
x: node.position.x + node.size.width + 20,
y: node.position.y
},
size: { width: node.size.width, height: node.size.height },
data: {
src: result.data.url,
status: 'completed',
prompt: `${action} 处理结果`,
originalSrc: data.src,
},
connections: [node.id],
});
toast.success(`${action} 处理完成`);
// 更新节点为完成状态
if (newNodeId) {
updateNode(newNodeId, {
data: {
src: result.data.url,
status: 'completed',
prompt: `${actionName}结果`,
originalSrc: data.src,
},
});
}
toast.success(`${actionName}处理完成`);
} else {
// 更新节点为失败状态
if (newNodeId) {
updateNode(newNodeId, {
data: {
src: '',
status: 'failed',
prompt: `${actionName}失败`,
originalSrc: data.src,
},
});
}
toast.error(result?.error || "处理失败");
}
} catch (error) {
console.error(`${action} 处理失败:`, error);
// 更新节点为失败状态
if (newNodeId) {
updateNode(newNodeId, {
data: {
src: '',
status: 'failed',
prompt: `${actionName}失败`,
originalSrc: data.src,
},
});
}
toast.error("处理失败,请重试");
} finally {
setIsProcessing(false);
}
};
@ -262,8 +308,96 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
</div>
)}
{/* 悬浮操作栏 */}
{isHovered && data.src && !isProcessing && (
{/* 选中时显示的顶部工具栏 - 和聊天窗口中的功能一致 */}
{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"
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"
onClick={() => handleAction('enhance')}
title="变清晰"
>
<Sparkles 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('extend')}
title="扩展"
>
<Expand 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('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"
onClick={() => handleAction('scene-fusion')}
title="场景"
>
<Layers className="h-3.5 w-3.5" />
<span className="hidden sm:inline">场景</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="下载"
>
<Download className="h-3.5 w-3.5" />
</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="删除"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
)}
{/* 悬浮时的更多操作按钮 (右上角) */}
{isHovered && data.src && !isProcessing && !isSelected && (
<div className="absolute top-2 right-2 flex items-center gap-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>