From 1fec5e2cf2c5fec3f753c98b1b93af39d97729aa Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 11 Apr 2026 16:26:47 +0800 Subject: [PATCH] feat(web): wire ProcessViewer and delegate UI into workflow pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InstanceMonitor: add '流程图' button that opens ProcessViewer modal with active node highlighting, loading flow definition via API - PendingTasks: add '委派' button with delegate modal (UUID input), wired to the existing delegateTask API function - Both ProcessViewer component and delegateTask API were previously dead code (never imported/called) --- .../src/pages/workflow/InstanceMonitor.tsx | 64 +++++++++++++++---- apps/web/src/pages/workflow/PendingTasks.tsx | 40 +++++++++++- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/apps/web/src/pages/workflow/InstanceMonitor.tsx b/apps/web/src/pages/workflow/InstanceMonitor.tsx index 9f16b9d..973d0bd 100644 --- a/apps/web/src/pages/workflow/InstanceMonitor.tsx +++ b/apps/web/src/pages/workflow/InstanceMonitor.tsx @@ -6,6 +6,8 @@ import { terminateInstance, type ProcessInstanceInfo, } from '../../api/workflowInstances'; +import { getProcessDefinition, type NodeDef, type EdgeDef } from '../../api/workflowDefinitions'; +import ProcessViewer from './ProcessViewer'; const statusColors: Record = { running: 'processing', @@ -20,6 +22,13 @@ export default function InstanceMonitor() { const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); + // ProcessViewer state + const [viewerOpen, setViewerOpen] = useState(false); + const [viewerNodes, setViewerNodes] = useState([]); + const [viewerEdges, setViewerEdges] = useState([]); + const [activeNodeIds, setActiveNodeIds] = useState([]); + const [viewerLoading, setViewerLoading] = useState(false); + const fetchData = useCallback(async () => { setLoading(true); try { @@ -33,6 +42,22 @@ export default function InstanceMonitor() { 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: '确认终止', @@ -66,22 +91,39 @@ export default function InstanceMonitor() { render: (v: string) => new Date(v).toLocaleString(), }, { - title: '操作', key: 'action', width: 100, + title: '操作', key: 'action', width: 150, render: (_, record) => ( - record.status === 'running' ? ( - - ) : null + <> + + {record.status === 'running' && ( + + )} + ), }, ]; return ( - + <> +
+ setViewerOpen(false)} + footer={null} + width={720} + loading={viewerLoading} + > + + + ); } diff --git a/apps/web/src/pages/workflow/PendingTasks.tsx b/apps/web/src/pages/workflow/PendingTasks.tsx index 61a9a5f..ec27513 100644 --- a/apps/web/src/pages/workflow/PendingTasks.tsx +++ b/apps/web/src/pages/workflow/PendingTasks.tsx @@ -1,9 +1,10 @@ import { useCallback, useEffect, useState } from 'react'; -import { Button, message, Modal, Space, Table, Tag } from 'antd'; +import { Button, Input, message, Modal, Space, Table, Tag } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { listPendingTasks, completeTask, + delegateTask, type TaskInfo, } from '../../api/workflowTasks'; @@ -18,6 +19,8 @@ export default function PendingTasks() { const [loading, setLoading] = useState(false); const [completeModal, setCompleteModal] = useState(null); const [outcome, setOutcome] = useState('approved'); + const [delegateModal, setDelegateModal] = useState(null); + const [delegateTo, setDelegateTo] = useState(''); const fetchData = useCallback(async () => { setLoading(true); @@ -44,6 +47,22 @@ export default function PendingTasks() { } }; + const handleDelegate = async () => { + if (!delegateModal || !delegateTo.trim()) { + message.warning('请输入委派目标用户 ID'); + return; + } + try { + await delegateTask(delegateModal.id, { delegate_to: delegateTo.trim() }); + message.success('委派成功'); + setDelegateModal(null); + setDelegateTo(''); + fetchData(); + } catch { + message.error('委派失败'); + } + }; + const columns: ColumnsType = [ { title: '任务名称', dataIndex: 'node_name', key: 'node_name' }, { title: '流程', dataIndex: 'definition_name', key: 'definition_name' }, @@ -56,12 +75,15 @@ export default function PendingTasks() { render: (v: string) => new Date(v).toLocaleString(), }, { - title: '操作', key: 'action', width: 120, + title: '操作', key: 'action', width: 160, render: (_, record) => ( + ), }, @@ -92,6 +114,20 @@ export default function PendingTasks() { + { setDelegateModal(null); setDelegateTo(''); }} + okText="确认委派" + > +

任务: {delegateModal?.node_name}

+ setDelegateTo(e.target.value)} + /> +
); }