Files
hms/apps/web/src/pages/workflow/InstanceMonitor.tsx
iven 8f3d2d58e7 feat(web): 采用 Notion 设计系统 — 暖色调 + 白色侧边栏 + Inter 字体
引入 Notion 风格的 DESIGN.md 设计系统文件,并全面重构前端 UI:

- 主色从 Indigo (#4F46E5) 迁移到 Notion Blue (#0075de)
- 页面背景从冷灰 (#F1F5F9) 迁移到暖白 (#f6f5f4)
- 侧边栏从深色 (#0F172A) 迁移到白色,活跃项用蓝色指示
- 文字从 Slate 冷色迁移到暖灰系列 (Warm Gray 500/300)
- 圆角从 8px 缩小到 4px(按钮/输入),8px(卡片)
- 阴影改为多层超轻 Notion 风格(最大 opacity 0.05)
- 字体优先使用 Inter,保留中文回退
- 暗色模式适配暖黑色调 (#191918)
- 更新 27 个前端文件的硬编码颜色值
2026-04-20 13:08:22 +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: '#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<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: '#f6f5f4', color: '#615d59', 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 ? '#615d59' : '#a39e98', 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 ? '#1e1e1d' : '#f6f5f4'}`,
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>
</>
);
}