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
This commit is contained in:
imeepos
2025-08-07 13:50:35 +08:00
parent 83768cb8a6
commit c1351689cf
22 changed files with 8380 additions and 145 deletions

View File

@@ -659,7 +659,7 @@ pub fn run() {
// 初始化 ComfyUI 服务 - 使用本地 ComfyUI 服务器
let comfyui_config = data::models::comfyui::ComfyuiConfig {
base_url: "http://192.168.0.193:8188".to_string(),
base_url: "https://bowongai-dev--waas-demo-fastapi-webapp.modal.run".to_string(),
timeout: Some(600), // 10分钟超时适应 ComfyUI 工作流的长时间处理
retry_attempts: Some(3),
enable_cache: Some(true),

View File

@@ -38,6 +38,7 @@ import VideoGeneration from './pages/VideoGeneration';
import { OutfitPhotoGenerationPage } from './pages/OutfitPhotoGeneration';
import ComfyUIManagement from './pages/ComfyUIManagement';
import ComfyUIWorkflowTest from './pages/ComfyUIWorkflowTest';
import { WorkflowPage } from './pages/WorkflowPage';
// import CanvasTool from './pages/CanvasTool';
import Navigation from './components/Navigation';
@@ -137,6 +138,7 @@ function App() {
<Route path="/outfit-photo-generation/:projectId" element={<OutfitPhotoGenerationPage />} />
<Route path="/outfit-photo-generation/:projectId/:modelId" element={<OutfitPhotoGenerationPage />} />
<Route path="/workflows" element={<WorkflowPage />} />
<Route path="/comfyui-cluster" element={<ComfyUIManagement />} />
<Route path="/comfyui-node" element={<ComfyUIWorkflowTest />} />
<Route path="/tools" element={<Tools />} />

View File

@@ -7,8 +7,7 @@ import {
DocumentDuplicateIcon,
WrenchScrewdriverIcon,
SparklesIcon,
CommandLineIcon,
PaintBrushIcon,
Cog6ToothIcon,
} from '@heroicons/react/24/outline';
const Navigation: React.FC = () => {
@@ -53,16 +52,10 @@ const Navigation: React.FC = () => {
description: 'AI穿搭方案推荐与素材检索'
},
{
name: 'ComfyUI 集群管理',
href: '/comfyui-cluster',
icon: CommandLineIcon,
description: '管理分布式ComfyUI集群和工作流调度'
},
{
name: 'ComfyUI 节点管理',
href: '/comfyui-node',
icon: CommandLineIcon,
description: '直接管理本地ComfyUI节点 (192.168.0.193:8188)'
name: 'AI工作流',
href: '/workflows',
icon: Cog6ToothIcon,
description: '管理和执行各种AI生成任务工作流'
},
{
name: '工具',

View File

@@ -0,0 +1,629 @@
/**
* 批量操作管理器组件
*
* 提供批量执行、删除、导出等操作功能
* 遵循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>
);
};

View File

@@ -0,0 +1,641 @@
/**
* 数据导出管理器组件
*
* 提供多种格式的数据导出功能,支持自定义导出配置
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
Download,
FileText,
Database,
Image,
Archive,
Settings,
Calendar,
Filter,
CheckCircle,
XCircle,
RefreshCw,
AlertCircle,
Info,
Eye,
Clock,
BarChart3
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import type {
WorkflowTemplate,
WorkflowExecutionRecord
} from '../../types/workflow';
// 导出格式枚举
type ExportFormat = 'json' | 'csv' | 'excel' | 'pdf' | 'zip';
// 导出类型枚举
type ExportType = 'templates' | 'executions' | 'results' | 'logs' | 'analytics';
// 导出状态枚举
type ExportStatus = 'pending' | 'preparing' | 'exporting' | 'completed' | 'failed';
// 导出配置接口
interface ExportConfig {
format: ExportFormat;
type: ExportType;
include_metadata: boolean;
include_results: boolean;
include_logs: boolean;
date_range?: {
start: string;
end: string;
};
filters?: {
status?: string[];
workflow_types?: string[];
environments?: string[];
};
compression?: boolean;
password_protection?: boolean;
password?: string;
}
// 导出任务接口
interface ExportTask {
id: string;
config: ExportConfig;
status: ExportStatus;
progress: number;
total_items: number;
processed_items: number;
file_size?: number;
download_url?: string;
started_at: string;
completed_at?: string;
error_message?: string;
}
// 组件属性接口
interface DataExportManagerProps {
/** 是否显示模态框 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 预选的工作流模板 */
selectedTemplates?: WorkflowTemplate[];
/** 预选的执行记录 */
selectedExecutions?: WorkflowExecutionRecord[];
}
/**
* 数据导出管理器组件
*/
export const DataExportManager: React.FC<DataExportManagerProps> = ({
isOpen,
onClose,
selectedTemplates = [],
selectedExecutions = []
}) => {
const [exportConfig, setExportConfig] = useState<ExportConfig>({
format: 'json',
type: 'templates',
include_metadata: true,
include_results: true,
include_logs: false,
compression: false,
password_protection: false
});
const [activeTasks, setActiveTasks] = useState<ExportTask[]>([]);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
// 导出格式配置
const formatConfigs = {
json: {
label: 'JSON',
description: '结构化数据格式,适合程序处理',
icon: FileText,
supports: ['templates', 'executions', 'results', 'logs', 'analytics']
},
csv: {
label: 'CSV',
description: '表格数据格式适合Excel打开',
icon: Database,
supports: ['executions', 'analytics']
},
excel: {
label: 'Excel',
description: 'Excel工作簿格式支持多个工作表',
icon: BarChart3,
supports: ['templates', 'executions', 'analytics']
},
pdf: {
label: 'PDF',
description: '便携式文档格式,适合报告',
icon: FileText,
supports: ['analytics']
},
zip: {
label: 'ZIP压缩包',
description: '包含所有相关文件的压缩包',
icon: Archive,
supports: ['templates', 'executions', 'results']
}
};
// 导出类型配置
const typeConfigs = {
templates: {
label: '工作流模板',
description: '导出工作流模板配置和元数据',
icon: Settings
},
executions: {
label: '执行记录',
description: '导出工作流执行历史记录',
icon: Clock
},
results: {
label: '执行结果',
description: '导出工作流执行的输出结果',
icon: Image
},
logs: {
label: '执行日志',
description: '导出详细的执行日志信息',
icon: FileText
},
analytics: {
label: '分析报告',
description: '导出统计分析和性能报告',
icon: BarChart3
}
};
// 获取支持的导出类型
const getSupportedTypes = (format: ExportFormat): ExportType[] => {
return formatConfigs[format].supports as ExportType[];
};
// 更新导出配置
const updateConfig = (updates: Partial<ExportConfig>) => {
setExportConfig(prev => ({ ...prev, ...updates }));
};
// 开始导出
const startExport = async () => {
const taskId = `export_${Date.now()}`;
const newTask: ExportTask = {
id: taskId,
config: exportConfig,
status: 'pending',
progress: 0,
total_items: getEstimatedItemCount(),
processed_items: 0,
started_at: new Date().toISOString()
};
setActiveTasks(prev => [...prev, newTask]);
try {
// 更新状态为准备中
updateTaskStatus(taskId, 'preparing');
// 模拟导出过程
await simulateExport(taskId);
// 完成导出
updateTaskStatus(taskId, 'completed');
} catch (error) {
console.error('导出失败:', error);
updateTaskStatus(taskId, 'failed', error as Error);
}
};
// 模拟导出过程
const simulateExport = async (taskId: string) => {
const task = activeTasks.find(t => t.id === taskId);
if (!task) return;
updateTaskStatus(taskId, 'exporting');
// 模拟处理进度
for (let i = 0; i <= task.total_items; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
updateTaskProgress(taskId, i);
}
// 生成下载链接
const downloadUrl = generateDownloadUrl(task.config);
updateTaskDownloadUrl(taskId, downloadUrl, Math.random() * 10000000); // 随机文件大小
};
// 生成下载链接
const generateDownloadUrl = (config: ExportConfig): string => {
// 在实际应用中这里会调用后端API生成真实的下载链接
const filename = `export_${config.type}_${new Date().toISOString().split('T')[0]}.${config.format}`;
return `#download/${filename}`;
};
// 更新任务状态
const updateTaskStatus = (taskId: string, status: ExportStatus, 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, processed: number) => {
setActiveTasks(prev => prev.map(task =>
task.id === taskId
? {
...task,
processed_items: processed,
progress: Math.round((processed / task.total_items) * 100)
}
: task
));
};
// 更新下载链接
const updateTaskDownloadUrl = (taskId: string, downloadUrl: string, fileSize: number) => {
setActiveTasks(prev => prev.map(task =>
task.id === taskId
? {
...task,
download_url: downloadUrl,
file_size: fileSize
}
: task
));
};
// 估算项目数量
const getEstimatedItemCount = (): number => {
switch (exportConfig.type) {
case 'templates':
return selectedTemplates.length || 50; // 默认估算
case 'executions':
return selectedExecutions.length || 100;
case 'results':
return selectedExecutions.length || 100;
case 'logs':
return selectedExecutions.length * 10 || 500;
case 'analytics':
return 20; // 分析报告项目较少
default:
return 10;
}
};
// 获取状态图标
const getStatusIcon = (status: ExportStatus) => {
switch (status) {
case 'pending':
return <Clock className="w-4 h-4 text-yellow-500" />;
case 'preparing':
return <Settings className="w-4 h-4 text-blue-500 animate-spin" />;
case 'exporting':
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" />;
default:
return <Clock className="w-4 h-4 text-gray-400" />;
}
};
// 格式化文件大小
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '-';
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
};
if (!isOpen) return null;
const currentFormatConfig = formatConfigs[exportConfig.format];
const currentTypeConfig = typeConfigs[exportConfig.type];
const supportedTypes = getSupportedTypes(exportConfig.format);
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-[90vh] 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">
<Download className="w-6 h-6 text-blue-600" />
<h2 className="text-lg font-semibold text-gray-900"></h2>
</div>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
>
<XCircle className="w-5 h-5" />
</button>
</div>
{/* 内容 */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* 导出格式选择 */}
<div>
<h3 className="text-sm font-medium text-gray-900 mb-3"></h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{Object.entries(formatConfigs).map(([format, config]) => {
const Icon = config.icon;
return (
<button
key={format}
onClick={() => {
updateConfig({ format: format as ExportFormat });
// 如果当前类型不支持新格式,切换到支持的第一个类型
const newSupportedTypes = config.supports as ExportType[];
if (!newSupportedTypes.includes(exportConfig.type)) {
updateConfig({ type: newSupportedTypes[0] });
}
}}
className={`p-3 border-2 rounded-lg text-left transition-all ${
exportConfig.format === format
? '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 text-blue-600" />
<span className="font-medium text-sm">{config.label}</span>
</div>
<p className="text-xs text-gray-600">{config.description}</p>
</button>
);
})}
</div>
</div>
{/* 导出类型选择 */}
<div>
<h3 className="text-sm font-medium text-gray-900 mb-3"></h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{supportedTypes.map((type) => {
const config = typeConfigs[type];
const Icon = config.icon;
return (
<button
key={type}
onClick={() => updateConfig({ type })}
className={`p-3 border-2 rounded-lg text-left transition-all ${
exportConfig.type === type
? '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 text-blue-600" />
<span className="font-medium text-sm">{config.label}</span>
</div>
<p className="text-xs text-gray-600">{config.description}</p>
</button>
);
})}
</div>
</div>
{/* 导出选项 */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-900"></h3>
<button
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
className="text-sm text-blue-600 hover:text-blue-800"
>
{showAdvancedOptions ? '隐藏高级选项' : '显示高级选项'}
</button>
</div>
<div className="space-y-4">
{/* 基本选项 */}
<div className="grid grid-cols-2 gap-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={exportConfig.include_metadata}
onChange={(e) => updateConfig({ include_metadata: e.target.checked })}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700"></span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={exportConfig.include_results}
onChange={(e) => updateConfig({ include_results: e.target.checked })}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700"></span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={exportConfig.include_logs}
onChange={(e) => updateConfig({ include_logs: e.target.checked })}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700"></span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={exportConfig.compression}
onChange={(e) => updateConfig({ compression: e.target.checked })}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700"></span>
</label>
</div>
{/* 高级选项 */}
{showAdvancedOptions && (
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
{/* 日期范围 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
()
</label>
<div className="grid grid-cols-2 gap-2">
<input
type="date"
value={exportConfig.date_range?.start || ''}
onChange={(e) => updateConfig({
date_range: {
...exportConfig.date_range,
start: e.target.value
}
})}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500"
/>
<input
type="date"
value={exportConfig.date_range?.end || ''}
onChange={(e) => updateConfig({
date_range: {
...exportConfig.date_range,
end: e.target.value
}
})}
className="px-3 py-2 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
{/* 密码保护 */}
<div>
<label className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={exportConfig.password_protection}
onChange={(e) => updateConfig({ password_protection: e.target.checked })}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700"></span>
</label>
{exportConfig.password_protection && (
<input
type="password"
placeholder="设置导出文件密码"
value={exportConfig.password || ''}
onChange={(e) => updateConfig({ password: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500"
/>
)}
</div>
</div>
)}
</div>
</div>
{/* 导出预览 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start space-x-2">
<Info className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<h4 className="text-sm font-medium text-blue-900"></h4>
<p className="text-sm text-blue-700 mt-1">
{getEstimatedItemCount()} {currentTypeConfig.label}
{currentFormatConfig.label}
{exportConfig.compression && '(压缩)'}
{exportConfig.password_protection && '(密码保护)'}
</p>
</div>
</div>
</div>
</div>
{/* 底部操作栏 */}
<div className="flex items-center justify-between p-6 border-t border-gray-200">
<div className="text-sm text-gray-500">
: {formatFileSize(getEstimatedItemCount() * 1024)}
</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={startExport}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Download className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
{/* 导出任务列表 */}
{activeTasks.length > 0 && (
<div className="border-t border-gray-200 p-6">
<h3 className="text-sm font-medium text-gray-900 mb-4"></h3>
<div className="space-y-3">
{activeTasks.map((task) => (
<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">
<currentFormatConfig.icon className="w-4 h-4 text-blue-600" />
<span className="font-medium text-sm">
{typeConfigs[task.config.type].label} - {formatConfigs[task.config.format].label}
</span>
{getStatusIcon(task.status)}
</div>
{task.download_url && task.status === 'completed' && (
<a
href={task.download_url}
download
className="px-3 py-1 bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors text-sm flex items-center space-x-1"
>
<Download className="w-3 h-3" />
<span></span>
</a>
)}
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">
: {task.processed_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.file_size && (
<div className="text-xs text-gray-500">
: {formatFileSize(task.file_size)}
</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>
</div>
);
};

View File

@@ -0,0 +1,575 @@
/**
* 环境配置器组件
*
* 用于创建和编辑工作流执行环境
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
X,
Save,
TestTube,
CheckCircle,
XCircle,
RefreshCw,
Monitor,
Cloud,
Globe,
Server,
AlertCircle,
Info
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
// 环境类型枚举
type EnvironmentType = 'local_comfyui' | 'modal_cloud' | 'runpod_cloud' | 'custom';
// 执行环境接口
interface WorkflowExecutionEnvironment {
id?: number;
name: string;
environment_type: EnvironmentType;
description?: string;
base_url: string;
api_key?: string;
connection_config_json?: any;
supported_workflow_types: string[];
max_concurrent_jobs: number;
priority: number;
is_active: boolean;
is_available: boolean;
max_memory_mb?: number;
max_execution_time_seconds?: number;
metadata_json?: any;
tags?: string[];
}
// 创建环境请求接口
interface CreateEnvironmentRequest {
name: string;
environment_type: EnvironmentType;
description?: string;
base_url: string;
api_key?: string;
connection_config_json?: any;
supported_workflow_types: string[];
max_concurrent_jobs?: number;
priority?: number;
max_memory_mb?: number;
max_execution_time_seconds?: number;
metadata_json?: any;
tags?: string[];
}
// 组件属性接口
interface EnvironmentConfiguratorProps {
/** 编辑的环境(为空时表示创建新环境) */
environment?: WorkflowExecutionEnvironment;
/** 是否显示模态框 */
isOpen: boolean;
/** 保存回调 */
onSave: (env: CreateEnvironmentRequest) => void;
/** 关闭回调 */
onClose: () => void;
}
/**
* 环境配置器组件
*/
export const EnvironmentConfigurator: React.FC<EnvironmentConfiguratorProps> = ({
environment,
isOpen,
onSave,
onClose
}) => {
const [formData, setFormData] = useState<CreateEnvironmentRequest>({
name: '',
environment_type: 'local_comfyui',
description: '',
base_url: '',
api_key: '',
supported_workflow_types: [],
max_concurrent_jobs: 1,
priority: 0,
max_memory_mb: undefined,
max_execution_time_seconds: undefined,
tags: []
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isTestingConnection, setIsTestingConnection] = useState(false);
const [connectionTestResult, setConnectionTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
// 可用的工作流类型
const availableWorkflowTypes = [
'outfit_generation',
'background_replacement',
'portrait_enhancement',
'image_upscaling',
'style_transfer'
];
// 环境类型配置
const environmentTypeConfigs = {
local_comfyui: {
icon: Monitor,
label: '本地ComfyUI',
description: '连接到本地运行的ComfyUI实例',
defaultUrl: 'http://localhost:8188',
requiresApiKey: false
},
modal_cloud: {
icon: Cloud,
label: 'Modal云端',
description: '使用Modal云端服务执行工作流',
defaultUrl: 'https://your-modal-endpoint.modal.run',
requiresApiKey: true
},
runpod_cloud: {
icon: Globe,
label: 'RunPod云端',
description: '使用RunPod云端GPU服务',
defaultUrl: 'https://api.runpod.ai',
requiresApiKey: true
},
custom: {
icon: Server,
label: '自定义',
description: '自定义API服务',
defaultUrl: '',
requiresApiKey: false
}
};
// 初始化表单数据
useEffect(() => {
if (environment) {
setFormData({
name: environment.name,
environment_type: environment.environment_type,
description: environment.description || '',
base_url: environment.base_url,
api_key: environment.api_key || '',
supported_workflow_types: environment.supported_workflow_types,
max_concurrent_jobs: environment.max_concurrent_jobs,
priority: environment.priority,
max_memory_mb: environment.max_memory_mb,
max_execution_time_seconds: environment.max_execution_time_seconds,
tags: environment.tags || []
});
} else {
// 重置为默认值
setFormData({
name: '',
environment_type: 'local_comfyui',
description: '',
base_url: environmentTypeConfigs.local_comfyui.defaultUrl,
api_key: '',
supported_workflow_types: [],
max_concurrent_jobs: 1,
priority: 0,
max_memory_mb: undefined,
max_execution_time_seconds: undefined,
tags: []
});
}
setErrors({});
setConnectionTestResult(null);
}, [environment, isOpen]);
// 更新表单字段
const updateField = (field: keyof CreateEnvironmentRequest, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除该字段的错误
if (errors[field]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
// 处理环境类型变化
const handleEnvironmentTypeChange = (type: EnvironmentType) => {
const config = environmentTypeConfigs[type];
updateField('environment_type', type);
updateField('base_url', config.defaultUrl);
if (!config.requiresApiKey) {
updateField('api_key', '');
}
};
// 验证表单
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = '环境名称不能为空';
}
if (!formData.base_url.trim()) {
newErrors.base_url = 'URL不能为空';
} else {
try {
new URL(formData.base_url);
} catch {
newErrors.base_url = 'URL格式不正确';
}
}
const config = environmentTypeConfigs[formData.environment_type];
if (config.requiresApiKey && !formData.api_key?.trim()) {
newErrors.api_key = 'API密钥不能为空';
}
if (formData.max_concurrent_jobs < 1) {
newErrors.max_concurrent_jobs = '最大并发数必须大于0';
}
if (formData.supported_workflow_types.length === 0) {
newErrors.supported_workflow_types = '至少选择一种支持的工作流类型';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 测试连接
const testConnection = async () => {
if (!validateForm()) return;
setIsTestingConnection(true);
setConnectionTestResult(null);
try {
// TODO: 实现实际的连接测试
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟测试
setConnectionTestResult({
success: true,
message: '连接测试成功!环境可以正常使用。'
});
} catch (error) {
setConnectionTestResult({
success: false,
message: '连接测试失败:' + (error as Error).message
});
} finally {
setIsTestingConnection(false);
}
};
// 处理保存
const handleSave = () => {
if (!validateForm()) return;
onSave(formData);
};
// 处理工作流类型选择
const handleWorkflowTypeToggle = (type: string) => {
const currentTypes = formData.supported_workflow_types;
const newTypes = currentTypes.includes(type)
? currentTypes.filter(t => t !== type)
: [...currentTypes, type];
updateField('supported_workflow_types', newTypes);
};
if (!isOpen) return null;
const currentConfig = environmentTypeConfigs[formData.environment_type];
const IconComponent = currentConfig.icon;
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-2xl w-full max-h-[90vh] 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">
<IconComponent className="w-6 h-6 text-blue-600" />
<h2 className="text-lg font-semibold text-gray-900">
{environment ? '编辑执行环境' : '添加执行环境'}
</h2>
</div>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* 内容 */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* 基本信息 */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-900"></h3>
<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="例如本地ComfyUI环境"
/>
{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">
<span className="text-red-500">*</span>
</label>
<div className="grid grid-cols-2 gap-3">
{Object.entries(environmentTypeConfigs).map(([type, config]) => {
const Icon = config.icon;
return (
<button
key={type}
type="button"
onClick={() => handleEnvironmentTypeChange(type as EnvironmentType)}
className={`p-3 border-2 rounded-lg text-left transition-all ${
formData.environment_type === type
? '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" />
<span className="font-medium text-sm">{config.label}</span>
</div>
<p className="text-xs text-gray-600">{config.description}</p>
</button>
);
})}
</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={2}
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 className="space-y-4">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
URL <span className="text-red-500">*</span>
</label>
<input
type="url"
value={formData.base_url}
onChange={(e) => updateField('base_url', e.target.value)}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.base_url ? 'border-red-300' : 'border-gray-300'
}`}
placeholder={currentConfig.defaultUrl}
/>
{errors.base_url && (
<p className="mt-1 text-sm text-red-600">{errors.base_url}</p>
)}
</div>
{currentConfig.requiresApiKey && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
API密钥 <span className="text-red-500">*</span>
</label>
<input
type="password"
value={formData.api_key}
onChange={(e) => updateField('api_key', e.target.value)}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.api_key ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="输入API密钥"
/>
{errors.api_key && (
<p className="mt-1 text-sm text-red-600">{errors.api_key}</p>
)}
</div>
)}
{/* 连接测试 */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700"></span>
<button
onClick={testConnection}
disabled={isTestingConnection}
className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors flex items-center space-x-1 disabled:opacity-50"
>
{isTestingConnection ? (
<RefreshCw className="w-3 h-3 animate-spin" />
) : (
<TestTube className="w-3 h-3" />
)}
<span>{isTestingConnection ? '测试中...' : '测试连接'}</span>
</button>
</div>
{connectionTestResult && (
<div className={`flex items-start space-x-2 text-sm ${
connectionTestResult.success ? 'text-green-700' : 'text-red-700'
}`}>
{connectionTestResult.success ? (
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
) : (
<XCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
)}
<span>{connectionTestResult.message}</span>
</div>
)}
</div>
</div>
{/* 能力配置 */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<div className="grid grid-cols-2 gap-2">
{availableWorkflowTypes.map((type) => (
<label key={type} className="flex items-center space-x-2">
<input
type="checkbox"
checked={formData.supported_workflow_types.includes(type)}
onChange={() => handleWorkflowTypeToggle(type)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">{type}</span>
</label>
))}
</div>
{errors.supported_workflow_types && (
<p className="mt-1 text-sm text-red-600">{errors.supported_workflow_types}</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="number"
min="1"
max="100"
value={formData.max_concurrent_jobs}
onChange={(e) => updateField('max_concurrent_jobs', parseInt(e.target.value) || 1)}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.max_concurrent_jobs ? 'border-red-300' : 'border-gray-300'
}`}
/>
{errors.max_concurrent_jobs && (
<p className="mt-1 text-sm text-red-600">{errors.max_concurrent_jobs}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="number"
min="0"
max="100"
value={formData.priority}
onChange={(e) => updateField('priority', parseInt(e.target.value) || 0)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<p className="mt-1 text-xs text-gray-500"></p>
</div>
</div>
</div>
{/* 资源限制 */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
(MB)
</label>
<input
type="number"
min="0"
value={formData.max_memory_mb || ''}
onChange={(e) => updateField('max_memory_mb', e.target.value ? parseInt(e.target.value) : undefined)}
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="number"
min="0"
value={formData.max_execution_time_seconds || ''}
onChange={(e) => updateField('max_execution_time_seconds', e.target.value ? parseInt(e.target.value) : undefined)}
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>
</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}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Save className="w-4 h-4" />
<span>{environment ? '更新' : '创建'}</span>
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,514 @@
/**
* 执行环境列表组件
*
* 显示和管理工作流执行环境,包括健康检查和性能统计
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
Server,
CheckCircle,
XCircle,
AlertTriangle,
RefreshCw,
Plus,
Settings,
Trash2,
Activity,
Clock,
Zap,
Globe,
Monitor,
Cloud,
HardDrive
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
// 环境类型枚举
type EnvironmentType = 'local_comfyui' | 'modal_cloud' | 'runpod_cloud' | 'custom';
// 健康状态枚举
type HealthStatus = 'healthy' | 'unhealthy' | 'unknown';
// 执行环境接口
interface WorkflowExecutionEnvironment {
id: number;
name: string;
environment_type: EnvironmentType;
description?: string;
base_url: string;
api_key?: string;
connection_config_json?: any;
supported_workflow_types: string[];
max_concurrent_jobs: number;
priority: number;
is_active: boolean;
is_available: boolean;
last_health_check?: string;
health_status: HealthStatus;
average_response_time_ms?: number;
success_rate: number;
total_executions: number;
failed_executions: number;
max_memory_mb?: number;
max_execution_time_seconds?: number;
metadata_json?: any;
tags?: string[];
created_at: string;
updated_at: string;
}
// 环境筛选器接口
interface EnvironmentFilter {
environment_type?: EnvironmentType;
is_active?: boolean;
is_available?: boolean;
health_status?: HealthStatus;
}
// 组件属性接口
interface EnvironmentListProps {
/** 环境列表 */
environments?: WorkflowExecutionEnvironment[];
/** 添加环境回调 */
onAdd: () => void;
/** 编辑环境回调 */
onEdit: (env: WorkflowExecutionEnvironment) => void;
/** 删除环境回调 */
onDelete: (envId: number) => void;
/** 健康检查回调 */
onHealthCheck: (envId: number) => void;
/** 切换激活状态回调 */
onToggleActive?: (envId: number, isActive: boolean) => void;
}
/**
* 执行环境列表组件
*/
export const EnvironmentList: React.FC<EnvironmentListProps> = ({
environments: propEnvironments,
onAdd,
onEdit,
onDelete,
onHealthCheck,
onToggleActive
}) => {
const [environments, setEnvironments] = useState<WorkflowExecutionEnvironment[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState<EnvironmentFilter>({});
const [healthCheckingIds, setHealthCheckingIds] = useState<Set<number>>(new Set());
// 加载环境列表
const loadEnvironments = useCallback(async () => {
try {
setLoading(true);
setError(null);
const result = await invoke<WorkflowExecutionEnvironment[]>('get_execution_environments', {
filter
});
setEnvironments(result);
} catch (err) {
console.error('加载执行环境失败:', err);
setError('加载执行环境失败');
} finally {
setLoading(false);
}
}, [filter]);
// 初始加载
useEffect(() => {
if (propEnvironments) {
setEnvironments(propEnvironments);
setLoading(false);
} else {
loadEnvironments();
}
}, [propEnvironments, loadEnvironments]);
// 获取环境类型图标
const getEnvironmentTypeIcon = (type: EnvironmentType) => {
switch (type) {
case 'local_comfyui':
return <Monitor className="w-5 h-5 text-blue-500" />;
case 'modal_cloud':
return <Cloud className="w-5 h-5 text-purple-500" />;
case 'runpod_cloud':
return <Globe className="w-5 h-5 text-green-500" />;
case 'custom':
return <Server className="w-5 h-5 text-gray-500" />;
default:
return <Server className="w-5 h-5 text-gray-400" />;
}
};
// 获取环境类型文本
const getEnvironmentTypeText = (type: EnvironmentType) => {
const typeMap = {
local_comfyui: '本地ComfyUI',
modal_cloud: 'Modal云端',
runpod_cloud: 'RunPod云端',
custom: '自定义'
};
return typeMap[type] || '未知';
};
// 获取健康状态图标
const getHealthStatusIcon = (status: HealthStatus) => {
switch (status) {
case 'healthy':
return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'unhealthy':
return <XCircle className="w-4 h-4 text-red-500" />;
case 'unknown':
return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
default:
return <AlertTriangle className="w-4 h-4 text-gray-400" />;
}
};
// 获取健康状态文本
const getHealthStatusText = (status: HealthStatus) => {
const statusMap = {
healthy: '健康',
unhealthy: '异常',
unknown: '未知'
};
return statusMap[status] || '未知';
};
// 格式化响应时间
const formatResponseTime = (ms?: number) => {
if (!ms) return '-';
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(1)}s`;
};
// 格式化成功率
const formatSuccessRate = (rate: number) => {
return `${(rate * 100).toFixed(1)}%`;
};
// 处理健康检查
const handleHealthCheck = async (envId: number) => {
setHealthCheckingIds(prev => new Set(prev).add(envId));
try {
await onHealthCheck(envId);
// 重新加载环境列表以获取最新状态
if (!propEnvironments) {
await loadEnvironments();
}
} finally {
setHealthCheckingIds(prev => {
const newSet = new Set(prev);
newSet.delete(envId);
return newSet;
});
}
};
// 处理激活状态切换
const handleToggleActive = async (env: WorkflowExecutionEnvironment) => {
if (onToggleActive) {
await onToggleActive(env.id, !env.is_active);
// 重新加载环境列表
if (!propEnvironments) {
await loadEnvironments();
}
}
};
// 筛选环境
const filteredEnvironments = environments.filter(env => {
if (filter.environment_type && env.environment_type !== filter.environment_type) return false;
if (filter.is_active !== undefined && env.is_active !== filter.is_active) return false;
if (filter.is_available !== undefined && env.is_available !== filter.is_available) return false;
if (filter.health_status && env.health_status !== filter.health_status) return false;
return true;
});
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<RefreshCw className="w-8 h-8 text-blue-500 animate-spin" />
<span className="ml-2 text-gray-600">...</span>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<XCircle className="w-5 h-5 text-red-500 mr-2" />
<span className="text-red-700">{error}</span>
</div>
<button
onClick={loadEnvironments}
className="mt-2 px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors"
>
</button>
</div>
);
}
return (
<div className="space-y-6">
{/* 筛选和操作栏 */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div className="flex flex-wrap gap-4">
{/* 环境类型筛选 */}
<select
value={filter.environment_type || ''}
onChange={(e) => setFilter(prev => ({
...prev,
environment_type: e.target.value as EnvironmentType || undefined
}))}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
<option value="local_comfyui">ComfyUI</option>
<option value="modal_cloud">Modal云端</option>
<option value="runpod_cloud">RunPod云端</option>
<option value="custom"></option>
</select>
{/* 状态筛选 */}
<select
value={filter.health_status || ''}
onChange={(e) => setFilter(prev => ({
...prev,
health_status: e.target.value as HealthStatus || undefined
}))}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
<option value="healthy"></option>
<option value="unhealthy"></option>
<option value="unknown"></option>
</select>
{/* 激活状态筛选 */}
<select
value={filter.is_active !== undefined ? filter.is_active.toString() : ''}
onChange={(e) => setFilter(prev => ({
...prev,
is_active: e.target.value ? e.target.value === 'true' : undefined
}))}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<div className="flex items-center space-x-2">
<button
onClick={loadEnvironments}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center space-x-2"
>
<RefreshCw className="w-4 h-4" />
<span></span>
</button>
<button
onClick={onAdd}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Plus className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
{/* 环境列表 */}
{filteredEnvironments.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<Server 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 mb-4">
{Object.keys(filter).length > 0
? '没有找到匹配的执行环境'
: '还没有配置任何执行环境'}
</p>
<button
onClick={onAdd}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2 mx-auto"
>
<Plus className="w-4 h-4" />
<span></span>
</button>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filteredEnvironments.map((env) => (
<div
key={env.id}
className={`bg-white rounded-lg shadow-sm border-2 transition-all ${
env.is_active
? env.health_status === 'healthy'
? 'border-green-200 hover:border-green-300'
: 'border-yellow-200 hover:border-yellow-300'
: 'border-gray-200 hover:border-gray-300'
}`}
>
{/* 环境头部 */}
<div className="p-4 border-b border-gray-200">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
{getEnvironmentTypeIcon(env.environment_type)}
<div className="flex-1">
<div className="flex items-center space-x-2">
<h3 className="text-lg font-medium text-gray-900">
{env.name}
</h3>
{!env.is_active && (
<span className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">
</span>
)}
</div>
<p className="text-sm text-gray-500 mt-1">
{getEnvironmentTypeText(env.environment_type)}
</p>
{env.description && (
<p className="text-sm text-gray-600 mt-1">
{env.description}
</p>
)}
</div>
</div>
<div className="flex items-center space-x-2">
{/* 健康状态 */}
<div className="flex items-center space-x-1">
{getHealthStatusIcon(env.health_status)}
<span className="text-sm text-gray-600">
{getHealthStatusText(env.health_status)}
</span>
</div>
</div>
</div>
</div>
{/* 环境详情 */}
<div className="p-4 space-y-4">
{/* 性能统计 */}
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Activity className="w-4 h-4 text-blue-500" />
<div>
<div className="text-sm text-gray-500"></div>
<div className="text-sm font-medium text-gray-900">
{formatResponseTime(env.average_response_time_ms)}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Zap className="w-4 h-4 text-green-500" />
<div>
<div className="text-sm text-gray-500"></div>
<div className="text-sm font-medium text-gray-900">
{formatSuccessRate(env.success_rate)}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4 text-purple-500" />
<div>
<div className="text-sm text-gray-500"></div>
<div className="text-sm font-medium text-gray-900">
{env.total_executions}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<HardDrive className="w-4 h-4 text-orange-500" />
<div>
<div className="text-sm text-gray-500"></div>
<div className="text-sm font-medium text-gray-900">
{env.max_concurrent_jobs}
</div>
</div>
</div>
</div>
{/* 支持的工作流类型 */}
{env.supported_workflow_types.length > 0 && (
<div>
<div className="text-sm text-gray-500 mb-2"></div>
<div className="flex flex-wrap gap-1">
{env.supported_workflow_types.map((type, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{type}
</span>
))}
</div>
</div>
)}
{/* 操作按钮 */}
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<div className="flex items-center space-x-2">
<button
onClick={() => handleHealthCheck(env.id)}
disabled={healthCheckingIds.has(env.id)}
className="px-3 py-1 bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors flex items-center space-x-1 disabled:opacity-50"
>
<RefreshCw className={`w-3 h-3 ${healthCheckingIds.has(env.id) ? 'animate-spin' : ''}`} />
<span></span>
</button>
{onToggleActive && (
<button
onClick={() => handleToggleActive(env)}
className={`px-3 py-1 rounded transition-colors flex items-center space-x-1 ${
env.is_active
? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
}`}
>
<span>{env.is_active ? '禁用' : '启用'}</span>
</button>
)}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => onEdit(env)}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
title="编辑"
>
<Settings className="w-4 h-4" />
</button>
<button
onClick={() => onDelete(env.id)}
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
title="删除"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,587 @@
/**
* 错误分析器组件
*
* 分析和展示工作流执行错误,提供错误分类和解决建议
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
AlertTriangle,
XCircle,
RefreshCw,
Search,
Filter,
TrendingUp,
Clock,
Code,
Server,
Wifi,
HardDrive,
Zap,
ChevronDown,
ChevronRight,
Copy,
ExternalLink
} from 'lucide-react';
// 错误类型枚举
type ErrorType = 'network' | 'timeout' | 'memory' | 'validation' | 'server' | 'unknown';
// 错误严重程度枚举
type ErrorSeverity = 'low' | 'medium' | 'high' | 'critical';
// 错误记录接口
interface ErrorRecord {
id: number;
execution_id: number;
workflow_name: string;
error_type: ErrorType;
severity: ErrorSeverity;
error_message: string;
error_details: any;
stack_trace?: string;
environment_name?: string;
occurred_at: string;
resolved: boolean;
resolution_notes?: string;
}
// 错误统计接口
interface ErrorStatistics {
total_errors: number;
errors_by_type: Record<ErrorType, number>;
errors_by_severity: Record<ErrorSeverity, number>;
error_rate_trend: Array<{ date: string; count: number }>;
most_common_errors: Array<{ message: string; count: number }>;
resolution_rate: number;
}
// 错误解决建议接口
interface ErrorSolution {
error_type: ErrorType;
title: string;
description: string;
steps: string[];
prevention_tips: string[];
}
// 组件属性接口
interface ErrorAnalyzerProps {
/** 错误记录列表 */
errors?: ErrorRecord[];
/** 错误统计信息 */
statistics?: ErrorStatistics;
/** 标记错误已解决回调 */
onMarkResolved?: (errorId: number, notes: string) => void;
/** 重试执行回调 */
onRetryExecution?: (executionId: number) => void;
}
/**
* 错误分析器组件
*/
export const ErrorAnalyzer: React.FC<ErrorAnalyzerProps> = ({
errors = [],
statistics,
onMarkResolved,
onRetryExecution
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [typeFilter, setTypeFilter] = useState<ErrorType | ''>('');
const [severityFilter, setSeverityFilter] = useState<ErrorSeverity | ''>('');
const [expandedErrors, setExpandedErrors] = useState<Set<number>>(new Set());
const [resolutionNotes, setResolutionNotes] = useState<Record<number, string>>({});
// 错误类型配置
const errorTypeConfigs = {
network: {
icon: Wifi,
label: '网络错误',
color: 'text-red-500',
bgColor: 'bg-red-50',
borderColor: 'border-red-200'
},
timeout: {
icon: Clock,
label: '超时错误',
color: 'text-yellow-500',
bgColor: 'bg-yellow-50',
borderColor: 'border-yellow-200'
},
memory: {
icon: HardDrive,
label: '内存错误',
color: 'text-purple-500',
bgColor: 'bg-purple-50',
borderColor: 'border-purple-200'
},
validation: {
icon: Code,
label: '验证错误',
color: 'text-blue-500',
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200'
},
server: {
icon: Server,
label: '服务器错误',
color: 'text-orange-500',
bgColor: 'bg-orange-50',
borderColor: 'border-orange-200'
},
unknown: {
icon: AlertTriangle,
label: '未知错误',
color: 'text-gray-500',
bgColor: 'bg-gray-50',
borderColor: 'border-gray-200'
}
};
// 严重程度配置
const severityConfigs = {
low: { label: '低', color: 'text-green-600', bgColor: 'bg-green-100' },
medium: { label: '中', color: 'text-yellow-600', bgColor: 'bg-yellow-100' },
high: { label: '高', color: 'text-orange-600', bgColor: 'bg-orange-100' },
critical: { label: '严重', color: 'text-red-600', bgColor: 'bg-red-100' }
};
// 错误解决方案
const errorSolutions: Record<ErrorType, ErrorSolution> = {
network: {
error_type: 'network',
title: '网络连接问题',
description: '执行环境网络连接异常或不稳定',
steps: [
'检查网络连接状态',
'验证执行环境URL是否正确',
'检查防火墙设置',
'尝试重新连接执行环境'
],
prevention_tips: [
'使用稳定的网络连接',
'配置网络重试机制',
'监控网络状态'
]
},
timeout: {
error_type: 'timeout',
title: '执行超时',
description: '工作流执行时间超过预设限制',
steps: [
'检查工作流复杂度',
'增加超时时间限制',
'优化工作流配置',
'检查执行环境性能'
],
prevention_tips: [
'合理设置超时时间',
'优化工作流性能',
'监控执行时间趋势'
]
},
memory: {
error_type: 'memory',
title: '内存不足',
description: '执行过程中内存使用超出限制',
steps: [
'检查输入数据大小',
'增加内存限制',
'优化数据处理',
'分批处理大数据'
],
prevention_tips: [
'监控内存使用情况',
'合理配置内存限制',
'优化数据处理算法'
]
},
validation: {
error_type: 'validation',
title: '数据验证失败',
description: '输入数据不符合工作流要求',
steps: [
'检查输入数据格式',
'验证必填字段',
'检查数据类型',
'修正输入数据'
],
prevention_tips: [
'完善数据验证规则',
'提供清晰的输入说明',
'实现前端验证'
]
},
server: {
error_type: 'server',
title: '服务器错误',
description: '执行环境服务器内部错误',
steps: [
'检查服务器状态',
'查看服务器日志',
'重启执行环境',
'联系技术支持'
],
prevention_tips: [
'定期维护服务器',
'监控服务器健康状态',
'配置自动重启机制'
]
},
unknown: {
error_type: 'unknown',
title: '未知错误',
description: '无法确定具体错误原因',
steps: [
'收集详细错误信息',
'检查系统日志',
'尝试重新执行',
'联系技术支持'
],
prevention_tips: [
'完善错误日志记录',
'定期更新系统',
'建立错误监控机制'
]
}
};
// 筛选错误
const filteredErrors = errors.filter(error => {
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
if (!error.error_message.toLowerCase().includes(searchLower) &&
!error.workflow_name.toLowerCase().includes(searchLower)) {
return false;
}
}
if (typeFilter && error.error_type !== typeFilter) return false;
if (severityFilter && error.severity !== severityFilter) return false;
return true;
});
// 切换错误展开状态
const toggleErrorExpanded = (errorId: number) => {
const newExpanded = new Set(expandedErrors);
if (newExpanded.has(errorId)) {
newExpanded.delete(errorId);
} else {
newExpanded.add(errorId);
}
setExpandedErrors(newExpanded);
};
// 复制错误信息
const copyErrorInfo = async (error: ErrorRecord) => {
const errorInfo = `
错误ID: ${error.id}
执行ID: ${error.execution_id}
工作流: ${error.workflow_name}
错误类型: ${errorTypeConfigs[error.error_type].label}
严重程度: ${severityConfigs[error.severity].label}
错误信息: ${error.error_message}
发生时间: ${new Date(error.occurred_at).toLocaleString('zh-CN')}
环境: ${error.environment_name || '未知'}
${error.stack_trace ? `\n堆栈跟踪:\n${error.stack_trace}` : ''}
`.trim();
try {
await navigator.clipboard.writeText(errorInfo);
// TODO: 显示复制成功提示
} catch (err) {
console.error('复制失败:', err);
}
};
// 标记错误已解决
const handleMarkResolved = (errorId: number) => {
const notes = resolutionNotes[errorId] || '';
if (onMarkResolved) {
onMarkResolved(errorId, notes);
}
};
return (
<div className="space-y-6">
{/* 错误统计概览 */}
{statistics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{statistics.total_errors}
</p>
</div>
<XCircle className="w-8 h-8 text-red-500" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{(statistics.resolution_rate * 100).toFixed(1)}%
</p>
</div>
<TrendingUp className="w-8 h-8 text-green-500" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-sm font-medium text-gray-900 truncate">
{statistics.most_common_errors[0]?.message || '无'}
</p>
<p className="text-xs text-gray-500">
{statistics.most_common_errors[0]?.count || 0}
</p>
</div>
<AlertTriangle className="w-8 h-8 text-yellow-500" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{statistics.errors_by_severity.critical || 0}
</p>
</div>
<AlertTriangle className="w-8 h-8 text-red-500" />
</div>
</div>
</div>
)}
{/* 筛选和搜索 */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="搜索错误信息或工作流名称..."
value={searchTerm}
onChange={(e) => setSearchTerm(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-transparent"
/>
</div>
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value as ErrorType | '')}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
{Object.entries(errorTypeConfigs).map(([type, config]) => (
<option key={type} value={type}>{config.label}</option>
))}
</select>
<select
value={severityFilter}
onChange={(e) => setSeverityFilter(e.target.value as ErrorSeverity | '')}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
{Object.entries(severityConfigs).map(([severity, config]) => (
<option key={severity} value={severity}>{config.label}</option>
))}
</select>
</div>
{/* 错误列表 */}
{filteredErrors.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<AlertTriangle 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">
{searchTerm || typeFilter || severityFilter
? '没有找到匹配的错误记录'
: '系统运行正常,没有错误记录'}
</p>
</div>
) : (
<div className="space-y-4">
{filteredErrors.map((error) => {
const typeConfig = errorTypeConfigs[error.error_type];
const severityConfig = severityConfigs[error.severity];
const isExpanded = expandedErrors.has(error.id);
const IconComponent = typeConfig.icon;
const solution = errorSolutions[error.error_type];
return (
<div
key={error.id}
className={`bg-white rounded-lg shadow-sm border-2 ${typeConfig.borderColor} ${
error.resolved ? 'opacity-60' : ''
}`}
>
{/* 错误头部 */}
<div className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
<IconComponent className={`w-5 h-5 mt-0.5 ${typeConfig.color}`} />
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<h3 className="text-sm font-medium text-gray-900">
{error.workflow_name}
</h3>
<span className={`px-2 py-1 text-xs rounded-full ${severityConfig.bgColor} ${severityConfig.color}`}>
{severityConfig.label}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
{error.resolved && (
<span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">
</span>
)}
</div>
<p className="text-sm text-gray-700 mb-2">
{error.error_message}
</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>ID: {error.id}</span>
<span>ID: {error.execution_id}</span>
<span>: {new Date(error.occurred_at).toLocaleString('zh-CN')}</span>
{error.environment_name && (
<span>: {error.environment_name}</span>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => copyErrorInfo(error)}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
title="复制错误信息"
>
<Copy className="w-4 h-4" />
</button>
{onRetryExecution && !error.resolved && (
<button
onClick={() => onRetryExecution(error.execution_id)}
className="p-1 text-gray-400 hover:text-green-600 transition-colors"
title="重试执行"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
<button
onClick={() => toggleErrorExpanded(error.id)}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
title={isExpanded ? '收起详情' : '展开详情'}
>
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
</div>
</div>
</div>
{/* 错误详情 */}
{isExpanded && (
<div className="border-t border-gray-200 p-4 space-y-4">
{/* 错误详细信息 */}
{error.error_details && (
<div>
<h4 className="text-sm font-medium text-gray-900 mb-2"></h4>
<pre className="text-xs text-gray-700 bg-gray-50 p-3 rounded overflow-auto max-h-32">
{JSON.stringify(error.error_details, null, 2)}
</pre>
</div>
)}
{/* 堆栈跟踪 */}
{error.stack_trace && (
<div>
<h4 className="text-sm font-medium text-gray-900 mb-2"></h4>
<pre className="text-xs text-gray-700 bg-gray-50 p-3 rounded overflow-auto max-h-32">
{error.stack_trace}
</pre>
</div>
)}
{/* 解决方案 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-2"></h4>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="text-sm font-medium text-blue-900 mb-1">{solution.title}</h5>
<p className="text-sm text-blue-700 mb-2">{solution.description}</p>
<div className="space-y-2">
<div>
<h6 className="text-xs font-medium text-blue-900">:</h6>
<ul className="text-xs text-blue-700 list-disc list-inside space-y-1">
{solution.steps.map((step, index) => (
<li key={index}>{step}</li>
))}
</ul>
</div>
<div>
<h6 className="text-xs font-medium text-blue-900">:</h6>
<ul className="text-xs text-blue-700 list-disc list-inside space-y-1">
{solution.prevention_tips.map((tip, index) => (
<li key={index}>{tip}</li>
))}
</ul>
</div>
</div>
</div>
</div>
{/* 解决标记 */}
{!error.resolved && onMarkResolved && (
<div className="bg-gray-50 rounded-lg p-3">
<h4 className="text-sm font-medium text-gray-900 mb-2"></h4>
<textarea
value={resolutionNotes[error.id] || ''}
onChange={(e) => setResolutionNotes(prev => ({
...prev,
[error.id]: e.target.value
}))}
placeholder="请输入解决方案或备注..."
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
onClick={() => handleMarkResolved(error.id)}
className="mt-2 px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700 transition-colors"
>
</button>
</div>
)}
{/* 已解决信息 */}
{error.resolved && error.resolution_notes && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<h4 className="text-sm font-medium text-green-900 mb-1"></h4>
<p className="text-sm text-green-700">{error.resolution_notes}</p>
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,421 @@
/**
* 执行详情查看器组件
*
* 显示工作流执行的完整详情信息,包括输入输出数据、错误日志等
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState } from 'react';
import {
X,
Clock,
CheckCircle,
XCircle,
AlertCircle,
RefreshCw,
Download,
Copy,
Eye,
Code,
Image as ImageIcon,
FileText,
Calendar,
Server,
User,
Tag
} from 'lucide-react';
// 执行状态枚举
type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
// 执行记录接口
interface WorkflowExecutionRecord {
id: number;
workflow_template_id: number;
workflow_name: string;
workflow_version: string;
execution_environment_id?: number;
execution_environment_name?: string;
input_data_json: any;
output_data_json?: any;
status: ExecutionStatus;
progress: number;
comfyui_prompt_id?: string;
comfyui_workflow_name?: string;
started_at?: string;
completed_at?: string;
duration_seconds?: number;
error_message?: string;
error_details_json?: any;
user_id?: string;
session_id?: string;
metadata_json?: any;
tags?: string[];
created_at: string;
updated_at: string;
}
interface ExecutionDetailViewerProps {
/** 执行记录 */
record: WorkflowExecutionRecord;
/** 是否显示模态框 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 下载结果回调 */
onDownload?: (record: WorkflowExecutionRecord) => void;
/** 重试回调 */
onRetry?: (record: WorkflowExecutionRecord) => void;
}
/**
* 执行详情查看器组件
*/
export const ExecutionDetailViewer: React.FC<ExecutionDetailViewerProps> = ({
record,
isOpen,
onClose,
onDownload,
onRetry
}) => {
const [activeTab, setActiveTab] = useState<'overview' | 'input' | 'output' | 'logs' | 'timeline'>('overview');
const [showRawData, setShowRawData] = useState(false);
if (!isOpen) return null;
// 获取状态图标和颜色
const getStatusIcon = (status: ExecutionStatus) => {
switch (status) {
case 'pending':
return <Clock className="w-5 h-5 text-yellow-500" />;
case 'running':
return <RefreshCw className="w-5 h-5 text-blue-500 animate-spin" />;
case 'completed':
return <CheckCircle className="w-5 h-5 text-green-500" />;
case 'failed':
return <XCircle className="w-5 h-5 text-red-500" />;
case 'cancelled':
return <AlertCircle className="w-5 h-5 text-gray-500" />;
default:
return <Clock className="w-5 h-5 text-gray-400" />;
}
};
// 获取状态文本
const getStatusText = (status: ExecutionStatus) => {
const statusMap = {
pending: '等待中',
running: '执行中',
completed: '已完成',
failed: '失败',
cancelled: '已取消'
};
return statusMap[status] || '未知';
};
// 格式化时间
const formatTime = (timeString?: string) => {
if (!timeString) return '-';
return new Date(timeString).toLocaleString('zh-CN');
};
// 格式化持续时间
const formatDuration = (seconds?: number) => {
if (!seconds) return '-';
if (seconds < 60) {
return `${seconds}`;
} else if (seconds < 3600) {
return `${Math.floor(seconds / 60)}${seconds % 60}`;
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}小时${minutes}分钟`;
}
};
// 复制到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
// TODO: 显示成功提示
} catch (err) {
console.error('复制失败:', err);
}
};
// 渲染JSON数据
const renderJsonData = (data: any, title: string) => {
if (!data) {
return (
<div className="text-center py-8 text-gray-500">
{title}
</div>
);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">{title}</h4>
<div className="flex items-center space-x-2">
<button
onClick={() => setShowRawData(!showRawData)}
className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center space-x-1"
>
<Code className="w-3 h-3" />
<span>{showRawData ? '格式化' : '原始数据'}</span>
</button>
<button
onClick={() => copyToClipboard(JSON.stringify(data, null, 2))}
className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center space-x-1"
>
<Copy className="w-3 h-3" />
<span></span>
</button>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 overflow-auto max-h-96">
<pre className="text-sm text-gray-800 whitespace-pre-wrap">
{showRawData ? JSON.stringify(data) : JSON.stringify(data, null, 2)}
</pre>
</div>
</div>
);
};
// 渲染概览标签页
const renderOverviewTab = () => (
<div className="space-y-6">
{/* 基本信息 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 flex items-center space-x-2">
<FileText className="w-4 h-4" />
<span></span>
</h4>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500">ID:</span>
<span className="text-sm text-gray-900 font-mono">{record.id}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">{record.workflow_name}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">v{record.workflow_version}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<div className="flex items-center space-x-2">
{getStatusIcon(record.status)}
<span className="text-sm text-gray-900">{getStatusText(record.status)}</span>
</div>
</div>
{record.progress > 0 && record.status === 'running' && (
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">{record.progress}%</span>
</div>
)}
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 flex items-center space-x-2">
<Calendar className="w-4 h-4" />
<span></span>
</h4>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">{formatTime(record.created_at)}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">{formatTime(record.started_at)}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">{formatTime(record.completed_at)}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">{formatDuration(record.duration_seconds)}</span>
</div>
</div>
</div>
</div>
{/* 执行环境信息 */}
{record.execution_environment_name && (
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 flex items-center space-x-2">
<Server className="w-4 h-4" />
<span></span>
</h4>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex justify-between">
<span className="text-sm text-gray-500">:</span>
<span className="text-sm text-gray-900">{record.execution_environment_name}</span>
</div>
{record.comfyui_prompt_id && (
<div className="flex justify-between mt-2">
<span className="text-sm text-gray-500">ComfyUI任务ID:</span>
<span className="text-sm text-gray-900 font-mono">{record.comfyui_prompt_id}</span>
</div>
)}
</div>
</div>
)}
{/* 错误信息 */}
{record.error_message && (
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 flex items-center space-x-2">
<XCircle className="w-4 h-4 text-red-500" />
<span></span>
</h4>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm text-red-700">{record.error_message}</p>
{record.error_details_json && (
<details className="mt-2">
<summary className="text-sm text-red-600 cursor-pointer hover:text-red-800">
</summary>
<pre className="mt-2 text-xs text-red-600 bg-red-100 p-2 rounded overflow-auto">
{JSON.stringify(record.error_details_json, null, 2)}
</pre>
</details>
)}
</div>
</div>
)}
{/* 标签 */}
{record.tags && record.tags.length > 0 && (
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 flex items-center space-x-2">
<Tag className="w-4 h-4" />
<span></span>
</h4>
<div className="flex flex-wrap gap-2">
{record.tags.map((tag, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
</div>
)}
</div>
);
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-[90vh] 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">
{getStatusIcon(record.status)}
<div>
<h2 className="text-lg font-semibold text-gray-900">
- {record.workflow_name}
</h2>
<p className="text-sm text-gray-500">
ID: {record.id} | v{record.workflow_version}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
{record.status === 'failed' && onRetry && (
<button
onClick={() => onRetry(record)}
className="px-3 py-1 bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors flex items-center space-x-1"
>
<RefreshCw className="w-4 h-4" />
<span></span>
</button>
)}
{record.status === 'failed' && (
<button
onClick={() => {
// TODO: 打开错误分析器
console.log('分析错误:', record);
}}
className="px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors flex items-center space-x-1"
>
<AlertCircle className="w-4 h-4" />
<span></span>
</button>
)}
{record.output_data_json && onDownload && (
<button
onClick={() => onDownload(record)}
className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors flex items-center space-x-1"
>
<Download className="w-4 h-4" />
<span></span>
</button>
)}
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* 标签导航 */}
<div className="border-b border-gray-200">
<nav className="flex space-x-8 px-6">
{[
{ id: 'overview', label: '概览', icon: Eye },
{ id: 'input', label: '输入数据', icon: FileText },
{ id: 'output', label: '输出结果', icon: ImageIcon },
{ id: 'logs', label: '执行日志', icon: Code },
{ id: 'timeline', label: '时间线', icon: Clock }
].map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setActiveTab(id as any)}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center space-x-2 ${
activeTab === id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="w-4 h-4" />
<span>{label}</span>
</button>
))}
</nav>
</div>
{/* 内容区域 */}
<div className="flex-1 overflow-y-auto p-6">
{activeTab === 'overview' && renderOverviewTab()}
{activeTab === 'input' && renderJsonData(record.input_data_json, '输入数据')}
{activeTab === 'output' && renderJsonData(record.output_data_json, '输出结果')}
{activeTab === 'logs' && renderJsonData(record.error_details_json, '执行日志')}
{activeTab === 'timeline' && (
<div className="text-center py-8 text-gray-500">
线...
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,576 @@
/**
* 执行历史列表组件
*
* 显示工作流执行历史记录,支持分页、筛选、搜索和批量操作
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
Clock,
CheckCircle,
XCircle,
AlertCircle,
Play,
Download,
Trash2,
RefreshCw,
Search,
Filter,
Calendar,
MoreHorizontal,
Eye
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
// 执行状态枚举
type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
// 执行记录接口
interface WorkflowExecutionRecord {
id: number;
workflow_template_id: number;
workflow_name: string;
workflow_version: string;
execution_environment_id?: number;
execution_environment_name?: string;
input_data_json: any;
output_data_json?: any;
status: ExecutionStatus;
progress: number;
comfyui_prompt_id?: string;
comfyui_workflow_name?: string;
started_at?: string;
completed_at?: string;
duration_seconds?: number;
error_message?: string;
error_details_json?: any;
user_id?: string;
session_id?: string;
metadata_json?: any;
tags?: string[];
created_at: string;
updated_at: string;
}
// 筛选器接口
interface ExecutionRecordFilter {
workflow_template_id?: number;
status?: ExecutionStatus;
user_id?: string;
session_id?: string;
comfyui_prompt_id?: string;
date_from?: string;
date_to?: string;
}
// 组件属性接口
interface ExecutionHistoryListProps {
/** 筛选条件 */
filter?: ExecutionRecordFilter;
/** 查看详情回调 */
onViewDetails: (record: WorkflowExecutionRecord) => void;
/** 重试回调 */
onRetry: (record: WorkflowExecutionRecord) => void;
/** 删除回调 */
onDelete: (recordId: number) => void;
/** 批量删除回调 */
onBatchDelete?: (recordIds: number[]) => void;
/** 下载结果回调 */
onDownload?: (record: WorkflowExecutionRecord) => void;
}
/**
* 执行历史列表组件
*/
export const ExecutionHistoryList: React.FC<ExecutionHistoryListProps> = ({
filter,
onViewDetails,
onRetry,
onDelete,
onBatchDelete,
onDownload
}) => {
const [records, setRecords] = useState<WorkflowExecutionRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<ExecutionStatus | ''>('');
const [dateFilter, setDateFilter] = useState<'today' | 'week' | 'month' | 'all'>('all');
const [selectedRecords, setSelectedRecords] = useState<Set<number>>(new Set());
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [pageSize] = useState(20);
// 加载执行历史
const loadExecutionHistory = useCallback(async () => {
try {
setLoading(true);
setError(null);
// 构建筛选条件
const filterConditions: ExecutionRecordFilter = {
...filter,
status: statusFilter || undefined,
};
// 添加日期筛选
if (dateFilter !== 'all') {
const now = new Date();
let dateFrom: Date;
switch (dateFilter) {
case 'today':
dateFrom = new Date(now.getFullYear(), now.getMonth(), now.getDate());
break;
case 'week':
dateFrom = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'month':
dateFrom = new Date(now.getFullYear(), now.getMonth(), 1);
break;
default:
dateFrom = new Date(0);
}
filterConditions.date_from = dateFrom.toISOString();
}
const result = await invoke<WorkflowExecutionRecord[]>('get_execution_history', {
filter: filterConditions,
limit: pageSize,
offset: (currentPage - 1) * pageSize
});
setRecords(result);
// TODO: 获取总数来计算总页数
setTotalPages(Math.ceil(result.length / pageSize));
} catch (err) {
console.error('加载执行历史失败:', err);
setError('加载执行历史失败');
} finally {
setLoading(false);
}
}, [filter, statusFilter, dateFilter, currentPage, pageSize]);
// 初始加载和依赖更新时重新加载
useEffect(() => {
loadExecutionHistory();
}, [loadExecutionHistory]);
// 获取状态图标和颜色
const getStatusIcon = (status: ExecutionStatus) => {
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 <AlertCircle className="w-4 h-4 text-gray-500" />;
default:
return <Clock className="w-4 h-4 text-gray-400" />;
}
};
// 获取状态文本
const getStatusText = (status: ExecutionStatus) => {
const statusMap = {
pending: '等待中',
running: '执行中',
completed: '已完成',
failed: '失败',
cancelled: '已取消'
};
return statusMap[status] || '未知';
};
// 格式化持续时间
const formatDuration = (seconds?: number) => {
if (!seconds) return '-';
if (seconds < 60) {
return `${seconds}`;
} else if (seconds < 3600) {
return `${Math.floor(seconds / 60)}${seconds % 60}`;
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}小时${minutes}分钟`;
}
};
// 格式化时间
const formatTime = (timeString?: string) => {
if (!timeString) return '-';
return new Date(timeString).toLocaleString('zh-CN');
};
// 处理全选/取消全选
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedRecords(new Set(records.map(r => r.id)));
} else {
setSelectedRecords(new Set());
}
};
// 处理单个选择
const handleSelectRecord = (recordId: number, checked: boolean) => {
const newSelected = new Set(selectedRecords);
if (checked) {
newSelected.add(recordId);
} else {
newSelected.delete(recordId);
}
setSelectedRecords(newSelected);
};
// 处理批量删除
const handleBatchDelete = () => {
if (selectedRecords.size > 0 && onBatchDelete) {
onBatchDelete(Array.from(selectedRecords));
setSelectedRecords(new Set());
}
};
// 筛选记录
const filteredRecords = records.filter(record => {
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
return (
record.workflow_name.toLowerCase().includes(searchLower) ||
record.comfyui_prompt_id?.toLowerCase().includes(searchLower) ||
record.error_message?.toLowerCase().includes(searchLower)
);
}
return true;
});
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<RefreshCw className="w-8 h-8 text-blue-500 animate-spin" />
<span className="ml-2 text-gray-600">...</span>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<XCircle className="w-5 h-5 text-red-500 mr-2" />
<span className="text-red-700">{error}</span>
</div>
<button
onClick={loadExecutionHistory}
className="mt-2 px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors"
>
</button>
</div>
);
}
return (
<div className="space-y-6">
{/* 搜索和筛选栏 */}
<div className="flex flex-col sm:flex-row gap-4">
{/* 搜索框 */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="搜索工作流名称、任务ID或错误信息..."
value={searchTerm}
onChange={(e) => setSearchTerm(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-transparent"
/>
</div>
{/* 状态筛选 */}
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as ExecutionStatus | '')}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
<option value="pending"></option>
<option value="running"></option>
<option value="completed"></option>
<option value="failed"></option>
<option value="cancelled"></option>
</select>
{/* 日期筛选 */}
<select
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value as 'today' | 'week' | 'month' | 'all')}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all"></option>
<option value="today"></option>
<option value="week"></option>
<option value="month"></option>
</select>
{/* 刷新按钮 */}
<button
onClick={loadExecutionHistory}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center space-x-2"
>
<RefreshCw className="w-4 h-4" />
<span></span>
</button>
</div>
{/* 批量操作栏 */}
{selectedRecords.size > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-blue-700">
{selectedRecords.size}
</span>
<div className="flex items-center space-x-2">
{onBatchDelete && (
<button
onClick={handleBatchDelete}
className="px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors flex items-center space-x-1"
>
<Trash2 className="w-4 h-4" />
<span></span>
</button>
)}
<button
onClick={() => setSelectedRecords(new Set())}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
>
</button>
</div>
</div>
</div>
)}
{/* 执行记录表格 */}
{filteredRecords.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<Clock 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">
{searchTerm || statusFilter || dateFilter !== 'all'
? '没有找到匹配的执行记录'
: '还没有执行过任何工作流'}
</p>
</div>
) : (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left">
<input
type="checkbox"
checked={selectedRecords.size === filteredRecords.length && filteredRecords.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecords.map((record) => (
<tr key={record.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<input
type="checkbox"
checked={selectedRecords.has(record.id)}
onChange={(e) => handleSelectRecord(record.id, e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
</td>
<td className="px-6 py-4">
<div className="flex flex-col">
<div className="text-sm font-medium text-gray-900">
{record.workflow_name}
</div>
<div className="text-sm text-gray-500">
v{record.workflow_version}
</div>
{record.comfyui_prompt_id && (
<div className="text-xs text-gray-400 font-mono">
ID: {record.comfyui_prompt_id}
</div>
)}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center space-x-2">
{getStatusIcon(record.status)}
<span className="text-sm text-gray-900">
{getStatusText(record.status)}
</span>
</div>
{record.status === 'running' && (
<div className="mt-1">
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-blue-600 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${record.progress}%` }}
/>
</div>
<div className="text-xs text-gray-500 mt-1">
{record.progress}%
</div>
</div>
)}
{record.error_message && (
<div className="mt-1 text-xs text-red-600 truncate max-w-xs" title={record.error_message}>
{record.error_message}
</div>
)}
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{record.execution_environment_name || '-'}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{formatTime(record.started_at)}
</div>
<div className="text-xs text-gray-500">
: {formatTime(record.created_at)}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{formatDuration(record.duration_seconds)}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center space-x-2">
<button
onClick={() => onViewDetails(record)}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
title="查看详情"
>
<Eye className="w-4 h-4" />
</button>
{record.status === 'failed' && (
<button
onClick={() => onRetry(record)}
className="p-1 text-gray-400 hover:text-green-600 transition-colors"
title="重试"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
{record.output_data_json && onDownload && (
<button
onClick={() => onDownload(record)}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
title="下载结果"
>
<Download className="w-4 h-4" />
</button>
)}
<button
onClick={() => onDelete(record.id)}
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
title="删除"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex items-center">
<p className="text-sm text-gray-700">
{(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, filteredRecords.length)}
{filteredRecords.length}
</p>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const page = i + 1;
return (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-1 text-sm border rounded ${
currentPage === page
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 hover:bg-gray-50'
}`}
>
{page}
</button>
);
})}
</div>
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
</div>
)}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,375 @@
/**
* 执行监控器组件
*
* 实时监控活跃的工作流执行状态,提供进度显示和控制功能
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
Activity,
Clock,
RefreshCw,
Square,
Eye,
AlertCircle,
CheckCircle,
XCircle,
Zap,
Server,
Timer,
Gauge
} from 'lucide-react';
// 执行状态枚举
type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
// 执行记录接口
interface WorkflowExecutionRecord {
id: number;
workflow_template_id: number;
workflow_name: string;
workflow_version: string;
execution_environment_id?: number;
execution_environment_name?: string;
input_data_json: any;
output_data_json?: any;
status: ExecutionStatus;
progress: number;
comfyui_prompt_id?: string;
comfyui_workflow_name?: string;
started_at?: string;
completed_at?: string;
duration_seconds?: number;
error_message?: string;
error_details_json?: any;
user_id?: string;
session_id?: string;
metadata_json?: any;
tags?: string[];
created_at: string;
updated_at: string;
}
// 组件属性接口
interface ExecutionMonitorProps {
/** 活跃的执行记录 */
activeExecutions: WorkflowExecutionRecord[];
/** 取消执行回调 */
onCancel: (executionId: number) => void;
/** 查看日志回调 */
onViewLogs: (executionId: number) => void;
/** 查看详情回调 */
onViewDetails?: (record: WorkflowExecutionRecord) => void;
/** 刷新回调 */
onRefresh?: () => void;
/** 自动刷新间隔(毫秒) */
refreshInterval?: number;
}
/**
* 执行监控器组件
*/
export const ExecutionMonitor: React.FC<ExecutionMonitorProps> = ({
activeExecutions,
onCancel,
onViewLogs,
onViewDetails,
onRefresh,
refreshInterval = 2000
}) => {
const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date());
// 自动刷新
useEffect(() => {
if (!onRefresh || refreshInterval <= 0) return;
const interval = setInterval(() => {
onRefresh();
setLastUpdateTime(new Date());
}, refreshInterval);
return () => clearInterval(interval);
}, [onRefresh, refreshInterval]);
// 获取状态图标
const getStatusIcon = (status: ExecutionStatus) => {
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 <AlertCircle className="w-4 h-4 text-gray-500" />;
default:
return <Clock className="w-4 h-4 text-gray-400" />;
}
};
// 获取状态文本
const getStatusText = (status: ExecutionStatus) => {
const statusMap = {
pending: '等待中',
running: '执行中',
completed: '已完成',
failed: '失败',
cancelled: '已取消'
};
return statusMap[status] || '未知';
};
// 计算执行时间
const getExecutionTime = (startedAt?: string) => {
if (!startedAt) return '-';
const start = new Date(startedAt);
const now = new Date();
const diffMs = now.getTime() - start.getTime();
const diffSeconds = Math.floor(diffMs / 1000);
if (diffSeconds < 60) {
return `${diffSeconds}`;
} else if (diffSeconds < 3600) {
const minutes = Math.floor(diffSeconds / 60);
const seconds = diffSeconds % 60;
return `${minutes}${seconds}`;
} else {
const hours = Math.floor(diffSeconds / 3600);
const minutes = Math.floor((diffSeconds % 3600) / 60);
return `${hours}小时${minutes}分钟`;
}
};
// 估算剩余时间
const getEstimatedTimeRemaining = (progress: number, startedAt?: string) => {
if (!startedAt || progress <= 0) return '-';
const start = new Date(startedAt);
const now = new Date();
const elapsedMs = now.getTime() - start.getTime();
if (progress >= 100) return '即将完成';
const estimatedTotalMs = (elapsedMs / progress) * 100;
const remainingMs = estimatedTotalMs - elapsedMs;
const remainingSeconds = Math.floor(remainingMs / 1000);
if (remainingSeconds < 60) {
return `${remainingSeconds}`;
} else if (remainingSeconds < 3600) {
const minutes = Math.floor(remainingSeconds / 60);
return `${minutes}分钟`;
} else {
const hours = Math.floor(remainingSeconds / 3600);
const minutes = Math.floor((remainingSeconds % 3600) / 60);
return `${hours}小时${minutes}分钟`;
}
};
// 格式化时间
const formatTime = (date: Date) => {
return date.toLocaleTimeString('zh-CN');
};
// 统计信息
const stats = {
total: activeExecutions.length,
pending: activeExecutions.filter(e => e.status === 'pending').length,
running: activeExecutions.filter(e => e.status === 'running').length,
completed: activeExecutions.filter(e => e.status === 'completed').length,
failed: activeExecutions.filter(e => e.status === 'failed').length
};
return (
<div className="space-y-6">
{/* 监控头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Activity className="w-6 h-6 text-blue-600" />
<div>
<h2 className="text-lg font-medium text-gray-900">
</h2>
<p className="text-sm text-gray-500">
: {formatTime(lastUpdateTime)}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
{/* 统计信息 */}
<div className="flex items-center space-x-4 text-sm">
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<span className="text-gray-600">: {stats.pending}</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-gray-600">: {stats.running}</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-gray-600">: {stats.completed}</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<span className="text-gray-600">: {stats.failed}</span>
</div>
</div>
{onRefresh && (
<button
onClick={onRefresh}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center space-x-1"
>
<RefreshCw className="w-4 h-4" />
<span></span>
</button>
)}
</div>
</div>
{/* 执行列表 */}
{activeExecutions.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<Activity 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>
) : (
<div className="space-y-4">
{activeExecutions.map((execution) => (
<div
key={execution.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-4"
>
<div className="flex items-start justify-between">
<div className="flex-1">
{/* 执行基本信息 */}
<div className="flex items-center space-x-3 mb-3">
{getStatusIcon(execution.status)}
<div className="flex-1">
<div className="flex items-center space-x-2">
<h3 className="text-sm font-medium text-gray-900">
{execution.workflow_name}
</h3>
<span className="text-xs text-gray-500">
v{execution.workflow_version}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${
execution.status === 'running'
? 'bg-blue-100 text-blue-800'
: execution.status === 'pending'
? 'bg-yellow-100 text-yellow-800'
: execution.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{getStatusText(execution.status)}
</span>
</div>
<div className="flex items-center space-x-4 mt-1 text-xs text-gray-500">
<div className="flex items-center space-x-1">
<Timer className="w-3 h-3" />
<span>ID: {execution.id}</span>
</div>
{execution.execution_environment_name && (
<div className="flex items-center space-x-1">
<Server className="w-3 h-3" />
<span>{execution.execution_environment_name}</span>
</div>
)}
{execution.comfyui_prompt_id && (
<div className="flex items-center space-x-1">
<Zap className="w-3 h-3" />
<span>ComfyUI: {execution.comfyui_prompt_id}</span>
</div>
)}
</div>
</div>
</div>
{/* 进度条 */}
{execution.status === 'running' && (
<div className="mb-3">
<div className="flex items-center justify-between text-xs text-gray-600 mb-1">
<span></span>
<span>{execution.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: `${execution.progress}%` }}
/>
</div>
</div>
)}
{/* 时间信息 */}
<div className="grid grid-cols-2 gap-4 text-xs text-gray-600">
<div className="flex items-center space-x-1">
<Clock className="w-3 h-3" />
<span>: {getExecutionTime(execution.started_at)}</span>
</div>
{execution.status === 'running' && (
<div className="flex items-center space-x-1">
<Gauge className="w-3 h-3" />
<span>: {getEstimatedTimeRemaining(execution.progress, execution.started_at)}</span>
</div>
)}
</div>
{/* 错误信息 */}
{execution.error_message && (
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700">
{execution.error_message}
</div>
)}
</div>
{/* 操作按钮 */}
<div className="flex items-center space-x-2 ml-4">
<button
onClick={() => onViewLogs(execution.id)}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
title="查看日志"
>
<Eye className="w-4 h-4" />
</button>
{onViewDetails && (
<button
onClick={() => onViewDetails(execution)}
className="p-1 text-gray-400 hover:text-green-600 transition-colors"
title="查看详情"
>
<Activity className="w-4 h-4" />
</button>
)}
{(execution.status === 'pending' || execution.status === 'running') && (
<button
onClick={() => onCancel(execution.id)}
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
title="取消执行"
>
<Square className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,289 @@
/**
* 监控仪表板组件
*
* 综合的监控界面,包含执行监控器和系统状态面板
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect, useCallback } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { ExecutionMonitor } from './ExecutionMonitor';
import { SystemStatusPanel } from './SystemStatusPanel';
// 执行状态枚举
type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
// 执行记录接口
interface WorkflowExecutionRecord {
id: number;
workflow_template_id: number;
workflow_name: string;
workflow_version: string;
execution_environment_id?: number;
execution_environment_name?: string;
input_data_json: any;
output_data_json?: any;
status: ExecutionStatus;
progress: number;
comfyui_prompt_id?: string;
comfyui_workflow_name?: string;
started_at?: string;
completed_at?: string;
duration_seconds?: number;
error_message?: string;
error_details_json?: any;
user_id?: string;
session_id?: string;
metadata_json?: any;
tags?: string[];
created_at: string;
updated_at: string;
}
// 系统统计信息接口
interface SystemStatistics {
execution_stats: {
total_executions_today: number;
successful_executions_today: number;
failed_executions_today: number;
average_execution_time_seconds: number;
current_queue_size: number;
active_executions: number;
};
environment_stats: {
total_environments: number;
healthy_environments: number;
unhealthy_environments: number;
average_response_time_ms: number;
total_capacity: number;
used_capacity: number;
};
system_resources: {
cpu_usage_percent: number;
memory_usage_percent: number;
disk_usage_percent: number;
network_status: 'connected' | 'disconnected' | 'limited';
};
performance_metrics: {
requests_per_minute: number;
average_response_time_ms: number;
error_rate_percent: number;
uptime_hours: number;
};
}
// 组件属性接口
interface MonitoringDashboardProps {
/** 查看执行详情回调 */
onViewExecutionDetails?: (record: WorkflowExecutionRecord) => void;
/** 自动刷新间隔(毫秒) */
refreshInterval?: number;
}
/**
* 监控仪表板组件
*/
export const MonitoringDashboard: React.FC<MonitoringDashboardProps> = ({
onViewExecutionDetails,
refreshInterval = 3000
}) => {
const [activeExecutions, setActiveExecutions] = useState<WorkflowExecutionRecord[]>([]);
const [systemStats, setSystemStats] = useState<SystemStatistics>({
execution_stats: {
total_executions_today: 0,
successful_executions_today: 0,
failed_executions_today: 0,
average_execution_time_seconds: 0,
current_queue_size: 0,
active_executions: 0
},
environment_stats: {
total_environments: 0,
healthy_environments: 0,
unhealthy_environments: 0,
average_response_time_ms: 0,
total_capacity: 0,
used_capacity: 0
},
system_resources: {
cpu_usage_percent: 0,
memory_usage_percent: 0,
disk_usage_percent: 0,
network_status: 'connected'
},
performance_metrics: {
requests_per_minute: 0,
average_response_time_ms: 0,
error_rate_percent: 0,
uptime_hours: 0
}
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 加载活跃执行
const loadActiveExecutions = useCallback(async () => {
try {
const result = await invoke<WorkflowExecutionRecord[]>('get_execution_history', {
filter: {
status: 'running' // 只获取正在执行的
},
limit: 50,
offset: 0
});
// 同时获取等待中的执行
const pendingResult = await invoke<WorkflowExecutionRecord[]>('get_execution_history', {
filter: {
status: 'pending'
},
limit: 50,
offset: 0
});
setActiveExecutions([...result, ...pendingResult]);
} catch (err) {
console.error('加载活跃执行失败:', err);
// 使用模拟数据
setActiveExecutions([]);
}
}, []);
// 加载系统统计
const loadSystemStats = useCallback(async () => {
try {
// TODO: 实现实际的系统统计API
// const result = await invoke<SystemStatistics>('get_system_statistics');
// setSystemStats(result);
// 暂时使用模拟数据
const mockStats: SystemStatistics = {
execution_stats: {
total_executions_today: 156,
successful_executions_today: 142,
failed_executions_today: 14,
average_execution_time_seconds: 45.6,
current_queue_size: activeExecutions.filter(e => e.status === 'pending').length,
active_executions: activeExecutions.filter(e => e.status === 'running').length
},
environment_stats: {
total_environments: 3,
healthy_environments: 2,
unhealthy_environments: 1,
average_response_time_ms: 1250,
total_capacity: 16,
used_capacity: activeExecutions.filter(e => e.status === 'running').length
},
system_resources: {
cpu_usage_percent: Math.random() * 30 + 20, // 20-50%
memory_usage_percent: Math.random() * 20 + 40, // 40-60%
disk_usage_percent: Math.random() * 10 + 65, // 65-75%
network_status: 'connected'
},
performance_metrics: {
requests_per_minute: 24,
average_response_time_ms: 1250,
error_rate_percent: 8.97,
uptime_hours: 72.5
}
};
setSystemStats(mockStats);
} catch (err) {
console.error('加载系统统计失败:', err);
}
}, [activeExecutions]);
// 刷新所有数据
const refreshData = useCallback(async () => {
setError(null);
try {
await Promise.all([
loadActiveExecutions(),
loadSystemStats()
]);
} catch (err) {
setError('刷新数据失败');
} finally {
setLoading(false);
}
}, [loadActiveExecutions, loadSystemStats]);
// 初始加载
useEffect(() => {
refreshData();
}, [refreshData]);
// 处理取消执行
const handleCancelExecution = async (executionId: number) => {
try {
await invoke('cancel_execution', { executionId });
// 刷新数据
await loadActiveExecutions();
} catch (err) {
console.error('取消执行失败:', err);
}
};
// 处理查看日志
const handleViewLogs = async (executionId: number) => {
try {
// TODO: 实现查看日志功能
console.log('查看执行日志:', executionId);
} catch (err) {
console.error('查看日志失败:', err);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-red-700">{error}</span>
<button
onClick={refreshData}
className="px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors"
>
</button>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* 系统状态面板 */}
<SystemStatusPanel
systemStats={systemStats}
refreshInterval={refreshInterval}
onRefresh={refreshData}
/>
{/* 执行监控器 */}
<ExecutionMonitor
activeExecutions={activeExecutions}
onCancel={handleCancelExecution}
onViewLogs={handleViewLogs}
onViewDetails={onViewExecutionDetails}
onRefresh={loadActiveExecutions}
refreshInterval={refreshInterval}
/>
</div>
);
};

View File

@@ -0,0 +1,365 @@
/**
* 快速导出按钮组件
*
* 提供快速导出功能的下拉菜单按钮
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useRef, useEffect } from 'react';
import {
Download,
ChevronDown,
FileText,
Database,
BarChart3,
Archive,
Settings
} from 'lucide-react';
import type {
WorkflowTemplate,
WorkflowExecutionRecord
} from '../../types/workflow';
// 快速导出选项接口
interface QuickExportOption {
id: string;
label: string;
description: string;
icon: React.ComponentType<any>;
format: string;
type: string;
}
// 组件属性接口
interface QuickExportButtonProps {
/** 选中的工作流模板 */
selectedTemplates?: WorkflowTemplate[];
/** 选中的执行记录 */
selectedExecutions?: WorkflowExecutionRecord[];
/** 导出回调 */
onExport?: (option: QuickExportOption, data: any[]) => void;
/** 打开完整导出器回调 */
onOpenFullExporter?: () => void;
/** 按钮大小 */
size?: 'sm' | 'md' | 'lg';
/** 是否禁用 */
disabled?: boolean;
}
/**
* 快速导出按钮组件
*/
export const QuickExportButton: React.FC<QuickExportButtonProps> = ({
selectedTemplates = [],
selectedExecutions = [],
onExport,
onOpenFullExporter,
size = 'md',
disabled = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// 快速导出选项
const quickExportOptions: QuickExportOption[] = [
{
id: 'templates_json',
label: '模板 (JSON)',
description: '导出工作流模板为JSON格式',
icon: FileText,
format: 'json',
type: 'templates'
},
{
id: 'executions_csv',
label: '执行记录 (CSV)',
description: '导出执行记录为CSV表格',
icon: Database,
format: 'csv',
type: 'executions'
},
{
id: 'executions_excel',
label: '执行记录 (Excel)',
description: '导出执行记录为Excel工作簿',
icon: BarChart3,
format: 'excel',
type: 'executions'
},
{
id: 'results_zip',
label: '执行结果 (ZIP)',
description: '打包下载所有执行结果',
icon: Archive,
format: 'zip',
type: 'results'
}
];
// 获取可用的导出选项
const getAvailableOptions = (): QuickExportOption[] => {
const options: QuickExportOption[] = [];
if (selectedTemplates.length > 0) {
options.push(quickExportOptions.find(opt => opt.id === 'templates_json')!);
}
if (selectedExecutions.length > 0) {
options.push(
quickExportOptions.find(opt => opt.id === 'executions_csv')!,
quickExportOptions.find(opt => opt.id === 'executions_excel')!,
quickExportOptions.find(opt => opt.id === 'results_zip')!
);
}
return options.filter(Boolean);
};
// 处理点击外部关闭下拉菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// 处理快速导出
const handleQuickExport = async (option: QuickExportOption) => {
setIsOpen(false);
if (!onExport) return;
// 根据导出类型准备数据
let data: any[] = [];
switch (option.type) {
case 'templates':
data = selectedTemplates;
break;
case 'executions':
case 'results':
data = selectedExecutions;
break;
}
// 执行快速导出
try {
await performQuickExport(option, data);
if (onExport) {
onExport(option, data);
}
} catch (error) {
console.error('快速导出失败:', error);
}
};
// 执行快速导出
const performQuickExport = async (option: QuickExportOption, data: any[]) => {
// 根据格式生成导出数据
let exportData: any;
let filename: string;
let mimeType: string;
switch (option.format) {
case 'json':
exportData = JSON.stringify({
export_type: option.type,
export_time: new Date().toISOString(),
count: data.length,
data: data
}, null, 2);
filename = `${option.type}_${new Date().toISOString().split('T')[0]}.json`;
mimeType = 'application/json';
break;
case 'csv':
exportData = convertToCSV(data);
filename = `${option.type}_${new Date().toISOString().split('T')[0]}.csv`;
mimeType = 'text/csv';
break;
case 'excel':
// 简化的Excel导出实际应用中可能需要使用专门的库
exportData = convertToCSV(data);
filename = `${option.type}_${new Date().toISOString().split('T')[0]}.csv`;
mimeType = 'text/csv';
break;
case 'zip':
// ZIP格式需要特殊处理这里简化为JSON
exportData = JSON.stringify({
export_type: option.type,
export_time: new Date().toISOString(),
count: data.length,
data: data
}, null, 2);
filename = `${option.type}_${new Date().toISOString().split('T')[0]}.json`;
mimeType = 'application/json';
break;
default:
throw new Error(`不支持的导出格式: ${option.format}`);
}
// 创建并下载文件
const blob = new Blob([exportData], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};
// 转换为CSV格式
const convertToCSV = (data: any[]): string => {
if (data.length === 0) return '';
// 获取所有字段名
const fields = new Set<string>();
data.forEach(item => {
Object.keys(item).forEach(key => fields.add(key));
});
const fieldArray = Array.from(fields);
// 生成CSV头部
const header = fieldArray.join(',');
// 生成CSV数据行
const rows = data.map(item => {
return fieldArray.map(field => {
const value = item[field];
if (value === null || value === undefined) return '';
if (typeof value === 'object') return JSON.stringify(value);
if (typeof value === 'string' && value.includes(',')) return `"${value}"`;
return value;
}).join(',');
});
return [header, ...rows].join('\n');
};
// 获取按钮样式类
const getButtonClasses = () => {
const baseClasses = 'inline-flex items-center border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors';
switch (size) {
case 'sm':
return `${baseClasses} px-2 py-1 text-xs rounded`;
case 'lg':
return `${baseClasses} px-4 py-3 text-base rounded-lg`;
default:
return `${baseClasses} px-3 py-2 text-sm rounded-lg`;
}
};
const availableOptions = getAvailableOptions();
const hasData = selectedTemplates.length > 0 || selectedExecutions.length > 0;
if (!hasData && !onOpenFullExporter) {
return null;
}
return (
<div className="relative" ref={dropdownRef}>
<div className="flex">
{/* 主导出按钮 */}
<button
onClick={() => {
if (availableOptions.length === 1) {
handleQuickExport(availableOptions[0]);
} else if (onOpenFullExporter) {
onOpenFullExporter();
}
}}
disabled={disabled || (!hasData && !onOpenFullExporter)}
className={`${getButtonClasses()} ${
availableOptions.length > 1 ? 'rounded-r-none border-r-0' : ''
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<Download className={`${size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'} mr-1`} />
<span>
{availableOptions.length === 1
? availableOptions[0].label
: hasData
? `导出 (${selectedTemplates.length + selectedExecutions.length})`
: '导出'
}
</span>
</button>
{/* 下拉按钮 */}
{(availableOptions.length > 1 || onOpenFullExporter) && (
<button
onClick={() => setIsOpen(!isOpen)}
disabled={disabled}
className={`${getButtonClasses()} rounded-l-none border-l border-gray-300 px-2 disabled:opacity-50 disabled:cursor-not-allowed`}
>
<ChevronDown className={`${size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'}`} />
</button>
)}
</div>
{/* 下拉菜单 */}
{isOpen && (availableOptions.length > 1 || onOpenFullExporter) && (
<div className="absolute right-0 mt-1 w-64 bg-white border border-gray-200 rounded-lg shadow-lg z-10">
<div className="py-1">
{/* 快速导出选项 */}
{availableOptions.map((option) => {
const Icon = option.icon;
return (
<button
key={option.id}
onClick={() => handleQuickExport(option)}
className="w-full px-4 py-2 text-left hover:bg-gray-50 flex items-start space-x-3"
>
<Icon className="w-4 h-4 text-gray-500 mt-0.5" />
<div>
<div className="text-sm font-medium text-gray-900">
{option.label}
</div>
<div className="text-xs text-gray-500">
{option.description}
</div>
</div>
</button>
);
})}
{/* 分隔线 */}
{availableOptions.length > 0 && onOpenFullExporter && (
<div className="border-t border-gray-200 my-1" />
)}
{/* 完整导出器选项 */}
{onOpenFullExporter && (
<button
onClick={() => {
setIsOpen(false);
onOpenFullExporter();
}}
className="w-full px-4 py-2 text-left hover:bg-gray-50 flex items-start space-x-3"
>
<Settings className="w-4 h-4 text-gray-500 mt-0.5" />
<div>
<div className="text-sm font-medium text-gray-900">
</div>
<div className="text-xs text-gray-500">
</div>
</div>
</button>
)}
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,595 @@
/**
* 重试管理器组件
*
* 提供智能重试策略和批量重试功能
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
RefreshCw,
Play,
Pause,
Square,
Settings,
Clock,
TrendingUp,
AlertTriangle,
CheckCircle,
XCircle,
Zap,
Timer,
BarChart3
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
// 重试策略枚举
type RetryStrategy = 'immediate' | 'exponential' | 'fixed_interval' | 'smart';
// 重试状态枚举
type RetryStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
// 重试配置接口
interface RetryConfig {
strategy: RetryStrategy;
max_attempts: number;
initial_delay_seconds: number;
max_delay_seconds: number;
backoff_multiplier: number;
timeout_seconds: number;
retry_on_errors: string[];
stop_on_errors: string[];
}
// 重试任务接口
interface RetryTask {
id: number;
execution_id: number;
workflow_name: string;
original_error: string;
retry_config: RetryConfig;
status: RetryStatus;
current_attempt: number;
next_retry_at?: string;
started_at: string;
completed_at?: string;
success: boolean;
final_error?: string;
}
// 重试统计接口
interface RetryStatistics {
total_retries: number;
successful_retries: number;
failed_retries: number;
success_rate: number;
average_attempts: number;
most_retried_workflows: Array<{ workflow_name: string; count: number }>;
}
// 组件属性接口
interface RetryManagerProps {
/** 重试任务列表 */
retryTasks?: RetryTask[];
/** 重试统计信息 */
statistics?: RetryStatistics;
/** 开始重试回调 */
onStartRetry?: (executionId: number, config: RetryConfig) => void;
/** 取消重试回调 */
onCancelRetry?: (taskId: number) => void;
/** 批量重试回调 */
onBatchRetry?: (executionIds: number[], config: RetryConfig) => void;
}
/**
* 重试管理器组件
*/
export const RetryManager: React.FC<RetryManagerProps> = ({
retryTasks = [],
statistics,
onStartRetry,
onCancelRetry,
onBatchRetry
}) => {
const [selectedTasks, setSelectedTasks] = useState<Set<number>>(new Set());
const [showConfigModal, setShowConfigModal] = useState(false);
const [retryConfig, setRetryConfig] = useState<RetryConfig>({
strategy: 'exponential',
max_attempts: 3,
initial_delay_seconds: 30,
max_delay_seconds: 300,
backoff_multiplier: 2,
timeout_seconds: 600,
retry_on_errors: ['network', 'timeout', 'server'],
stop_on_errors: ['validation']
});
// 重试策略配置
const strategyConfigs = {
immediate: {
label: '立即重试',
description: '失败后立即重试,不等待',
icon: Zap
},
exponential: {
label: '指数退避',
description: '每次重试间隔时间呈指数增长',
icon: TrendingUp
},
fixed_interval: {
label: '固定间隔',
description: '每次重试间隔时间固定',
icon: Clock
},
smart: {
label: '智能重试',
description: '根据错误类型和历史数据智能调整',
icon: BarChart3
}
};
// 获取状态图标
const getStatusIcon = (status: RetryStatus) => {
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 <Square className="w-4 h-4 text-gray-500" />;
default:
return <Clock className="w-4 h-4 text-gray-400" />;
}
};
// 获取状态文本
const getStatusText = (status: RetryStatus) => {
const statusMap = {
pending: '等待重试',
running: '重试中',
completed: '重试成功',
failed: '重试失败',
cancelled: '已取消'
};
return statusMap[status] || '未知';
};
// 计算下次重试时间
const getNextRetryTime = (task: RetryTask) => {
if (!task.next_retry_at) return '-';
const nextTime = new Date(task.next_retry_at);
const now = new Date();
const diffMs = nextTime.getTime() - now.getTime();
if (diffMs <= 0) return '即将重试';
const diffSeconds = Math.floor(diffMs / 1000);
if (diffSeconds < 60) {
return `${diffSeconds}秒后`;
} else if (diffSeconds < 3600) {
return `${Math.floor(diffSeconds / 60)}分钟后`;
} else {
return `${Math.floor(diffSeconds / 3600)}小时后`;
}
};
// 处理任务选择
const handleTaskSelection = (taskId: number, selected: boolean) => {
const newSelected = new Set(selectedTasks);
if (selected) {
newSelected.add(taskId);
} else {
newSelected.delete(taskId);
}
setSelectedTasks(newSelected);
};
// 处理全选
const handleSelectAll = (selected: boolean) => {
if (selected) {
setSelectedTasks(new Set(retryTasks.map(task => task.id)));
} else {
setSelectedTasks(new Set());
}
};
// 处理批量重试
const handleBatchRetry = () => {
const selectedExecutionIds = retryTasks
.filter(task => selectedTasks.has(task.id))
.map(task => task.execution_id);
if (selectedExecutionIds.length > 0 && onBatchRetry) {
onBatchRetry(selectedExecutionIds, retryConfig);
setSelectedTasks(new Set());
}
};
// 处理配置保存
const handleConfigSave = () => {
setShowConfigModal(false);
// 配置已更新,可以应用到后续的重试任务
};
return (
<div className="space-y-6">
{/* 重试统计概览 */}
{statistics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{statistics.total_retries}
</p>
</div>
<RefreshCw className="w-8 h-8 text-blue-500" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{(statistics.success_rate * 100).toFixed(1)}%
</p>
</div>
<CheckCircle className="w-8 h-8 text-green-500" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{statistics.average_attempts.toFixed(1)}
</p>
</div>
<BarChart3 className="w-8 h-8 text-purple-500" />
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{retryTasks.filter(task => task.status === 'running' || task.status === 'pending').length}
</p>
</div>
<Timer className="w-8 h-8 text-yellow-500" />
</div>
</div>
</div>
)}
{/* 操作栏 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<h2 className="text-lg font-medium text-gray-900"></h2>
{selectedTasks.size > 0 && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">
{selectedTasks.size}
</span>
<button
onClick={handleBatchRetry}
className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors flex items-center space-x-1"
>
<RefreshCw className="w-3 h-3" />
<span></span>
</button>
<button
onClick={() => setSelectedTasks(new Set())}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
>
</button>
</div>
)}
</div>
<button
onClick={() => setShowConfigModal(true)}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center space-x-2"
>
<Settings className="w-4 h-4" />
<span></span>
</button>
</div>
{/* 重试任务列表 */}
{retryTasks.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<RefreshCw 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>
) : (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left">
<input
type="checkbox"
checked={selectedTasks.size === retryTasks.length && retryTasks.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{retryTasks.map((task) => (
<tr key={task.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<input
type="checkbox"
checked={selectedTasks.has(task.id)}
onChange={(e) => handleTaskSelection(task.id, e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
</td>
<td className="px-6 py-4">
<div className="text-sm font-medium text-gray-900">
{task.workflow_name}
</div>
<div className="text-sm text-gray-500">
ID: {task.execution_id}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center space-x-2">
{getStatusIcon(task.status)}
<span className="text-sm text-gray-900">
{getStatusText(task.status)}
</span>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{task.current_attempt} / {task.retry_config.max_attempts}
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-1">
<div
className="bg-blue-600 h-1.5 rounded-full"
style={{
width: `${(task.current_attempt / task.retry_config.max_attempts) * 100}%`
}}
/>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{getNextRetryTime(task)}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900 truncate max-w-xs" title={task.original_error}>
{task.original_error}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center space-x-2">
{(task.status === 'pending' || task.status === 'running') && onCancelRetry && (
<button
onClick={() => onCancelRetry(task.id)}
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
title="取消重试"
>
<Square className="w-4 h-4" />
</button>
)}
{task.status === 'failed' && onStartRetry && (
<button
onClick={() => onStartRetry(task.execution_id, task.retry_config)}
className="p-1 text-gray-400 hover:text-green-600 transition-colors"
title="重新开始重试"
>
<Play className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* 重试配置模态框 */}
{showConfigModal && (
<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-2xl w-full max-h-[90vh] flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900"></h2>
<button
onClick={() => setShowConfigModal(false)}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
>
<XCircle className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* 重试策略 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="grid grid-cols-2 gap-3">
{Object.entries(strategyConfigs).map(([strategy, config]) => {
const Icon = config.icon;
return (
<button
key={strategy}
type="button"
onClick={() => setRetryConfig(prev => ({ ...prev, strategy: strategy as RetryStrategy }))}
className={`p-3 border-2 rounded-lg text-left transition-all ${
retryConfig.strategy === strategy
? '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" />
<span className="font-medium text-sm">{config.label}</span>
</div>
<p className="text-xs text-gray-600">{config.description}</p>
</button>
);
})}
</div>
</div>
{/* 重试参数 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="number"
min="1"
max="10"
value={retryConfig.max_attempts}
onChange={(e) => setRetryConfig(prev => ({
...prev,
max_attempts: parseInt(e.target.value) || 1
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
()
</label>
<input
type="number"
min="0"
value={retryConfig.initial_delay_seconds}
onChange={(e) => setRetryConfig(prev => ({
...prev,
initial_delay_seconds: parseInt(e.target.value) || 0
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
()
</label>
<input
type="number"
min="0"
value={retryConfig.max_delay_seconds}
onChange={(e) => setRetryConfig(prev => ({
...prev,
max_delay_seconds: parseInt(e.target.value) || 0
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
退
</label>
<input
type="number"
min="1"
step="0.1"
value={retryConfig.backoff_multiplier}
onChange={(e) => setRetryConfig(prev => ({
...prev,
backoff_multiplier: parseFloat(e.target.value) || 1
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
{/* 错误类型配置 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="space-y-2">
{['network', 'timeout', 'memory', 'server', 'unknown'].map((errorType) => (
<label key={errorType} className="flex items-center space-x-2">
<input
type="checkbox"
checked={retryConfig.retry_on_errors.includes(errorType)}
onChange={(e) => {
const newErrors = e.target.checked
? [...retryConfig.retry_on_errors, errorType]
: retryConfig.retry_on_errors.filter(t => t !== errorType);
setRetryConfig(prev => ({ ...prev, retry_on_errors: newErrors }));
}}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">{errorType}</span>
</label>
))}
</div>
</div>
</div>
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200">
<button
onClick={() => setShowConfigModal(false)}
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
</button>
<button
onClick={handleConfigSave}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,418 @@
/**
* 系统状态面板组件
*
* 显示系统整体状态概览,包括环境健康状态、执行队列统计等
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
BarChart3,
TrendingUp,
Server,
Activity,
Clock,
CheckCircle,
XCircle,
AlertTriangle,
Zap,
Users,
Database,
Cpu,
HardDrive,
Wifi,
RefreshCw
} from 'lucide-react';
// 系统统计信息接口
interface SystemStatistics {
// 执行统计
execution_stats: {
total_executions_today: number;
successful_executions_today: number;
failed_executions_today: number;
average_execution_time_seconds: number;
current_queue_size: number;
active_executions: number;
};
// 环境统计
environment_stats: {
total_environments: number;
healthy_environments: number;
unhealthy_environments: number;
average_response_time_ms: number;
total_capacity: number;
used_capacity: number;
};
// 系统资源
system_resources: {
cpu_usage_percent: number;
memory_usage_percent: number;
disk_usage_percent: number;
network_status: 'connected' | 'disconnected' | 'limited';
};
// 性能指标
performance_metrics: {
requests_per_minute: number;
average_response_time_ms: number;
error_rate_percent: number;
uptime_hours: number;
};
}
// 组件属性接口
interface SystemStatusPanelProps {
/** 系统统计信息 */
systemStats: SystemStatistics;
/** 刷新间隔(毫秒) */
refreshInterval?: number;
/** 刷新回调 */
onRefresh?: () => void;
}
/**
* 系统状态面板组件
*/
export const SystemStatusPanel: React.FC<SystemStatusPanelProps> = ({
systemStats,
refreshInterval = 5000,
onRefresh
}) => {
const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date());
// 自动刷新
useEffect(() => {
if (!onRefresh || refreshInterval <= 0) return;
const interval = setInterval(() => {
onRefresh();
setLastUpdateTime(new Date());
}, refreshInterval);
return () => clearInterval(interval);
}, [onRefresh, refreshInterval]);
// 格式化时间
const formatTime = (date: Date) => {
return date.toLocaleTimeString('zh-CN');
};
// 格式化数字
const formatNumber = (num: number) => {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
};
// 格式化时长
const formatDuration = (seconds: number) => {
if (seconds < 60) {
return `${seconds.toFixed(1)}`;
} else if (seconds < 3600) {
return `${(seconds / 60).toFixed(1)}分钟`;
} else {
return `${(seconds / 3600).toFixed(1)}小时`;
}
};
// 获取健康状态颜色
const getHealthColor = (healthy: number, total: number) => {
const ratio = total > 0 ? healthy / total : 0;
if (ratio >= 0.8) return 'text-green-600';
if (ratio >= 0.5) return 'text-yellow-600';
return 'text-red-600';
};
// 获取使用率颜色
const getUsageColor = (percentage: number) => {
if (percentage < 70) return 'bg-green-500';
if (percentage < 90) return 'bg-yellow-500';
return 'bg-red-500';
};
// 计算成功率
const successRate = systemStats.execution_stats.total_executions_today > 0
? (systemStats.execution_stats.successful_executions_today / systemStats.execution_stats.total_executions_today) * 100
: 0;
// 计算环境健康率
const environmentHealthRate = systemStats.environment_stats.total_environments > 0
? (systemStats.environment_stats.healthy_environments / systemStats.environment_stats.total_environments) * 100
: 0;
// 计算容量使用率
const capacityUsageRate = systemStats.environment_stats.total_capacity > 0
? (systemStats.environment_stats.used_capacity / systemStats.environment_stats.total_capacity) * 100
: 0;
return (
<div className="space-y-6">
{/* 状态面板头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<BarChart3 className="w-6 h-6 text-blue-600" />
<div>
<h2 className="text-lg font-medium text-gray-900">
</h2>
<p className="text-sm text-gray-500">
: {formatTime(lastUpdateTime)}
</p>
</div>
</div>
{onRefresh && (
<button
onClick={onRefresh}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center space-x-1"
>
<RefreshCw className="w-4 h-4" />
<span></span>
</button>
)}
</div>
{/* 关键指标卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* 今日执行统计 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{formatNumber(systemStats.execution_stats.total_executions_today)}
</p>
<p className="text-xs text-green-600">
{successRate.toFixed(1)}%
</p>
</div>
<Activity className="w-8 h-8 text-blue-500" />
</div>
</div>
{/* 当前队列 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{systemStats.execution_stats.current_queue_size}
</p>
<p className="text-xs text-blue-600">
{systemStats.execution_stats.active_executions}
</p>
</div>
<Clock className="w-8 h-8 text-yellow-500" />
</div>
</div>
{/* 环境健康 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{systemStats.environment_stats.healthy_environments}/{systemStats.environment_stats.total_environments}
</p>
<p className={`text-xs ${getHealthColor(systemStats.environment_stats.healthy_environments, systemStats.environment_stats.total_environments)}`}>
{environmentHealthRate.toFixed(1)}%
</p>
</div>
<Server className="w-8 h-8 text-green-500" />
</div>
</div>
{/* 平均响应时间 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-semibold text-gray-900">
{systemStats.performance_metrics.average_response_time_ms}ms
</p>
<p className="text-xs text-purple-600">
{systemStats.performance_metrics.requests_per_minute}/
</p>
</div>
<Zap className="w-8 h-8 text-purple-500" />
</div>
</div>
</div>
{/* 详细统计 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-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 flex items-center space-x-2">
<TrendingUp className="w-5 h-5" />
<span></span>
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<div className="flex items-center space-x-2">
<CheckCircle className="w-4 h-4 text-green-500" />
<span className="text-sm font-medium text-gray-900">
{formatNumber(systemStats.execution_stats.successful_executions_today)}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<div className="flex items-center space-x-2">
<XCircle className="w-4 h-4 text-red-500" />
<span className="text-sm font-medium text-gray-900">
{formatNumber(systemStats.execution_stats.failed_executions_today)}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<span className="text-sm font-medium text-gray-900">
{formatDuration(systemStats.execution_stats.average_execution_time_seconds)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<span className={`text-sm font-medium ${
systemStats.performance_metrics.error_rate_percent < 5
? 'text-green-600'
: systemStats.performance_metrics.error_rate_percent < 10
? 'text-yellow-600'
: 'text-red-600'
}`}>
{systemStats.performance_metrics.error_rate_percent.toFixed(2)}%
</span>
</div>
</div>
</div>
{/* 系统资源 */}
<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 flex items-center space-x-2">
<Cpu className="w-5 h-5" />
<span></span>
</h3>
<div className="space-y-4">
{/* CPU使用率 */}
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-600">CPU使用率</span>
<span className="text-sm font-medium text-gray-900">
{systemStats.system_resources.cpu_usage_percent.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getUsageColor(systemStats.system_resources.cpu_usage_percent)}`}
style={{ width: `${systemStats.system_resources.cpu_usage_percent}%` }}
/>
</div>
</div>
{/* 内存使用率 */}
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-600">使</span>
<span className="text-sm font-medium text-gray-900">
{systemStats.system_resources.memory_usage_percent.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getUsageColor(systemStats.system_resources.memory_usage_percent)}`}
style={{ width: `${systemStats.system_resources.memory_usage_percent}%` }}
/>
</div>
</div>
{/* 磁盘使用率 */}
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-600">使</span>
<span className="text-sm font-medium text-gray-900">
{systemStats.system_resources.disk_usage_percent.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getUsageColor(systemStats.system_resources.disk_usage_percent)}`}
style={{ width: `${systemStats.system_resources.disk_usage_percent}%` }}
/>
</div>
</div>
{/* 网络状态 */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<div className="flex items-center space-x-2">
<Wifi className={`w-4 h-4 ${
systemStats.system_resources.network_status === 'connected'
? 'text-green-500'
: systemStats.system_resources.network_status === 'limited'
? 'text-yellow-500'
: 'text-red-500'
}`} />
<span className="text-sm font-medium text-gray-900">
{systemStats.system_resources.network_status === 'connected'
? '正常'
: systemStats.system_resources.network_status === 'limited'
? '受限'
: '断开'}
</span>
</div>
</div>
{/* 系统运行时间 */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<span className="text-sm font-medium text-gray-900">
{formatDuration(systemStats.performance_metrics.uptime_hours * 3600)}
</span>
</div>
</div>
</div>
</div>
{/* 容量使用情况 */}
<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 flex items-center space-x-2">
<HardDrive className="w-5 h-5" />
<span></span>
</h3>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">
使 {systemStats.environment_stats.used_capacity} / {systemStats.environment_stats.total_capacity}
</span>
<span className="text-sm font-medium text-gray-900">
{capacityUsageRate.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all duration-300 ${getUsageColor(capacityUsageRate)}`}
style={{ width: `${capacityUsageRate}%` }}
/>
</div>
<div className="flex items-center justify-between mt-2 text-xs text-gray-500">
<span>: {systemStats.environment_stats.total_capacity - systemStats.environment_stats.used_capacity}</span>
<span>: {systemStats.environment_stats.total_capacity}</span>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,490 @@
/**
* UI字段编辑器组件
*
* 用于可视化编辑工作流的UI字段配置
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState } from 'react';
import {
Plus,
Trash2,
Edit3,
Move,
Settings,
Eye,
FileText,
Layers,
CheckCircle,
Upload,
Sliders,
Palette,
Calendar,
ChevronDown,
ChevronRight,
Copy,
ArrowUp,
ArrowDown
} from 'lucide-react';
// UI字段类型枚举
type UIFieldType = 'text' | 'textarea' | 'number' | 'select' | 'checkbox' | 'image_upload' | 'file_upload' | 'slider' | 'color' | 'date';
// UI字段配置接口
interface UIField {
name: string;
type: UIFieldType;
label: string;
description?: string;
required?: boolean;
default_value?: any;
validation?: {
min?: number;
max?: number;
pattern?: string;
options?: string[];
};
ui_config?: {
placeholder?: string;
help_text?: string;
group?: string;
order?: number;
width?: 'full' | 'half' | 'third';
};
}
// 组件属性接口
interface UIFieldEditorProps {
/** UI字段列表 */
fields: UIField[];
/** 字段变化回调 */
onFieldsChange: (fields: UIField[]) => void;
/** 是否只读 */
readonly?: boolean;
}
/**
* UI字段编辑器组件
*/
export const UIFieldEditor: React.FC<UIFieldEditorProps> = ({
fields,
onFieldsChange,
readonly = false
}) => {
const [selectedFieldIndex, setSelectedFieldIndex] = useState<number | null>(null);
const [expandedFields, setExpandedFields] = useState<Set<number>>(new Set());
// UI字段类型配置
const fieldTypeConfigs = {
text: { label: '文本输入', icon: Edit3, description: '单行文本输入框', color: 'text-blue-500' },
textarea: { label: '多行文本', icon: FileText, description: '多行文本输入框', color: 'text-green-500' },
number: { label: '数字输入', icon: Settings, description: '数字输入框', color: 'text-purple-500' },
select: { label: '下拉选择', icon: Layers, description: '下拉选择框', color: 'text-orange-500' },
checkbox: { label: '复选框', icon: CheckCircle, description: '复选框', color: 'text-teal-500' },
image_upload: { label: '图片上传', icon: Upload, description: '图片文件上传', color: 'text-pink-500' },
file_upload: { label: '文件上传', icon: Upload, description: '通用文件上传', color: 'text-indigo-500' },
slider: { label: '滑块', icon: Sliders, description: '数值滑块', color: 'text-yellow-500' },
color: { label: '颜色选择', icon: Palette, description: '颜色选择器', color: 'text-red-500' },
date: { label: '日期选择', icon: Calendar, description: '日期选择器', color: 'text-gray-500' }
};
// 添加新字段
const addField = (type: UIFieldType) => {
const newField: UIField = {
name: `field_${Date.now()}`,
type,
label: `${fieldTypeConfigs[type].label}`,
required: false,
ui_config: {
width: 'full',
order: fields.length
}
};
if (type === 'select') {
newField.validation = { options: ['选项1', '选项2', '选项3'] };
} else if (type === 'number' || type === 'slider') {
newField.validation = { min: 0, max: 100 };
}
const newFields = [...fields, newField];
onFieldsChange(newFields);
setSelectedFieldIndex(newFields.length - 1);
};
// 删除字段
const deleteField = (index: number) => {
const newFields = fields.filter((_, i) => i !== index);
onFieldsChange(newFields);
if (selectedFieldIndex === index) {
setSelectedFieldIndex(null);
} else if (selectedFieldIndex !== null && selectedFieldIndex > index) {
setSelectedFieldIndex(selectedFieldIndex - 1);
}
};
// 复制字段
const duplicateField = (index: number) => {
const field = fields[index];
const newField = {
...field,
name: `${field.name}_copy_${Date.now()}`,
label: `${field.label} (副本)`
};
const newFields = [...fields];
newFields.splice(index + 1, 0, newField);
onFieldsChange(newFields);
};
// 移动字段
const moveField = (index: number, direction: 'up' | 'down') => {
const newFields = [...fields];
const targetIndex = direction === 'up' ? index - 1 : index + 1;
if (targetIndex >= 0 && targetIndex < newFields.length) {
[newFields[index], newFields[targetIndex]] = [newFields[targetIndex], newFields[index]];
onFieldsChange(newFields);
setSelectedFieldIndex(targetIndex);
}
};
// 更新字段
const updateField = (index: number, updates: Partial<UIField>) => {
const newFields = [...fields];
newFields[index] = { ...newFields[index], ...updates };
onFieldsChange(newFields);
};
// 切换字段展开状态
const toggleFieldExpanded = (index: number) => {
const newExpanded = new Set(expandedFields);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedFields(newExpanded);
};
return (
<div className="space-y-6">
{/* 字段类型选择器 */}
{!readonly && (
<div>
<h3 className="text-sm font-medium text-gray-900 mb-3"></h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{Object.entries(fieldTypeConfigs).map(([type, config]) => {
const Icon = config.icon;
return (
<button
key={type}
onClick={() => addField(type as UIFieldType)}
className="p-3 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-all text-left"
title={config.description}
>
<div className="flex items-center space-x-2">
<Icon className={`w-4 h-4 ${config.color}`} />
<span className="text-xs font-medium text-gray-700">{config.label}</span>
</div>
</button>
);
})}
</div>
</div>
)}
{/* 字段列表 */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-900">
({fields.length})
</h3>
{fields.length > 0 && (
<div className="text-xs text-gray-500">
</div>
)}
</div>
{fields.length === 0 ? (
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
<Layers className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600 mb-2">UI字段</p>
<p className="text-xs text-gray-500">
</p>
</div>
) : (
<div className="space-y-2">
{fields.map((field, index) => {
const config = fieldTypeConfigs[field.type];
const Icon = config.icon;
const isExpanded = expandedFields.has(index);
const isSelected = selectedFieldIndex === index;
return (
<div
key={index}
className={`border rounded-lg transition-all ${
isSelected
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
{/* 字段头部 */}
<div className="p-3">
<div className="flex items-center justify-between">
<div
className="flex items-center space-x-3 flex-1 cursor-pointer"
onClick={() => {
setSelectedFieldIndex(isSelected ? null : index);
if (!isSelected) {
toggleFieldExpanded(index);
}
}}
>
<button
onClick={(e) => {
e.stopPropagation();
toggleFieldExpanded(index);
}}
className="text-gray-400 hover:text-gray-600"
>
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
<Icon className={`w-4 h-4 ${config.color}`} />
<div className="flex-1">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-900">
{field.label}
</span>
<span className="text-xs text-gray-500">
({config.label})
</span>
{field.required && (
<span className="text-xs text-red-500"></span>
)}
</div>
<div className="text-xs text-gray-500">
: {field.name}
</div>
</div>
</div>
{!readonly && (
<div className="flex items-center space-x-1">
<button
onClick={() => moveField(index, 'up')}
disabled={index === 0}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
title="上移"
>
<ArrowUp className="w-3 h-3" />
</button>
<button
onClick={() => moveField(index, 'down')}
disabled={index === fields.length - 1}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
title="下移"
>
<ArrowDown className="w-3 h-3" />
</button>
<button
onClick={() => duplicateField(index)}
className="p-1 text-gray-400 hover:text-blue-600"
title="复制"
>
<Copy className="w-3 h-3" />
</button>
<button
onClick={() => deleteField(index)}
className="p-1 text-gray-400 hover:text-red-600"
title="删除"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
)}
</div>
</div>
{/* 字段详细配置 */}
{isExpanded && (
<div className="border-t border-gray-200 p-4 bg-gray-50">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 基本配置 */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<input
type="text"
value={field.name}
onChange={(e) => updateField(index, { name: e.target.value })}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
disabled={readonly}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<input
type="text"
value={field.label}
onChange={(e) => updateField(index, { label: e.target.value })}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
disabled={readonly}
/>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<input
type="text"
value={field.description || ''}
onChange={(e) => updateField(index, { description: e.target.value })}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
placeholder="字段的详细说明..."
disabled={readonly}
/>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={field.required || false}
onChange={(e) => updateField(index, { required: e.target.checked })}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
disabled={readonly}
/>
<span className="text-xs text-gray-700"></span>
</label>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<select
value={field.ui_config?.width || 'full'}
onChange={(e) => updateField(index, {
ui_config: {
...field.ui_config,
width: e.target.value as 'full' | 'half' | 'third'
}
})}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
disabled={readonly}
>
<option value="full"></option>
<option value="half"></option>
<option value="third"></option>
</select>
</div>
{/* 类型特定配置 */}
{(field.type === 'text' || field.type === 'textarea') && (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<input
type="text"
value={field.ui_config?.placeholder || ''}
onChange={(e) => updateField(index, {
ui_config: {
...field.ui_config,
placeholder: e.target.value
}
})}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
disabled={readonly}
/>
</div>
)}
{(field.type === 'number' || field.type === 'slider') && (
<>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<input
type="number"
value={field.validation?.min || ''}
onChange={(e) => updateField(index, {
validation: {
...field.validation,
min: e.target.value ? Number(e.target.value) : undefined
}
})}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
disabled={readonly}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
</label>
<input
type="number"
value={field.validation?.max || ''}
onChange={(e) => updateField(index, {
validation: {
...field.validation,
max: e.target.value ? Number(e.target.value) : undefined
}
})}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
disabled={readonly}
/>
</div>
</>
)}
{field.type === 'select' && (
<div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-700 mb-1">
()
</label>
<textarea
value={field.validation?.options?.join('\n') || ''}
onChange={(e) => updateField(index, {
validation: {
...field.validation,
options: e.target.value.split('\n').filter(opt => opt.trim())
}
})}
rows={3}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent"
placeholder="选项1&#10;选项2&#10;选项3"
disabled={readonly}
/>
</div>
)}
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,870 @@
/**
* 工作流创建器组件
*
* 提供可视化的工作流创建和编辑界面支持JSON配置和UI设计
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState, useEffect } from 'react';
import {
X,
Save,
Eye,
Code,
Settings,
Plus,
Trash2,
Copy,
Upload,
Download,
TestTube,
AlertCircle,
CheckCircle,
Info,
Layers,
Edit3,
FileText
} from 'lucide-react';
import { UIFieldEditor } from './UIFieldEditor';
import { WorkflowPreview } from './WorkflowPreview';
// 工作流类型枚举
type WorkflowType = 'outfit_generation' | 'background_replacement' | 'portrait_enhancement' | 'image_upscaling' | 'style_transfer' | 'custom';
// UI字段类型枚举
type UIFieldType = 'text' | 'textarea' | 'number' | 'select' | 'checkbox' | 'image_upload' | 'file_upload' | 'slider' | 'color' | 'date';
// UI字段配置接口
interface UIField {
name: string;
type: UIFieldType;
label: string;
description?: string;
required?: boolean;
default_value?: any;
validation?: {
min?: number;
max?: number;
pattern?: string;
options?: string[];
};
ui_config?: {
placeholder?: string;
help_text?: string;
group?: string;
order?: number;
width?: 'full' | 'half' | 'third';
};
}
// 工作流模板接口
interface WorkflowTemplate {
id?: number;
name: string;
base_name: string;
version: string;
type: WorkflowType;
description?: string;
comfyui_workflow_json: any;
ui_config_json: {
form_fields: UIField[];
layout?: any;
theme?: string;
};
execution_config_json?: any;
input_schema_json?: any;
output_schema_json?: any;
is_active: boolean;
is_published: boolean;
tags?: string[];
category?: string;
author?: string;
}
// 创建工作流请求接口
interface CreateWorkflowTemplateRequest {
name: string;
base_name: string;
version: string;
type: WorkflowType;
description?: string;
comfyui_workflow_json: any;
ui_config_json: any;
execution_config_json?: any;
input_schema_json?: any;
output_schema_json?: any;
tags?: string[];
category?: string;
author?: string;
}
// 组件属性接口
interface WorkflowCreatorProps {
/** 是否显示模态框 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 保存回调 */
onSave: (template: CreateWorkflowTemplateRequest) => void;
/** 编辑的模板(为空时表示创建新模板) */
editingTemplate?: WorkflowTemplate;
}
/**
* 工作流创建器组件
*/
export const WorkflowCreator: React.FC<WorkflowCreatorProps> = ({
isOpen,
onClose,
onSave,
editingTemplate
}) => {
const [activeTab, setActiveTab] = useState<'basic' | 'ui' | 'comfyui' | 'advanced'>('basic');
const [formData, setFormData] = useState<CreateWorkflowTemplateRequest>({
name: '',
base_name: '',
version: '1.0',
type: 'custom',
description: '',
comfyui_workflow_json: {},
ui_config_json: {
form_fields: []
},
execution_config_json: {},
tags: [],
category: '',
author: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isPreviewMode, setIsPreviewMode] = useState(false);
const [isTesting, setIsTesting] = useState(false);
// 工作流类型配置
const workflowTypeConfigs = {
outfit_generation: { label: '穿搭生成', description: '生成时尚穿搭搭配' },
background_replacement: { label: '背景替换', description: '替换图片背景' },
portrait_enhancement: { label: '人像美化', description: '人像照片美化处理' },
image_upscaling: { label: '图片放大', description: '提升图片分辨率' },
style_transfer: { label: '风格转换', description: '图片风格迁移' },
custom: { label: '自定义', description: '自定义工作流类型' }
};
// UI字段类型配置
const fieldTypeConfigs = {
text: { label: '文本输入', icon: Edit3, description: '单行文本输入框' },
textarea: { label: '多行文本', icon: FileText, description: '多行文本输入框' },
number: { label: '数字输入', icon: Settings, description: '数字输入框' },
select: { label: '下拉选择', icon: Layers, description: '下拉选择框' },
checkbox: { label: '复选框', icon: CheckCircle, description: '复选框' },
image_upload: { label: '图片上传', icon: Upload, description: '图片文件上传' },
file_upload: { label: '文件上传', icon: Upload, description: '通用文件上传' },
slider: { label: '滑块', icon: Settings, description: '数值滑块' },
color: { label: '颜色选择', icon: Settings, description: '颜色选择器' },
date: { label: '日期选择', icon: Settings, description: '日期选择器' }
};
// 初始化表单数据
useEffect(() => {
if (editingTemplate) {
setFormData({
name: editingTemplate.name,
base_name: editingTemplate.base_name,
version: editingTemplate.version,
type: editingTemplate.type,
description: editingTemplate.description || '',
comfyui_workflow_json: editingTemplate.comfyui_workflow_json,
ui_config_json: editingTemplate.ui_config_json,
execution_config_json: editingTemplate.execution_config_json || {},
input_schema_json: editingTemplate.input_schema_json,
output_schema_json: editingTemplate.output_schema_json,
tags: editingTemplate.tags || [],
category: editingTemplate.category || '',
author: editingTemplate.author || ''
});
} else {
// 重置为默认值
setFormData({
name: '',
base_name: '',
version: '1.0',
type: 'custom',
description: '',
comfyui_workflow_json: {},
ui_config_json: {
form_fields: []
},
execution_config_json: {},
tags: [],
category: '',
author: ''
});
}
setErrors({});
setActiveTab('basic');
setIsPreviewMode(false);
}, [editingTemplate, isOpen]);
// 更新表单字段
const updateField = (field: keyof CreateWorkflowTemplateRequest, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除该字段的错误
if (errors[field]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
// 生成base_name
const generateBaseName = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '_')
.trim();
};
// 处理名称变化
const handleNameChange = (name: string) => {
updateField('name', name);
if (!editingTemplate) {
// 只在创建新模板时自动生成base_name
updateField('base_name', generateBaseName(name));
}
};
// 验证表单
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = '工作流名称不能为空';
}
if (!formData.base_name.trim()) {
newErrors.base_name = '基础名称不能为空';
} else if (!/^[a-z0-9_]+$/.test(formData.base_name)) {
newErrors.base_name = '基础名称只能包含小写字母、数字和下划线';
}
if (!formData.version.trim()) {
newErrors.version = '版本号不能为空';
}
if (Object.keys(formData.comfyui_workflow_json).length === 0) {
newErrors.comfyui_workflow_json = 'ComfyUI工作流配置不能为空';
}
if (formData.ui_config_json.form_fields.length === 0) {
newErrors.ui_config = '至少需要配置一个UI字段';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理保存
const handleSave = () => {
if (!validateForm()) return;
onSave(formData);
};
// 处理测试
const handleTest = async () => {
if (!validateForm()) return;
setIsTesting(true);
try {
// TODO: 实现工作流测试逻辑
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟测试
console.log('工作流测试成功');
} catch (error) {
console.error('工作流测试失败:', error);
} finally {
setIsTesting(false);
}
};
// 导入ComfyUI工作流
const handleImportComfyUI = () => {
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);
updateField('comfyui_workflow_json', workflow);
} catch (error) {
console.error('解析ComfyUI工作流失败:', error);
}
};
reader.readAsText(file);
}
};
input.click();
};
// 导出工作流配置
const handleExportConfig = () => {
const config = {
...formData,
exported_at: new Date().toISOString(),
version_info: {
creator_version: '1.0.0',
format_version: '1.0'
}
};
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${formData.base_name || 'workflow'}_v${formData.version}.json`;
a.click();
URL.revokeObjectURL(url);
};
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-6xl 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">
{editingTemplate ? '编辑工作流' : '创建工作流'}
</h2>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setIsPreviewMode(!isPreviewMode)}
className={`px-3 py-1 rounded transition-colors flex items-center space-x-1 ${
isPreviewMode
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<Eye className="w-4 h-4" />
<span>{isPreviewMode ? '编辑模式' : '预览模式'}</span>
</button>
<button
onClick={handleTest}
disabled={isTesting}
className="px-3 py-1 bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors flex items-center space-x-1 disabled:opacity-50"
>
{isTesting ? (
<Settings className="w-4 h-4 animate-spin" />
) : (
<TestTube className="w-4 h-4" />
)}
<span>{isTesting ? '测试中...' : '测试'}</span>
</button>
<button
onClick={handleExportConfig}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center space-x-1"
>
<Download className="w-4 h-4" />
<span></span>
</button>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* 标签导航 */}
<div className="border-b border-gray-200">
<nav className="flex space-x-8 px-6">
{[
{ id: 'basic', label: '基本信息', icon: Info },
{ id: 'ui', label: 'UI配置', icon: Layers },
{ id: 'comfyui', label: 'ComfyUI', icon: Code },
{ id: 'advanced', label: '高级设置', icon: Settings }
].map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setActiveTab(id as any)}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center space-x-2 ${
activeTab === id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="w-4 h-4" />
<span>{label}</span>
</button>
))}
</nav>
</div>
{/* 内容区域 */}
<div className="flex-1 overflow-hidden flex">
{/* 主要内容 */}
<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) => handleNameChange(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">
<span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.base_name}
onChange={(e) => updateField('base_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.base_name ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="例如advanced_outfit_generator"
/>
{errors.base_name && (
<p className="mt-1 text-sm text-red-600">{errors.base_name}</p>
)}
<p className="mt-1 text-xs text-gray-500">
API调用的唯一标识符线
</p>
</div>
<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.version}
onChange={(e) => updateField('version', e.target.value)}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.version ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="例如1.0"
/>
{errors.version && (
<p className="mt-1 text-sm text-red-600">{errors.version}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<select
value={formData.type}
onChange={(e) => updateField('type', e.target.value as WorkflowType)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{Object.entries(workflowTypeConfigs).map(([type, config]) => (
<option key={type} value={type}>
{config.label} - {config.description}
</option>
))}
</select>
</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>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
value={formData.author}
onChange={(e) => updateField('author', 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-2">
</label>
<div className="flex flex-wrap gap-2 mb-2">
{formData.tags?.map((tag, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-sm rounded-full flex items-center space-x-1"
>
<span>{tag}</span>
<button
onClick={() => {
const newTags = formData.tags?.filter((_, i) => i !== index) || [];
updateField('tags', newTags);
}}
className="text-blue-600 hover:text-blue-800"
>
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
<div className="flex space-x-2">
<input
type="text"
placeholder="添加标签..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
onKeyPress={(e) => {
if (e.key === 'Enter') {
const input = e.target as HTMLInputElement;
const tag = input.value.trim();
if (tag && !formData.tags?.includes(tag)) {
updateField('tags', [...(formData.tags || []), tag]);
input.value = '';
}
}
}}
/>
<button
onClick={() => {
const input = document.querySelector('input[placeholder="添加标签..."]') as HTMLInputElement;
const tag = input?.value.trim();
if (tag && !formData.tags?.includes(tag)) {
updateField('tags', [...(formData.tags || []), tag]);
input.value = '';
}
}}
className="px-3 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
</div>
)}
{/* UI配置标签页 */}
{activeTab === 'ui' && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">UI字段配置</h3>
<div className="text-sm text-gray-500">
</div>
</div>
<UIFieldEditor
fields={formData.ui_config_json.form_fields}
onFieldsChange={(fields) => updateField('ui_config_json', {
...formData.ui_config_json,
form_fields: fields
})}
/>
{errors.ui_config && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<div className="flex items-center space-x-2">
<AlertCircle className="w-4 h-4 text-red-500" />
<span className="text-sm text-red-700">{errors.ui_config}</span>
</div>
</div>
)}
</div>
)}
{/* ComfyUI配置标签页 */}
{activeTab === 'comfyui' && (
<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={handleImportComfyUI}
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">
ComfyUI工作流JSON <span className="text-red-500">*</span>
</label>
<textarea
value={JSON.stringify(formData.comfyui_workflow_json, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
updateField('comfyui_workflow_json', parsed);
} catch (error) {
// 保持原始文本,让用户继续编辑
}
}}
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.comfyui_workflow_json ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="粘贴ComfyUI导出的工作流JSON..."
/>
{errors.comfyui_workflow_json && (
<p className="mt-1 text-sm text-red-600">{errors.comfyui_workflow_json}</p>
)}
<p className="mt-1 text-xs text-gray-500">
ComfyUI界面导出工作流JSON并粘贴到此处
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-blue-900 mb-2">ComfyUI工作流JSON</h4>
<ol className="text-sm text-blue-700 space-y-1 list-decimal list-inside">
<li>ComfyUI界面中设计好你的工作流</li>
<li>"Save"</li>
<li>"Export"JSON文件</li>
<li>JSON文件并复制内容到上方文本框</li>
</ol>
</div>
</div>
)}
{/* 高级设置标签页 */}
{activeTab === 'advanced' && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900"></h3>
{/* 执行配置 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
()
</label>
<input
type="number"
min="0"
value={formData.execution_config_json?.timeout_seconds || ''}
onChange={(e) => updateField('execution_config_json', {
...formData.execution_config_json,
timeout_seconds: e.target.value ? Number(e.target.value) : undefined
})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="600"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
(MB)
</label>
<input
type="number"
min="0"
value={formData.execution_config_json?.max_memory_mb || ''}
onChange={(e) => updateField('execution_config_json', {
...formData.execution_config_json,
max_memory_mb: e.target.value ? Number(e.target.value) : undefined
})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="4096"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
value={formData.execution_config_json?.priority || 'normal'}
onChange={(e) => updateField('execution_config_json', {
...formData.execution_config_json,
priority: 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"
>
<option value="low"></option>
<option value="normal"></option>
<option value="high"></option>
<option value="urgent"></option>
</select>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={formData.execution_config_json?.auto_retry || false}
onChange={(e) => updateField('execution_config_json', {
...formData.execution_config_json,
auto_retry: e.target.checked
})}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700"></span>
</label>
</div>
</div>
</div>
{/* 输入输出Schema */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Schema (JSON)
</label>
<textarea
value={JSON.stringify(formData.input_schema_json || {}, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
updateField('input_schema_json', parsed);
} catch (error) {
// 保持原始文本
}
}}
rows={8}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm"
placeholder='{"type": "object", "properties": {...}}'
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Schema (JSON)
</label>
<textarea
value={JSON.stringify(formData.output_schema_json || {}, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
updateField('output_schema_json', parsed);
} catch (error) {
// 保持原始文本
}
}}
rows={8}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm"
placeholder='{"type": "object", "properties": {...}}'
/>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start space-x-2">
<Info className="w-5 h-5 text-yellow-600 mt-0.5" />
<div>
<h4 className="text-sm font-medium text-yellow-800">Schema配置</h4>
<p className="text-sm text-yellow-700 mt-1">
Schema用于验证输入输出数据格式UI字段自动生成基础Schema
</p>
</div>
</div>
</div>
</div>
)}
</div>
{/* 预览面板 */}
{isPreviewMode && (
<div className="w-1/3 border-l border-gray-200 bg-gray-50 overflow-y-auto">
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div className="flex items-center space-x-2">
<button
onClick={() => {/* TODO: 切换预览模式 */}}
className="text-xs text-gray-500 hover:text-gray-700"
>
</button>
</div>
</div>
<WorkflowPreview
workflowName={formData.name || '未命名工作流'}
description={formData.description}
fields={formData.ui_config_json.form_fields}
mode="desktop"
/>
</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}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Save className="w-4 h-4" />
<span>{editingTemplate ? '更新' : '创建'}</span>
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -8,23 +8,7 @@
import React, { useState, useEffect } from 'react';
import { Play, Settings, Eye, Edit, Trash2, Plus, Search, Filter } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
// 工作流类型定义
interface WorkflowTemplate {
id: number;
name: string;
base_name: string;
version: string;
type: string;
description?: string;
is_active: boolean;
is_published: boolean;
category?: string;
author?: string;
tags?: string[];
created_at: string;
updated_at: string;
}
import type { WorkflowTemplate } from '../../types/workflow';
interface WorkflowListProps {
/** 选择工作流回调 */
@@ -35,6 +19,12 @@ interface WorkflowListProps {
onEditWorkflow?: (workflow: WorkflowTemplate) => void;
/** 是否显示管理按钮 */
showManagement?: boolean;
/** 是否支持批量选择 */
enableBatchSelection?: boolean;
/** 选中的工作流列表 */
selectedWorkflows?: WorkflowTemplate[];
/** 选择变化回调 */
onSelectionChange?: (workflows: WorkflowTemplate[]) => void;
}
/**
@@ -44,7 +34,10 @@ export const WorkflowList: React.FC<WorkflowListProps> = ({
onSelectWorkflow,
onExecuteWorkflow,
onEditWorkflow,
showManagement = false
showManagement = false,
enableBatchSelection = false,
selectedWorkflows = [],
onSelectionChange
}) => {
const [workflows, setWorkflows] = useState<WorkflowTemplate[]>([]);
const [loading, setLoading] = useState(true);
@@ -53,6 +46,31 @@ export const WorkflowList: React.FC<WorkflowListProps> = ({
const [selectedCategory, setSelectedCategory] = useState<string>('');
const [selectedType, setSelectedType] = useState<string>('');
// 批量选择处理函数
const handleSelectAll = (checked: boolean) => {
if (!enableBatchSelection || !onSelectionChange) return;
if (checked) {
onSelectionChange(filteredWorkflows);
} else {
onSelectionChange([]);
}
};
const handleSelectWorkflow = (workflow: WorkflowTemplate, checked: boolean) => {
if (!enableBatchSelection || !onSelectionChange) return;
if (checked) {
onSelectionChange([...selectedWorkflows, workflow]);
} else {
onSelectionChange(selectedWorkflows.filter(w => w.id !== workflow.id));
}
};
const isWorkflowSelected = (workflow: WorkflowTemplate) => {
return selectedWorkflows.some(w => w.id === workflow.id);
};
// 加载工作流列表
const loadWorkflows = async () => {
try {
@@ -109,13 +127,26 @@ export const WorkflowList: React.FC<WorkflowListProps> = ({
const renderWorkflowCard = (workflow: WorkflowTemplate) => (
<div
key={workflow.id}
className="bg-white rounded-lg shadow-md border border-gray-200 hover:shadow-lg transition-shadow p-6"
className={`bg-white rounded-lg shadow-md border-2 hover:shadow-lg transition-all p-6 ${
enableBatchSelection && isWorkflowSelected(workflow)
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'
}`}
>
<div className="flex justify-between items-start mb-4">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">
{workflow.name}
</h3>
<div className="flex items-start space-x-3 flex-1">
{enableBatchSelection && (
<input
type="checkbox"
checked={isWorkflowSelected(workflow)}
onChange={(e) => handleSelectWorkflow(workflow, e.target.checked)}
className="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
)}
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">
{workflow.name}
</h3>
<div className="flex items-center space-x-2 mb-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{typeDisplayNames[workflow.type] || workflow.type}
@@ -127,6 +158,7 @@ export const WorkflowList: React.FC<WorkflowListProps> = ({
{workflow.description}
</p>
)}
</div>
</div>
</div>
@@ -233,6 +265,20 @@ export const WorkflowList: React.FC<WorkflowListProps> = ({
<div className="space-y-6">
{/* 搜索和筛选栏 */}
<div className="flex flex-col sm:flex-row gap-4">
{enableBatchSelection && (
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedWorkflows.length === filteredWorkflows.length && filteredWorkflows.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
({selectedWorkflows.length}/{filteredWorkflows.length})
</span>
</div>
)}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
@@ -267,18 +313,6 @@ export const WorkflowList: React.FC<WorkflowListProps> = ({
</option>
))}
</select>
{showManagement && (
<button
onClick={() => {
// TODO: 打开创建工作流对话框
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Plus className="w-4 h-4" />
<span></span>
</button>
)}
</div>
{/* 工作流列表 */}

View File

@@ -0,0 +1,380 @@
/**
* 工作流预览组件
*
* 实时预览工作流的UI界面效果
* 遵循Tauri开发规范的组件设计原则
*/
import React, { useState } from 'react';
import {
Eye,
Upload,
Calendar,
Palette,
Sliders,
CheckCircle,
FileText,
Edit3,
Layers,
Settings,
AlertCircle
} from 'lucide-react';
// UI字段类型枚举
type UIFieldType = 'text' | 'textarea' | 'number' | 'select' | 'checkbox' | 'image_upload' | 'file_upload' | 'slider' | 'color' | 'date';
// UI字段配置接口
interface UIField {
name: string;
type: UIFieldType;
label: string;
description?: string;
required?: boolean;
default_value?: any;
validation?: {
min?: number;
max?: number;
pattern?: string;
options?: string[];
};
ui_config?: {
placeholder?: string;
help_text?: string;
group?: string;
order?: number;
width?: 'full' | 'half' | 'third';
};
}
// 组件属性接口
interface WorkflowPreviewProps {
/** 工作流名称 */
workflowName: string;
/** 工作流描述 */
description?: string;
/** UI字段列表 */
fields: UIField[];
/** 预览模式 */
mode?: 'desktop' | 'mobile';
}
/**
* 工作流预览组件
*/
export const WorkflowPreview: React.FC<WorkflowPreviewProps> = ({
workflowName,
description,
fields,
mode = 'desktop'
}) => {
const [formData, setFormData] = useState<Record<string, any>>({});
// 获取字段图标
const getFieldIcon = (type: UIFieldType) => {
const iconMap = {
text: Edit3,
textarea: FileText,
number: Settings,
select: Layers,
checkbox: CheckCircle,
image_upload: Upload,
file_upload: Upload,
slider: Sliders,
color: Palette,
date: Calendar
};
return iconMap[type] || Settings;
};
// 更新表单数据
const updateFormData = (fieldName: string, value: any) => {
setFormData(prev => ({ ...prev, [fieldName]: value }));
};
// 渲染字段
const renderField = (field: UIField) => {
const Icon = getFieldIcon(field.type);
const value = formData[field.name] ?? field.default_value ?? '';
const fieldElement = (() => {
switch (field.type) {
case 'text':
return (
<input
type="text"
value={value}
onChange={(e) => updateFormData(field.name, e.target.value)}
placeholder={field.ui_config?.placeholder}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required={field.required}
/>
);
case 'textarea':
return (
<textarea
value={value}
onChange={(e) => updateFormData(field.name, e.target.value)}
placeholder={field.ui_config?.placeholder}
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"
required={field.required}
/>
);
case 'number':
return (
<input
type="number"
value={value}
onChange={(e) => updateFormData(field.name, Number(e.target.value))}
min={field.validation?.min}
max={field.validation?.max}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required={field.required}
/>
);
case 'select':
return (
<select
value={value}
onChange={(e) => updateFormData(field.name, 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"
required={field.required}
>
<option value="">...</option>
{field.validation?.options?.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
);
case 'checkbox':
return (
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={value}
onChange={(e) => updateFormData(field.name, e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
{field.ui_config?.help_text || '启用此选项'}
</span>
</label>
);
case 'slider':
return (
<div className="space-y-2">
<input
type="range"
value={value}
onChange={(e) => updateFormData(field.name, Number(e.target.value))}
min={field.validation?.min || 0}
max={field.validation?.max || 100}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
/>
<div className="flex justify-between text-xs text-gray-500">
<span>{field.validation?.min || 0}</span>
<span className="font-medium">{value}</span>
<span>{field.validation?.max || 100}</span>
</div>
</div>
);
case 'color':
return (
<div className="flex items-center space-x-2">
<input
type="color"
value={value || '#000000'}
onChange={(e) => updateFormData(field.name, e.target.value)}
className="w-12 h-10 border border-gray-300 rounded cursor-pointer"
/>
<input
type="text"
value={value || '#000000'}
onChange={(e) => updateFormData(field.name, e.target.value)}
placeholder="#000000"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
);
case 'date':
return (
<input
type="date"
value={value}
onChange={(e) => updateFormData(field.name, 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"
required={field.required}
/>
);
case 'image_upload':
case 'file_upload':
return (
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
<Upload className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600 mb-1">
</p>
<p className="text-xs text-gray-500">
{field.type === 'image_upload' ? '支持 JPG, PNG, GIF 格式' : '支持所有文件格式'}
</p>
<input
type="file"
accept={field.type === 'image_upload' ? 'image/*' : '*/*'}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
updateFormData(field.name, file.name);
}
}}
className="hidden"
id={`file-${field.name}`}
/>
<label
htmlFor={`file-${field.name}`}
className="mt-2 inline-block px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 cursor-pointer transition-colors"
>
</label>
{value && (
<p className="mt-2 text-sm text-green-600">
: {value}
</p>
)}
</div>
);
default:
return (
<div className="p-4 bg-gray-100 rounded-lg text-center">
<AlertCircle className="w-6 h-6 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600">
: {field.type}
</p>
</div>
);
}
})();
// 根据宽度配置确定CSS类
const widthClass = {
full: 'col-span-full',
half: 'col-span-6',
third: 'col-span-4'
}[field.ui_config?.width || 'full'];
return (
<div key={field.name} className={`${widthClass}`}>
<div className="space-y-2">
{/* 字段标签 */}
<div className="flex items-center space-x-2">
<Icon className="w-4 h-4 text-gray-500" />
<label className="text-sm font-medium text-gray-700">
{field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</label>
</div>
{/* 字段描述 */}
{field.description && (
<p className="text-xs text-gray-600">{field.description}</p>
)}
{/* 字段输入 */}
{fieldElement}
{/* 帮助文本 */}
{field.ui_config?.help_text && (
<p className="text-xs text-gray-500">{field.ui_config.help_text}</p>
)}
</div>
</div>
);
};
// 按组分组字段
const groupedFields = fields.reduce((groups, field) => {
const group = field.ui_config?.group || 'default';
if (!groups[group]) {
groups[group] = [];
}
groups[group].push(field);
return groups;
}, {} as Record<string, UIField[]>);
// 排序字段
Object.keys(groupedFields).forEach(group => {
groupedFields[group].sort((a, b) => (a.ui_config?.order || 0) - (b.ui_config?.order || 0));
});
return (
<div className={`bg-white rounded-lg border border-gray-200 ${mode === 'mobile' ? 'max-w-sm' : 'max-w-2xl'}`}>
{/* 预览头部 */}
<div className="p-4 border-b border-gray-200">
<div className="flex items-center space-x-2 mb-2">
<Eye className="w-5 h-5 text-blue-600" />
<h3 className="text-lg font-medium text-gray-900">{workflowName || '工作流预览'}</h3>
</div>
{description && (
<p className="text-sm text-gray-600">{description}</p>
)}
</div>
{/* 预览内容 */}
<div className="p-4">
{fields.length === 0 ? (
<div className="text-center py-8">
<Layers className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600"></p>
<p className="text-sm text-gray-500 mt-1">
UI配置中添加字段后
</p>
</div>
) : (
<form className="space-y-6">
{Object.entries(groupedFields).map(([groupName, groupFields]) => (
<div key={groupName}>
{groupName !== 'default' && (
<h4 className="text-sm font-medium text-gray-900 mb-3 pb-2 border-b border-gray-200">
{groupName}
</h4>
)}
<div className="grid grid-cols-12 gap-4">
{groupFields.map(renderField)}
</div>
</div>
))}
{/* 预览提交按钮 */}
<div className="pt-4 border-t border-gray-200">
<button
type="button"
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
disabled
>
()
</button>
</div>
</form>
)}
</div>
{/* 预览底部信息 */}
<div className="px-4 py-3 bg-gray-50 border-t border-gray-200 rounded-b-lg">
<div className="flex items-center justify-between text-xs text-gray-500">
<span> - {mode === 'mobile' ? '移动端' : '桌面端'}</span>
<span>{fields.length} </span>
</div>
</div>
</div>
);
};

View File

@@ -236,7 +236,6 @@ const ComfyUIManagement: React.FC = () => {
<div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-sm text-amber-700">
<strong></strong> ComfyUI
使 "ComfyUI 节点管理"
</p>
</div>
</div>

View File

@@ -6,26 +6,27 @@
*/
import React, { useState } from 'react';
import { Settings, History, Plus } from 'lucide-react';
import { Settings, History, Plus, Activity } from 'lucide-react';
import { WorkflowList } from '../components/workflow/WorkflowList';
import { WorkflowExecutionModal } from '../components/workflow/WorkflowExecutionModal';
import { ExecutionHistoryList } from '../components/workflow/ExecutionHistoryList';
import { ExecutionDetailViewer } from '../components/workflow/ExecutionDetailViewer';
import { EnvironmentList } from '../components/workflow/EnvironmentList';
import { EnvironmentConfigurator } from '../components/workflow/EnvironmentConfigurator';
import { MonitoringDashboard } from '../components/workflow/MonitoringDashboard';
import { WorkflowCreator } from '../components/workflow/WorkflowCreator';
import { BatchOperationManager } from '../components/workflow/BatchOperationManager';
import { DataExportManager } from '../components/workflow/DataExportManager';
import { QuickExportButton } from '../components/workflow/QuickExportButton';
import type {
WorkflowTemplate,
WorkflowExecutionRecord,
WorkflowExecutionEnvironment,
CreateEnvironmentRequest,
CreateWorkflowTemplateRequest
} from '../types/workflow';
interface WorkflowTemplate {
id: number;
name: string;
base_name: string;
version: string;
type: string;
description?: string;
ui_config_json: any;
is_active: boolean;
is_published: boolean;
category?: string;
author?: string;
tags?: string[];
created_at: string;
updated_at: string;
}
// 所有类型定义现在从 ../types/workflow 导入
/**
* 工作流页面组件
@@ -33,7 +34,27 @@ interface WorkflowTemplate {
export const WorkflowPage: React.FC = () => {
const [selectedWorkflow, setSelectedWorkflow] = useState<WorkflowTemplate | null>(null);
const [isExecutionModalOpen, setIsExecutionModalOpen] = useState(false);
const [activeTab, setActiveTab] = useState<'workflows' | 'history' | 'environments'>('workflows');
const [activeTab, setActiveTab] = useState<'workflows' | 'history' | 'environments' | 'monitoring'>('workflows');
// 执行历史相关状态
const [selectedExecutionRecord, setSelectedExecutionRecord] = useState<WorkflowExecutionRecord | null>(null);
const [isDetailViewerOpen, setIsDetailViewerOpen] = useState(false);
// 环境管理相关状态
const [selectedEnvironment, setSelectedEnvironment] = useState<WorkflowExecutionEnvironment | null>(null);
const [isEnvironmentConfiguratorOpen, setIsEnvironmentConfiguratorOpen] = useState(false);
// 工作流创建相关状态
const [selectedWorkflowTemplate, setSelectedWorkflowTemplate] = useState<WorkflowTemplate | null>(null);
const [isWorkflowCreatorOpen, setIsWorkflowCreatorOpen] = useState(false);
// 批量操作相关状态
const [selectedWorkflows, setSelectedWorkflows] = useState<WorkflowTemplate[]>([]);
const [selectedExecutions, setSelectedExecutions] = useState<WorkflowExecutionRecord[]>([]);
const [showBatchOperations, setShowBatchOperations] = useState(false);
// 数据导出相关状态
const [isDataExportManagerOpen, setIsDataExportManagerOpen] = useState(false);
// 处理工作流选择
const handleSelectWorkflow = (workflow: WorkflowTemplate) => {
@@ -47,11 +68,7 @@ export const WorkflowPage: React.FC = () => {
setIsExecutionModalOpen(true);
};
// 处理工作流编辑
const handleEditWorkflow = (workflow: WorkflowTemplate) => {
// TODO: 打开工作流编辑对话框
console.log('编辑工作流:', workflow);
};
// 处理工作流编辑 - 将在后面重新定义
// 关闭执行模态框
const handleCloseExecutionModal = () => {
@@ -59,13 +76,188 @@ export const WorkflowPage: React.FC = () => {
setSelectedWorkflow(null);
};
// 执行历史相关处理函数
const handleViewExecutionDetails = (record: WorkflowExecutionRecord) => {
setSelectedExecutionRecord(record);
setIsDetailViewerOpen(true);
};
const handleRetryExecution = async (record: WorkflowExecutionRecord) => {
try {
// TODO: 实现重试逻辑
console.log('重试执行:', record);
} catch (error) {
console.error('重试失败:', error);
}
};
const handleDeleteExecution = async (recordId: number) => {
try {
// TODO: 实现删除逻辑
console.log('删除执行记录:', recordId);
} catch (error) {
console.error('删除失败:', error);
}
};
const handleBatchDeleteExecutions = async (recordIds: number[]) => {
try {
// TODO: 实现批量删除逻辑
console.log('批量删除执行记录:', recordIds);
} catch (error) {
console.error('批量删除失败:', error);
}
};
const handleDownloadExecutionResult = async (record: WorkflowExecutionRecord) => {
try {
// TODO: 实现下载逻辑
console.log('下载执行结果:', record);
} catch (error) {
console.error('下载失败:', error);
}
};
const handleCloseDetailViewer = () => {
setIsDetailViewerOpen(false);
setSelectedExecutionRecord(null);
};
// 环境管理相关处理函数
const handleAddEnvironment = () => {
setSelectedEnvironment(null);
setIsEnvironmentConfiguratorOpen(true);
};
const handleEditEnvironment = (env: WorkflowExecutionEnvironment) => {
setSelectedEnvironment(env);
setIsEnvironmentConfiguratorOpen(true);
};
const handleDeleteEnvironment = async (envId: number) => {
try {
// TODO: 实现删除逻辑
console.log('删除执行环境:', envId);
} catch (error) {
console.error('删除环境失败:', error);
}
};
const handleHealthCheckEnvironment = async (envId: number) => {
try {
// TODO: 实现健康检查逻辑
console.log('健康检查环境:', envId);
} catch (error) {
console.error('健康检查失败:', error);
}
};
const handleToggleEnvironmentActive = async (envId: number, isActive: boolean) => {
try {
// TODO: 实现激活状态切换逻辑
console.log('切换环境激活状态:', envId, isActive);
} catch (error) {
console.error('切换激活状态失败:', error);
}
};
const handleSaveEnvironment = async (envData: CreateEnvironmentRequest) => {
try {
if (selectedEnvironment) {
// TODO: 实现更新逻辑
console.log('更新环境:', envData);
} else {
// TODO: 实现创建逻辑
console.log('创建环境:', envData);
}
setIsEnvironmentConfiguratorOpen(false);
setSelectedEnvironment(null);
} catch (error) {
console.error('保存环境失败:', error);
}
};
const handleCloseEnvironmentConfigurator = () => {
setIsEnvironmentConfiguratorOpen(false);
setSelectedEnvironment(null);
};
// 工作流创建相关处理函数
const handleCreateWorkflow = () => {
setSelectedWorkflowTemplate(null);
setIsWorkflowCreatorOpen(true);
};
const handleEditWorkflow = (template: WorkflowTemplate) => {
setSelectedWorkflowTemplate(template);
setIsWorkflowCreatorOpen(true);
};
const handleSaveWorkflow = async (workflowData: any) => {
try {
if (selectedWorkflowTemplate) {
// TODO: 实现更新逻辑
console.log('更新工作流:', workflowData);
} else {
// TODO: 实现创建逻辑
console.log('创建工作流:', workflowData);
}
setIsWorkflowCreatorOpen(false);
setSelectedWorkflowTemplate(null);
} catch (error) {
console.error('保存工作流失败:', error);
}
};
const handleCloseWorkflowCreator = () => {
setIsWorkflowCreatorOpen(false);
setSelectedWorkflowTemplate(null);
};
// 批量操作相关处理函数
const handleWorkflowSelectionChange = (workflows: WorkflowTemplate[]) => {
setSelectedWorkflows(workflows);
setShowBatchOperations(workflows.length > 0);
};
const handleExecutionSelectionChange = (executions: WorkflowExecutionRecord[]) => {
setSelectedExecutions(executions);
setShowBatchOperations(executions.length > 0);
};
const handleBatchOperationComplete = (task: any) => {
console.log('批量操作完成:', task);
// 刷新相关数据
setSelectedWorkflows([]);
setSelectedExecutions([]);
setShowBatchOperations(false);
};
const handleCloseBatchOperations = () => {
setShowBatchOperations(false);
};
// 数据导出相关处理函数
const handleOpenDataExportManager = () => {
setIsDataExportManagerOpen(true);
};
const handleCloseDataExportManager = () => {
setIsDataExportManagerOpen(false);
};
const handleQuickExport = (option: any, data: any[]) => {
console.log('快速导出:', option, data);
// 导出完成后的处理逻辑
};
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen">
{/* 页面头部 */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div>
<div className="page-header flex items-center justify-between h-16">
<div className='py-4'>
<h1 className="text-2xl font-bold text-gray-900">
AI工作流
</h1>
@@ -73,27 +265,9 @@ export const WorkflowPage: React.FC = () => {
AI生成任务
</p>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => {
// TODO: 打开工作流创建对话框
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Plus className="w-4 h-4" />
<span></span>
</button>
<button
onClick={() => {
// TODO: 打开设置对话框
}}
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
title="设置"
>
<Settings className="w-5 h-5" />
</button>
</div>
</div>
</div>
@@ -105,38 +279,46 @@ export const WorkflowPage: React.FC = () => {
<nav className="flex space-x-8">
<button
onClick={() => setActiveTab('workflows')}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'workflows'
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${activeTab === 'workflows'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
}`}
>
</button>
<button
onClick={() => setActiveTab('history')}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center space-x-2 ${
activeTab === 'history'
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center space-x-2 ${activeTab === 'history'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
}`}
>
<History className="w-4 h-4" />
<span></span>
</button>
<button
onClick={() => setActiveTab('environments')}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center space-x-2 ${
activeTab === 'environments'
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center space-x-2 ${activeTab === 'environments'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
}`}
>
<Settings className="w-4 h-4" />
<span></span>
</button>
<button
onClick={() => setActiveTab('monitoring')}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center space-x-2 ${activeTab === 'monitoring'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Activity className="w-4 h-4" />
<span></span>
</button>
</nav>
</div>
</div>
@@ -145,47 +327,83 @@ export const WorkflowPage: React.FC = () => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{activeTab === 'workflows' && (
<div>
<div className="mb-6">
<h2 className="text-lg font-medium text-gray-900 mb-2">
</h2>
<p className="text-sm text-gray-600">
AI生成任务
</p>
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-lg font-medium text-gray-900 mb-2">
</h2>
<p className="text-sm text-gray-600">
AI生成任务
</p>
</div>
<div className="flex items-center space-x-2">
<QuickExportButton
selectedTemplates={selectedWorkflows}
onExport={handleQuickExport}
onOpenFullExporter={handleOpenDataExportManager}
size="md"
/>
<button
onClick={handleCreateWorkflow}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Plus className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
<WorkflowList
onSelectWorkflow={handleSelectWorkflow}
onExecuteWorkflow={handleExecuteWorkflow}
onEditWorkflow={handleEditWorkflow}
showManagement={true}
enableBatchSelection={true}
selectedWorkflows={selectedWorkflows}
onSelectionChange={handleWorkflowSelectionChange}
/>
</div>
)}
{activeTab === 'history' && (
<div>
<div className="mb-6">
<h2 className="text-lg font-medium text-gray-900 mb-2">
</h2>
<p className="text-sm text-gray-600">
</p>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<History 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 mb-4">
</p>
<p className="text-sm text-gray-500">
...
</p>
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-lg font-medium text-gray-900 mb-2">
</h2>
<p className="text-sm text-gray-600">
</p>
</div>
<QuickExportButton
selectedExecutions={selectedExecutions}
onExport={handleQuickExport}
onOpenFullExporter={handleOpenDataExportManager}
size="md"
/>
</div>
<ExecutionHistoryList
onViewDetails={handleViewExecutionDetails}
onRetry={handleRetryExecution}
onDelete={handleDeleteExecution}
onBatchDelete={handleBatchDeleteExecutions}
onDownload={handleDownloadExecutionResult}
/>
{/* 批量操作面板 */}
{showBatchOperations && (selectedWorkflows.length > 0 || selectedExecutions.length > 0) && (
<div className="mt-6">
<BatchOperationManager
selectedTemplates={selectedWorkflows}
selectedExecutions={selectedExecutions}
onOperationComplete={handleBatchOperationComplete}
onClose={handleCloseBatchOperations}
/>
</div>
)}
</div>
)}
@@ -199,19 +417,32 @@ export const WorkflowPage: React.FC = () => {
AI工作流的执行环境ComfyUI和云端服务
</p>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
<Settings 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 mb-4">
</p>
<p className="text-sm text-gray-500">
...
<EnvironmentList
onAdd={handleAddEnvironment}
onEdit={handleEditEnvironment}
onDelete={handleDeleteEnvironment}
onHealthCheck={handleHealthCheckEnvironment}
onToggleActive={handleToggleEnvironmentActive}
/>
</div>
)}
{activeTab === 'monitoring' && (
<div>
<div className="mb-6">
<h2 className="text-lg font-medium text-gray-900 mb-2">
</h2>
<p className="text-sm text-gray-600">
</p>
</div>
<MonitoringDashboard
onViewExecutionDetails={handleViewExecutionDetails}
refreshInterval={3000}
/>
</div>
)}
</div>
@@ -222,6 +453,41 @@ export const WorkflowPage: React.FC = () => {
onClose={handleCloseExecutionModal}
workflow={selectedWorkflow}
/>
{/* 执行详情查看器 */}
{selectedExecutionRecord && (
<ExecutionDetailViewer
record={selectedExecutionRecord}
isOpen={isDetailViewerOpen}
onClose={handleCloseDetailViewer}
onDownload={handleDownloadExecutionResult}
onRetry={handleRetryExecution}
/>
)}
{/* 环境配置器 */}
<EnvironmentConfigurator
environment={selectedEnvironment || undefined}
isOpen={isEnvironmentConfiguratorOpen}
onSave={handleSaveEnvironment}
onClose={handleCloseEnvironmentConfigurator}
/>
{/* 工作流创建器 */}
<WorkflowCreator
isOpen={isWorkflowCreatorOpen}
onClose={handleCloseWorkflowCreator}
onSave={handleSaveWorkflow}
editingTemplate={selectedWorkflowTemplate || undefined}
/>
{/* 数据导出管理器 */}
<DataExportManager
isOpen={isDataExportManagerOpen}
onClose={handleCloseDataExportManager}
selectedTemplates={selectedWorkflows}
selectedExecutions={selectedExecutions}
/>
</div>
);
};

View File

@@ -0,0 +1,216 @@
/**
* 工作流相关的类型定义
*
* 统一管理工作流系统中使用的所有类型定义
*/
// 工作流类型枚举
export type WorkflowType = 'outfit_generation' | 'background_replacement' | 'portrait_enhancement' | 'image_upscaling' | 'style_transfer' | 'custom';
// 执行状态枚举
export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
// 环境类型枚举
export type EnvironmentType = 'local_comfyui' | 'modal_cloud' | 'runpod_cloud' | 'custom';
// 健康状态枚举
export type HealthStatus = 'healthy' | 'unhealthy' | 'unknown';
// UI字段类型枚举
export type UIFieldType = 'text' | 'textarea' | 'number' | 'select' | 'checkbox' | 'image_upload' | 'file_upload' | 'slider' | 'color' | 'date';
// UI字段配置接口
export interface UIField {
name: string;
type: UIFieldType;
label: string;
description?: string;
required?: boolean;
default_value?: any;
validation?: {
min?: number;
max?: number;
pattern?: string;
options?: string[];
};
ui_config?: {
placeholder?: string;
help_text?: string;
group?: string;
order?: number;
width?: 'full' | 'half' | 'third';
};
}
// 工作流模板接口
export interface WorkflowTemplate {
id: number;
name: string;
base_name: string;
version: string;
type: WorkflowType;
description?: string;
comfyui_workflow_json: any;
ui_config_json: {
form_fields: UIField[];
layout?: any;
theme?: string;
};
execution_config_json?: any;
input_schema_json?: any;
output_schema_json?: any;
is_active: boolean;
is_published: boolean;
category?: string;
author?: string;
tags?: string[];
created_at: string;
updated_at: string;
}
// 创建工作流请求接口
export interface CreateWorkflowTemplateRequest {
name: string;
base_name: string;
version: string;
type: WorkflowType;
description?: string;
comfyui_workflow_json: any;
ui_config_json: {
form_fields: UIField[];
layout?: any;
theme?: string;
};
execution_config_json?: any;
input_schema_json?: any;
output_schema_json?: any;
tags?: string[];
category?: string;
author?: string;
}
// 执行记录接口
export interface WorkflowExecutionRecord {
id: number;
workflow_template_id: number;
workflow_name: string;
workflow_version: string;
execution_environment_id?: number;
execution_environment_name?: string;
input_data_json: any;
output_data_json?: any;
status: ExecutionStatus;
progress: number;
comfyui_prompt_id?: string;
comfyui_workflow_name?: string;
started_at?: string;
completed_at?: string;
duration_seconds?: number;
error_message?: string;
error_details_json?: any;
user_id?: string;
session_id?: string;
metadata_json?: any;
tags?: string[];
created_at: string;
updated_at: string;
}
// 执行环境接口
export interface WorkflowExecutionEnvironment {
id: number;
name: string;
environment_type: EnvironmentType;
description?: string;
base_url: string;
api_key?: string;
connection_config_json?: any;
supported_workflow_types: string[];
max_concurrent_jobs: number;
priority: number;
is_active: boolean;
is_available: boolean;
last_health_check?: string;
health_status: HealthStatus;
average_response_time_ms?: number;
success_rate: number;
total_executions: number;
failed_executions: number;
max_memory_mb?: number;
max_execution_time_seconds?: number;
metadata_json?: any;
tags?: string[];
created_at: string;
updated_at: string;
}
// 创建环境请求接口
export interface CreateEnvironmentRequest {
name: string;
environment_type: EnvironmentType;
description?: string;
base_url: string;
api_key?: string;
connection_config_json?: any;
supported_workflow_types: string[];
max_concurrent_jobs?: number;
priority?: number;
max_memory_mb?: number;
max_execution_time_seconds?: number;
metadata_json?: any;
tags?: string[];
}
// 执行记录筛选器接口
export interface ExecutionRecordFilter {
workflow_template_id?: number;
status?: ExecutionStatus;
user_id?: string;
session_id?: string;
comfyui_prompt_id?: string;
date_from?: string;
date_to?: string;
}
// 环境筛选器接口
export interface EnvironmentFilter {
environment_type?: EnvironmentType;
is_active?: boolean;
is_available?: boolean;
health_status?: HealthStatus;
}
// 系统统计信息接口
export interface SystemStatistics {
execution_stats: {
total_executions_today: number;
successful_executions_today: number;
failed_executions_today: number;
average_execution_time_seconds: number;
current_queue_size: number;
active_executions: number;
};
environment_stats: {
total_environments: number;
healthy_environments: number;
unhealthy_environments: number;
average_response_time_ms: number;
total_capacity: number;
used_capacity: number;
};
system_resources: {
cpu_usage_percent: number;
memory_usage_percent: number;
disk_usage_percent: number;
network_status: 'connected' | 'disconnected' | 'limited';
};
performance_metrics: {
requests_per_minute: number;
average_response_time_ms: number;
error_rate_percent: number;
uptime_hours: number;
};
}