import React, { useState, useRef, useEffect, useMemo, useCallback, memo } from 'react'; import { ResizeBox, Tabs, Message, Spin, Table, TableColumnProps, Button } from '@arco-design/web-react'; import { IconCheckCircleFill, IconLoading, IconCloseCircleFill, IconLeft } from '@arco-design/web-react/icon'; import FlowEditor from '@/pages/flowEditor/index'; import { getInstanceDefinition } from '@/api/appIns'; import { formatSeconds } from '@/utils/common'; import dayjs from 'dayjs'; import styles from './index.module.less'; const TabPane = Tabs.TabPane; // 使用 memo 包装 FlowEditor,防止不必要的重新渲染 const MemoizedFlowEditor = memo(({ flowData, instanceId }: { flowData: any; instanceId: string }) => { return ( ); }, (prevProps, nextProps) => { // 自定义比较函数:只有当 instanceId 变化时才重新渲染 return prevProps.instanceId === nextProps.instanceId; }); interface LogMessage { id: number; type: string; message: string; timestamp: string; } interface RuntimeData { [key: string]: any; } interface InstanceCanvasProps { instanceData: any; // 实例数据 title?: string; // 实例标题 onBack?: () => void; // 返回回调 } const InstanceCanvas: React.FC = ({ instanceData, title, onBack }) => { const [activeTab, setActiveTab] = useState('1'); const [logBarExpanded, setLogBarExpanded] = useState(true); const [logContainerHeight, setLogContainerHeight] = useState('250px'); const [runtimeLogs, setRuntimeLogs] = useState([]); const [runtimeData, setRuntimeData] = useState({}); const [flowData, setFlowData] = useState(null); // 流程数据 const [nodeStatusMap, setNodeStatusMap] = useState>({}); // 节点状态映射 const [loading, setLoading] = useState(false); // 加载状态 const resizeBoxRef = useRef(null); const flowDataRef = useRef(null); // 使用 ref 来存储 flowData,避免重复设置 // 获取实例定义数据 const fetchInstanceDefinition = useCallback(async () => { if (!instanceData?.id) return; setLoading(true); try { const res: any = await getInstanceDefinition(instanceData.id); if (res.code === 200 && res.data) { const components = res.data.main?.flow?.components; // 包装数据格式,使其符合 FlowEditor 的期望 // FlowEditor 使用 useDefault=true 时,会调用 projectFlowHandle // projectFlowHandle 期望 initialData?.main?.components 或 initialData?.components if (components && typeof components === 'object') { // 转换连接格式:将单 $ 转换为 $$ const convertedComponents = Object.entries(components).reduce((acc, [nodeId, nodeConfig]: [string, any]) => { const converted = { ...nodeConfig }; // 转换 apiDownstream if (converted.apiDownstream && Array.isArray(converted.apiDownstream)) { converted.apiDownstream = converted.apiDownstream.map((targetArray: string[]) => { if (Array.isArray(targetArray)) { return targetArray.map((target: string) => { if (typeof target === 'string' && target.includes('$') && !target.includes('$$')) { return target.replace('$', '$$'); } return target; }); } return targetArray; }); } // 转换 apiUpstream if (converted.apiUpstream && Array.isArray(converted.apiUpstream)) { converted.apiUpstream = converted.apiUpstream.map((sourceArray: string[]) => { if (Array.isArray(sourceArray)) { return sourceArray.map((source: string) => { if (typeof source === 'string' && source.includes('$') && !source.includes('$$')) { return source.replace('$', '$$'); } return source; }); } return sourceArray; }); } acc[nodeId] = converted; return acc; }, {} as any); const newFlowData = { components: convertedComponents, main: { components: convertedComponents } }; // 只在数据真正变化时才更新 if (!flowDataRef.current) { flowDataRef.current = newFlowData; setFlowData(newFlowData); } } // 处理运行日志和节点状态 if (res.data.main?.nodeLogs && Array.isArray(res.data.main.nodeLogs) && res.data.main.nodeLogs.length > 0) { const logs = res.data.main.nodeLogs.map((log: any, index: number) => ({ id: index, type: 'runtime', message: log.runLog || '', timestamp: new Date(log.startTime).toLocaleString('zh-CN'), nodeId: log.nodeId, nodeName: log.name, duration: log.duration, state: log.state })); setRuntimeLogs(logs); // 构建节点状态映射 const statusMap: Record = {}; res.data.main.nodeLogs.forEach((log: any) => { // state: 1=成功(success), 0=运行中(running), -1=失败(failed) if (log.state === 1) { statusMap[log.nodeId] = 'success'; } else if (log.state === -1) { statusMap[log.nodeId] = 'failed'; } else if (log.state === 0) { statusMap[log.nodeId] = 'running'; } }); setNodeStatusMap(statusMap); } // 处理运行数据(input/output) if (res.data.main?.nodeLogs && Array.isArray(res.data.main.nodeLogs) && res.data.main.nodeLogs.length > 0) { const dataMap: RuntimeData = {}; res.data.main.nodeLogs.forEach((log: any) => { if (log.input || log.output) { dataMap[log.nodeId] = { nodeName: log.name, input: log.input || {}, output: log.output || {}, duration: log.duration, startTime: log.startTime, endTime: log.endTime, state: log.state === 1 ? '成功' : log.state === -1 ? '失败' : '运行中' }; } }); setRuntimeData(dataMap); } } else { Message.error(res.msg || '获取实例数据失败'); } } catch (error) { console.error('获取实例定义失败:', error); Message.error('获取实例数据失败'); } finally { setLoading(false); } }, [instanceData?.id]); // 当 instanceData 变化时,重新获取数据 useEffect(() => { flowDataRef.current = null; // 重置 ref fetchInstanceDefinition(); }, [instanceData?.id, fetchInstanceDefinition]); // 处理 Tab 点击事件 const handleTabClick = (key: string) => { if (key === activeTab) { setLogBarExpanded(!logBarExpanded); } else { setActiveTab(key); setLogBarExpanded(true); } }; // 当展开/收起状态改变时,更新元素样式 useEffect(() => { if (resizeBoxRef.current) { resizeBoxRef.current.style.height = logBarExpanded ? logContainerHeight : '0px'; } }, [logBarExpanded, logContainerHeight]); // 处理 ResizeBox 手动调整大小事件 const handleResize = useCallback((e: MouseEvent, size: { width: number; height: number }) => { if (size.height <= 40) { setLogBarExpanded(false); } else { setLogBarExpanded(true); setLogContainerHeight(`${size.height}px`); } }, []); // 使用 useMemo 创建带有节点状态的 flowData const flowDataWithStatus = useMemo(() => { if (!flowData || Object.keys(nodeStatusMap).length === 0) { return flowData; } // 深拷贝 flowData 并注入节点状态 const componentsWithStatus = { ...flowData.components }; Object.keys(nodeStatusMap).forEach((nodeId) => { if (componentsWithStatus[nodeId]) { componentsWithStatus[nodeId] = { ...componentsWithStatus[nodeId], status: nodeStatusMap[nodeId], isStatusVisible: true // 显示状态指示器 }; } }); const result = { ...flowData, components: componentsWithStatus, main: { ...flowData.main, components: componentsWithStatus } }; return result; }, [flowData, nodeStatusMap]); // 渲染运行日志内容 const renderRuntimeLogs = () => { return (
{runtimeLogs.length === 0 ? (

暂无运行日志

) : ( runtimeLogs.map((log: any) => (
{log.timestamp}
{log.message}
)) )}
); }; // 定义运行数据表格列 const dataColumns: TableColumnProps[] = [ { title: '节点', dataIndex: 'nodeName', width: 120 }, { title: '时间', width: 160, render: (_: any, record: any) => ( <> 开始 {dayjs(record.startTime).format('MM-DD HH:mm:ss')}
{record.endTime > 0 && 结束 {dayjs(record.endTime).format('MM-DD HH:mm:ss')}} ) }, { title: '耗时', dataIndex: 'duration', width: 80, render: (_: any, record: any) => ( record.duration > 1000 ? {formatSeconds((record.duration / 1000).toString())} : {record.duration}ms ) }, { title: '状态', dataIndex: 'state', width: 60, align: 'center', render: (_: any, record: any) => ( <> {record.state === 1 && } {record.state === 0 && } {record.state === -1 && } ) }, { title: '输入参数', dataIndex: 'input', width: 200, render: (_: any, record: any) => (
{JSON.stringify(record.input)}
) }, { title: '输出参数', dataIndex: 'output', width: 200, render: (_: any, record: any) => (
{JSON.stringify(record.output)}
) } ]; // 渲染运行数据内容 const renderRuntimeData = () => { // 将 runtimeData 对象转换为表格数据数组 const tableData = Object.entries(runtimeData).map(([nodeId, data]: [string, any]) => ({ key: nodeId, nodeId, nodeName: data.nodeName, startTime: data.startTime || 0, endTime: data.endTime || 0, duration: data.duration, state: data.state === '成功' ? 1 : data.state === '失败' ? -1 : 0, input: data.input, output: data.output })); return (
{tableData.length === 0 ? (

暂无运行数据

) : ( )} ); }; return (
{/* 顶部标题栏 */} {title && (
{onBack && ( )}

实例名称:{title}

)} {/* 主内容区域 */}
{/* 流程画布 */}
{loading ? (
加载中...
) : flowDataWithStatus ? ( ) : (
暂无流程数据
)}
{/* 底部日志栏 */}
{renderRuntimeLogs()} {renderRuntimeData()}
{/* Tab 标签栏(始终可见) */} {!logBarExpanded && (
)}
); }; export default InstanceCanvas;