feat(web): wire ProcessViewer and delegate UI into workflow pages

- 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)
This commit is contained in:
iven
2026-04-11 16:26:47 +08:00
parent 6a08b99ed8
commit 1fec5e2cf2
2 changed files with 91 additions and 13 deletions

View File

@@ -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<string, string> = {
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<NodeDef[]>([]);
const [viewerEdges, setViewerEdges] = useState<EdgeDef[]>([]);
const [activeNodeIds, setActiveNodeIds] = useState<string[]>([]);
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,16 +91,22 @@ 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' ? (
<>
<Button size="small" onClick={() => handleViewFlow(record)} style={{ marginRight: 8 }}>
</Button>
{record.status === 'running' && (
<Button size="small" danger onClick={() => handleTerminate(record.id)}></Button>
) : null
)}
</>
),
},
];
return (
<>
<Table
rowKey="id"
columns={columns}
@@ -83,5 +114,16 @@ export default function InstanceMonitor() {
loading={loading}
pagination={{ current: page, total, pageSize: 20, onChange: setPage }}
/>
<Modal
title="流程图查看"
open={viewerOpen}
onCancel={() => setViewerOpen(false)}
footer={null}
width={720}
loading={viewerLoading}
>
<ProcessViewer nodes={viewerNodes} edges={viewerEdges} activeNodeIds={activeNodeIds} />
</Modal>
</>
);
}

View File

@@ -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<TaskInfo | null>(null);
const [outcome, setOutcome] = useState('approved');
const [delegateModal, setDelegateModal] = useState<TaskInfo | null>(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<TaskInfo> = [
{ 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) => (
<Space>
<Button size="small" type="primary" onClick={() => { setCompleteModal(record); setOutcome('approved'); }}>
</Button>
<Button size="small" onClick={() => { setDelegateModal(record); setDelegateTo(''); }}>
</Button>
</Space>
),
},
@@ -92,6 +114,20 @@ export default function PendingTasks() {
</Button>
</Space>
</Modal>
<Modal
title="委派任务"
open={!!delegateModal}
onOk={handleDelegate}
onCancel={() => { setDelegateModal(null); setDelegateTo(''); }}
okText="确认委派"
>
<p>: {delegateModal?.node_name}</p>
<Input
placeholder="输入目标用户 ID (UUID)"
value={delegateTo}
onChange={(e) => setDelegateTo(e.target.value)}
/>
</Modal>
</>
);
}