735 lines
25 KiB
TypeScript
735 lines
25 KiB
TypeScript
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
import {
|
|||
|
|
Workflow,
|
|||
|
|
CheckCircle,
|
|||
|
|
FileText,
|
|||
|
|
Upload,
|
|||
|
|
ChevronRight,
|
|||
|
|
RefreshCw,
|
|||
|
|
Zap,
|
|||
|
|
Users,
|
|||
|
|
ArrowRight,
|
|||
|
|
AlertTriangle,
|
|||
|
|
} from 'lucide-react';
|
|||
|
|
import { api } from '../lib/api';
|
|||
|
|
import type { Workflow as WorkflowType, WorkflowMeeting } from '../types';
|
|||
|
|
|
|||
|
|
// 工作流详情面板
|
|||
|
|
function WorkflowDetailPanel({
|
|||
|
|
workflow,
|
|||
|
|
onClose,
|
|||
|
|
onReload,
|
|||
|
|
}: {
|
|||
|
|
workflow: WorkflowType;
|
|||
|
|
onClose: () => void;
|
|||
|
|
onReload: () => void;
|
|||
|
|
}) {
|
|||
|
|
const [loading, setLoading] = useState<string | null>(null);
|
|||
|
|
const [executionStatus, setExecutionStatus] = useState<Record<string, any>>({});
|
|||
|
|
|
|||
|
|
// 刷新执行节点状态
|
|||
|
|
const refreshExecutionStatus = async (meetingId: string) => {
|
|||
|
|
if (workflow.meetings.find(m => m.meeting_id === meetingId)?.node_type === 'execution') {
|
|||
|
|
try {
|
|||
|
|
const status = await api.workflow.getExecutionStatus(workflow.workflow_id, meetingId);
|
|||
|
|
setExecutionStatus(prev => ({ ...prev, [meetingId]: status }));
|
|||
|
|
} catch (err) {
|
|||
|
|
// 忽略错误
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
// 刷新所有执行节点的状态
|
|||
|
|
workflow.meetings.forEach(m => {
|
|||
|
|
if (m.node_type === 'execution') {
|
|||
|
|
refreshExecutionStatus(m.meeting_id);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}, [workflow]);
|
|||
|
|
|
|||
|
|
const handleCompleteMeeting = async (meetingId: string) => {
|
|||
|
|
setLoading(meetingId);
|
|||
|
|
try {
|
|||
|
|
await api.workflow.complete(workflow.workflow_id, meetingId);
|
|||
|
|
onReload();
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('标记完成失败');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(null);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleFailAndJump = async (meetingId: string) => {
|
|||
|
|
const meeting = workflow.meetings.find(m => m.meeting_id === meetingId);
|
|||
|
|
if (!meeting?.on_failure) {
|
|||
|
|
alert('此节点未配置失败跳转目标');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!confirm(`确定要跳转到 "${meeting.on_failure}" 吗?`)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setLoading(`fail-${meetingId}`);
|
|||
|
|
try {
|
|||
|
|
const result = await api.workflow.handleFailure(workflow.workflow_id, meetingId);
|
|||
|
|
if (result.target) {
|
|||
|
|
onReload();
|
|||
|
|
} else {
|
|||
|
|
alert('未配置失败跳转');
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('操作失败');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(null);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getMeetingStatus = (meeting: WorkflowMeeting) => {
|
|||
|
|
if (meeting.completed) return 'completed';
|
|||
|
|
const allPrevCompleted = workflow.meetings
|
|||
|
|
.slice(0, workflow.meetings.findIndex((m) => m.meeting_id === meeting.meeting_id))
|
|||
|
|
.every((m) => m.completed);
|
|||
|
|
return allPrevCompleted ? 'active' : 'pending';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
position: 'fixed',
|
|||
|
|
right: 0,
|
|||
|
|
top: 0,
|
|||
|
|
bottom: 0,
|
|||
|
|
width: 480,
|
|||
|
|
background: 'rgba(17, 24, 39, 0.98)',
|
|||
|
|
borderLeft: '1px solid rgba(0, 240, 255, 0.1)',
|
|||
|
|
padding: 24,
|
|||
|
|
zIndex: 100,
|
|||
|
|
overflow: 'auto',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 20 }}>
|
|||
|
|
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: 0 }}>工作流详情</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
style={{
|
|||
|
|
background: 'none',
|
|||
|
|
border: 'none',
|
|||
|
|
color: 'rgba(255, 255, 255, 0.5)',
|
|||
|
|
fontSize: 24,
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Workflow Info */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
padding: 20,
|
|||
|
|
background: 'rgba(0, 0, 0, 0.3)',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
marginBottom: 20,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<h3 style={{ fontSize: 18, fontWeight: 600, color: '#fff', margin: '0 0 8px 0' }}>
|
|||
|
|
{workflow.name}
|
|||
|
|
</h3>
|
|||
|
|
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
|
|||
|
|
{workflow.workflow_id}
|
|||
|
|
</p>
|
|||
|
|
<p style={{ fontSize: 14, color: 'rgba(255, 255, 255, 0.6)', margin: '12px 0 0 0' }}>
|
|||
|
|
{workflow.description}
|
|||
|
|
</p>
|
|||
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
|||
|
|
<span
|
|||
|
|
style={{
|
|||
|
|
padding: '4px 12px',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
background:
|
|||
|
|
workflow.status === 'completed'
|
|||
|
|
? '#00ff9d20'
|
|||
|
|
: workflow.status === 'in_progress'
|
|||
|
|
? '#00f0ff20'
|
|||
|
|
: '#ff950020',
|
|||
|
|
color:
|
|||
|
|
workflow.status === 'completed'
|
|||
|
|
? '#00ff9d'
|
|||
|
|
: workflow.status === 'in_progress'
|
|||
|
|
? '#00f0ff'
|
|||
|
|
: '#ff9500',
|
|||
|
|
fontSize: 12,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{workflow.status === 'completed' ? '已完成' : workflow.status === 'in_progress' ? '进行中' : '等待中'}
|
|||
|
|
</span>
|
|||
|
|
<span
|
|||
|
|
style={{
|
|||
|
|
padding: '4px 12px',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
background: 'rgba(139, 92, 246, 0.2)',
|
|||
|
|
color: '#8b5cf6',
|
|||
|
|
fontSize: 12,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
进度: {workflow.progress}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Progress Bar */}
|
|||
|
|
<div style={{ marginBottom: 20 }}>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
height: 8,
|
|||
|
|
background: 'rgba(255, 255, 255, 0.1)',
|
|||
|
|
borderRadius: 4,
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: workflow.progress,
|
|||
|
|
height: '100%',
|
|||
|
|
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
|
|||
|
|
borderRadius: 4,
|
|||
|
|
transition: 'width 0.5s ease',
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Meetings */}
|
|||
|
|
<div>
|
|||
|
|
<h4 style={{ fontSize: 14, fontWeight: 600, color: 'rgba(255, 255, 255, 0.7)', margin: '0 0 12px 0' }}>
|
|||
|
|
流程节点 ({workflow.meetings.length})
|
|||
|
|
</h4>
|
|||
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|||
|
|
{workflow.meetings.map((meeting, index) => {
|
|||
|
|
const status = getMeetingStatus(meeting);
|
|||
|
|
const isExecution = meeting.node_type === 'execution';
|
|||
|
|
const execStatus = executionStatus[meeting.meeting_id];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={meeting.meeting_id}
|
|||
|
|
style={{
|
|||
|
|
padding: 16,
|
|||
|
|
background:
|
|||
|
|
status === 'completed'
|
|||
|
|
? 'rgba(0, 255, 157, 0.1)'
|
|||
|
|
: status === 'active'
|
|||
|
|
? 'rgba(0, 240, 255, 0.1)'
|
|||
|
|
: 'rgba(0, 0, 0, 0.2)',
|
|||
|
|
borderRadius: 10,
|
|||
|
|
border:
|
|||
|
|
status === 'active'
|
|||
|
|
? '1px solid rgba(0, 240, 255, 0.3)'
|
|||
|
|
: '1px solid transparent',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
|||
|
|
{/* 节点图标 */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: 28,
|
|||
|
|
height: 28,
|
|||
|
|
borderRadius: '50%',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
background:
|
|||
|
|
status === 'completed'
|
|||
|
|
? '#00ff9d'
|
|||
|
|
: status === 'active'
|
|||
|
|
? isExecution ? '#ff9500' : '#00f0ff'
|
|||
|
|
: 'rgba(255, 255, 255, 0.1)',
|
|||
|
|
color: status === 'pending' ? 'rgba(255, 255, 255, 0.4)' : '#000',
|
|||
|
|
fontSize: 12,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
flexShrink: 0,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{status === 'completed' ? <CheckCircle size={14} /> : isExecution ? <Zap size={14} /> : index + 1}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|||
|
|
{/* 标题和类型 */}
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|||
|
|
<p style={{ fontSize: 14, fontWeight: 500, color: '#fff', margin: 0 }}>
|
|||
|
|
{meeting.title}
|
|||
|
|
</p>
|
|||
|
|
{/* 节点类型标签 */}
|
|||
|
|
<span
|
|||
|
|
style={{
|
|||
|
|
padding: '2px 8px',
|
|||
|
|
borderRadius: 4,
|
|||
|
|
background: isExecution ? '#ff950020' : '#00f0ff20',
|
|||
|
|
color: isExecution ? '#ff9500' : '#00f0ff',
|
|||
|
|
fontSize: 10,
|
|||
|
|
fontWeight: 500,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{isExecution ? '执行' : '会议'}
|
|||
|
|
</span>
|
|||
|
|
{/* 失败跳转标签 */}
|
|||
|
|
{meeting.on_failure && (
|
|||
|
|
<span
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 4,
|
|||
|
|
padding: '2px 8px',
|
|||
|
|
borderRadius: 4,
|
|||
|
|
background: '#ff006e20',
|
|||
|
|
color: '#ff006e',
|
|||
|
|
fontSize: 10,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<AlertTriangle size={10} />
|
|||
|
|
失败 → {meeting.on_failure}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ID 和参会者 */}
|
|||
|
|
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
|||
|
|
{meeting.meeting_id}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{/* 参会者列表 */}
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, flexWrap: 'wrap' }}>
|
|||
|
|
<Users size={12} color="rgba(255, 255, 255, 0.4)" />
|
|||
|
|
{meeting.attendees.map((attendee, i) => (
|
|||
|
|
<span
|
|||
|
|
key={i}
|
|||
|
|
style={{
|
|||
|
|
padding: '2px 6px',
|
|||
|
|
borderRadius: 4,
|
|||
|
|
background: 'rgba(255, 255, 255, 0.1)',
|
|||
|
|
color: 'rgba(255, 255, 255, 0.6)',
|
|||
|
|
fontSize: 10,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{attendee}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 执行节点进度 */}
|
|||
|
|
{isExecution && execStatus && (
|
|||
|
|
<div style={{ marginTop: 8 }}>
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
|||
|
|
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)' }}>
|
|||
|
|
进度: {execStatus.progress}
|
|||
|
|
</span>
|
|||
|
|
<span style={{ fontSize: 11, color: execStatus.is_ready ? '#00ff9d' : '#ff9500' }}>
|
|||
|
|
{execStatus.is_ready ? '就绪' : '进行中'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
{execStatus.missing && execStatus.missing.length > 0 && (
|
|||
|
|
<div style={{ fontSize: 10, color: 'rgba(255, 255, 255, 0.4)' }}>
|
|||
|
|
等待中: {execStatus.missing.join(', ')}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 依赖关系 */}
|
|||
|
|
{meeting.depends_on && meeting.depends_on.length > 0 && (
|
|||
|
|
<div style={{ marginTop: 6, fontSize: 10, color: 'rgba(255, 255, 255, 0.3)' }}>
|
|||
|
|
依赖: {meeting.depends_on.join(', ')}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 操作按钮 */}
|
|||
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'flex-end' }}>
|
|||
|
|
{status === 'active' && !meeting.completed && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleCompleteMeeting(meeting.meeting_id)}
|
|||
|
|
disabled={loading === meeting.meeting_id}
|
|||
|
|
style={{
|
|||
|
|
padding: '6px 12px',
|
|||
|
|
background: '#00ff9d',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: 6,
|
|||
|
|
color: '#000',
|
|||
|
|
fontSize: 12,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
cursor: loading === meeting.meeting_id ? 'not-allowed' : 'pointer',
|
|||
|
|
opacity: loading === meeting.meeting_id ? 0.6 : 1,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{loading === meeting.meeting_id ? '...' : '完成'}
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{meeting.completed && meeting.on_failure && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleFailAndJump(meeting.meeting_id)}
|
|||
|
|
disabled={loading === `fail-${meeting.meeting_id}`}
|
|||
|
|
style={{
|
|||
|
|
padding: '4px 10px',
|
|||
|
|
background: '#ff006e20',
|
|||
|
|
border: '1px solid #ff006e50',
|
|||
|
|
borderRadius: 6,
|
|||
|
|
color: '#ff006e',
|
|||
|
|
fontSize: 11,
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
触发失败跳转
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{meeting.completed && (
|
|||
|
|
<span
|
|||
|
|
style={{
|
|||
|
|
padding: '4px 10px',
|
|||
|
|
background: '#00ff9d20',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
color: '#00ff9d',
|
|||
|
|
fontSize: 11,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
已完成
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 节点间的连接线 */}
|
|||
|
|
{index < workflow.meetings.length - 1 && (
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'center', marginTop: 8 }}>
|
|||
|
|
<ArrowRight size={16} color="rgba(255, 255, 255, 0.2)" />
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function WorkflowPage() {
|
|||
|
|
const [workflows, setWorkflows] = useState<WorkflowType[]>([]);
|
|||
|
|
const [selectedWorkflow, setSelectedWorkflow] = useState<WorkflowType | null>(null);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
const [uploading, setUploading] = useState(false);
|
|||
|
|
const [starting, setStarting] = useState<string | null>(null);
|
|||
|
|
|
|||
|
|
const loadWorkflows = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
// 加载已加载的工作流列表
|
|||
|
|
const listRes = await api.workflow.list();
|
|||
|
|
const loadedWorkflows: WorkflowType[] = [];
|
|||
|
|
|
|||
|
|
// 从已加载的工作流中获取详情
|
|||
|
|
for (const wf of listRes.workflows) {
|
|||
|
|
try {
|
|||
|
|
const detail = await api.workflow.get(wf.workflow_id);
|
|||
|
|
loadedWorkflows.push(detail);
|
|||
|
|
} catch {
|
|||
|
|
// 忽略加载失败的
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setWorkflows(loadedWorkflows);
|
|||
|
|
setError(null);
|
|||
|
|
} catch (err) {
|
|||
|
|
setError(err instanceof Error ? err.message : '加载失败');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const startDefaultWorkflow = async () => {
|
|||
|
|
setStarting('default-dev-flow');
|
|||
|
|
try {
|
|||
|
|
const workflow = await api.workflow.start('default-dev-flow.yaml');
|
|||
|
|
setWorkflows(prev => [...prev.filter(w => w.workflow_id !== workflow.workflow_id), workflow]);
|
|||
|
|
setSelectedWorkflow(workflow);
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('启动默认工作流失败: ' + (err instanceof Error ? err.message : '未知错误'));
|
|||
|
|
} finally {
|
|||
|
|
setStarting(null);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadWorkflows();
|
|||
|
|
const interval = setInterval(loadWorkflows, 30000);
|
|||
|
|
return () => clearInterval(interval);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|||
|
|
const file = e.target.files?.[0];
|
|||
|
|
if (!file) return;
|
|||
|
|
|
|||
|
|
if (!file.name.endsWith('.yaml') && !file.name.endsWith('.yml')) {
|
|||
|
|
alert('请上传 YAML 文件');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setUploading(true);
|
|||
|
|
try {
|
|||
|
|
await file.text();
|
|||
|
|
// 这里应该调用后端 API 上传文件
|
|||
|
|
// 暂时模拟成功
|
|||
|
|
alert(`文件 ${file.name} 上传成功`);
|
|||
|
|
loadWorkflows();
|
|||
|
|
} catch (err) {
|
|||
|
|
alert('上传失败');
|
|||
|
|
} finally {
|
|||
|
|
setUploading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getStatusColor = (status: string) => {
|
|||
|
|
switch (status) {
|
|||
|
|
case 'completed':
|
|||
|
|
return '#00ff9d';
|
|||
|
|
case 'in_progress':
|
|||
|
|
return '#00f0ff';
|
|||
|
|
default:
|
|||
|
|
return '#ff9500';
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (loading && workflows.length === 0) {
|
|||
|
|
return (
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
|
|||
|
|
<div style={{ color: '#00f0ff' }}>加载中...</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
{/* Header */}
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
|||
|
|
<div>
|
|||
|
|
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}>工作流</h1>
|
|||
|
|
<p style={{ fontSize: 14, color: 'rgba(255, 255, 255, 0.5)', margin: '8px 0 0 0' }}>
|
|||
|
|
管理和执行工作流 - 包含会议节点和执行节点
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|||
|
|
<button
|
|||
|
|
onClick={loadWorkflows}
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 8,
|
|||
|
|
padding: '10px 16px',
|
|||
|
|
background: 'rgba(255, 255, 255, 0.05)',
|
|||
|
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|||
|
|
borderRadius: 8,
|
|||
|
|
color: 'rgba(255, 255, 255, 0.7)',
|
|||
|
|
fontSize: 14,
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<RefreshCw size={16} />
|
|||
|
|
刷新
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={startDefaultWorkflow}
|
|||
|
|
disabled={starting === 'default-dev-flow'}
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 8,
|
|||
|
|
padding: '10px 16px',
|
|||
|
|
background: 'rgba(0, 240, 255, 0.1)',
|
|||
|
|
border: '1px solid rgba(0, 240, 255, 0.3)',
|
|||
|
|
borderRadius: 8,
|
|||
|
|
color: '#00f0ff',
|
|||
|
|
fontSize: 14,
|
|||
|
|
cursor: starting === 'default-dev-flow' ? 'not-allowed' : 'pointer',
|
|||
|
|
opacity: starting === 'default-dev-flow' ? 0.6 : 1,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Zap size={16} />
|
|||
|
|
{starting === 'default-dev-flow' ? '启动中...' : '启动默认工作流'}
|
|||
|
|
</button>
|
|||
|
|
<label
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 8,
|
|||
|
|
padding: '10px 16px',
|
|||
|
|
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: 8,
|
|||
|
|
color: '#000',
|
|||
|
|
fontSize: 14,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
cursor: uploading ? 'not-allowed' : 'pointer',
|
|||
|
|
opacity: uploading ? 0.6 : 1,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Upload size={18} />
|
|||
|
|
{uploading ? '上传中...' : '上传工作流'}
|
|||
|
|
<input type="file" accept=".yaml,.yml" onChange={handleFileUpload} disabled={uploading} style={{ display: 'none' }} />
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
padding: '12px 16px',
|
|||
|
|
background: '#ff006e20',
|
|||
|
|
border: '1px solid #ff006e50',
|
|||
|
|
borderRadius: 8,
|
|||
|
|
color: '#ff006e',
|
|||
|
|
marginBottom: 24,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
连接后端失败: {error}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Workflows Grid */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'grid',
|
|||
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
|
|||
|
|
gap: 16,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{workflows.map((workflow) => (
|
|||
|
|
<div
|
|||
|
|
key={workflow.workflow_id}
|
|||
|
|
onClick={() => setSelectedWorkflow(workflow)}
|
|||
|
|
style={{
|
|||
|
|
padding: 20,
|
|||
|
|
background: 'rgba(17, 24, 39, 0.7)',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
border: '1px solid rgba(0, 240, 255, 0.1)',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
transition: 'all 0.2s ease',
|
|||
|
|
}}
|
|||
|
|
onMouseEnter={(e) => {
|
|||
|
|
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.3)';
|
|||
|
|
e.currentTarget.style.transform = 'translateY(-2px)';
|
|||
|
|
}}
|
|||
|
|
onMouseLeave={(e) => {
|
|||
|
|
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.1)';
|
|||
|
|
e.currentTarget.style.transform = 'translateY(0)';
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: 48,
|
|||
|
|
height: 48,
|
|||
|
|
borderRadius: 10,
|
|||
|
|
background: `linear-gradient(135deg, ${getStatusColor(workflow.status)}40 0%, ${getStatusColor(workflow.status)}10 100%)`,
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Workflow size={24} color={getStatusColor(workflow.status)} />
|
|||
|
|
</div>
|
|||
|
|
<span
|
|||
|
|
style={{
|
|||
|
|
padding: '4px 10px',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
background: `${getStatusColor(workflow.status)}20`,
|
|||
|
|
color: getStatusColor(workflow.status),
|
|||
|
|
fontSize: 11,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{workflow.status === 'completed' ? '已完成' : workflow.status === 'in_progress' ? '进行中' : '等待中'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: '0 0 4px 0' }}>
|
|||
|
|
{workflow.name}
|
|||
|
|
</h3>
|
|||
|
|
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '0 0 12px 0' }}>
|
|||
|
|
{workflow.workflow_id}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<p
|
|||
|
|
style={{
|
|||
|
|
fontSize: 13,
|
|||
|
|
color: 'rgba(255, 255, 255, 0.6)',
|
|||
|
|
margin: '0 0 16px 0',
|
|||
|
|
lineHeight: 1.5,
|
|||
|
|
display: '-webkit-box',
|
|||
|
|
WebkitLineClamp: 2,
|
|||
|
|
WebkitBoxOrient: 'vertical',
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{workflow.description}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{/* Progress */}
|
|||
|
|
<div style={{ marginBottom: 12 }}>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
height: 6,
|
|||
|
|
background: 'rgba(255, 255, 255, 0.1)',
|
|||
|
|
borderRadius: 3,
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: workflow.progress,
|
|||
|
|
height: '100%',
|
|||
|
|
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
|
|||
|
|
borderRadius: 3,
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|||
|
|
<FileText size={14} color="rgba(255, 255, 255, 0.4)" />
|
|||
|
|
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>
|
|||
|
|
{workflow.meetings.length} 个节点
|
|||
|
|
</span>
|
|||
|
|
{/* 统计节点类型 */}
|
|||
|
|
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.3)' }}>
|
|||
|
|
({workflow.meetings.filter(m => m.node_type === 'execution').length} 执行 + {workflow.meetings.filter(m => m.node_type === 'meeting').length} 会议)
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|||
|
|
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>查看详情</span>
|
|||
|
|
<ChevronRight size={14} color="rgba(255, 255, 255, 0.4)" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{workflows.length === 0 && !loading && (
|
|||
|
|
<div style={{ textAlign: 'center', padding: 80 }}>
|
|||
|
|
<Workflow size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
|
|||
|
|
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>暂无工作流</p>
|
|||
|
|
<p style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: 14 }}>上传 YAML 文件创建</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Detail Panel */}
|
|||
|
|
{selectedWorkflow && (
|
|||
|
|
<WorkflowDetailPanel
|
|||
|
|
workflow={selectedWorkflow}
|
|||
|
|
onClose={() => setSelectedWorkflow(null)}
|
|||
|
|
onReload={loadWorkflows}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|