diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/index.tsx new file mode 100644 index 00000000..976aaa58 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodeConfig/index.tsx @@ -0,0 +1,142 @@ +import React, { useMemo } from 'react'; +import { Form, Input, Select, InputNumber, Switch } from 'antd'; +import { NodeType, JsonSchema, JsonSchemaProperty } from '../../service'; + +interface NodeConfigProps { + nodeType: NodeType; + form: any; +} + +const NodeConfig: React.FC = ({ nodeType, form }) => { + // 解析配置模式 + const schema = useMemo(() => { + try { + return JSON.parse(nodeType.configSchema) as JsonSchema; + } catch (error) { + console.error('解析配置模式失败:', error); + return null; + } + }, [nodeType.configSchema]); + + // 解析默认配置 + const defaultConfig = useMemo(() => { + try { + return nodeType.defaultConfig ? JSON.parse(nodeType.defaultConfig) : {}; + } catch (error) { + console.error('解析默认配置失败:', error); + return {}; + } + }, [nodeType.defaultConfig]); + + // 根据属性类型渲染表单控件 + const renderFormItem = (key: string, property: JsonSchemaProperty) => { + const { + type, + title, + description, + minimum, + maximum, + minLength, + maxLength, + enum: enumValues, + enumNames, + pattern, + format, + } = property; + + let formItem; + const rules = []; + + // 添加必填规则 + if (schema?.required?.includes(key)) { + rules.push({ required: true, message: `请输入${title}` }); + } + + // 添加长度限制 + if (minLength !== undefined) { + rules.push({ min: minLength, message: `最少输入${minLength}个字符` }); + } + if (maxLength !== undefined) { + rules.push({ max: maxLength, message: `最多输入${maxLength}个字符` }); + } + + // 添加数值范围限制 + if (minimum !== undefined) { + rules.push({ min: minimum, message: `不能小于${minimum}` }); + } + if (maximum !== undefined) { + rules.push({ max: maximum, message: `不能大于${maximum}` }); + } + + // 添加正则校验 + if (pattern) { + rules.push({ pattern: new RegExp(pattern), message: `格式不正确` }); + } + + switch (type) { + case 'string': + if (enumValues) { + formItem = ( + ; + } + break; + case 'number': + formItem = ( + + ); + break; + case 'boolean': + formItem = ; + break; + default: + formItem = ; + } + + return ( + + {formItem} + + ); + }; + + // 渲染表单项 + const renderFormItems = () => { + if (!schema?.properties) return null; + + return Object.entries(schema.properties).map(([key, property]) => + renderFormItem(key, property) + ); + }; + + return ( +
+ {renderFormItems()} +
+ ); +}; + +export default NodeConfig; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/Toolbar/index.less b/frontend/src/pages/Workflow/Definition/Designer/components/Toolbar/index.less new file mode 100644 index 00000000..f65143bc --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/components/Toolbar/index.less @@ -0,0 +1,54 @@ +.workflow-toolbar { + padding: 8px 16px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + + .ant-btn { + padding: 4px 8px; + border: none; + background: transparent; + + &:hover { + color: #1890ff; + background: #f5f5f5; + } + + &[disabled] { + color: #d9d9d9; + background: transparent; + cursor: not-allowed; + + &:hover { + color: #d9d9d9; + background: transparent; + } + } + + &-dangerous { + color: #ff4d4f; + + &:hover { + color: #ff7875; + background: #fff1f0; + } + + &[disabled] { + color: #d9d9d9; + background: transparent; + + &:hover { + color: #d9d9d9; + background: transparent; + } + } + } + } + + .ant-divider { + height: 16px; + margin: 0 8px; + } +} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/Toolbar/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/components/Toolbar/index.tsx new file mode 100644 index 00000000..148d7007 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/components/Toolbar/index.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import { Space, Button, Tooltip, Divider } from 'antd'; +import { + ZoomInOutlined, + ZoomOutOutlined, + FullscreenOutlined, + OneToOneOutlined, + SelectOutlined, + DeleteOutlined, + UndoOutlined, + RedoOutlined, + CopyOutlined, + SnippetsOutlined, +} from '@ant-design/icons'; +import { Graph } from '@antv/x6'; +import './index.less'; + +interface ToolbarProps { + graph: Graph | undefined; +} + +const Toolbar: React.FC = ({ graph }) => { + // 缩放画布 + const zoom = (delta: number) => { + if (!graph) return; + const zoom = graph.zoom(); + graph.zoom(zoom + delta); + }; + + // 适应画布 + const fitContent = () => { + if (!graph) return; + graph.zoomToFit({ padding: 20 }); + }; + + // 实际大小 + const resetZoom = () => { + if (!graph) return; + graph.scale(1); + graph.centerContent(); + }; + + // 全选 + const selectAll = () => { + if (!graph) return; + const nodes = graph.getNodes(); + const edges = graph.getEdges(); + graph.resetSelection(); + graph.select(nodes); + graph.select(edges); + }; + + // 删除选中 + const deleteSelected = () => { + if (!graph) return; + const cells = graph.getSelectedCells(); + graph.removeCells(cells); + }; + + // 撤销 + const undo = () => { + if (!graph) return; + if (graph.canUndo()) { + graph.undo(); + } + }; + + // 重做 + const redo = () => { + if (!graph) return; + if (graph.canRedo()) { + graph.redo(); + } + }; + + // 复制 + const copy = () => { + if (!graph) return; + const cells = graph.getSelectedCells(); + if (cells.length > 0) { + graph.copy(cells); + } + }; + + // 粘贴 + const paste = () => { + if (!graph) return; + if (!graph.isClipboardEmpty()) { + const cells = graph.paste({ offset: 20 }); + graph.cleanSelection(); + graph.select(cells); + } + }; + + return ( +
+ }> + + +
+ ); +}; + +export default Toolbar; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx index eb58df34..58a190c9 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -1,17 +1,26 @@ import React, {useEffect, useRef, useState} from 'react'; import {useNavigate, useParams} from 'react-router-dom'; -import {Button, Card, Layout, message, Space, Spin} from 'antd'; +import {Button, Card, Layout, message, Space, Spin, Drawer, Form} from 'antd'; import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons'; import {getDefinition, updateDefinition} from '../../service'; import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types'; -import {Graph} from '@antv/x6'; +import {Graph, Node, Cell} from '@antv/x6'; import '@antv/x6-react-shape'; import './index.module.less'; import NodePanel from './components/NodePanel'; +import NodeConfig from './components/NodeConfig'; +import Toolbar from './components/Toolbar'; import { NodeType } from './service'; const {Sider, Content} = Layout; +interface NodeData { + type: string; + name?: string; + description?: string; + config: Record; +} + const FlowDesigner: React.FC = () => { const navigate = useNavigate(); const {id} = useParams<{ id: string }>(); @@ -20,6 +29,10 @@ const FlowDesigner: React.FC = () => { const containerRef = useRef(null); const graphRef = useRef(); const draggedNodeRef = useRef(); + const [configVisible, setConfigVisible] = useState(false); + const [currentNode, setCurrentNode] = useState(); + const [currentNodeType, setCurrentNodeType] = useState(); + const [form] = Form.useForm(); // 初始化图形 const initGraph = () => { @@ -127,6 +140,26 @@ const FlowDesigner: React.FC = () => { // 监听画布拖拽事件 containerRef.current.addEventListener('dragover', handleDragOver); containerRef.current.addEventListener('drop', handleDrop); + + // 监听节点点击事件 + graph.on('node:click', ({node}: {node: Node}) => { + setCurrentNode(node); + const data = node.getData() as NodeData; + if (data) { + // 获取节点类型 + const nodeType = draggedNodeRef.current; + if (nodeType && nodeType.code === data.type) { + setCurrentNodeType(nodeType); + } + // 设置表单值 + form.setFieldsValue({ + name: data.name, + description: data.description, + ...data.config, + }); + setConfigVisible(true); + } + }); }; // 处理拖拽移动 @@ -166,7 +199,7 @@ const FlowDesigner: React.FC = () => { y: position.y - 20, // 节点高度的一半,使节点中心对准鼠标 width: 180, height: 40, - label: nodeType.name, + shape: 'rect', attrs: { body: { fill: '#fff', @@ -176,6 +209,7 @@ const FlowDesigner: React.FC = () => { ry: 4, }, label: { + text: nodeType.name, fill: '#333', fontSize: 14, refX: 0.5, @@ -224,14 +258,51 @@ const FlowDesigner: React.FC = () => { }, data: { type: nodeType.code, + name: nodeType.name, config: {}, }, }); // 选中新创建的节点 - const cells = graphRef.current.getSelectedCells(); - cells.forEach(cell => cell.unselect()); - node.select(); + graphRef.current.getNodes().forEach(n => { + n.setAttrByPath('body/strokeWidth', 1); + }); + node.setAttrByPath('body/strokeWidth', 2); + + // 打开配置抽屉 + setCurrentNode(node); + setCurrentNodeType(nodeType); + form.setFieldsValue({ + name: nodeType.name, + }); + setConfigVisible(true); + }; + + // 处理配置保存 + const handleConfigSave = async () => { + try { + const values = await form.validateFields(); + if (currentNode) { + const data = currentNode.getData() as NodeData; + const { name, description, ...config } = values; + + // 更新节点数据 + currentNode.setData({ + ...data, + name, + description, + config, + }); + + // 更新节点标签 + currentNode.setAttrByPath('label/text', name); + + message.success('配置保存成功'); + setConfigVisible(false); + } + } catch (error) { + // 表单验证失败 + } }; // 获取详情 @@ -286,6 +357,9 @@ const FlowDesigner: React.FC = () => { containerRef.current.removeEventListener('dragover', handleDragOver); containerRef.current.removeEventListener('drop', handleDrop); } + if (graphRef.current) { + graphRef.current.dispose(); + } }; }, [detail, containerRef.current]); @@ -325,10 +399,32 @@ const FlowDesigner: React.FC = () => { - -
- + + + +
+ + + + setConfigVisible(false)} + extra={ + + + + + } + > + {currentNodeType && ( + + )} + ); }; diff --git a/frontend/src/pages/Workflow/Definition/Designer/service.ts b/frontend/src/pages/Workflow/Definition/Designer/service.ts index d1896ee2..1f8fa331 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/service.ts +++ b/frontend/src/pages/Workflow/Definition/Designer/service.ts @@ -4,26 +4,55 @@ export interface NodeExecutor { code: string; name: string; description: string; - configSchema: Record; + configSchema: string; + defaultConfig: string | null; +} + +export interface JsonSchemaProperty { + type: string; + title: string; + description?: string; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + default?: any; + format?: string; + pattern?: string; + enum?: string[]; + enumNames?: string[]; +} + +export interface JsonSchema { + type: string; + properties: Record; + required?: string[]; } export interface NodeType { id: number; code: string; name: string; - category: 'TASK' | 'EVENT' | 'GATEWAY'; + category: 'BASIC' | 'TASK' | 'EVENT' | 'GATEWAY'; description: string; enabled: boolean; icon: string; color: string; executors: NodeExecutor[]; + configSchema: string; + defaultConfig: string; createTime: string; updateTime: string; + version: number; + deleted: boolean; + createBy: string; + updateBy: string; + extraData: any; } export interface NodeTypeQuery { enabled?: boolean; - category?: 'TASK' | 'EVENT' | 'GATEWAY'; + category?: 'BASIC' | 'TASK' | 'EVENT' | 'GATEWAY'; } // 获取节点类型列表