从 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 个组件文件
248 lines
6.9 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|