This commit is contained in:
dengqichen 2025-10-20 21:38:09 +08:00
parent a542ee9d2f
commit 20c4184d45
12 changed files with 1799 additions and 82 deletions

View File

@ -152,8 +152,8 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
fitViewOptions={{ fitViewOptions={{
padding: 0.1, padding: 0.1,
includeHiddenNodes: false, includeHiddenNodes: false,
minZoom: 0.5, minZoom: 1.0,
maxZoom: 1.5, maxZoom: 1.0,
duration: 800, duration: 800,
}} }}
minZoom={0.1} minZoom={0.1}

View File

@ -0,0 +1,256 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Input, Tabs, Card, Button, Space, message } from 'antd';
import { SaveOutlined, ReloadOutlined } from '@ant-design/icons';
import type { FlowNode, FlowNodeData } from '../types';
interface NodeConfigModalProps {
visible: boolean;
node: FlowNode | null;
onCancel: () => void;
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
}
const { TextArea } = Input;
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible,
node,
onCancel,
onOk
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('basic');
// 重置表单数据
useEffect(() => {
if (visible && node) {
form.setFieldsValue({
label: node.data.label,
description: node.data.nodeDefinition?.description || '',
// 基础配置
timeout: node.data.configs?.timeout || 3600,
retryCount: node.data.configs?.retryCount || 0,
priority: node.data.configs?.priority || 'normal',
// 输入映射
inputMapping: JSON.stringify(node.data.inputMapping || {}, null, 2),
// 输出映射
outputMapping: JSON.stringify(node.data.outputMapping || {}, null, 2),
});
} else {
form.resetFields();
}
}, [visible, node, form]);
const handleSubmit = async () => {
if (!node) return;
try {
const values = await form.validateFields();
setLoading(true);
// 解析JSON字符串
let inputMapping = {};
let outputMapping = {};
try {
inputMapping = values.inputMapping ? JSON.parse(values.inputMapping) : {};
} catch (error) {
message.error('输入映射JSON格式错误');
return;
}
try {
outputMapping = values.outputMapping ? JSON.parse(values.outputMapping) : {};
} catch (error) {
message.error('输出映射JSON格式错误');
return;
}
const updatedData: Partial<FlowNodeData> = {
label: values.label,
configs: {
...node.data.configs,
timeout: values.timeout,
retryCount: values.retryCount,
priority: values.priority,
},
inputMapping,
outputMapping,
};
onOk(node.id, updatedData);
message.success('节点配置保存成功');
onCancel();
} catch (error) {
console.error('保存节点配置失败:', error);
} finally {
setLoading(false);
}
};
const handleReset = () => {
form.resetFields();
if (node) {
form.setFieldsValue({
label: node.data.label,
description: node.data.nodeDefinition?.description || '',
timeout: 3600,
retryCount: 0,
priority: 'normal',
inputMapping: '{}',
outputMapping: '{}',
});
}
};
const renderBasicConfig = () => (
<Card size="small" title="基本信息">
<Form.Item
label="节点名称"
name="label"
rules={[{ required: true, message: '请输入节点名称' }]}
>
<Input placeholder="请输入节点名称" />
</Form.Item>
<Form.Item label="节点描述" name="description">
<TextArea
placeholder="请输入节点描述"
rows={3}
maxLength={500}
showCount
/>
</Form.Item>
<Form.Item
label="超时时间(秒)"
name="timeout"
rules={[{ required: true, message: '请输入超时时间' }]}
>
<Input type="number" min={1} placeholder="3600" />
</Form.Item>
<Form.Item
label="重试次数"
name="retryCount"
>
<Input type="number" min={0} max={10} placeholder="0" />
</Form.Item>
<Form.Item
label="优先级"
name="priority"
>
<Input placeholder="normal" />
</Form.Item>
</Card>
);
const renderInputMapping = () => (
<Card size="small" title="输入参数映射">
<Form.Item
label="输入映射配置"
name="inputMapping"
extra="请输入有效的JSON格式用于配置节点的输入参数映射"
>
<TextArea
placeholder={`示例:
{
"param1": "\${workflow.variable1}",
"param2": "固定值",
"param3": "\${previous.output.result}"
}`}
rows={12}
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
/>
</Form.Item>
</Card>
);
const renderOutputMapping = () => (
<Card size="small" title="输出参数映射">
<Form.Item
label="输出映射配置"
name="outputMapping"
extra="请输入有效的JSON格式用于配置节点的输出参数映射"
>
<TextArea
placeholder={`示例:
{
"result": "\${task.output.result}",
"status": "\${task.status}",
"message": "\${task.output.message}"
}`}
rows={12}
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
/>
</Form.Item>
</Card>
);
return (
<Modal
title={`配置节点: ${node?.data?.label || '未知节点'}`}
open={visible}
onCancel={onCancel}
width={800}
style={{ top: 20 }}
footer={
<Space>
<Button onClick={onCancel}></Button>
<Button
icon={<ReloadOutlined />}
onClick={handleReset}
>
</Button>
<Button
type="primary"
loading={loading}
icon={<SaveOutlined />}
onClick={handleSubmit}
>
</Button>
</Space>
}
>
<Form
form={form}
layout="vertical"
initialValues={{
timeout: 3600,
retryCount: 0,
priority: 'normal',
inputMapping: '{}',
outputMapping: '{}'
}}
>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'basic',
label: '基本配置',
children: renderBasicConfig()
},
{
key: 'input',
label: '输入映射',
children: renderInputMapping()
},
{
key: 'output',
label: '输出映射',
children: renderOutputMapping()
}
]}
/>
</Form>
</Modal>
);
};
export default NodeConfigModal;

View File

