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",
|
"absolute",
|
||||||
isSelected && "ring-2 ring-primary ring-offset-2"
|
isSelected && "ring-2 ring-primary ring-offset-2"
|
||||||
)}
|
)}
|
||||||
|
style={{ overflow: 'visible' }} // 允许工具栏显示在节点外部
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
selectNode(node.id, e.shiftKey);
|
selectNode(node.id, e.shiftKey);
|
||||||
@ -191,6 +192,7 @@ export function CanvasWorkspace() {
|
|||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
|
style={{ overflow: 'visible' }} // 允许工具栏显示在节点外部
|
||||||
>
|
>
|
||||||
{node.type === 'image' && (
|
{node.type === 'image' && (
|
||||||
<ImageNode node={node} isSelected={isSelected} />
|
<ImageNode node={node} isSelected={isSelected} />
|
||||||
|
|||||||
@ -101,6 +101,19 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
e.target.value = '';
|
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
|
// 处理图片编辑操作 - 集成我们现有的 API
|
||||||
const handleAction = async (action: string) => {
|
const handleAction = async (action: string) => {
|
||||||
if (!data.src) {
|
if (!data.src) {
|
||||||
@ -122,7 +135,25 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
return;
|
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;
|
let result;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -137,37 +168,52 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
result = await imageGenerationService.extendImage(data.src);
|
result = await imageGenerationService.extendImage(data.src);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
setIsProcessing(false);
|
// 删除创建的节点
|
||||||
|
if (newNodeId) deleteNode(newNodeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result?.success && result.data?.url) {
|
if (result?.success && result.data?.url) {
|
||||||
// 创建新的图片节点显示处理结果
|
// 更新节点为完成状态
|
||||||
addNode({
|
if (newNodeId) {
|
||||||
type: 'image',
|
updateNode(newNodeId, {
|
||||||
position: {
|
data: {
|
||||||
x: node.position.x + node.size.width + 20,
|
src: result.data.url,
|
||||||
y: node.position.y
|
status: 'completed',
|
||||||
},
|
prompt: `${actionName}结果`,
|
||||||
size: { width: node.size.width, height: node.size.height },
|
originalSrc: data.src,
|
||||||
data: {
|
},
|
||||||
src: result.data.url,
|
});
|
||||||
status: 'completed',
|
}
|
||||||
prompt: `${action} 处理结果`,
|
toast.success(`${actionName}处理完成`);
|
||||||
originalSrc: data.src,
|
|
||||||
},
|
|
||||||
connections: [node.id],
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(`${action} 处理完成`);
|
|
||||||
} else {
|
} else {
|
||||||
|
// 更新节点为失败状态
|
||||||
|
if (newNodeId) {
|
||||||
|
updateNode(newNodeId, {
|
||||||
|
data: {
|
||||||
|
src: '',
|
||||||
|
status: 'failed',
|
||||||
|
prompt: `${actionName}失败`,
|
||||||
|
originalSrc: data.src,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
toast.error(result?.error || "处理失败");
|
toast.error(result?.error || "处理失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`${action} 处理失败:`, error);
|
console.error(`${action} 处理失败:`, error);
|
||||||
|
// 更新节点为失败状态
|
||||||
|
if (newNodeId) {
|
||||||
|
updateNode(newNodeId, {
|
||||||
|
data: {
|
||||||
|
src: '',
|
||||||
|
status: 'failed',
|
||||||
|
prompt: `${actionName}失败`,
|
||||||
|
originalSrc: data.src,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
toast.error("处理失败,请重试");
|
toast.error("处理失败,请重试");
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -262,8 +308,96 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
</div>
|
</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">
|
<div className="absolute top-2 right-2 flex items-center gap-1">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user