import { useEffect, useCallback, useState } from 'react'; import { Button, message, Modal, Table, Tag, theme } from 'antd'; import { EyeOutlined, PauseCircleOutlined, PlayCircleOutlined, StopOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { listInstances, resumeInstance, suspendInstance, terminateInstance, type ProcessInstanceInfo, } from '../../api/workflowInstances'; import { getProcessDefinition, type NodeDef, type EdgeDef } from '../../api/workflowDefinitions'; import ProcessViewer from './ProcessViewer'; const statusStyles: Record = { running: { bg: '#f2f9ff', color: '#0075de', text: '运行中' }, suspended: { bg: '#FFFBEB', color: '#dd5b00', text: '已挂起' }, completed: { bg: '#ECFDF5', color: '#1aae39', text: '已完成' }, terminated: { bg: '#FEF2F2', color: '#e5534b', text: '已终止' }, }; export default function InstanceMonitor() { const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [viewerOpen, setViewerOpen] = useState(false); const [viewerNodes, setViewerNodes] = useState([]); const [viewerEdges, setViewerEdges] = useState([]); const [activeNodeIds, setActiveNodeIds] = useState([]); const [viewerLoading, setViewerLoading] = useState(false); const { token } = theme.useToken(); const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)'; const fetchData = useCallback(async () => { setLoading(true); try { const res = await listInstances(page, 20); setData(res.data); setTotal(res.total); } finally { setLoading(false); } }, [page]); useEffect(() => { fetchData(); }, [fetchData]); const handleViewFlow = async (record: ProcessInstanceInfo) => { setViewerLoading(true); setViewerOpen(true); try { const def = await getProcessDefinition(record.definition_id); setViewerNodes(def.nodes); setViewerEdges(def.edges); setActiveNodeIds(record.active_tokens.map((t) => t.node_id)); } catch { message.error('加载流程图失败'); setViewerOpen(false); } finally { setViewerLoading(false); } }; const handleTerminate = async (id: string) => { Modal.confirm({ title: '确认终止', content: '确定要终止该流程实例吗?此操作不可撤销。', okText: '确定终止', okType: 'danger', cancelText: '取消', onOk: async () => { try { await terminateInstance(id); message.success('已终止'); fetchData(); } catch { message.error('操作失败'); } }, }); }; const handleSuspend = async (id: string) => { Modal.confirm({ title: '确认挂起', content: '确定要挂起该流程实例吗?挂起后可通过"恢复"按钮继续执行。', okText: '确定挂起', okType: 'default', cancelText: '取消', onOk: async () => { try { await suspendInstance(id); message.success('已挂起'); fetchData(); } catch { message.error('操作失败'); } }, }); }; const handleResume = async (id: string) => { try { await resumeInstance(id); message.success('已恢复'); fetchData(); } catch { message.error('操作失败'); } }; const columns: ColumnsType = [ { title: '流程', dataIndex: 'definition_name', key: 'definition_name', render: (v: string) => {v}, }, { title: '业务键', dataIndex: 'business_key', key: 'business_key', render: (v: string | undefined) => v || '-', }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (s: string) => { const info = statusStyles[s] || { bg: '#f6f5f4', color: '#615d59', text: s }; return ( {info.text} ); }, }, { title: '当前节点', key: 'current_nodes', width: 150, render: (_, record) => record.active_tokens.map(t => t.node_id).join(', ') || '-', }, { title: '发起时间', dataIndex: 'started_at', key: 'started_at', width: 180, render: (v: string) => ( {new Date(v).toLocaleString()} ), }, { title: '操作', key: 'action', width: 240, render: (_, record) => (
{record.status === 'running' && ( <> )} {record.status === 'suspended' && ( )}
), }, ]; return ( <>
`共 ${t} 条记录`, }} /> setViewerOpen(false)} footer={null} width={720} loading={viewerLoading} > ); }