@ -0,0 +1,238 @@
import React, { useState, useEffect } from 'react';
import { Card, Form, Input, Select, Button, Space, message, Divider } from 'antd';
import { SaveOutlined, ClearOutlined } from '@ant-design/icons';
import type { FlowNode, FlowEdge } from '../types';
interface PropertyPanelProps {
selectedNode?: FlowNode | null;
selectedEdge?: FlowEdge | null;
onNodeUpdate?: (nodeId: string, updates: any) => void;
onEdgeUpdate?: (edgeId: string, updates: any) => void;
className?: string;
}
const PropertyPanel: React.FC<PropertyPanelProps> = ({
selectedNode,
selectedEdge,
onNodeUpdate,
onEdgeUpdate,
className = ''
}) => {
const [form] = Form.useForm();
const [hasChanges, setHasChanges] = useState(false);
// 重置表单
useEffect(() => {
if (selectedNode) {
form.setFieldsValue({
label: selectedNode.data.label,
timeout: selectedNode.data.configs?.timeout || 3600,
retryCount: selectedNode.data.configs?.retryCount || 0,
priority: selectedNode.data.configs?.priority || 'normal',
});
} else if (selectedEdge) {
form.setFieldsValue({
label: selectedEdge.data?.label || '',
conditionType: selectedEdge.data?.condition?.type || 'DEFAULT',
expression: selectedEdge.data?.condition?.expression || '',
});
} else {
form.resetFields();
}
setHasChanges(false);
}, [selectedNode, selectedEdge, form]);
// 监听表单变化
const handleFormChange = () => {
setHasChanges(true);
};
// 保存配置
const handleSave = async () => {
try {
const values = await form.validateFields();
if (selectedNode && onNodeUpdate) {
const updates = {
label: values.label,
configs: {
...selectedNode.data.configs,
timeout: values.timeout,
retryCount: values.retryCount,
priority: values.priority,
},
};
onNodeUpdate(selectedNode.id, updates);
message.success('节点配置已保存');
} else if (selectedEdge && onEdgeUpdate) {
const updates = {
label: values.label,
condition: {
type: values.conditionType,
expression: values.expression,
priority: 0,
},
};
onEdgeUpdate(selectedEdge.id, updates);
message.success('连接配置已保存');
}
setHasChanges(false);
} catch (error) {
message.error('保存失败');
}
};
// 清空选择
const handleClear = () => {
form.resetFields();
setHasChanges(false);
};
// 渲染节点属性
const renderNodeProperties = () => {
if (!selectedNode) return null;
return (
<Card size="small" title={`节点属性: ${selectedNode.data.nodeType}`}>
<Form.Item label="节点名称" name="label" rules={[{ required: true }]}>
<Input placeholder="请输入节点名称" />
</Form.Item>
<Form.Item label="超时时间(秒)" name="timeout">
<Input type="number" min={1} placeholder="3600" />
</Form.Item>
<Form.Item label="重试次数" name="retryCount">
<Input type="number" min={0} max={10} placeholder="0" />
</Form.Item>
<Form.Item label="优先级" name="priority">
<Select placeholder="选择优先级">
<Select.Option value="low"></Select.Option>
<Select.Option value="normal"></Select.Option>
<Select.Option value="high"></Select.Option>
<Select.Option value="urgent"></Select.Option>
</Select>
</Form.Item>
</Card>
);
};
// 渲染边属性
const renderEdgeProperties = () => {
if (!selectedEdge) return null;
return (
<Card size="small" title="连接属性">
<Form.Item label="连接名称" name="label">
<Input placeholder="请输入连接名称" />
</Form.Item>
<Form.Item label="条件类型" name="conditionType">
<Select placeholder="选择条件类型">
<Select.Option value="DEFAULT"></Select.Option>
<Select.Option value="EXPRESSION"></Select.Option>
<Select.Option value="SCRIPT"></Select.Option>
</Select>
</Form.Item>
<Form.Item label="条件表达式" name="expression">
<Input.TextArea
placeholder="请输入条件表达式"
rows={4}
/>
</Form.Item>
</Card>
);
};
return (
<div className={`property-panel ${className}`} style={{
width: '300px',
height: '100%',
background: '#f8fafc',
borderLeft: '1px solid #e5e7eb',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}>
<div style={{
padding: '16px',
background: '#ffffff',
borderBottom: '1px solid #e5e7eb',
flexShrink: 0
}}>
<h3 style={{
margin: 0,
fontSize: '14px',
fontWeight: '600',
color: '#374151'
}}>
</h3>
<p style={{
margin: '4px 0 0 0',
fontSize: '12px',
color: '#6b7280'
}}>
{selectedNode ? '节点配置' : selectedEdge ? '连接配置' : '请选择节点或连接'}
</p>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
{!selectedNode && !selectedEdge && (
<div style={{
textAlign: 'center',
color: '#9ca3af',
fontSize: '14px',
marginTop: '40px'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📋</div>
<p></p>
<p style={{ fontSize: '12px' }}></p>
</div>
)}
<Form
form={form}
layout="vertical"
onValuesChange={handleFormChange}
>
{renderNodeProperties()}
{renderEdgeProperties()}
</Form>
</div>
{(selectedNode || selectedEdge) && (
<div style={{
padding: '16px',
background: '#ffffff',
borderTop: '1px solid #e5e7eb',
flexShrink: 0
}}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button
icon={<ClearOutlined />}
onClick={handleClear}
size="small"
>
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
disabled={!hasChanges}
size="small"
>
</Button>
</Space>
</div>
)}
</div>
);
};
export default PropertyPanel;

View File

@ -0,0 +1,163 @@
import { useCallback, useState } from 'react';
import { message } from 'antd';
import * as definitionService from '../../Definition/service';
import type { FlowNode, FlowEdge } from '../types';
import type { WorkflowDefinition } from '../../Definition/types';
interface LoadedWorkflowData {
nodes: FlowNode[];
edges: FlowEdge[];
definition: WorkflowDefinition;
}
export const useWorkflowLoad = () => {
const [loading, setLoading] = useState(false);
const [workflowDefinition, setWorkflowDefinition] = useState<WorkflowDefinition | null>(null);
// 从后端数据转换为React Flow格式
const convertToFlowFormat = useCallback((definition: WorkflowDefinition): LoadedWorkflowData => {
const nodes: FlowNode[] = (definition.graph?.nodes || []).map(node => ({
id: node.id.toString(),
type: node.nodeType,
position: node.uiVariables?.position || { x: 100, y: 100 },
data: {
label: node.nodeName,
nodeType: node.nodeType as any,
category: 'TASK' as any, // 默认分类可以根据nodeType推导
icon: getNodeIcon(node.nodeType),
color: getNodeColor(node.nodeType),
configs: node.panelVariables || {},
inputMapping: node.localVariables?.inputMapping || {},
outputMapping: node.localVariables?.outputMapping || {},
nodeDefinition: {
nodeCode: node.nodeCode,
nodeName: node.nodeName,
nodeType: node.nodeType as any,
category: 'TASK' as any,
icon: getNodeIcon(node.nodeType),
color: getNodeColor(node.nodeType)
}
},
selected: node.uiVariables?.selected || false,
dragging: node.uiVariables?.dragging || false
}));
const edges: FlowEdge[] = (definition.graph?.edges || []).map(edge => ({
id: edge.id,
source: edge.source,
target: edge.target,
type: edge.type || 'default',
animated: edge.animated || true,
data: edge.data || {
label: '连接',
condition: {
type: 'DEFAULT',
priority: 0
}
}
}));
return {
nodes,
edges,
definition
};
}, []);
// 根据节点类型获取图标
const getNodeIcon = (nodeType: string): string => {
const iconMap: Record<string, string> = {
'START_EVENT': '▶️',
'END_EVENT': '⏹️',
'USER_TASK': '👤',
'SERVICE_TASK': '⚙️',
'SCRIPT_TASK': '📜',
'DEPLOY_NODE': '🚀',
'JENKINS_BUILD': '🔨',
'GATEWAY_NODE': '◇',
'SUB_PROCESS': '📦',
'CALL_ACTIVITY': '📞'
};
return iconMap[nodeType] || '📋';
};
// 根据节点类型获取颜色
const getNodeColor = (nodeType: string): string => {
const colorMap: Record<string, string> = {
'START_EVENT': '#10b981',
'END_EVENT': '#ef4444',
'USER_TASK': '#6366f1',
'SERVICE_TASK': '#f59e0b',
'SCRIPT_TASK': '#8b5cf6',
'DEPLOY_NODE': '#06b6d4',
'JENKINS_BUILD': '#f97316',
'GATEWAY_NODE': '#84cc16',
'SUB_PROCESS': '#ec4899',
'CALL_ACTIVITY': '#14b8a6'
};
return colorMap[nodeType] || '#6b7280';
};
// 加载工作流定义
const loadWorkflow = useCallback(async (workflowId: number): Promise<LoadedWorkflowData | null> => {
setLoading(true);
try {
const definition = await definitionService.getDefinitionDetail(workflowId);
setWorkflowDefinition(definition);
const flowData = convertToFlowFormat(definition);
message.success(`工作流 "${definition.name}" 加载成功`);
return flowData;
} catch (error) {
console.error('加载工作流失败:', error);
message.error('加载工作流失败');
return null;
} finally {
setLoading(false);
}
}, [convertToFlowFormat]);
// 创建新的空白工作流
const createNewWorkflow = useCallback((): LoadedWorkflowData => {
const emptyDefinition: WorkflowDefinition = {
id: 0,
name: '新建工作流',
key: `workflow_${Date.now()}`,
description: '',
category: 'GENERAL',
triggers: ['MANUAL'],
status: 'DRAFT',
graph: {
nodes: [],
edges: []
},
formConfig: {
formItems: []
}
};
setWorkflowDefinition(emptyDefinition);
return {
nodes: [],
edges: [],
definition: emptyDefinition
};
}, []);
// 重置加载状态
const resetLoad = useCallback(() => {
setWorkflowDefinition(null);
setLoading(false);
}, []);
return {
loading,
workflowDefinition,
loadWorkflow,
createNewWorkflow,
resetLoad
};
};

View File

@ -0,0 +1,125 @@
import { useCallback, useState } from 'react';
import { message } from 'antd';
import * as definitionService from '../../Definition/service';
import type { FlowNode, FlowEdge } from '../types';
import type { WorkflowDefinition } from '../../Definition/types';
interface WorkflowSaveData {
nodes: FlowNode[];
edges: FlowEdge[];
workflowId?: number;
name?: string;
description?: string;
}
export const useWorkflowSave = () => {
const [saving, setSaving] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// 保存工作流数据
const saveWorkflow = useCallback(async (data: WorkflowSaveData): Promise<boolean> => {
setSaving(true);
try {
// 转换节点和边数据为后端格式
const graph = {
nodes: data.nodes.map(node => ({
id: parseInt(node.id) || 0,
nodeCode: `node_${node.id}`,
nodeType: node.data.nodeType,
nodeName: node.data.label,
uiVariables: {
position: node.position,
selected: node.selected || false,
dragging: node.dragging || false
},
panelVariables: node.data.configs || {},
localVariables: {
inputMapping: node.data.inputMapping || {},
outputMapping: node.data.outputMapping || {}
}
})),
edges: data.edges.map(edge => ({
id: edge.id,
source: edge.source,
target: edge.target,
type: edge.type || 'default',
animated: edge.animated || false,
data: edge.data || {}
}))
};
const workflowData: Partial<WorkflowDefinition> = {
name: data.name || '未命名工作流',
description: data.description || '',
graph,
// 生成简单的表单配置
formConfig: {
formItems: []
}
};
let result: WorkflowDefinition;
if (data.workflowId) {
// 更新现有工作流
result = await definitionService.updateDefinition(data.workflowId, workflowData);
message.success('工作流保存成功');
} else {
// 创建新工作流
result = await definitionService.createDefinition({
...workflowData,
key: `workflow_${Date.now()}`,
category: 'GENERAL',
triggers: ['MANUAL']
} as any);
message.success('工作流创建成功');
}
setLastSaved(new Date());
setHasUnsavedChanges(false);
return true;
} catch (error) {
console.error('保存工作流失败:', error);
message.error('保存工作流失败');
return false;
} finally {
setSaving(false);
}
}, []);
// 自动保存功能
const autoSave = useCallback(async (data: WorkflowSaveData) => {
if (!hasUnsavedChanges || !data.workflowId) return;
try {
await saveWorkflow(data);
console.log('自动保存成功');
} catch (error) {
console.error('自动保存失败:', error);
}
}, [hasUnsavedChanges, saveWorkflow]);
// 标记有未保存更改
const markUnsaved = useCallback(() => {
setHasUnsavedChanges(true);
}, []);
// 标记已保存
const markSaved = useCallback(() => {
setHasUnsavedChanges(false);
setLastSaved(new Date());
}, []);
return {
saving,
lastSaved,
hasUnsavedChanges,
saveWorkflow,
autoSave,
markUnsaved,
markSaved
};
};

View File

@ -6,8 +6,12 @@ import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
import WorkflowToolbar from './components/WorkflowToolbar'; import WorkflowToolbar from './components/WorkflowToolbar';
import NodePanel from './components/NodePanel'; import NodePanel from './components/NodePanel';
import FlowCanvas from './components/FlowCanvas'; import FlowCanvas from './components/FlowCanvas';
import type { FlowNode, FlowEdge, DragNodeData } from './types'; import NodeConfigModal from './components/NodeConfigModal';
import PropertyPanel from './components/PropertyPanel';
import type { FlowNode, FlowEdge, DragNodeData, FlowNodeData } from './types';
import { NodeType } from './types'; import { NodeType } from './types';
import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad';
// 样式 // 样式
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
@ -31,6 +35,34 @@ const WorkflowDesignInner: React.FC = () => {
const [workflowTitle, setWorkflowTitle] = useState('新建工作流'); const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
const reactFlowWrapper = useRef<HTMLDivElement>(null); const reactFlowWrapper = useRef<HTMLDivElement>(null);
// 当前工作流ID
const currentWorkflowId = id ? parseInt(id) : undefined;
// 节点配置模态框状态
const [configModalVisible, setConfigModalVisible] = useState(false);
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
const [selectedEdge, setSelectedEdge] = useState<FlowEdge | null>(null);
// 保存和加载hooks
const { saving, hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { loading: loadingWorkflow, workflowDefinition, loadWorkflow } = useWorkflowLoad();
// 加载工作流数据
useEffect(() => {
const loadData = async () => {
if (currentWorkflowId) {
const data = await loadWorkflow(currentWorkflowId);
if (data) {
setNodes(data.nodes);
setEdges(data.edges);
setWorkflowTitle(data.definition.name);
}
}
};
loadData();
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
// 自动适应视图 // 自动适应视图
useEffect(() => { useEffect(() => {
// 延迟执行fitView以确保节点已渲染 // 延迟执行fitView以确保节点已渲染
@ -38,8 +70,8 @@ const WorkflowDesignInner: React.FC = () => {
fitView({ fitView({
padding: 0.1, padding: 0.1,
duration: 800, duration: 800,
minZoom: 0.5, minZoom: 1.0, // 最小缩放100%
maxZoom: 1.5 maxZoom: 1.0 // 最大缩放100%确保默认100%
}); });
}, 100); }, 100);
@ -118,12 +150,22 @@ const WorkflowDesignInner: React.FC = () => {
]; ];
// 工具栏事件处理 // 工具栏事件处理
const handleSave = useCallback(() => { const handleSave = useCallback(async () => {
const nodes = getNodes(); const nodes = getNodes() as FlowNode[];
const edges = getEdges(); const edges = getEdges() as FlowEdge[];
console.log('保存工作流:', { nodes, edges });
message.success('工作流保存成功!'); const success = await saveWorkflow({
}, [getNodes, getEdges]); nodes,
edges,
workflowId: currentWorkflowId,
name: workflowTitle,
description: workflowDefinition?.description || ''
});
if (success) {
console.log('保存工作流成功:', { nodes, edges });
}
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
navigate('/workflow2/definition'); navigate('/workflow2/definition');
@ -180,22 +222,21 @@ const WorkflowDesignInner: React.FC = () => {
zoomOut({ duration: 300 }); zoomOut({ duration: 300 });
}, [zoomOut]); }, [zoomOut]);
// 处理节点拖拽放置 // 处理节点拖拽放置 - 使用官方推荐的screenToFlowPosition方法
const handleDrop = useCallback((event: React.DragEvent) => { const handleDrop = useCallback((event: React.DragEvent) => {
event.preventDefault(); event.preventDefault();
const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect();
if (!reactFlowBounds) return;
const dragData = event.dataTransfer.getData('application/reactflow'); const dragData = event.dataTransfer.getData('application/reactflow');
if (!dragData) return; if (!dragData) return;
try { try {
const { nodeType, nodeDefinition }: DragNodeData = JSON.parse(dragData); const { nodeType, nodeDefinition }: DragNodeData = JSON.parse(dragData);
// 根据React Flow官方文档screenToFlowPosition会自动处理所有边界计算
// 不需要手动减去容器边界!
const position = screenToFlowPosition({ const position = screenToFlowPosition({
x: event.clientX - reactFlowBounds.left, x: event.clientX,
y: event.clientY - reactFlowBounds.top, y: event.clientY,
}); });
const newNode: FlowNode = { const newNode: FlowNode = {
@ -220,16 +261,73 @@ const WorkflowDesignInner: React.FC = () => {
} }
}, [screenToFlowPosition, setNodes]); }, [screenToFlowPosition, setNodes]);
// 处理节点点击 // 处理节点点击 - 单击选择,双击打开配置面板
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => { const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
console.log('节点点击:', node); console.log('节点点击:', node);
message.info(`点击了节点: ${node.data.label}`);
// 清除边选择
setSelectedEdge(null);
// 检查是否是双击
if (event.detail === 2) {
setSelectedNode(node);
setConfigModalVisible(true);
} else {
setSelectedNode(node);
message.info(`选择了节点: ${node.data.label}`);
}
}, []); }, []);
// 处理边点击 // 处理边点击
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => { const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
console.log('边点击:', edge); console.log('边点击:', edge);
message.info(`点击了连接: ${edge.data?.label || '连接'}`);
// 清除节点选择
setSelectedNode(null);
setSelectedEdge(edge);
message.info(`选择了连接: ${edge.data?.label || '连接'}`);
}, []);
// 处理节点配置更新
const handleNodeConfigUpdate = useCallback((nodeId: string, updatedData: Partial<FlowNodeData>) => {
setNodes((nodes) =>
nodes.map((node) =>
node.id === nodeId
? {
...node,
data: {
...node.data,
...updatedData,
},
}
: node
)
);
markUnsaved(); // 标记有未保存更改
}, [setNodes, markUnsaved]);
// 处理边配置更新
const handleEdgeConfigUpdate = useCallback((edgeId: string, updatedData: any) => {
setEdges((edges) =>
edges.map((edge) =>
edge.id === edgeId
? {
...edge,
data: {
...edge.data,
...updatedData,
},
}
: edge
)
);
markUnsaved(); // 标记有未保存更改
}, [setEdges, markUnsaved]);
// 关闭配置模态框
const handleCloseConfigModal = useCallback(() => {
setConfigModalVisible(false);
setSelectedNode(null);
}, []); }, []);
return ( return (
@ -242,9 +340,10 @@ const WorkflowDesignInner: React.FC = () => {
> >
{/* 工具栏 */} {/* 工具栏 */}
<WorkflowToolbar <WorkflowToolbar
title={workflowTitle} title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
onSave={handleSave} onSave={handleSave}
onBack={handleBack} onBack={handleBack}
saving={saving}
onUndo={handleUndo} onUndo={handleUndo}
onRedo={handleRedo} onRedo={handleRedo}
onCopy={handleCopy} onCopy={handleCopy}
@ -277,7 +376,23 @@ const WorkflowDesignInner: React.FC = () => {
className="workflow-canvas" className="workflow-canvas"
/> />
</div> </div>
{/* 属性面板 */}
<PropertyPanel
selectedNode={selectedNode}
selectedEdge={selectedEdge}
onNodeUpdate={handleNodeConfigUpdate}
onEdgeUpdate={handleEdgeConfigUpdate}
/>
</div> </div>
{/* 节点配置弹窗 */}
<NodeConfigModal
visible={configModalVisible}
node={selectedNode}
onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate}
/>
</div> </div>
); );
}; };

View File

@ -41,9 +41,14 @@ export interface FlowNodeData extends Record<string, unknown> {
icon: string; icon: string;
color: string; color: string;
// 节点配置数据 // 节点配置数据
configs?: Record<string, any>; configs?: {
inputMapping?: Record<string, any>; timeout?: number; // 超时时间(秒)
outputMapping?: Record<string, any>; retryCount?: number; // 重试次数
priority?: string; // 优先级
[key: string]: any; // 其他自定义配置
};
inputMapping?: Record<string, any>; // 输入参数映射
outputMapping?: Record<string, any>; // 输出参数映射
// 原始节点定义 // 原始节点定义
nodeDefinition?: NodeDefinitionData; nodeDefinition?: NodeDefinitionData;
} }

View File

@ -1,24 +1,369 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Card } from 'antd'; import {
Table,
Card,
Button,
Space,
Tag,
message,
Modal,
Row,
Col,
Statistic
} from 'antd';
import {
PlayCircleOutlined,
PauseCircleOutlined,
StopOutlined,
DeleteOutlined,
ReloadOutlined,
EyeOutlined,
BarChartOutlined
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import * as service from './service';
import type {
WorkflowInstance,
WorkflowInstanceQuery,
WorkflowInstanceStatus,
InstanceStatistics
} from './types';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
const { confirm } = Modal;
const WorkflowInstanceList: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [pageData, setPageData] = useState<{
content: WorkflowInstance[];
totalElements: number;
size: number;
number: number;
} | null>(null);
const [statistics, setStatistics] = useState<InstanceStatistics | null>(null);
const [query, setQuery] = useState<WorkflowInstanceQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE
});
// 加载数据
const loadData = async (params: WorkflowInstanceQuery) => {
setLoading(true);
try {
const [instanceData, statsData] = await Promise.all([
service.getInstances(params),
service.getInstanceStatistics()
]);
setPageData(instanceData);
setStatistics(statsData);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData(query);
}, [query]);
// 状态标签渲染
const renderStatus = (status: WorkflowInstanceStatus) => {
const statusConfig = {
RUNNING: { color: 'processing', text: '运行中' },
COMPLETED: { color: 'success', text: '已完成' },
FAILED: { color: 'error', text: '失败' },
SUSPENDED: { color: 'warning', text: '已挂起' },
TERMINATED: { color: 'default', text: '已终止' }
};
const config = statusConfig[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
};
// 操作处理
const handleSuspend = async (instance: WorkflowInstance) => {
confirm({
title: '确认挂起',
content: `确定要挂起实例 "${instance.workflowName}" 吗?`,
onOk: async () => {
try {
await service.suspendInstance(instance.id);
message.success('实例已挂起');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
}
});
};
const handleActivate = async (instance: WorkflowInstance) => {
try {
await service.activateInstance(instance.id);
message.success('实例已激活');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
};
const handleTerminate = async (instance: WorkflowInstance) => {
confirm({
title: '确认终止',
content: `确定要终止实例 "${instance.workflowName}" 吗?此操作不可逆。`,
onOk: async () => {
try {
await service.terminateInstance(instance.id);
message.success('实例已终止');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
}
});
};
const handleDelete = async (instance: WorkflowInstance) => {
confirm({
title: '确认删除',
content: `确定要删除实例 "${instance.workflowName}" 吗?此操作不可逆。`,
onOk: async () => {
try {
await service.deleteInstance(instance.id);
message.success('实例已删除');
loadData(query);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
}
});
};
// 查看详情
const handleViewDetail = (instance: WorkflowInstance) => {
navigate(`/workflow2/instance/${instance.id}`);
};
// 表格列定义
const columns = [
{
title: '实例ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '工作流名称',
dataIndex: 'workflowName',
key: 'workflowName',
},
{
title: '业务键',
dataIndex: 'businessKey',
key: 'businessKey',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: renderStatus,
},
{
title: '启动人',
dataIndex: 'startUserName',
key: 'startUserName',
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '耗时',
dataIndex: 'duration',
key: 'duration',
render: (duration: number) => {
if (!duration) return '-';
const hours = Math.floor(duration / 3600000);
const minutes = Math.floor((duration % 3600000) / 60000);
const seconds = Math.floor((duration % 60000) / 1000);
return `${hours}h ${minutes}m ${seconds}s`;
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 200,
render: (_: any, record: WorkflowInstance) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
>
</Button>
{record.status === 'RUNNING' && (
<Button
type="link"
size="small"
icon={<PauseCircleOutlined />}
onClick={() => handleSuspend(record)}
>
</Button>
)}
{record.status === 'SUSPENDED' && (
<Button
type="link"
size="small"
icon={<PlayCircleOutlined />}
onClick={() => handleActivate(record)}
>
</Button>
)}
{(record.status === 'RUNNING' || record.status === 'SUSPENDED') && (
<Button
type="link"
size="small"
danger
icon={<StopOutlined />}
onClick={() => handleTerminate(record)}
>
</Button>
)}
{(record.status === 'COMPLETED' || record.status === 'FAILED' || record.status === 'TERMINATED') && (
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
</Button>
)}
</Space>
),
},
];
const WorkflowInstance: React.FC = () => {
return ( return (
<Card title="工作流实例管理 (React Flow版本)"> <div>
<div style={{ {/* 统计信息 */}
textAlign: 'center', {statistics && (
color: '#6b7280', <Row gutter={16} style={{ marginBottom: 16 }}>
padding: '40px', <Col span={4}>
fontSize: '16px' <Card>
}}> <Statistic
🚧 ... title="总实例数"
<br /> value={statistics.total}
<br /> prefix={<BarChartOutlined />}
<span style={{ fontSize: '14px' }}> />
APIReact Flow版本的实例查看功能
</span>
</div>
</Card> </Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="运行中"
value={statistics.running}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="已完成"
value={statistics.completed}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="失败"
value={statistics.failed}
valueStyle={{ color: '#ff4d4f' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="今日启动"
value={statistics.todayStarted}
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="平均耗时"
value={Math.round(statistics.avgDuration / 1000)}
suffix="秒"
valueStyle={{ color: '#fa8c16' }}
/>
</Card>
</Col>
</Row>
)}
{/* 主表格 */}
<Card
title="工作流实例列表"
extra={
<Space>
<Button
icon={<ReloadOutlined />}
onClick={() => loadData(query)}
>
</Button>
</Space>
}
>
<Table
columns={columns}
dataSource={pageData?.content}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
current: (query.pageNum || 0) + 1,
pageSize: query.pageSize,
total: pageData?.totalElements || 0,
onChange: (page, pageSize) => setQuery({
...query,
pageNum: page - 1,
pageSize
}),
}}
/>
</Card>
</div>
); );
}; };
export default WorkflowInstance; export default WorkflowInstanceList;

View File

@ -0,0 +1,90 @@
import request from '@/utils/request';
import type { Page } from '@/types/base';
import type {
WorkflowInstance,
WorkflowInstanceQuery,
TaskInstance,
TaskQuery,
InstanceStatistics
} from './types';
// 获取工作流实例列表
export async function getInstances(params: WorkflowInstanceQuery): Promise<Page<WorkflowInstance>> {
return request.get('/api/v1/workflow/instances/page', { params });
}
// 获取单个工作流实例详情
export async function getInstanceDetail(id: number): Promise<WorkflowInstance> {
return request.get(`/api/v1/workflow/instances/${id}`);
}
// 启动工作流实例
export async function startInstance(data: {
workflowKey: string;
businessKey?: string;
variables?: Record<string, any>;
}): Promise<WorkflowInstance> {
return request.post('/api/v1/workflow/instances/start', data);
}
// 终止工作流实例
export async function terminateInstance(id: number, reason?: string): Promise<void> {
return request.post(`/api/v1/workflow/instances/${id}/terminate`, { reason });
}
// 挂起工作流实例
export async function suspendInstance(id: number, reason?: string): Promise<void> {
return request.post(`/api/v1/workflow/instances/${id}/suspend`, { reason });
}
// 激活工作流实例
export async function activateInstance(id: number): Promise<void> {
return request.post(`/api/v1/workflow/instances/${id}/activate`);
}
// 删除工作流实例
export async function deleteInstance(id: number): Promise<void> {
return request.delete(`/api/v1/workflow/instances/${id}`);
}
// 获取工作流实例的任务列表
export async function getInstanceTasks(instanceId: number): Promise<TaskInstance[]> {
return request.get(`/api/v1/workflow/instances/${instanceId}/tasks`);
}
// 获取任务列表
export async function getTasks(params: TaskQuery): Promise<Page<TaskInstance>> {
return request.get('/api/v1/workflow/tasks/page', { params });
}
// 完成任务
export async function completeTask(taskId: number, variables?: Record<string, any>): Promise<void> {
return request.post(`/api/v1/workflow/tasks/${taskId}/complete`, { variables });
}
// 委派任务
export async function delegateTask(taskId: number, assignee: string): Promise<void> {
return request.post(`/api/v1/workflow/tasks/${taskId}/delegate`, { assignee });
}
// 认领任务
export async function claimTask(taskId: number): Promise<void> {
return request.post(`/api/v1/workflow/tasks/${taskId}/claim`);
}
// 获取实例统计信息
export async function getInstanceStatistics(): Promise<InstanceStatistics> {
return request.get('/api/v1/workflow/instances/statistics');
}
// 获取实例执行历史
export async function getInstanceHistory(instanceId: number): Promise<any[]> {
return request.get(`/api/v1/workflow/instances/${instanceId}/history`);
}
// 获取实例流程图
export async function getInstanceDiagram(instanceId: number): Promise<string> {
return request.get(`/api/v1/workflow/instances/${instanceId}/diagram`, {
responseType: 'text'
});
}

View File

@ -0,0 +1,88 @@
import { BaseResponse, BaseQuery } from '@/types/base';
// 工作流实例状态枚举
export enum WorkflowInstanceStatus {
RUNNING = 'RUNNING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
SUSPENDED = 'SUSPENDED',
TERMINATED = 'TERMINATED'
}
// 任务状态枚举
export enum TaskStatus {
PENDING = 'PENDING',
RUNNING = 'RUNNING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
SKIPPED = 'SKIPPED'
}
// 工作流实例
export interface WorkflowInstance extends BaseResponse {
id: number;
workflowKey: string;
workflowName: string;
workflowVersion: number;
businessKey?: string;
status: WorkflowInstanceStatus;
startTime: string;
endTime?: string;
duration?: number;
startUserId?: number;
startUserName?: string;
currentTasks: TaskInstance[];
variables: Record<string, any>;
processDefinitionId: string;
}
// 任务实例
export interface TaskInstance extends BaseResponse {
id: number;
taskName: string;
taskType: string;
assignee?: string;
assigneeName?: string;
status: TaskStatus;
startTime: string;
endTime?: string;
duration?: number;
workflowInstanceId: number;
nodeId: string;
description?: string;
formKey?: string;
priority: number;
}
// 工作流实例查询条件
export interface WorkflowInstanceQuery extends BaseQuery {
workflowKey?: string;
workflowName?: string;
status?: WorkflowInstanceStatus;
businessKey?: string;
startUserId?: number;
startTimeFrom?: string;
startTimeTo?: string;
}
// 任务查询条件
export interface TaskQuery extends BaseQuery {
taskName?: string;
assignee?: string;
status?: TaskStatus;
workflowInstanceId?: number;
taskType?: string;
}
// 实例统计信息
export interface InstanceStatistics {
total: number;
running: number;
completed: number;
failed: number;
suspended: number;
terminated: number;
avgDuration: number;
todayStarted: number;
todayCompleted: number;
}

View File

@ -1,23 +1,297 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Card } from 'antd'; import {
Card,
Row,
Col,
Statistic,
Progress,
Table,
Tag,
Button,
Space,
message
} from 'antd';
import {
SyncOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
PauseCircleOutlined,
BarChartOutlined,
LineChartOutlined,
ReloadOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import * as instanceService from '../Instance/service';
import type {
InstanceStatistics,
WorkflowInstance,
WorkflowInstanceStatus
} from '../Instance/types';
const WorkflowMonitor: React.FC = () => { const WorkflowMonitor: React.FC = () => {
const [loading, setLoading] = useState(false);
const [statistics, setStatistics] = useState<InstanceStatistics | null>(null);
const [recentInstances, setRecentInstances] = useState<WorkflowInstance[]>([]);
// 加载监控数据
const loadMonitorData = async () => {
setLoading(true);
try {
const [statsData, instancesData] = await Promise.all([
instanceService.getInstanceStatistics(),
instanceService.getInstances({ pageNum: 0, pageSize: 10 })
]);
setStatistics(statsData);
setRecentInstances(instancesData.content);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
loadMonitorData();
// 定时刷新数据
const interval = setInterval(loadMonitorData, 30000); // 30秒刷新一次
return () => clearInterval(interval);
}, []);
// 状态渲染
const renderStatus = (status: WorkflowInstanceStatus) => {
const statusConfig = {
RUNNING: { color: 'processing', text: '运行中', icon: <SyncOutlined spin /> },
COMPLETED: { color: 'success', text: '已完成', icon: <CheckCircleOutlined /> },
FAILED: { color: 'error', text: '失败', icon: <CloseCircleOutlined /> },
SUSPENDED: { color: 'warning', text: '已挂起', icon: <PauseCircleOutlined /> },
TERMINATED: { color: 'default', text: '已终止', icon: <CloseCircleOutlined /> }
};
const config = statusConfig[status] || { color: 'default', text: status, icon: null };
return ( return (
<Card title="工作流监控 (React Flow版本)"> <Tag color={config.color} icon={config.icon}>
<div style={{ {config.text}
textAlign: 'center', </Tag>
color: '#6b7280', );
padding: '40px', };
fontSize: '16px'
}}> // 计算健康度
📊 ... const getHealthScore = () => {
<br /> if (!statistics || statistics.total === 0) return 0;
<br /> const successRate = (statistics.completed / statistics.total) * 100;
<span style={{ fontSize: '14px' }}> return Math.round(successRate);
};
</span>
const columns = [
{
title: '实例ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '工作流名称',
dataIndex: 'workflowName',
key: 'workflowName',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: renderStatus,
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
render: (time: string) => dayjs(time).format('MM-DD HH:mm'),
},
{
title: '耗时',
dataIndex: 'duration',
key: 'duration',
render: (duration: number) => {
if (!duration) return '-';
return `${Math.round(duration / 1000)}s`;
},
},
];
return (
<div>
{/* 顶部操作栏 */}
<Card style={{ marginBottom: 16 }}>
<Row justify="space-between" align="middle">
<Col>
<Space>
<h2 style={{ margin: 0 }}></h2>
<Tag color="blue"></Tag>
</Space>
</Col>
<Col>
<Space>
<Button
icon={<ReloadOutlined />}
onClick={loadMonitorData}
loading={loading}
>
</Button>
</Space>
</Col>
</Row>
</Card>
{/* 统计卡片 */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="总实例数"
value={statistics?.total || 0}
prefix={<BarChartOutlined />}
loading={loading}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="今日启动"
value={statistics?.todayStarted || 0}
prefix={<LineChartOutlined />}
valueStyle={{ color: '#1890ff' }}
loading={loading}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="今日完成"
value={statistics?.todayCompleted || 0}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
loading={loading}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '14px', color: '#8c8c8c', marginBottom: '8px' }}>
</div>
<Progress
type="circle"
percent={getHealthScore()}
size={80}
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
/>
</div> </div>
</Card> </Card>
</Col>
</Row>
{/* 图表和列表 */}
<Row gutter={16}>
<Col span={12}>
<Card title="实例运行状态" loading={loading}>
<Row gutter={16}>
<Col span={12}>
<Statistic
title="运行中"
value={statistics?.running || 0}
valueStyle={{ color: '#1890ff' }}
prefix={<SyncOutlined />}
/>
</Col>
<Col span={12}>
<Statistic
title="已完成"
value={statistics?.completed || 0}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircleOutlined />}
/>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: 24 }}>
<Col span={12}>
<Statistic
title="失败"
value={statistics?.failed || 0}
valueStyle={{ color: '#ff4d4f' }}
prefix={<CloseCircleOutlined />}
/>
</Col>
<Col span={12}>
<Statistic
title="已挂起"
value={statistics?.suspended || 0}
valueStyle={{ color: '#faad14' }}
prefix={<PauseCircleOutlined />}
/>
</Col>
</Row>
</Card>
</Col>
<Col span={12}>
<Card title="性能指标" loading={loading}>
<Row gutter={16}>
<Col span={24}>
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: '#1890ff' }}>
</div>
<div style={{ fontSize: '24px', color: '#fa8c16', marginTop: '8px' }}>
{Math.round((statistics?.avgDuration || 0) / 1000)}
</div>
</div>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '14px' }}></div>
<div style={{ fontSize: '20px', color: '#52c41a' }}>
{getHealthScore()}%
</div>
</div>
</Col>
<Col span={12}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '14px' }}></div>
<div style={{ fontSize: '20px', color: '#1890ff' }}>
{(statistics?.running || 0) + (statistics?.suspended || 0)}
</div>
</div>
</Col>
</Row>
</Card>
</Col>
</Row>
{/* 最近实例 */}
<Card
title="最近实例"
style={{ marginTop: 16 }}
loading={loading}
>
<Table
columns={columns}
dataSource={recentInstances}
rowKey="id"
size="small"
pagination={false}
/>
</Card>
</div>
); );
}; };

View File

@ -48,6 +48,8 @@ const External = lazy(() => import('../pages/Deploy/External'));
// Workflow2 React Flow 版本 // Workflow2 React Flow 版本
const Workflow2DefinitionList = lazy(() => import('../pages/Workflow2/Definition')); const Workflow2DefinitionList = lazy(() => import('../pages/Workflow2/Definition'));
const Workflow2Design = lazy(() => import('../pages/Workflow2/Design')); const Workflow2Design = lazy(() => import('../pages/Workflow2/Design'));
const Workflow2InstanceList = lazy(() => import('../pages/Workflow2/Instance'));
const Workflow2Monitor = lazy(() => import('../pages/Workflow2/Monitor'));
// 创建路由 // 创建路由
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -246,6 +248,22 @@ const router = createBrowserRouter([
<Workflow2Design/> <Workflow2Design/>
</Suspense> </Suspense>
) )
},
{
path: 'instance',
element: (
<Suspense fallback={<LoadingComponent/>}>
<Workflow2InstanceList/>
</Suspense>
)
},
{
path: 'monitor',
element: (
<Suspense fallback={<LoadingComponent/>}>
<Workflow2Monitor/>
</Suspense>
)
} }
] ]
}, },