fix: 修复ComfyUI V2工作流TAB新建工作流功能
- 在WorkflowManager组件中添加了WorkflowV2Creator模态框的渲染 - 创建了新的WorkflowV2Creator组件,专门用于ComfyUI V2工作流创建 - 添加了createWorkflow方法到useComfyUIV2Store的解构中 - 实现了完整的工作流创建流程,包括基本信息、工作流配置和高级设置 - 支持JSON文件导入和手动编辑工作流数据 - 添加了表单验证和错误处理 - 修复了点击新建工作流按钮没有反应的问题
This commit is contained in:
parent
59763b1bf4
commit
ada3eb94ed
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@heroicons/react/24/outline';
|
||||
import { useComfyUIV2Store, useFilteredWorkflows } from '../../store/comfyuiV2Store';
|
||||
import type { WorkflowV2 } from '../../services/comfyuiV2Service';
|
||||
import { WorkflowV2Creator } from './WorkflowV2Creator';
|
||||
|
||||
interface WorkflowManagerProps {
|
||||
className?: string;
|
||||
|
|
@ -33,6 +34,7 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|||
selectedWorkflowIds,
|
||||
workflowFilters,
|
||||
loadWorkflows,
|
||||
createWorkflow,
|
||||
deleteWorkflow,
|
||||
executeWorkflow,
|
||||
selectWorkflow,
|
||||
|
|
@ -329,6 +331,23 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 工作流创建模态框 */}
|
||||
<WorkflowV2Creator
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onSave={async (workflowData) => {
|
||||
try {
|
||||
await createWorkflow(workflowData);
|
||||
setShowCreateModal(false);
|
||||
// 重新加载工作流列表
|
||||
await loadWorkflows();
|
||||
} catch (error) {
|
||||
console.error('创建工作流失败:', error);
|
||||
// 这里可以添加错误提示
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,380 @@
|
|||
/**
|
||||
* ComfyUI V2 工作流创建器组件
|
||||
* 专门用于创建ComfyUI V2工作流的模态框
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
X,
|
||||
Save,
|
||||
Upload,
|
||||
FileText,
|
||||
Settings,
|
||||
Info,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
} from 'lucide-react';
|
||||
import type { CreateWorkflowRequest } from '../../services/comfyuiV2Service';
|
||||
|
||||
interface WorkflowV2CreatorProps {
|
||||
/** 是否显示模态框 */
|
||||
isOpen: boolean;
|
||||
/** 关闭回调 */
|
||||
onClose: () => void;
|
||||
/** 保存回调 */
|
||||
onSave: (workflowData: CreateWorkflowRequest) => Promise<void>;
|
||||
/** 编辑的工作流(为空时表示创建新工作流) */
|
||||
editingWorkflow?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* ComfyUI V2 工作流创建器组件
|
||||
*/
|
||||
export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
editingWorkflow
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'basic' | 'workflow' | 'advanced'>('basic');
|
||||
const [formData, setFormData] = useState<CreateWorkflowRequest>({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
workflow_data: {},
|
||||
tags: [],
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [workflowJsonText, setWorkflowJsonText] = useState('{}');
|
||||
|
||||
// 重置表单数据
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (editingWorkflow) {
|
||||
setFormData({
|
||||
name: editingWorkflow.name || '',
|
||||
description: editingWorkflow.description || '',
|
||||
category: editingWorkflow.category || '',
|
||||
workflow_data: editingWorkflow.workflow_data || {},
|
||||
tags: editingWorkflow.tags || [],
|
||||
});
|
||||
setWorkflowJsonText(JSON.stringify(editingWorkflow.workflow_data || {}, null, 2));
|
||||
} else {
|
||||
// 重置为默认值
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
workflow_data: {},
|
||||
tags: [],
|
||||
});
|
||||
setWorkflowJsonText('{}');
|
||||
}
|
||||
setErrors({});
|
||||
setActiveTab('basic');
|
||||
}
|
||||
}, [editingWorkflow, isOpen]);
|
||||
|
||||
// 更新表单字段
|
||||
const updateField = (field: keyof CreateWorkflowRequest, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 清除该字段的错误
|
||||
if (errors[field]) {
|
||||
setErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[field];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 验证表单
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = '工作流名称不能为空';
|
||||
}
|
||||
|
||||
if (!workflowJsonText.trim() || workflowJsonText.trim() === '{}') {
|
||||
newErrors.workflow_data = '工作流数据不能为空';
|
||||
} else {
|
||||
try {
|
||||
const parsed = JSON.parse(workflowJsonText);
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
newErrors.workflow_data = '工作流数据必须是有效的JSON对象';
|
||||
}
|
||||
} catch (error) {
|
||||
newErrors.workflow_data = 'JSON格式错误,请检查语法';
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
// 处理保存
|
||||
const handleSave = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const workflowData = JSON.parse(workflowJsonText);
|
||||
const requestData: CreateWorkflowRequest = {
|
||||
...formData,
|
||||
workflow_data: workflowData,
|
||||
};
|
||||
|
||||
await onSave(requestData);
|
||||
} catch (error) {
|
||||
console.error('保存工作流失败:', error);
|
||||
setErrors({ general: `保存失败: ${error}` });
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 导入ComfyUI工作流
|
||||
const handleImportWorkflow = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const workflow = JSON.parse(e.target?.result as string);
|
||||
setWorkflowJsonText(JSON.stringify(workflow, null, 2));
|
||||
updateField('workflow_data', workflow);
|
||||
} catch (error) {
|
||||
console.error('解析ComfyUI工作流失败:', error);
|
||||
setErrors({ workflow_data: '文件格式错误,请选择有效的JSON文件' });
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
// 处理标签输入
|
||||
const handleTagsChange = (tagsString: string) => {
|
||||
const tags = tagsString
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag.length > 0);
|
||||
updateField('tags', tags);
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[95vh] flex flex-col">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Settings className="w-6 h-6 text-blue-600" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{editingWorkflow ? '编辑工作流' : '创建工作流'}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{errors.general && (
|
||||
<div className="mx-6 mt-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center space-x-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
<span className="text-red-700">{errors.general}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签页导航 */}
|
||||
<div className="flex border-b border-gray-200 px-6">
|
||||
{[
|
||||
{ id: 'basic', label: '基本信息', icon: FileText },
|
||||
{ id: 'workflow', label: '工作流配置', icon: Settings },
|
||||
{ id: 'advanced', label: '高级设置', icon: Info },
|
||||
].map(tab => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
className={`flex items-center space-x-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{/* 基本信息标签页 */}
|
||||
{activeTab === 'basic' && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
工作流名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||
errors.name ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="例如:图像生成工作流"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分类
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.category || ''}
|
||||
onChange={(e) => updateField('category', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="例如:图像处理"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
描述
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description || ''}
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="描述这个工作流的功能和用途..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
标签
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.tags?.join(', ') || ''}
|
||||
onChange={(e) => handleTagsChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="用逗号分隔,例如:AI, 图像, 生成"
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">用逗号分隔多个标签</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 工作流配置标签页 */}
|
||||
{activeTab === 'workflow' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-900">ComfyUI工作流配置</h3>
|
||||
<button
|
||||
onClick={handleImportWorkflow}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors flex items-center space-x-1"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span>导入JSON</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
工作流JSON <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={workflowJsonText}
|
||||
onChange={(e) => setWorkflowJsonText(e.target.value)}
|
||||
rows={20}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm ${
|
||||
errors.workflow_data ? 'border-red-300' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="粘贴ComfyUI导出的工作流JSON..."
|
||||
/>
|
||||
{errors.workflow_data && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.workflow_data}</p>
|
||||
)}
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
从ComfyUI界面导出工作流JSON并粘贴到这里
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 高级设置标签页 */}
|
||||
{activeTab === 'advanced' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Info className="w-5 h-5 text-blue-600" />
|
||||
<h3 className="text-sm font-medium text-blue-900">高级设置</h3>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-blue-700">
|
||||
高级设置功能正在开发中,敬请期待...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部操作栏 */}
|
||||
<div className="flex items-center justify-between p-6 border-t border-gray-200">
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||
<Info className="w-4 h-4" />
|
||||
<span>保存后将自动验证工作流配置</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2 disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Settings className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)}
|
||||
<span>{isSaving ? '保存中...' : (editingWorkflow ? '更新' : '创建')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue