522 lines
18 KiB
TypeScript
522 lines
18 KiB
TypeScript
/**
|
|
* 工作流管理器组件
|
|
* 提供现代化的工作流管理界面
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
PlusIcon,
|
|
MagnifyingGlassIcon,
|
|
FunnelIcon,
|
|
PlayIcon,
|
|
PencilIcon,
|
|
TrashIcon,
|
|
TagIcon,
|
|
CalendarIcon,
|
|
EyeIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { useComfyUIV2Store, useFilteredWorkflows } from '../../store/comfyuiV2Store';
|
|
import type { WorkflowV2 } from '../../services/comfyuiV2Service';
|
|
import { WorkflowTemplateCreator } from './WorkflowTemplateCreator';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
|
|
interface WorkflowManagerProps {
|
|
className?: string;
|
|
}
|
|
|
|
export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
|
|
className = ''
|
|
}) => {
|
|
const {
|
|
workflows,
|
|
workflowsLoading,
|
|
workflowsError,
|
|
selectedWorkflowIds,
|
|
workflowFilters,
|
|
loadWorkflows,
|
|
deleteWorkflow,
|
|
executeWorkflow,
|
|
selectWorkflow,
|
|
deselectWorkflow,
|
|
clearWorkflowSelection,
|
|
setWorkflowFilters,
|
|
setCurrentWorkflow,
|
|
} = useComfyUIV2Store();
|
|
|
|
const filteredWorkflows = useFilteredWorkflows();
|
|
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
|
|
// 加载工作流
|
|
useEffect(() => {
|
|
loadWorkflows();
|
|
}, [loadWorkflows]);
|
|
|
|
const handleSearch = (query: string) => {
|
|
setWorkflowFilters({ searchQuery: query });
|
|
};
|
|
|
|
const handleCategoryFilter = (category: string) => {
|
|
setWorkflowFilters({
|
|
category: category === workflowFilters.category ? undefined : category
|
|
});
|
|
};
|
|
|
|
const handleExecuteWorkflow = async (workflow: WorkflowV2) => {
|
|
try {
|
|
await executeWorkflow({
|
|
workflow_id: workflow.id,
|
|
parameters: {},
|
|
});
|
|
} catch (error) {
|
|
console.error('执行工作流失败:', error);
|
|
}
|
|
};
|
|
|
|
const handleDeleteWorkflow = async (workflow: WorkflowV2) => {
|
|
if (window.confirm(`确定要删除工作流 "${workflow.name}" 吗?`)) {
|
|
try {
|
|
await deleteWorkflow(workflow.id);
|
|
} catch (error) {
|
|
console.error('删除工作流失败:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSelectWorkflow = (workflow: WorkflowV2, selected: boolean) => {
|
|
if (selected) {
|
|
selectWorkflow(workflow.id);
|
|
} else {
|
|
deselectWorkflow(workflow.id);
|
|
}
|
|
};
|
|
|
|
const getUniqueCategories = () => {
|
|
const categories = workflows
|
|
.map(w => w.category)
|
|
.filter((category): category is string => Boolean(category));
|
|
return [...new Set(categories)];
|
|
};
|
|
|
|
const getUniqueTags = () => {
|
|
const tags = workflows.flatMap(w => w.tags);
|
|
return [...new Set(tags)];
|
|
};
|
|
|
|
if (workflowsError) {
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-sm border border-gray-200 p-6 ${className}`}>
|
|
<div className="text-center">
|
|
<div className="text-red-600 mb-2">加载工作流失败</div>
|
|
<div className="text-sm text-gray-500">{workflowsError}</div>
|
|
<button
|
|
onClick={loadWorkflows}
|
|
className="mt-4 px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700"
|
|
>
|
|
重试
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-sm border border-gray-200 ${className}`}>
|
|
{/* 头部 */}
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900">工作流管理</h3>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
共 {filteredWorkflows.length} 个工作流
|
|
{selectedWorkflowIds.length > 0 && (
|
|
<span className="ml-2">
|
|
(已选择 {selectedWorkflowIds.length} 个)
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
className={`p-2 rounded-lg border ${
|
|
showFilters
|
|
? 'bg-blue-50 border-blue-200 text-blue-600'
|
|
: 'border-gray-300 text-gray-600 hover:bg-gray-50'
|
|
}`}
|
|
title="筛选"
|
|
>
|
|
<FunnelIcon className="w-5 h-5" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
|
|
className="p-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
title={viewMode === 'grid' ? '列表视图' : '网格视图'}
|
|
>
|
|
{viewMode === 'grid' ? (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 flex items-center space-x-2"
|
|
>
|
|
<PlusIcon className="w-4 h-4" />
|
|
<span>新建工作流</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 搜索和筛选 */}
|
|
<div className="px-6 py-4 border-b border-gray-200 space-y-4">
|
|
{/* 搜索框 */}
|
|
<div className="relative">
|
|
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="搜索工作流..."
|
|
value={workflowFilters.searchQuery || ''}
|
|
onChange={(e) => handleSearch(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* 筛选器 */}
|
|
{showFilters && (
|
|
<div className="space-y-3">
|
|
{/* 分类筛选 */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">分类</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
onClick={() => setWorkflowFilters({ category: undefined })}
|
|
className={`px-3 py-1 text-sm rounded-full border ${
|
|
!workflowFilters.category
|
|
? 'bg-blue-100 border-blue-300 text-blue-700'
|
|
: 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
全部
|
|
</button>
|
|
{getUniqueCategories().map(category => (
|
|
<button
|
|
key={category}
|
|
onClick={() => handleCategoryFilter(category)}
|
|
className={`px-3 py-1 text-sm rounded-full border ${
|
|
workflowFilters.category === category
|
|
? 'bg-blue-100 border-blue-300 text-blue-700'
|
|
: 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
{category}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 标签筛选 */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">标签</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{getUniqueTags().slice(0, 10).map(tag => (
|
|
<button
|
|
key={tag}
|
|
onClick={() => {
|
|
const currentTags = workflowFilters.tags || [];
|
|
const newTags = currentTags.includes(tag)
|
|
? currentTags.filter(t => t !== tag)
|
|
: [...currentTags, tag];
|
|
setWorkflowFilters({ tags: newTags.length > 0 ? newTags : undefined });
|
|
}}
|
|
className={`px-3 py-1 text-sm rounded-full border flex items-center space-x-1 ${
|
|
workflowFilters.tags?.includes(tag)
|
|
? 'bg-green-100 border-green-300 text-green-700'
|
|
: 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
<TagIcon className="w-3 h-3" />
|
|
<span>{tag}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 批量操作 */}
|
|
{selectedWorkflowIds.length > 0 && (
|
|
<div className="px-6 py-3 bg-blue-50 border-b border-blue-200">
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm text-blue-700">
|
|
已选择 {selectedWorkflowIds.length} 个工作流
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={clearWorkflowSelection}
|
|
className="px-3 py-1 text-sm text-blue-700 hover:text-blue-800"
|
|
>
|
|
取消选择
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (window.confirm(`确定要删除选中的 ${selectedWorkflowIds.length} 个工作流吗?`)) {
|
|
selectedWorkflowIds.forEach(id => {
|
|
const workflow = workflows.find(w => w.id === id);
|
|
if (workflow) deleteWorkflow(id);
|
|
});
|
|
}
|
|
}}
|
|
className="px-3 py-1 text-sm text-white bg-red-600 rounded hover:bg-red-700"
|
|
>
|
|
批量删除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 工作流列表 */}
|
|
<div className="p-6">
|
|
{workflowsLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<span className="ml-3 text-gray-600">加载中...</span>
|
|
</div>
|
|
) : filteredWorkflows.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-gray-500 mb-4">
|
|
{workflows.length === 0 ? '暂无工作流' : '没有符合条件的工作流'}
|
|
</div>
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700"
|
|
>
|
|
创建第一个工作流
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className={
|
|
viewMode === 'grid'
|
|
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'
|
|
: 'space-y-4'
|
|
}>
|
|
{filteredWorkflows.map(workflow => (
|
|
<WorkflowCard
|
|
key={workflow.id}
|
|
workflow={workflow}
|
|
viewMode={viewMode}
|
|
selected={selectedWorkflowIds.includes(workflow.id)}
|
|
onSelect={(selected) => handleSelectWorkflow(workflow, selected)}
|
|
onExecute={() => handleExecuteWorkflow(workflow)}
|
|
onEdit={() => setCurrentWorkflow(workflow)}
|
|
onDelete={() => handleDeleteWorkflow(workflow)}
|
|
onView={() => setCurrentWorkflow(workflow)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 工作流模板创建模态框 */}
|
|
<WorkflowTemplateCreator
|
|
isOpen={showCreateModal}
|
|
onClose={() => setShowCreateModal(false)}
|
|
onSave={async (templateData) => {
|
|
try {
|
|
// 调用ComfyUI V2模板创建接口
|
|
await invoke('comfyui_v2_create_template', { request: templateData });
|
|
setShowCreateModal(false);
|
|
// 重新加载工作流列表
|
|
await loadWorkflows();
|
|
} catch (error) {
|
|
console.error('创建工作流模板失败:', error);
|
|
// 这里可以添加错误提示
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 工作流卡片组件
|
|
interface WorkflowCardProps {
|
|
workflow: WorkflowV2;
|
|
viewMode: 'grid' | 'list';
|
|
selected: boolean;
|
|
onSelect: (selected: boolean) => void;
|
|
onExecute: () => void;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onView: () => void;
|
|
}
|
|
|
|
const WorkflowCard: React.FC<WorkflowCardProps> = ({
|
|
workflow,
|
|
viewMode,
|
|
selected,
|
|
onSelect,
|
|
onExecute,
|
|
onEdit,
|
|
onDelete,
|
|
onView,
|
|
}) => {
|
|
if (viewMode === 'list') {
|
|
return (
|
|
<div className={`flex items-center p-4 border rounded-lg hover:bg-gray-50 ${
|
|
selected ? 'border-blue-300 bg-blue-50' : 'border-gray-200'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected}
|
|
onChange={(e) => onSelect(e.target.checked)}
|
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
/>
|
|
|
|
<div className="flex-1 ml-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-gray-900">{workflow.name}</h4>
|
|
<p className="text-sm text-gray-500 mt-1">{workflow.description}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
{workflow.category && (
|
|
<span className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-700 rounded">
|
|
{workflow.category}
|
|
</span>
|
|
)}
|
|
|
|
<div className="flex items-center space-x-1">
|
|
<button
|
|
onClick={onView}
|
|
className="p-1 text-gray-400 hover:text-gray-600"
|
|
title="查看"
|
|
>
|
|
<EyeIcon className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onExecute}
|
|
className="p-1 text-green-600 hover:text-green-700"
|
|
title="执行"
|
|
>
|
|
<PlayIcon className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onEdit}
|
|
className="p-1 text-blue-600 hover:text-blue-700"
|
|
title="编辑"
|
|
>
|
|
<PencilIcon className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onDelete}
|
|
className="p-1 text-red-600 hover:text-red-700"
|
|
title="删除"
|
|
>
|
|
<TrashIcon className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`border rounded-lg p-4 hover:shadow-md transition-shadow ${
|
|
selected ? 'border-blue-300 bg-blue-50' : 'border-gray-200'
|
|
}`}>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected}
|
|
onChange={(e) => onSelect(e.target.checked)}
|
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-1"
|
|
/>
|
|
|
|
<div className="flex items-center space-x-1">
|
|
<button
|
|
onClick={onView}
|
|
className="p-1 text-gray-400 hover:text-gray-600"
|
|
title="查看"
|
|
>
|
|
<EyeIcon className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onExecute}
|
|
className="p-1 text-green-600 hover:text-green-700"
|
|
title="执行"
|
|
>
|
|
<PlayIcon className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onEdit}
|
|
className="p-1 text-blue-600 hover:text-blue-700"
|
|
title="编辑"
|
|
>
|
|
<PencilIcon className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onDelete}
|
|
className="p-1 text-red-600 hover:text-red-700"
|
|
title="删除"
|
|
>
|
|
<TrashIcon className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<h4 className="text-sm font-medium text-gray-900 mb-1">{workflow.name}</h4>
|
|
<p className="text-sm text-gray-500 line-clamp-2">{workflow.description}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
|
<div className="flex items-center space-x-2">
|
|
{workflow.category && (
|
|
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded">
|
|
{workflow.category}
|
|
</span>
|
|
)}
|
|
|
|
<div className="flex items-center space-x-1">
|
|
<CalendarIcon className="w-3 h-3" />
|
|
<span>{new Date(workflow.updated_at).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<div>v{workflow.version}</div>
|
|
{workflow.tags.length > 0 && (
|
|
<div className="flex items-center space-x-1 mt-1">
|
|
<TagIcon className="w-3 h-3" />
|
|
<span>{workflow.tags.slice(0, 2).join(', ')}</span>
|
|
{workflow.tags.length > 2 && <span>...</span>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|