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:
parent
14c8679ac6
commit
aa4fbdd158
@ -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} />
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user