Files
erp/apps/web/src/pages/workflow/InstanceMonitor.tsx
iven 89fc482d99
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(web): 采用 UI UX Pro Max Soft UI Evolution 设计系统
从 Pinterest 风格切换到 Soft UI Evolution 设计系统,使用 UI UX Pro Max
推理引擎生成适合跨行业 ERP 业务用户的专业设计方案。

设计变更:
- 主色从 Pinterest Red (#e60023) 切换到 Trust Blue (#2563EB)
- 字体从系统默认切换到 Noto Sans SC(中文优先)
- 圆角从 16-20px 调整到 10-12px(专业但不夸张)
- 中性色从暖橄榄调切换到 Slate 石板蓝调
- 成功色 #103c25 → #059669,警告色 #b56e1a → #d97706
- 暗色模式从暖黑 (#1a1a18) 切换到深海军蓝 (#0f172a)

涉及文件:DESIGN.md + index.css + App.tsx + 24 个组件文件
2026-04-20 23:27:24 +08:00

248 lines
6.9 KiB
TypeScript

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<string, { bg: string; color: string; text: string }> = {
running: { bg: '#eff6ff', color: '#2563eb', text: '运行中' },
suspended: { bg: '#FFFBEB', color: '#d97706', text: '已挂起' },
completed: { bg: '#ECFDF5', color: '#059669', text: '已完成' },
terminated: { bg: '#FEF2F2', color: '#dc2626', text: '已终止' },
};
export default function InstanceMonitor() {
const [data, setData] = useState<ProcessInstanceInfo[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
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 { 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<ProcessInstanceInfo> = [
{
title: '流程',
dataIndex: 'definition_name',
key: 'definition_name',
render: (v: string) => <span style={{ fontWeight: 500 }}>{v}</span>,
},
{
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: '#f8fafc', color: '#475569', text: s };
return (
<Tag style={{
background: info.bg,
border: 'none',
color: info.color,
fontWeight: 500,
}}>
{info.text}
</Tag>
);
},
},
{
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) => (
<span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>
{new Date(v).toLocaleString()}
</span>
),
},
{
title: '操作',
key: 'action',
width: 240,
render: (_, record) => (
<div style={{ display: 'flex', gap: 4 }}>
<Button
size="small"
type="text"
icon={<EyeOutlined />}
onClick={() => handleViewFlow(record)}
>
</Button>
{record.status === 'running' && (
<>
<Button
size="small"
type="text"
icon={<PauseCircleOutlined />}
onClick={() => handleSuspend(record.id)}
>
</Button>
<Button
size="small"
type="text"
danger
icon={<StopOutlined />}
onClick={() => handleTerminate(record.id)}
>
</Button>
</>
)}
{record.status === 'suspended' && (
<Button
size="small"
type="primary"
icon={<PlayCircleOutlined />}
onClick={() => handleResume(record.id)}
>
</Button>
)}
</div>
),
},
];
return (
<>
<div style={{
background: isDark ? '#111827' : '#FFFFFF',
borderRadius: 12,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
overflow: 'hidden',
}}>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
pagination={{
current: page,
total,
pageSize: 20,
onChange: setPage,
showTotal: (t) => `${t} 条记录`,
}}
/>
</div>
<Modal
title="流程图查看"
open={viewerOpen}
onCancel={() => setViewerOpen(false)}
footer={null}
width={720}
loading={viewerLoading}
>
<ProcessViewer nodes={viewerNodes} edges={viewerEdges} activeNodeIds={activeNodeIds} />
</Modal>
</>
);
}