mixvideo-v2/apps/desktop/src/components/comfyui/WorkflowManager.tsx

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>
);
};