diff --git a/frontend/src/pages/Workflow/Definition/Design/components/NodeConfigModal.tsx b/frontend/src/pages/Workflow/Definition/Design/components/NodeConfigModal.tsx new file mode 100644 index 00000000..0ae7997e --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Design/components/NodeConfigModal.tsx @@ -0,0 +1,206 @@ +import React, { useEffect } from 'react'; +import { Drawer, Form, Input, Select, Button, Space, Divider, Tooltip } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Cell } from '@antv/x6'; +import { NodeDefinition } from '../types'; + +interface NodeConfigDrawerProps { + visible: boolean; + node: Cell | null; + nodeDefinition: NodeDefinition | null; + onOk: (values: any) => void; + onCancel: () => void; +} + +interface SchemaProperty { + type: string; + title: string; + description?: string; + enum?: string[]; + enumNames?: string[]; + format?: string; +} + +const NodeConfigDrawer: React.FC = ({ + visible, + node, + nodeDefinition, + onOk, + onCancel, +}) => { + const [form] = Form.useForm(); + + useEffect(() => { + if (visible && node) { + // 获取节点当前的配置 + const currentConfig = { + code: node.getProp('code'), + name: node.attr('label/text'), + description: node.getProp('description'), + ...node.getProp('config'), + }; + + form.setFieldsValue(currentConfig); + } + }, [visible, node, form]); + + const handleOk = async () => { + try { + const values = await form.validateFields(); + onOk(values); + form.resetFields(); + } catch (error) { + console.error('Validation failed:', error); + } + }; + + const handleCancel = () => { + form.resetFields(); + onCancel(); + }; + + const renderFormItem = (key: string, property: SchemaProperty, required: boolean) => { + const baseProps = { + name: key, + label: ( + + {property.title} + {property.description && ( + + + + )} + + ), + rules: [{ required, message: `请输入${property.title}` }], + }; + + switch (property.type) { + case 'string': + if (property.enum) { + return ( + + + + ); + } + return ( + + {property.format === 'textarea' ? ( + + ) : ( + + )} + + ); + case 'number': + return ( + + + + ); + case 'boolean': + return ( + + + + ); + default: + return null; + } + }; + + const renderFormItems = () => { + if (!nodeDefinition?.graphConfig.configSchema) return null; + + const { configSchema } = nodeDefinition.graphConfig; + const formItems = []; + + // 根据 configSchema 生成表单项 + if (configSchema.properties) { + Object.entries(configSchema.properties).forEach(([key, property]: [string, SchemaProperty]) => { + const isRequired = configSchema.required?.includes(key) || false; + const formItem = renderFormItem(key, property, isRequired); + if (formItem) { + formItems.push(formItem); + } + }); + } + + return formItems; + }; + + // 显示节点详细信息 + const renderNodeDetails = () => { + if (!nodeDefinition?.graphConfig.details) return null; + + const { details } = nodeDefinition.graphConfig; + return ( + <> + 节点说明 +
+

{details.description}

+ {details.features && details.features.length > 0 && ( +
+ 功能特点: +
    + {details.features.map((feature, index) => ( +
  • {feature}
  • + ))} +
+
+ )} + {details.scenarios && details.scenarios.length > 0 && ( +
+ 适用场景: +
    + {details.scenarios.map((scenario, index) => ( +
  • {scenario}
  • + ))} +
+
+ )} +
+ + ); + }; + + return ( + + + + + } + > + {renderNodeDetails()} + 节点配置 +
+ {renderFormItems()} +
+
+ ); +}; + +export default NodeConfigDrawer; diff --git a/frontend/src/pages/Workflow/Definition/Design/index.tsx b/frontend/src/pages/Workflow/Definition/Design/index.tsx index ecb90257..9f3a4336 100644 --- a/frontend/src/pages/Workflow/Definition/Design/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Design/index.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Button, Space, Card, Row, Col } from 'antd'; +import { Button, Space, Card, Row, Col, message } from 'antd'; import { ArrowLeftOutlined, SaveOutlined, PlayCircleOutlined } from '@ant-design/icons'; -import { Graph, Shape } from '@antv/x6'; -import { getDefinitionDetail } from '../service'; +import { Graph, Cell } from '@antv/x6'; +import { getDefinitionDetail, saveDefinition } from '../service'; import NodePanel from './components/NodePanel'; +import NodeConfigDrawer from './components/NodeConfigModal'; import { NodeDefinition } from './types'; import { NODE_REGISTRY_CONFIG, @@ -20,6 +21,10 @@ const WorkflowDesign: React.FC = () => { const [title, setTitle] = useState('工作流设计'); const graphContainerRef = useRef(null); const [graph, setGraph] = useState(null); + const [selectedNode, setSelectedNode] = useState(null); + const [selectedNodeDefinition, setSelectedNodeDefinition] = useState(null); + const [configModalVisible, setConfigModalVisible] = useState(false); + const [definitionData, setDefinitionData] = useState(null); useEffect(() => { if (id) { @@ -99,6 +104,14 @@ const WorkflowDesign: React.FC = () => { }); }); + // 节点双击事件 + graph.on('node:dblclick', ({ node }) => { + const nodeDefinition = node.getProp('nodeDefinition'); + setSelectedNode(node); + setSelectedNodeDefinition(nodeDefinition); + setConfigModalVisible(true); + }); + setGraph(graph); return () => { @@ -111,6 +124,7 @@ const WorkflowDesign: React.FC = () => { try { const response = await getDefinitionDetail(id); setTitle(`工作流设计 - ${response.name}`); + setDefinitionData(response); } catch (error) { console.error('Failed to load workflow definition:', error); } @@ -155,6 +169,9 @@ const WorkflowDesign: React.FC = () => { ...nodeConfig, x: point.x, y: point.y, + data: { type: node.type }, + // 保存节点定义,用于后续编辑 + nodeDefinition: node, }); } } @@ -164,6 +181,72 @@ const WorkflowDesign: React.FC = () => { e.preventDefault(); }; + const handleNodeConfigUpdate = (values: any) => { + if (!selectedNode) return; + + // 更新节点配置 + selectedNode.setProp('config', values); + // 更新节点显示名称 + if (values.name) { + selectedNode.attr('label/text', values.name); + } + + setConfigModalVisible(false); + message.success('节点配置已更新'); + }; + + const handleSaveWorkflow = async () => { + if (!graph || !definitionData) return; + + try { + // 获取所有节点和边的数据 + const nodes = graph.getNodes().map(node => { + const nodeType = node.getProp('type'); + const nodeDefinition = NODE_REGISTRY_CONFIG[nodeType]; + + return { + id: node.id, + code: node.getProp('code'), + type: nodeType, + name: node.attr('label/text'), + graph: { + shape: nodeDefinition?.graphConfig.uiSchema.shape, + size: node.size(), + style: nodeDefinition?.graphConfig.uiSchema.style, + ports: nodeDefinition?.graphConfig.uiSchema.ports + }, + config: node.getProp('config') || {} + }; + }); + + const edges = graph.getEdges().map(edge => ({ + id: edge.id, + from: edge.getSourceCellId(), + to: edge.getTargetCellId(), + name: edge.getLabels()?.[0]?.attrs?.label?.text || '', + config: { + type: 'sequence' + } + })); + + // 构建保存数据 + const saveData = { + ...definitionData, + graph: { + nodes, + edges + } + }; + + // 调用保存接口 + await saveDefinition(saveData); + message.success('流程保存成功'); + } catch (error) { + console.error('保存流程失败:', error); + message.error('保存流程失败'); + } + }; + return (
{ @@ -222,6 +305,14 @@ const WorkflowDesign: React.FC = () => { + + setConfigModalVisible(false)} + />
); };