Files
mixvideo-v2/apps/desktop/src/components/workflow/BatchOperationManager.tsx
imeepos c1351689cf feat: implement comprehensive workflow management system
- Add workflow creation, editing, and preview components
- Implement execution monitoring and history tracking
- Add batch operations and retry management
- Create environment configuration system
- Add data export and error analysis features
- Update navigation and main app structure
- Enhance Tauri backend integration
- Add comprehensive workflow type definitions
2025-08-07 13:50:35 +08:00

630 lines
20 KiB
TypeScript

/**
* 批量操作管理器组件
*
* 提供批量执行、删除、导出等操作功能
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
Play,
Trash2,
Download,
Upload,
CheckSquare,
Square,
RefreshCw,
AlertTriangle,
CheckCircle,
XCircle,
Clock,
BarChart3,
Settings,
Filter,
Search,
FileText,
Archive
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import type {
WorkflowTemplate,
WorkflowExecutionRecord,
ExecutionStatus
} from '../../types/workflow';
// 批量操作类型枚举
type BatchOperationType = 'execute' | 'delete' | 'export' | 'archive' | 'activate' | 'deactivate';
// 批量操作状态枚举
type BatchOperationStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
// 批量操作任务接口
interface BatchOperationTask {
id: string;
type: BatchOperationType;
target_type: 'templates' | 'executions';
target_ids: number[];
status: BatchOperationStatus;
progress: number;
total_items: number;
completed_items: number;
failed_items: number;
started_at: string;
completed_at?: string;
error_message?: string;
result_data?: any;
}
// 批量执行配置接口
interface BatchExecutionConfig {
environment_id?: number;
parallel_limit: number;
retry_failed: boolean;
stop_on_error: boolean;
execution_delay_seconds: number;
}
// 组件属性接口
interface BatchOperationManagerProps {
/** 选中的工作流模板 */
selectedTemplates?: WorkflowTemplate[];
/** 选中的执行记录 */
selectedExecutions?: WorkflowExecutionRecord[];
/** 批量操作完成回调 */
onOperationComplete?: (task: BatchOperationTask) => void;
/** 关闭回调 */
onClose?: () => void;
}
/**
* 批量操作管理器组件
*/
export const BatchOperationManager: React.FC<BatchOperationManagerProps> = ({
selectedTemplates = [],
selectedExecutions = [],
onOperationComplete,
onClose
}) => {
const [activeTasks, setActiveTasks] = useState<BatchOperationTask[]>([]);
const [showConfig, setShowConfig] = useState(false);
const [operationType, setOperationType] = useState<BatchOperationType>('execute');
const [batchConfig, setBatchConfig] = useState<BatchExecutionConfig>({
parallel_limit: 3,
retry_failed: true,
stop_on_error: false,
execution_delay_seconds: 1
});
// 批量操作配置
const operationConfigs = {
execute: {
label: '批量执行',
icon: Play,
description: '批量执行选中的工作流模板',
color: 'text-green-600',
bgColor: 'bg-green-100',
requiresConfig: true
},
delete: {
label: '批量删除',
icon: Trash2,
description: '删除选中的项目',
color: 'text-red-600',
bgColor: 'bg-red-100',
requiresConfig: false
},
export: {
label: '批量导出',
icon: Download,
description: '导出选中项目的数据',
color: 'text-blue-600',
bgColor: 'bg-blue-100',
requiresConfig: false
},
archive: {
label: '批量归档',
icon: Archive,
description: '归档选中的项目',
color: 'text-yellow-600',
bgColor: 'bg-yellow-100',
requiresConfig: false
},
activate: {
label: '批量激活',
icon: CheckCircle,
description: '激活选中的工作流模板',
color: 'text-green-600',
bgColor: 'bg-green-100',
requiresConfig: false
},
deactivate: {
label: '批量禁用',
icon: XCircle,
description: '禁用选中的工作流模板',
color: 'text-gray-600',
bgColor: 'bg-gray-100',
requiresConfig: false
}
};
// 获取可用的操作类型
const getAvailableOperations = () => {
const operations: BatchOperationType[] = [];
if (selectedTemplates.length > 0) {
operations.push('execute', 'delete', 'export', 'archive', 'activate', 'deactivate');
}
if (selectedExecutions.length > 0) {
operations.push('delete', 'export');
}
return operations;
};
// 获取操作目标信息
const getOperationTarget = () => {
if (selectedTemplates.length > 0) {
return {
type: 'templates' as const,
count: selectedTemplates.length,
items: selectedTemplates
};
} else if (selectedExecutions.length > 0) {
return {
type: 'executions' as const,
count: selectedExecutions.length,
items: selectedExecutions
};
}
return null;
};
// 开始批量操作
const startBatchOperation = async () => {
const target = getOperationTarget();
if (!target) return;
const taskId = `batch_${operationType}_${Date.now()}`;
const newTask: BatchOperationTask = {
id: taskId,
type: operationType,
target_type: target.type,
target_ids: target.items.map(item => item.id),
status: 'pending',
progress: 0,
total_items: target.count,
completed_items: 0,
failed_items: 0,
started_at: new Date().toISOString()
};
setActiveTasks(prev => [...prev, newTask]);
try {
// 更新任务状态为运行中
updateTaskStatus(taskId, 'running');
// 根据操作类型执行不同的逻辑
switch (operationType) {
case 'execute':
await executeBatchExecution(taskId, selectedTemplates);
break;
case 'delete':
await executeBatchDelete(taskId, target);
break;
case 'export':
await executeBatchExport(taskId, target);
break;
case 'archive':
await executeBatchArchive(taskId, selectedTemplates);
break;
case 'activate':
await executeBatchActivate(taskId, selectedTemplates, true);
break;
case 'deactivate':
await executeBatchActivate(taskId, selectedTemplates, false);
break;
}
updateTaskStatus(taskId, 'completed');
if (onOperationComplete) {
const completedTask = activeTasks.find(t => t.id === taskId);
if (completedTask) {
onOperationComplete(completedTask);
}
}
} catch (error) {
console.error('批量操作失败:', error);
updateTaskStatus(taskId, 'failed', error as Error);
}
};
// 更新任务状态
const updateTaskStatus = (taskId: string, status: BatchOperationStatus, error?: Error) => {
setActiveTasks(prev => prev.map(task =>
task.id === taskId
? {
...task,
status,
completed_at: status === 'completed' || status === 'failed' ? new Date().toISOString() : undefined,
error_message: error?.message
}
: task
));
};
// 更新任务进度
const updateTaskProgress = (taskId: string, completed: number, failed: number = 0) => {
setActiveTasks(prev => prev.map(task =>
task.id === taskId
? {
...task,
completed_items: completed,
failed_items: failed,
progress: Math.round((completed / task.total_items) * 100)
}
: task
));
};
// 执行批量执行
const executeBatchExecution = async (taskId: string, templates: WorkflowTemplate[]) => {
let completed = 0;
let failed = 0;
for (const template of templates) {
try {
// TODO: 实现实际的批量执行逻辑
await new Promise(resolve => setTimeout(resolve, batchConfig.execution_delay_seconds * 1000));
// 模拟执行
const success = Math.random() > 0.2; // 80% 成功率
if (success) {
completed++;
} else {
failed++;
if (batchConfig.stop_on_error) {
throw new Error(`执行工作流 ${template.name} 失败`);
}
}
updateTaskProgress(taskId, completed, failed);
} catch (error) {
failed++;
updateTaskProgress(taskId, completed, failed);
if (batchConfig.stop_on_error) {
throw error;
}
}
}
};
// 执行批量删除
const executeBatchDelete = async (taskId: string, target: any) => {
let completed = 0;
let failed = 0;
for (const item of target.items) {
try {
// TODO: 实现实际的删除逻辑
await new Promise(resolve => setTimeout(resolve, 500));
completed++;
updateTaskProgress(taskId, completed, failed);
} catch (error) {
failed++;
updateTaskProgress(taskId, completed, failed);
}
}
};
// 执行批量导出
const executeBatchExport = async (taskId: string, target: any) => {
try {
// 准备导出数据
const exportData = {
export_type: target.type,
export_time: new Date().toISOString(),
items: target.items
};
// 创建并下载文件
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `batch_export_${target.type}_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
updateTaskProgress(taskId, target.count, 0);
} catch (error) {
throw new Error('导出失败: ' + (error as Error).message);
}
};
// 执行批量归档
const executeBatchArchive = async (taskId: string, templates: WorkflowTemplate[]) => {
let completed = 0;
let failed = 0;
for (const template of templates) {
try {
// TODO: 实现实际的归档逻辑
await new Promise(resolve => setTimeout(resolve, 300));
completed++;
updateTaskProgress(taskId, completed, failed);
} catch (error) {
failed++;
updateTaskProgress(taskId, completed, failed);
}
}
};
// 执行批量激活/禁用
const executeBatchActivate = async (taskId: string, templates: WorkflowTemplate[], activate: boolean) => {
let completed = 0;
let failed = 0;
for (const template of templates) {
try {
// TODO: 实现实际的激活/禁用逻辑
await new Promise(resolve => setTimeout(resolve, 200));
completed++;
updateTaskProgress(taskId, completed, failed);
} catch (error) {
failed++;
updateTaskProgress(taskId, completed, failed);
}
}
};
// 取消任务
const cancelTask = (taskId: string) => {
updateTaskStatus(taskId, 'cancelled');
};
// 获取状态图标
const getStatusIcon = (status: BatchOperationStatus) => {
switch (status) {
case 'pending':
return <Clock className="w-4 h-4 text-yellow-500" />;
case 'running':
return <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />;
case 'completed':
return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'failed':
return <XCircle className="w-4 h-4 text-red-500" />;
case 'cancelled':
return <AlertTriangle className="w-4 h-4 text-gray-500" />;
default:
return <Clock className="w-4 h-4 text-gray-400" />;
}
};
const target = getOperationTarget();
const availableOperations = getAvailableOperations();
const currentConfig = operationConfigs[operationType];
if (!target) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<BarChart3 className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
</h3>
<p className="text-gray-600">
</p>
</div>
);
}
return (
<div className="space-y-6">
{/* 操作选择 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">
- {target.count} {target.type === 'templates' ? '工作流模板' : '执行记录'}
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
{availableOperations.map((op) => {
const config = operationConfigs[op];
const Icon = config.icon;
return (
<button
key={op}
onClick={() => setOperationType(op)}
className={`p-3 border-2 rounded-lg text-left transition-all ${
operationType === op
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-2 mb-1">
<Icon className={`w-4 h-4 ${config.color}`} />
<span className="font-medium text-sm">{config.label}</span>
</div>
<p className="text-xs text-gray-600">{config.description}</p>
</button>
);
})}
</div>
{/* 配置选项 */}
{currentConfig.requiresConfig && (
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-gray-900"></h4>
<button
onClick={() => setShowConfig(!showConfig)}
className="text-sm text-blue-600 hover:text-blue-800"
>
{showConfig ? '隐藏配置' : '显示配置'}
</button>
</div>
{showConfig && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<input
type="number"
min="1"
max="10"
value={batchConfig.parallel_limit}
onChange={(e) => setBatchConfig(prev => ({
...prev,
parallel_limit: parseInt(e.target.value) || 1
}))}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
()
</label>
<input
type="number"
min="0"
value={batchConfig.execution_delay_seconds}
onChange={(e) => setBatchConfig(prev => ({
...prev,
execution_delay_seconds: parseInt(e.target.value) || 0
}))}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="col-span-2 space-y-2">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={batchConfig.retry_failed}
onChange={(e) => setBatchConfig(prev => ({
...prev,
retry_failed: e.target.checked
}))}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-xs text-gray-700"></span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={batchConfig.stop_on_error}
onChange={(e) => setBatchConfig(prev => ({
...prev,
stop_on_error: e.target.checked
}))}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-xs text-gray-700"></span>
</label>
</div>
</div>
)}
</div>
)}
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
{target.count} {currentConfig.label}
</div>
<div className="flex items-center space-x-2">
{onClose && (
<button
onClick={onClose}
className="px-3 py-1 text-gray-700 border border-gray-300 rounded hover:bg-gray-50 transition-colors"
>
</button>
)}
<button
onClick={startBatchOperation}
className={`px-4 py-2 text-white rounded hover:opacity-90 transition-colors flex items-center space-x-2 ${currentConfig.bgColor.replace('bg-', 'bg-').replace('-100', '-600')}`}
>
<currentConfig.icon className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
</div>
{/* 活跃任务列表 */}
{activeTasks.length > 0 && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
<div className="space-y-3">
{activeTasks.map((task) => {
const config = operationConfigs[task.type];
const Icon = config.icon;
return (
<div key={task.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-3">
<Icon className={`w-4 h-4 ${config.color}`} />
<span className="font-medium text-sm">{config.label}</span>
{getStatusIcon(task.status)}
</div>
{task.status === 'running' && (
<button
onClick={() => cancelTask(task.id)}
className="text-xs text-red-600 hover:text-red-800"
>
</button>
)}
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">
: {task.completed_items}/{task.total_items}
</span>
<span className="text-gray-600">
{task.progress}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${task.progress}%` }}
/>
</div>
{task.failed_items > 0 && (
<div className="text-xs text-red-600">
: {task.failed_items}
</div>
)}
{task.error_message && (
<div className="text-xs text-red-600 bg-red-50 p-2 rounded">
{task.error_message}
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
};