diff --git a/frontend/src/pages/Workflow2/Design/components/CustomEdge.tsx b/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx similarity index 100% rename from frontend/src/pages/Workflow2/Design/components/CustomEdge.tsx rename to frontend/src/pages/Workflow/Design/components/CustomEdge.tsx diff --git a/frontend/src/pages/Workflow2/Design/components/EdgeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx similarity index 100% rename from frontend/src/pages/Workflow2/Design/components/EdgeConfigModal.tsx rename to frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx diff --git a/frontend/src/pages/Workflow/Design/components/ExpressionModal.tsx b/frontend/src/pages/Workflow/Design/components/ExpressionModal.tsx deleted file mode 100644 index 01c3124b..00000000 --- a/frontend/src/pages/Workflow/Design/components/ExpressionModal.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import { Modal, Form, Input, InputNumber, Radio } from 'antd'; -import { Edge } from '@antv/x6'; -import { EdgeCondition } from '../nodes/types'; - -interface ExpressionModalProps { - visible: boolean; - edge: Edge; - onOk: (condition: EdgeCondition) => void; - onCancel: () => void; -} - -const ExpressionModal: React.FC = ({ - visible, - edge, - onOk, - onCancel -}) => { - const [form] = Form.useForm(); - const currentCondition = edge.getProp('condition') as EdgeCondition; - - const handleOk = async () => { - try { - const values = await form.validateFields(); - onOk(values); - } catch (error) { - console.error('表单验证失败:', error); - } - }; - - return ( - -
- - - 表达式 - 默认路径 - - - - prevValues.type !== currentValues.type} - > - {({ getFieldValue }) => { - const type = getFieldValue('type'); - return type === 'EXPRESSION' ? ( - - - - ) : null; - }} - - - - - -
-
- ); -}; - -export default ExpressionModal; \ No newline at end of file diff --git a/frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx similarity index 100% rename from frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx rename to frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx diff --git a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx index a1b2506d..a03cf6c4 100644 --- a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx +++ b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx @@ -1,92 +1,247 @@ -import React, { useEffect, useState } from 'react'; -import { Tabs } from 'antd'; -import { Cell } from '@antv/x6'; -import type { WorkflowNodeDefinition, ConfigurableNodeDefinition } from "../nodes/types"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; -import { Button } from "@/components/ui/button"; -import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils'; -import { BetaSchemaForm } from '@ant-design/pro-components'; +import React, { useState, useEffect, useMemo } from 'react'; +import { Drawer, Tabs, Button, Space, message } from 'antd'; +import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons'; +import { FormItem, Input, NumberPicker, Select, FormLayout, Switch } from '@formily/antd-v5'; +import { createForm } from '@formily/core'; +import { createSchemaField, FormProvider, ISchema } from '@formily/react'; +import type { FlowNode, FlowNodeData } from '../types'; +import type { WorkflowNodeDefinition } from '../nodes/types'; +import { isConfigurableNode } from '../nodes/types'; + +// 创建Schema组件 +const SchemaField = createSchemaField({ + components: { + FormItem, + Input, + NumberPicker, + Select, + FormLayout, + Switch, + 'Input.TextArea': Input.TextArea, + }, +}); interface NodeConfigModalProps { visible: boolean; - node: Cell | null; - nodeDefinition: WorkflowNodeDefinition | null; - onOk: (values: any) => void; + node: FlowNode | null; onCancel: () => void; -} - -interface FormData { - configs?: Record; - inputMapping?: Record; - outputMapping?: Record; + onOk: (nodeId: string, updatedData: Partial) => void; } const NodeConfigModal: React.FC = ({ visible, node, - nodeDefinition, - onOk, onCancel, + onOk }) => { - const [formData, setFormData] = useState({}); + const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState('config'); - // 判断是否为可配置节点 - const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => { - return 'inputMappingSchema' in def || 'outputMappingSchema' in def; - }; + // 获取节点定义 + const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null; + // 创建Formily表单实例 + const configForm = useMemo(() => createForm(), []); + const inputForm = useMemo(() => createForm(), []); + const outputForm = useMemo(() => createForm(), []); + + // 初始化表单数据 useEffect(() => { - if (nodeDefinition && node) { - // 从节点数据中获取现有配置 - const nodeData = node.getData() || {}; + if (visible && node && nodeDefinition) { + const nodeData = node.data || {}; - // 准备默认的基本信息配置 + // 准备默认配置 const defaultConfig = { - nodeName: nodeDefinition.nodeName, // 默认节点名称 - nodeCode: nodeDefinition.nodeCode, // 默认节点编码 - description: nodeDefinition.description // 默认节点描述 + nodeName: nodeDefinition.nodeName, + nodeCode: nodeDefinition.nodeCode, + description: nodeDefinition.description }; - // 合并默认值和已保存的配置 - setFormData({ - configs: { ...defaultConfig, ...(nodeData.configs || {}) }, - inputMapping: nodeData.inputMapping || {}, - outputMapping: nodeData.outputMapping || {}, - }); - } else { - setFormData({}); + // 设置表单初始值 + configForm.setInitialValues({ ...defaultConfig, ...(nodeData.configs || {}) }); + configForm.reset(); + + if (isConfigurableNode(nodeDefinition)) { + inputForm.setInitialValues(nodeData.inputMapping || {}); + inputForm.reset(); + + outputForm.setInitialValues(nodeData.outputMapping || {}); + outputForm.reset(); + } } - }, [nodeDefinition, node]); + }, [visible, node, nodeDefinition, configForm, inputForm, outputForm]); - const handleSubmit = () => { - onOk(formData); + // 递归处理表单值,将JSON字符串转换为对象 + const processFormValues = (values: Record, schema: ISchema | undefined): Record => { + const result: Record = {}; + + if (!schema?.properties || typeof schema.properties !== 'object') return values; + + Object.entries(values).forEach(([key, value]) => { + const propSchema = (schema.properties as Record)?.[key]; + + // 如果是object类型且值是字符串,尝试解析 + if (propSchema?.type === 'object' && typeof value === 'string') { + try { + result[key] = JSON.parse(value); + } catch { + result[key] = value; // 解析失败保持原值 + } + } else { + result[key] = value; + } + }); + + return result; }; - const handleConfigChange = (values: Record) => { - setFormData(prev => ({ - ...prev, - configs: values - })); + const handleSubmit = async () => { + if (!node || !nodeDefinition) return; + + try { + setLoading(true); + + // 获取表单值并转换 + const configs = processFormValues(configForm.values, nodeDefinition.configSchema); + const inputMapping = isConfigurableNode(nodeDefinition) + ? processFormValues(inputForm.values, nodeDefinition.inputMappingSchema) + : {}; + const outputMapping = isConfigurableNode(nodeDefinition) + ? processFormValues(outputForm.values, nodeDefinition.outputMappingSchema) + : {}; + + const updatedData: Partial = { + label: configs.nodeName || node.data.label, + configs, + inputMapping, + outputMapping, + }; + + onOk(node.id, updatedData); + message.success('节点配置保存成功'); + onCancel(); + } catch (error) { + if (error instanceof Error) { + message.error(error.message); + } + } finally { + setLoading(false); + } }; - const handleInputMappingChange = (values: Record) => { - setFormData(prev => ({ - ...prev, - inputMapping: values - })); + const handleReset = () => { + configForm.reset(); + inputForm.reset(); + outputForm.reset(); + message.info('已重置为初始值'); }; - const handleOutputMappingChange = (values: Record) => { - setFormData(prev => ({ - ...prev, - outputMapping: values - })); + // 将JSON Schema转换为Formily Schema(扩展配置) + const convertToFormilySchema = (jsonSchema: ISchema): ISchema => { + const schema: ISchema = { + type: 'object', + properties: {} + }; + + if (!jsonSchema.properties || typeof jsonSchema.properties !== 'object') return schema; + + Object.entries(jsonSchema.properties as Record).forEach(([key, prop]: [string, any]) => { + const field: any = { + title: prop.title || key, + description: prop.description, + 'x-decorator': 'FormItem', + 'x-decorator-props': { + tooltip: prop.description, + labelCol: 6, // 标签占6列 + wrapperCol: 18, // 内容占18列(剩余空间) + }, + }; + + // 根据类型设置组件 + switch (prop.type) { + case 'string': + if (prop.enum) { + field['x-component'] = 'Select'; + field['x-component-props'] = { + style: { width: '100%' }, // 统一宽度 + options: prop.enum.map((v: any, i: number) => ({ + label: prop.enumNames?.[i] || v, + value: v + })) + }; + } else if (prop.format === 'password') { + field['x-component'] = 'Input'; + field['x-component-props'] = { + type: 'password', + style: { width: '100%' } // 统一宽度 + }; + } else { + field['x-component'] = 'Input'; + field['x-component-props'] = { + style: { width: '100%' } // 统一宽度 + }; + } + break; + case 'number': + case 'integer': + field['x-component'] = 'NumberPicker'; + field['x-component-props'] = { + min: prop.minimum, + max: prop.maximum, + style: { width: '100%' } // 统一宽度 + }; + break; + case 'boolean': + field['x-component'] = 'Switch'; + break; + case 'object': + field['x-component'] = 'Input.TextArea'; + field['x-component-props'] = { + rows: 4, + style: { width: '100%' }, // 统一宽度 + placeholder: '请输入JSON格式,例如:{"key": "value"}' + }; + // ✅ 关键修复:将object类型的default值转换为JSON字符串 + if (prop.default !== undefined && typeof prop.default === 'object') { + field.default = JSON.stringify(prop.default, null, 2); + } else { + field.default = prop.default; + } + // Formily会自动处理object的序列化 + field['x-validator'] = (value: any) => { + if (!value) return true; + if (typeof value === 'string') { + try { + JSON.parse(value); + return true; + } catch { + return '请输入有效的JSON格式'; + } + } + return true; + }; + break; + default: + field['x-component'] = 'Input'; + field['x-component-props'] = { + style: { width: '100%' } // 统一宽度 + }; + } + + // 设置默认值(非object类型) + if (prop.type !== 'object' && prop.default !== undefined) { + field.default = prop.default; + } + + // 设置必填 + if (Array.isArray(jsonSchema.required) && jsonSchema.required.includes(key)) { + field.required = true; + } + + (schema.properties as Record)[key] = field; + }); + + return schema; }; if (!nodeDefinition) { @@ -98,18 +253,15 @@ const NodeConfigModal: React.FC = ({ { key: 'config', label: '基本配置', - children: ( + children: activeTab === 'config' ? (
- handleConfigChange(allValues)} - submitter={false} - /> + + + + +
- ), + ) : null, }, ]; @@ -119,18 +271,15 @@ const NodeConfigModal: React.FC = ({ tabItems.push({ key: 'input', label: '输入映射', - children: ( + children: activeTab === 'input' ? (
- handleInputMappingChange(allValues)} - submitter={false} - /> + + + + +
- ), + ) : null, }); } @@ -138,66 +287,81 @@ const NodeConfigModal: React.FC = ({ tabItems.push({ key: 'output', label: '输出映射', - children: ( + children: activeTab === 'output' ? (
- handleOutputMappingChange(allValues)} - submitter={false} - /> + + + + +
- ), + ) : null, }); } } return ( - !open && onCancel()}> - - - + + 编辑节点 - {nodeDefinition.nodeName} - - - -
- +
- -
- - + + + +
-
-
+ } + > +
+ +
+ ); }; diff --git a/frontend/src/pages/Workflow/Design/components/NodePanel.tsx b/frontend/src/pages/Workflow/Design/components/NodePanel.tsx index e69a0e18..24126562 100644 --- a/frontend/src/pages/Workflow/Design/components/NodePanel.tsx +++ b/frontend/src/pages/Workflow/Design/components/NodePanel.tsx @@ -1,91 +1,30 @@ -import React, {useState, useEffect} from 'react'; -import {Card, Collapse, Tooltip, message} from 'antd'; -import type {NodeCategory} from '../nodes/types'; -import { - PlayCircleOutlined, - StopOutlined, - UserOutlined, - ApiOutlined, - CodeOutlined, - NodeIndexOutlined, - SplitCellsOutlined, - AppstoreOutlined, - BranchesOutlined -} from '@ant-design/icons'; -import {getNodeDefinitionList} from "../nodes/nodeService"; -import type {WorkflowNodeDefinition} from "../nodes/types"; +import React from 'react'; +import { NodeCategory } from '../types'; +import { NODE_DEFINITIONS } from '../nodes'; +import type { WorkflowNodeDefinition } from '../nodes/types'; -// 使用 Collapse 组件,不需要解构 Panel - -// 图标映射配置 -const iconMap: Record = { - 'play-circle': PlayCircleOutlined, - 'stop': StopOutlined, - 'user': UserOutlined, - 'api': ApiOutlined, - 'code': CodeOutlined, - 'build': CodeOutlined, // 构建任务图标 - 'fork': NodeIndexOutlined, - 'branches': SplitCellsOutlined, - 'apartment': AppstoreOutlined -}; - -// 节点类型到图标的映射 -const typeIconMap: Record = { - 'START_EVENT': PlayCircleOutlined, - 'END_EVENT': StopOutlined, - 'USER_TASK': UserOutlined, - 'SERVICE_TASK': ApiOutlined, - 'SCRIPT_TASK': CodeOutlined, - 'DEPLOY_NODE': CodeOutlined, // 构建任务节点 - 'EXCLUSIVE_GATEWAY': NodeIndexOutlined, - 'PARALLEL_GATEWAY': SplitCellsOutlined, - 'SUB_PROCESS': AppstoreOutlined, - 'CALL_ACTIVITY': BranchesOutlined -}; - -// 节点分类配置 -const categoryConfig: Record = { - EVENT: {label: '事件节点', key: '1'}, - TASK: {label: '任务节点', key: '2'}, - GATEWAY: {label: '网关节点', key: '3'}, - CONTAINER: {label: '容器节点', key: '4'}, +// 图标映射函数 +const getNodeIcon = (iconName: string): string => { + const iconMap: Record = { + 'play-circle': '▶️', + 'stop-circle': '⏹️', + 'user': '👤', + 'api': '⚙️', + 'code': '📜', + 'build': '🚀', + 'jenkins': '🔨', + 'gateway': '💎' + }; + return iconMap[iconName] || '📋'; }; interface NodePanelProps { - onNodeDragStart?: (node: WorkflowNodeDefinition, e: React.DragEvent) => void, - nodeDefinitions?: WorkflowNodeDefinition[] + className?: string; } -const NodePanel: React.FC = ({onNodeDragStart}) => { - const [loading, setLoading] = useState(false); - const [nodeDefinitions, setNodeDefinitions] = useState([]); - - // 加载节点定义列表 - const loadNodeDefinitions = async () => { - setLoading(true); - try { - const data = await getNodeDefinitionList(); - console.log(data) - setNodeDefinitions(data); - } catch (error) { - if (error instanceof Error) { - message.error(error.message); - } - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadNodeDefinitions(); - }, []); - - // 按分类对节点进行分组 - const groupedNodes = nodeDefinitions.reduce((acc, node) => { +const NodePanel: React.FC = ({ className = '' }) => { + // 按分类分组节点 + const nodesByCategory = NODE_DEFINITIONS.reduce((acc, node) => { if (!acc[node.category]) { acc[node.category] = []; } @@ -93,145 +32,156 @@ const NodePanel: React.FC = ({onNodeDragStart}) => { return acc; }, {} as Record); - // 处理节点拖拽开始事件 - const handleDragStart = (node: WorkflowNodeDefinition, e: React.DragEvent) => { - e.dataTransfer.setData('node', JSON.stringify(node)); - onNodeDragStart?.(node, e); - }; - - // 渲染节点图标 - const renderNodeIcon = (node: WorkflowNodeDefinition) => { - const iconName = node.uiConfig?.style.icon; - // 首先尝试使用配置的图标 - let IconComponent = iconMap[iconName]; - - // 如果没有找到对应的图标,使用节点类型对应的默认图标 - if (!IconComponent) { - IconComponent = typeIconMap[node.nodeType] || AppstoreOutlined; - } - - return ( - - ); - }; - - const getNodeItemStyle = (node: WorkflowNodeDefinition) => ({ - width: '100%', - padding: '10px 12px', - border: `1px solid ${node.uiConfig?.style.stroke}`, - borderRadius: '6px', - cursor: 'move', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - gap: '10px', - background: node.uiConfig?.style.fill, - transition: 'all 0.3s', - boxShadow: '0 1px 2px rgba(0,0,0,0.05)', - '&:hover': { - transform: 'translateY(-1px)', - boxShadow: '0 3px 6px rgba(0,0,0,0.1)', - } - }); - - const tooltipStyle = { - maxWidth: '300px' - }; - - const tooltipOverlayInnerStyle = { - padding: '12px 16px' - }; - - // 构建折叠面板的 items,只包含有节点的分类 - const collapseItems = Object.entries(categoryConfig) - .filter(([category]) => groupedNodes[category as NodeCategory]?.length > 0) // 过滤掉没有节点的分类 - .map(([category, {label, key}]) => ({ - key, - label: {label}, - children: ( -
- {groupedNodes[category as NodeCategory]?.map(node => ( - -
{node.description}
-
- } - styles={{ - root: tooltipStyle, - body: tooltipOverlayInnerStyle - }} - placement="right" - arrow={false} - > -
handleDragStart(node, e)} - style={getNodeItemStyle(node)} - > -
- {renderNodeIcon(node)} - {node.nodeName}({node.nodeType}) -
-
- - ))} - - ) + // 拖拽开始处理 + const handleDragStart = (event: React.DragEvent, nodeDefinition: WorkflowNodeDefinition) => { + event.dataTransfer.setData('application/reactflow', JSON.stringify({ + nodeType: nodeDefinition.nodeType, + nodeDefinition })); + event.dataTransfer.effectAllowed = 'move'; + }; - return ( - ( +
{ + e.currentTarget.style.background = '#f9fafb'; + e.currentTarget.style.borderColor = nodeDefinition.uiConfig.style.fill; + e.currentTarget.style.transform = 'translateX(2px)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'white'; + e.currentTarget.style.borderColor = '#e5e7eb'; + e.currentTarget.style.transform = 'translateX(0)'; + }} + onDragStart={(e) => { + e.currentTarget.style.cursor = 'grabbing'; + handleDragStart(e, nodeDefinition); + }} + onDragEnd={(e) => { + e.currentTarget.style.cursor = 'grab'; }} > - - +
+ {getNodeIcon(nodeDefinition.uiConfig.style.icon)} +
+
+
+ {nodeDefinition.nodeName} +
+
+ {nodeDefinition.description} +
+
+
+ ); + + // 分类标题映射 + const categoryTitles = { + [NodeCategory.EVENT]: '🎯 事件节点', + [NodeCategory.TASK]: '📋 任务节点', + [NodeCategory.GATEWAY]: '🔀 网关节点', + [NodeCategory.CONTAINER]: '📦 容器节点' + }; + + return ( +
+
+

+ 节点面板 +

+

+ 拖拽节点到画布创建工作流 +

+
+ +
+ {Object.entries(nodesByCategory).map(([category, nodes]) => ( +
+
+ {categoryTitles[category as NodeCategory]} +
+ {nodes.map(renderNodeItem)} +
+ ))} +
+ + {/* 使用提示 */} +
+ 💡 提示: +
• 拖拽节点到画布创建 +
• 双击节点进行配置 +
• 连接节点创建流程 +
+
); }; diff --git a/frontend/src/pages/Workflow/Design/components/WorkflowCanvas.tsx b/frontend/src/pages/Workflow/Design/components/WorkflowCanvas.tsx deleted file mode 100644 index 4004fd4a..00000000 --- a/frontend/src/pages/Workflow/Design/components/WorkflowCanvas.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import NodePanel from './NodePanel'; -import type { WorkflowNodeDefinition } from "../nodes/nodeService"; - -interface WorkflowCanvasProps { - graphContainerRef: React.RefObject; - minimapContainerRef: React.RefObject; - nodeDefinitions: NodeDefinitionResponse[]; - onNodeDragStart: (node: NodeDefinitionResponse, e: React.DragEvent) => void; - onDrop: (e: React.DragEvent) => void; - onDragOver: (e: React.DragEvent) => void; -} - -/** - * 工作流画布组件 - */ -const WorkflowCanvas: React.FC = ({ - graphContainerRef, - minimapContainerRef, - nodeDefinitions, - onNodeDragStart, - onDrop, - onDragOver -}) => { - return ( -
-
- -
-
-
-
-
-
-
-
- ); -}; - -export default WorkflowCanvas; diff --git a/frontend/src/pages/Workflow/Design/components/WorkflowToolbar.tsx b/frontend/src/pages/Workflow/Design/components/WorkflowToolbar.tsx index f10d5a3c..e3639573 100644 --- a/frontend/src/pages/Workflow/Design/components/WorkflowToolbar.tsx +++ b/frontend/src/pages/Workflow/Design/components/WorkflowToolbar.tsx @@ -1,163 +1,168 @@ import React from 'react'; -import { Button, Space, Tooltip, Modal } from 'antd'; +import { Button, Divider, Tooltip } from 'antd'; import { - ArrowLeftOutlined, SaveOutlined, - CopyOutlined, - DeleteOutlined, UndoOutlined, RedoOutlined, - ScissorOutlined, - SnippetsOutlined, - SelectOutlined, ZoomInOutlined, ZoomOutOutlined, + ExpandOutlined, + ArrowLeftOutlined } from '@ant-design/icons'; interface WorkflowToolbarProps { - title: string; - scale: number; - canUndo: boolean; - canRedo: boolean; - onBack: () => void; - onSave: () => void; - onUndo: () => void; - onRedo: () => void; - onCopy: () => void; - onCut: () => void; - onPaste: () => void; - onZoomIn: () => void; - onZoomOut: () => void; - onSelectAll: () => void; - onDelete: () => void; + title?: string; + onSave?: () => void; + onUndo?: () => void; + onRedo?: () => void; + onZoomIn?: () => void; + onZoomOut?: () => void; + onFitView?: () => void; + onBack?: () => void; + canUndo?: boolean; + canRedo?: boolean; + zoom?: number; + className?: string; } -/** - * 工作流设计器工具栏组件 - */ const WorkflowToolbar: React.FC = ({ - title, - scale, - canUndo, - canRedo, - onBack, + title = '工作流设计器', onSave, onUndo, onRedo, - onCopy, - onCut, - onPaste, onZoomIn, onZoomOut, - onSelectAll, - onDelete + onFitView, + onBack, + canUndo = false, + canRedo = false, + zoom = 1, + className = '' }) => { - const handleDelete = () => { - Modal.confirm({ - title: '确认删除', - content: '确定要删除选中的元素吗?', - onOk: onDelete - }); - }; - return ( -
- - - - - - - - - - - - - - - - - - - - - + +

+ {title} +

+
+ + {/* 右侧:操作按钮区域 */} +
+ {/* 撤销/重做 */} + +
); diff --git a/frontend/src/pages/Workflow2/Design/hooks/useHistory.ts b/frontend/src/pages/Workflow/Design/hooks/useHistory.ts similarity index 100% rename from frontend/src/pages/Workflow2/Design/hooks/useHistory.ts rename to frontend/src/pages/Workflow/Design/hooks/useHistory.ts diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowData.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowData.ts deleted file mode 100644 index eed32336..00000000 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowData.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { message } from 'antd'; -import { Graph } from '@antv/x6'; -import { getDefinitionDetail, saveDefinition } from '../../Definition/service'; -import { getNodeDefinitionList } from '../nodes/nodeService'; -import { validateWorkflow } from '../utils/validator'; -import { restoreNodeFromData } from '../utils/nodeUtils'; -import type { WorkflowNodeDefinition } from '../nodes/types'; - -/** - * 工作流数据管理 Hook - */ -export const useWorkflowData = () => { - const [nodeDefinitions, setNodeDefinitions] = useState([]); - const [isNodeDefinitionsLoaded, setIsNodeDefinitionsLoaded] = useState(false); - const [definitionData, setDefinitionData] = useState(null); - const [title, setTitle] = useState('工作流设计'); - - // 加载节点定义列表 - useEffect(() => { - const loadNodeDefinitions = async () => { - try { - const data = await getNodeDefinitionList(); - setNodeDefinitions(data); - setIsNodeDefinitionsLoaded(true); - } catch (error) { - console.error('加载节点定义失败:', error); - message.error('加载节点定义失败'); - } - }; - loadNodeDefinitions(); - }, []); - - // 加载工作流定义详情 - const loadDefinitionDetail = useCallback(async (graphInstance: Graph, definitionId: string) => { - try { - console.log('正在加载工作流定义详情, ID:', definitionId); - const response = await getDefinitionDetail(Number(definitionId)); - console.log('工作流定义详情加载成功:', response); - setTitle(`工作流设计 - ${response.name}`); - setDefinitionData(response); - - if (!graphInstance) { - console.error('Graph instance is not initialized'); - return; - } - - // 确保节点定义已加载 - if (!nodeDefinitions || nodeDefinitions.length === 0) { - console.error('节点定义未加载,无法还原工作流'); - return; - } - - // 清空画布 - graphInstance.clearCells(); - const nodeMap = new Map(); - - // 创建节点 - response.graph?.nodes?.forEach((existingNode: any) => { - console.log('正在还原节点:', existingNode.nodeType, existingNode); - - // 查找节点定义 - const nodeDefinition = nodeDefinitions.find(def => def.nodeType === existingNode.nodeType); - if (!nodeDefinition) { - console.error('找不到节点定义:', existingNode.nodeType); - return; - } - - // 恢复节点 - const node = restoreNodeFromData(graphInstance, existingNode, nodeDefinition); - if (!node) { - console.error('节点创建失败:', existingNode); - return; - } - - nodeMap.set(existingNode.id, node); - console.log('节点创建成功,ID:', node.id, '映射 ID:', existingNode.id); - }); - - console.log('所有节点创建完成,nodeMap:', nodeMap); - console.log('准备创建边:', response.graph?.edges); - - // 创建边 - response.graph?.edges?.forEach((edge: any) => { - const sourceNode = nodeMap.get(edge.from); - const targetNode = nodeMap.get(edge.to); - if (sourceNode && targetNode) { - // 根据节点类型获取正确的端口组 - const getPortByGroup = (node: any, group: string) => { - const ports = node.getPorts(); - const port = ports.find((p: any) => p.group === group); - return port?.id; - }; - - // 获取源节点的输出端口(一定是out组) - const sourcePort = getPortByGroup(sourceNode, 'out'); - - // 获取目标节点的输入端口(一定是in组) - const targetPort = getPortByGroup(targetNode, 'in'); - - if (!sourcePort || !targetPort) { - console.error('无法找到正确的端口:', edge); - return; - } - - const newEdge = graphInstance.addEdge({ - source: { - cell: sourceNode.id, - port: sourcePort, - }, - target: { - cell: targetNode.id, - port: targetPort, - }, - vertices: edge?.vertices || [], // 恢复顶点信息 - attrs: { - line: { - stroke: '#5F95FF', - strokeWidth: 2, - targetMarker: { - name: 'classic', - size: 7, - }, - }, - }, - labels: [{ - attrs: { - label: { - text: edge.config?.condition?.expression || edge.name || '' - } - } - }] - }); - - // 设置边的条件属性 - if (edge.config?.condition) { - newEdge.setProp('condition', edge.config.condition); - } - } - }); - } catch (error) { - console.error('加载工作流定义失败:', error); - message.error('加载工作流定义失败'); - } - }, [nodeDefinitions]); - - - // 保存工作流 - const saveWorkflow = useCallback(async (graph: Graph) => { - if (!graph) { - console.error('Graph 实例为空'); - message.error('图形实例不存在,无法保存'); - return; - } - - if (!definitionData) { - console.error('definitionData 为空 - 工作流定义数据未加载'); - message.error('工作流定义数据未加载,请刷新页面重试'); - return; - } - - try { - // 校验流程图 - const validationResult = validateWorkflow(graph); - if (!validationResult.valid) { - message.error(validationResult.message); - return; - } - - // 获取所有节点和边的数据 - 只保存业务数据,不保存UI配置 - const nodes = graph.getNodes().map(node => { - const nodeData = node.getData(); - const position = node.getPosition(); - - return { - id: node.id, - nodeCode: nodeData.nodeCode, - nodeType: nodeData.nodeType, - nodeName: nodeData.nodeName, - position, // 只保存位置信息 - configs: nodeData.configs || {}, // 节点配置数据 - inputMapping: nodeData.inputMapping || {}, // 输入映射数据 - outputMapping: nodeData.outputMapping || {} // 输出映射数据 - }; - }); - - const edges = graph.getEdges().map(edge => { - const condition = edge.getProp('condition'); - const vertices = edge.getVertices(); // 获取边的顶点信息 - - return { - id: edge.id, - from: edge.getSourceCellId(), - to: edge.getTargetCellId(), - name: edge.getLabels()?.[0]?.attrs?.label?.text || '', - config: { - type: 'sequence', - condition: condition || undefined - }, - vertices: vertices // 保存顶点信息 - }; - }); - - // 构建保存数据 - 只保存图形结构,不保存Schema配置 - const saveData = { - ...definitionData, - graph: { - nodes, - edges - } - }; - - // 调用保存接口 - await saveDefinition(saveData); - - message.success('保存成功'); - } catch (error) { - console.error('保存流程失败:', error); - message.error('保存流程失败'); - } - }, [nodeDefinitions, definitionData]); - - return { - nodeDefinitions, - isNodeDefinitionsLoaded, - definitionData, - title, - loadDefinitionDetail, - saveWorkflow - }; -}; diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowDragDrop.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowDragDrop.ts deleted file mode 100644 index 08f682bf..00000000 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowDragDrop.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Graph } from '@antv/x6'; -import { message } from 'antd'; -import { createNodeFromDefinition } from '../utils/nodeUtils'; -import type { WorkflowNodeDefinition } from '../nodes/types'; - -/** - * 工作流拖拽功能 Hook - */ -export const useWorkflowDragDrop = ( - graph: Graph | null, - nodeDefinitions: WorkflowNodeDefinition[] -) => { - // 处理节点拖拽开始 - const handleNodeDragStart = (node: WorkflowNodeDefinition, e: React.DragEvent) => { - e.dataTransfer.setData('node', JSON.stringify(node)); - }; - - // 处理节点拖拽结束 - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - if (!graph) return; - - try { - const nodeData = e.dataTransfer.getData('node'); - if (!nodeData) return; - - const nodeDefinition = JSON.parse(nodeData); - const {clientX, clientY} = e; - const point = graph.clientToLocal({x: clientX, y: clientY}); - createNodeFromDefinition(graph, nodeDefinition, point); - } catch (error) { - console.error('创建节点失败:', error); - message.error('创建节点失败'); - } - }; - - // 处理拖拽过程中 - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - }; - - return { - handleNodeDragStart, - handleDrop, - handleDragOver - }; -}; diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowGraph.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowGraph.ts deleted file mode 100644 index b11e9283..00000000 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowGraph.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; -import { Graph, Edge } from '@antv/x6'; -import { message } from 'antd'; -import { GraphInitializer } from '../utils/graph/graphInitializer'; -import { EventRegistrar } from '../utils/graph/eventRegistrar'; -import type { WorkflowNodeDefinition } from '../nodes/types'; - -/** - * 工作流图形管理 Hook - */ -export const useWorkflowGraph = ( - nodeDefinitions: WorkflowNodeDefinition[], - isNodeDefinitionsLoaded: boolean, - onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void, - onEdgeEdit: (edge: Edge) => void -) => { - const [graph, setGraph] = useState(null); - const [scale, setScale] = useState(1); - const graphContainerRef = useRef(null); - const minimapContainerRef = useRef(null); - - // 初始化图形 - useEffect(() => { - if (!graphContainerRef.current || !minimapContainerRef.current || !isNodeDefinitionsLoaded) { - return; - } - - const initializer = new GraphInitializer( - graphContainerRef.current, - minimapContainerRef.current - ); - - const newGraph = initializer.initializeGraph(); - - if (newGraph) { - // 注册事件 - const eventRegistrar = new EventRegistrar( - newGraph, - nodeDefinitions, - onNodeEdit, - onEdgeEdit - ); - eventRegistrar.registerAllEvents(); - - setGraph(newGraph); - } - - return () => { - graph?.dispose(); - }; - }, [isNodeDefinitionsLoaded, nodeDefinitions, onNodeEdit, onEdgeEdit]); - - // 图形操作方法 - const graphOperations = { - // 撤销操作 - undo: () => { - if (!graph) return; - const history = (graph as any).history; - if (!history) { - console.error('History plugin not initialized'); - return; - } - - if (history.canUndo()) { - history.undo(); - message.success('已撤销'); - } else { - message.info('没有可撤销的操作'); - } - }, - - // 重做操作 - redo: () => { - if (!graph) return; - const history = (graph as any).history; - if (!history) { - console.error('History plugin not initialized'); - return; - } - - if (history.canRedo()) { - history.redo(); - message.success('已重做'); - } else { - message.info('没有可重做的操作'); - } - }, - - // 复制 - copy: () => { - if (!graph) return; - const cells = graph.getSelectedCells(); - if (cells.length === 0) { - message.info('请先选择要复制的节点'); - return; - } - graph.copy(cells); - message.success('已复制'); - }, - - // 剪切 - cut: () => { - if (!graph) return; - const cells = graph.getSelectedCells(); - if (cells.length === 0) { - message.info('请先选择要剪切的节点'); - return; - } - graph.cut(cells); - message.success('已剪切'); - }, - - // 粘贴 - paste: () => { - if (!graph) return; - if (graph.isClipboardEmpty()) { - message.info('剪贴板为空'); - return; - } - const cells = graph.paste({offset: 32}); - graph.cleanSelection(); - graph.select(cells); - message.success('已粘贴'); - }, - - // 缩放 - zoom: (delta: number) => { - if (!graph) return; - const currentScale = graph.scale(); - const newScale = Math.max(0.2, Math.min(2, currentScale.sx + delta)); - graph.scale(newScale, newScale); - setScale(newScale); - }, - - // 全选 - selectAll: () => { - if (!graph) return; - const cells = graph.getCells(); - if (cells.length === 0) { - message.info('当前没有可选择的元素'); - return; - } - graph.resetSelection(); - graph.select(cells); - // 为选中的元素添加高亮样式 - cells.forEach(cell => { - if (cell.isNode()) { - cell.setAttrByPath('body/stroke', '#1890ff'); - cell.setAttrByPath('body/strokeWidth', 3); - cell.setAttrByPath('body/strokeDasharray', '5 5'); - } else if (cell.isEdge()) { - cell.setAttrByPath('line/stroke', '#1890ff'); - cell.setAttrByPath('line/strokeWidth', 3); - cell.setAttrByPath('line/strokeDasharray', '5 5'); - } - }); - }, - - // 删除选中元素 - deleteSelected: () => { - if (!graph) return; - const cells = graph.getSelectedCells(); - if (cells.length === 0) { - message.info('请先选择要删除的元素'); - return; - } - graph.removeCells(cells); - }, - - // 检查是否可以撤销/重做 - canUndo: () => { - return (graph as any)?.history?.canUndo() || false; - }, - - canRedo: () => { - return (graph as any)?.history?.canRedo() || false; - } - }; - - return { - graph, - scale, - graphContainerRef, - minimapContainerRef, - graphOperations - }; -}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowLoad.ts similarity index 100% rename from frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts rename to frontend/src/pages/Workflow/Design/hooks/useWorkflowLoad.ts diff --git a/frontend/src/pages/Workflow/Design/hooks/useWorkflowModals.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowModals.ts deleted file mode 100644 index 4f44ee98..00000000 --- a/frontend/src/pages/Workflow/Design/hooks/useWorkflowModals.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { useState, useCallback } from 'react'; -import { Cell, Edge } from '@antv/x6'; -import { message } from 'antd'; -import type { WorkflowNodeDefinition } from '../nodes/types'; -import type { EdgeCondition } from '../nodes/types'; - -/** - * 工作流弹窗管理 Hook - */ -export const useWorkflowModals = () => { - const [selectedNode, setSelectedNode] = useState(null); - const [selectedNodeDefinition, setSelectedNodeDefinition] = useState(null); - const [selectedEdge, setSelectedEdge] = useState(null); - const [configModalVisible, setConfigModalVisible] = useState(false); - const [expressionModalVisible, setExpressionModalVisible] = useState(false); - - // 处理节点编辑 - const handleNodeEdit = useCallback((cell: Cell, nodeDefinition?: WorkflowNodeDefinition) => { - setSelectedNode(cell); - setSelectedNodeDefinition(nodeDefinition || null); - setConfigModalVisible(true); - }, []); - - // 处理边编辑 - const handleEdgeEdit = useCallback((edge: Edge) => { - setSelectedEdge(edge); - setExpressionModalVisible(true); - }, []); - - // 处理节点配置更新 - const handleNodeConfigUpdate = useCallback((formData: any) => { - if (!selectedNode) return; - - // 更新节点数据 - const nodeData = selectedNode.getData() || {}; - const updatedData = { - ...nodeData, - ...formData - }; - selectedNode.setData(updatedData); - - // 更新节点显示名称(如果配置中有nodeName) - if (formData.configs?.nodeName) { - selectedNode.attr('label/text', formData.configs.nodeName); - } - - setConfigModalVisible(false); - message.success('节点配置已更新'); - }, [selectedNode]); - - // 处理条件更新 - const handleConditionUpdate = useCallback((condition: EdgeCondition) => { - if (!selectedEdge) return; - - // 更新边的属性 - selectedEdge.setProp('condition', condition); - - // 更新边的标签显示 - const labelText = condition.type === 'EXPRESSION' - ? condition.expression - : '默认路径'; - - selectedEdge.setLabels([{ - attrs: { - label: { - text: labelText, - fill: '#333', - fontSize: 12 - }, - rect: { - fill: '#fff', - stroke: '#ccc', - rx: 3, - ry: 3, - padding: 5 - } - }, - position: { - distance: 0.5, - offset: 0 - } - }]); - - setExpressionModalVisible(false); - setSelectedEdge(null); - }, [selectedEdge]); - - // 关闭节点配置弹窗 - const closeNodeConfigModal = useCallback(() => { - setConfigModalVisible(false); - setSelectedNode(null); - setSelectedNodeDefinition(null); - }, []); - - // 关闭条件配置弹窗 - const closeExpressionModal = useCallback(() => { - setExpressionModalVisible(false); - setSelectedEdge(null); - }, []); - - return { - // 状态 - selectedNode, - selectedNodeDefinition, - selectedEdge, - configModalVisible, - expressionModalVisible, - - // 处理函数 - handleNodeEdit, - handleEdgeEdit, - handleNodeConfigUpdate, - handleConditionUpdate, - closeNodeConfigModal, - closeExpressionModal - }; -}; diff --git a/frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts b/frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts similarity index 100% rename from frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts rename to frontend/src/pages/Workflow/Design/hooks/useWorkflowSave.ts diff --git a/frontend/src/pages/Workflow/Design/index.less b/frontend/src/pages/Workflow/Design/index.less index 5de6f13b..44b2e2e5 100644 --- a/frontend/src/pages/Workflow/Design/index.less +++ b/frontend/src/pages/Workflow/Design/index.less @@ -1,171 +1,267 @@ -.workflow-design { - position: relative; - height: calc(100vh - 184px); +// 工作流设计器 - React Flow版本 +// 变量定义 +@header-height: 64px; +@toolbar-height: 56px; +@sidebar-width: 260px; +@container-padding: 24px; + +// 主容器 - 覆盖父容器样式 +.workflow-design-container { + // 使用负边距抵消父容器的padding + margin: -@container-padding; + // 精确计算高度:视口高度 - header高度 + height: calc(100vh - @header-height); + // 补偿左右padding + width: calc(100% + @container-padding * 2); display: flex; flex-direction: column; - background: #f8fafc; + overflow: hidden; + background: #ffffff; + position: relative; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - .header { - padding: 16px 24px; - border-bottom: 1px solid #f0f0f0; - background: #ffffff; - display: flex; - justify-content: space-between; - align-items: center; - flex-shrink: 0; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + // React Flow 样式覆盖 + .react-flow { + width: 100% !important; + height: 100% !important; - .back-button { - margin-right: 16px; + &__renderer { + width: 100% !important; + height: 100% !important; } - .actions { - .ant-space-compact { - margin-right: 8px; + // 自定义控制按钮样式 + &__controls { + button { + background: rgba(255, 255, 255, 0.95); + border: 1px solid #e5e7eb; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease-in-out; + + &:hover { + background: #f9fafb; + border-color: #3b82f6; + } } } + + // 小地图样式 + &__minimap { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } } - .content { + // 内容区域 + .workflow-content-area { flex: 1; - min-height: 0; display: flex; - padding: 16px; - gap: 16px; - overflow: hidden; // 防止外层出现滚动条 + overflow: hidden; + position: relative; + min-height: 0; - .sidebar { - width: 280px; - flex-shrink: 0; - border-radius: 8px; - background: #ffffff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - border: 1px solid #f0f0f0; + // 节点面板区域 + .node-panel { + width: @sidebar-width; + background: #f8fafc; + border-right: 1px solid #e5e7eb; + overflow: hidden; display: flex; flex-direction: column; - overflow: hidden; + box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05); - :global { - .ant-collapse { - border: none; - background: transparent; - flex: 1; - overflow-y: auto; // 只在折叠面板内部显示滚动条 + // 面板头部 + .panel-header { + padding: 16px; + background: #ffffff; + border-bottom: 1px solid #e5e7eb; - .ant-collapse-item { - border-radius: 0; + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #374151; + } - .ant-collapse-header { - padding: 8px 16px; + p { + margin: 4px 0 0 0; + font-size: 12px; + color: #6b7280; + } + } + + // 节点分组 + .node-group { + margin-bottom: 16px; + + .group-title { + font-size: 12px; + font-weight: 600; + color: #4b5563; + margin-bottom: 8px; + padding: 4px 0; + border-bottom: 1px solid #e5e7eb; + } + + // 节点项 + .node-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + border: 1px solid #e5e7eb; + background: #ffffff; + cursor: grab; + transition: all 0.2s ease-in-out; + margin-bottom: 6px; + + &:hover { + background: #f9fafb; + border-color: var(--node-color, #3b82f6); + transform: translateX(2px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + &:active { + cursor: grabbing; + } + + .node-icon { + font-size: 16px; + width: 20px; + text-align: center; + } + + .node-info { + .node-name { + font-size: 13px; + font-weight: 500; + color: #374151; + line-height: 1.2; } - .ant-collapse-content-box { - padding: 0; + .node-desc { + font-size: 11px; + color: #6b7280; + line-height: 1.2; + margin-top: 2px; } } } } - .node-item { - padding: 8px 16px; - margin: 4px 8px; - border: 1px solid #d9d9d9; - border-radius: 4px; - cursor: move; - transition: all 0.3s; - background: #ffffff; - - &:hover { - background: #f5f5f5; - border-color: #1890ff; - } + // 使用提示 + .panel-tips { + padding: 12px; + background: #f1f5f9; + border-top: 1px solid #e5e7eb; + font-size: 11px; + color: #64748b; + line-height: 1.4; } } - .main-area { + // 画布区域 + .workflow-canvas-area { flex: 1; - display: flex; - flex-direction: column; - min-width: 0; + position: relative; + overflow: hidden; + background: #f8fafc; - .workflow-container { - flex: 1; - position: relative; - border: 1px solid #d9d9d9; - border-radius: 4px; - background: #ffffff; - overflow: hidden; - - .workflow-canvas { - width: 100%; - height: 100%; - } - - .minimap-container { - position: absolute; - right: 20px; - bottom: 20px; - width: var(--minimap-width); - height: var(--minimap-height); - border: 1px solid #f0f0f0; - border-radius: 4px; - background: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - z-index: 1; - overflow: hidden; - } + // 画布状态面板 + .canvas-status { + position: absolute; + top: 12px; + left: 12px; + background: rgba(255, 255, 255, 0.95); + padding: 8px 12px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-size: 12px; + color: #6b7280; + z-index: 10; + backdrop-filter: blur(4px); } } } - :global { - .node-selected { - > rect { - stroke: #1890ff; - stroke-width: 2px; - } - - > path { - stroke: #1890ff; - stroke-width: 2px; - } + // 响应式适配 + @media (max-width: 1024px) { + .workflow-content-area .node-panel { + width: 220px; } + } - .x6-node-selected { - rect, ellipse { - stroke: #1890ff; - stroke-width: 2px; - } - } + @media (max-width: 768px) { + .workflow-content-area { + flex-direction: column; - .x6-edge-selected { - path { - stroke: #1890ff; - stroke-width: 2px !important; - } - } - - // 右键菜单样式 - .x6-context-menu { - background: #fff; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - padding: 4px 0; - min-width: 120px; - - &-item { - padding: 5px 16px; - cursor: pointer; - user-select: none; - transition: all 0.3s; - color: rgba(0, 0, 0, 0.85); - font-size: 14px; - - &:hover { - background: #f5f5f5; - } + .node-panel { + width: 100%; + height: 200px; + border-right: none; + border-bottom: 1px solid #e5e7eb; } } } } + +// 工具栏特殊样式 +.workflow-toolbar { + height: @toolbar-height; + background: #ffffff; + border-bottom: 1px solid #e5e7eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + flex-shrink: 0; + + .toolbar-section { + display: flex; + align-items: center; + gap: 12px; + } + + .toolbar-title { + h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #374151; + } + + .subtitle { + font-size: 12px; + color: #6b7280; + margin-top: 2px; + } + } + + .toolbar-status { + display: flex; + align-items: center; + gap: 12px; + font-size: 12px; + color: #6b7280; + + .status-item { + background: #f3f4f6; + padding: 4px 8px; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', monospace; + } + + .react-flow-badge { + background: #ecfdf5; + color: #065f46; + padding: 4px 8px; + border-radius: 4px; + font-weight: 500; + } + } +} diff --git a/frontend/src/pages/Workflow/Design/index.tsx b/frontend/src/pages/Workflow/Design/index.tsx index 5584ba25..f23e1c26 100644 --- a/frontend/src/pages/Workflow/Design/index.tsx +++ b/frontend/src/pages/Workflow/Design/index.tsx @@ -1,148 +1,617 @@ -import React, { useEffect } from 'react'; +import React, { useState, useCallback, useRef, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { message } from 'antd'; +import { ReactFlowProvider, useReactFlow } from '@xyflow/react'; + import WorkflowToolbar from './components/WorkflowToolbar'; -import WorkflowCanvas from './components/WorkflowCanvas'; -import NodeConfigDrawer from './components/NodeConfigModal'; -import ExpressionModal from './components/ExpressionModal'; -import { useWorkflowGraph } from './hooks/useWorkflowGraph'; -import { useWorkflowData } from './hooks/useWorkflowData'; -import { useWorkflowModals } from './hooks/useWorkflowModals'; -import { useWorkflowDragDrop } from './hooks/useWorkflowDragDrop'; +import NodePanel from './components/NodePanel'; +import FlowCanvas from './components/FlowCanvas'; +import NodeConfigModal from './components/NodeConfigModal'; +import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal'; +import type { FlowNode, FlowEdge, FlowNodeData } from './types'; +import type { WorkflowNodeDefinition } from './nodes/types'; +import { NodeType } from './types'; +import { useWorkflowSave } from './hooks/useWorkflowSave'; +import { useWorkflowLoad } from './hooks/useWorkflowLoad'; +import { useHistory } from './hooks/useHistory'; + +// 样式 +import '@xyflow/react/dist/style.css'; import './index.less'; -/** - * 重构后的工作流设计器主组件 - */ -const WorkflowDesign: React.FC = () => { +const WorkflowDesignInner: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const { + getNodes, + setNodes, + getEdges, + setEdges, + screenToFlowPosition, + fitView, + zoomIn, + zoomOut, + getZoom + } = useReactFlow(); - // 数据管理 - const { - nodeDefinitions, - isNodeDefinitionsLoaded, - title, - loadDefinitionDetail, - saveWorkflow - } = useWorkflowData(); - - // 弹窗管理 - const { - selectedNode, - selectedNodeDefinition, - selectedEdge, - configModalVisible, - expressionModalVisible, - handleNodeEdit, - handleEdgeEdit, - handleNodeConfigUpdate, - handleConditionUpdate, - closeNodeConfigModal, - closeExpressionModal - } = useWorkflowModals(); - - // 图形管理 - const { - graph, - scale, - graphContainerRef, - minimapContainerRef, - graphOperations - } = useWorkflowGraph( - nodeDefinitions, - isNodeDefinitionsLoaded, - handleNodeEdit, - handleEdgeEdit - ); - - // 拖拽管理 - const { - handleNodeDragStart, - handleDrop, - handleDragOver - } = useWorkflowDragDrop(graph, nodeDefinitions); + const [workflowTitle, setWorkflowTitle] = useState('新建工作流'); + const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例 + const reactFlowWrapper = useRef(null); + + // 当前工作流ID + const currentWorkflowId = id ? parseInt(id) : undefined; + + // 节点配置模态框状态 + const [configModalVisible, setConfigModalVisible] = useState(false); + const [configNode, setConfigNode] = useState(null); + + // 边配置模态框状态 + const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false); + const [configEdge, setConfigEdge] = useState(null); + + // 保存和加载hooks + const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave(); + const { workflowDefinition, loadWorkflow } = useWorkflowLoad(); + + // 历史记录管理 + const history = useHistory(); + + // 剪贴板(用于复制粘贴) + const clipboard = useRef<{ nodes: FlowNode[]; edges: FlowEdge[] } | null>(null); // 加载工作流数据 useEffect(() => { - console.log('工作流数据加载检查:', { - hasGraph: !!graph, - hasId: !!id, - id, - isNodeDefinitionsLoaded - }); + const loadData = async () => { + if (currentWorkflowId) { + const data = await loadWorkflow(currentWorkflowId); + if (data) { + setNodes(data.nodes); + setEdges(data.edges); + setWorkflowTitle(data.definition.name); + } + } + }; - if (!id) { - console.error('工作流ID缺失,无法加载定义数据'); - return; - } + loadData(); + }, [currentWorkflowId, loadWorkflow, setNodes, setEdges]); + + // 初始化缩放比例 + useEffect(() => { + setCurrentZoom(getZoom()); + }, [getZoom]); + + // 自动适应视图 + useEffect(() => { + // 延迟执行fitView以确保节点已渲染 + const timer = setTimeout(() => { + fitView({ + padding: 0.1, + duration: 800, + minZoom: 1.0, // 最小缩放100% + maxZoom: 1.0 // 最大缩放100%,确保默认100% + }); + // 更新zoom显示 + setTimeout(() => setCurrentZoom(getZoom()), 850); + }, 100); - if (graph && id && isNodeDefinitionsLoaded) { - console.log('开始加载工作流定义详情:', id); - loadDefinitionDetail(graph, id); + return () => clearTimeout(timer); + }, [fitView, getZoom]); + + // 初始化示例节点 - 优化位置和布局 + const initialNodes: FlowNode[] = [ + { + id: '1', + type: 'START_EVENT', + position: { x: 250, y: 50 }, + data: { + label: '开始', + nodeType: NodeType.START_EVENT, + category: 'EVENT' as any, + icon: '▶️', + color: '#10b981' + } + }, + { + id: '2', + type: 'USER_TASK', + position: { x: 200, y: 150 }, + data: { + label: '用户审批', + nodeType: NodeType.USER_TASK, + category: 'TASK' as any, + icon: '👤', + color: '#6366f1' + } + }, + { + id: '3', + type: 'END_EVENT', + position: { x: 250, y: 250 }, + data: { + label: '结束', + nodeType: NodeType.END_EVENT, + category: 'EVENT' as any, + icon: '⏹️', + color: '#ef4444' + } } - }, [graph, id, isNodeDefinitionsLoaded, loadDefinitionDetail]); + ]; + + const initialEdges: FlowEdge[] = [ + { + id: 'e1-2', + source: '1', + target: '2', + type: 'default', + animated: true, + data: { + label: '提交', + condition: { + type: 'DEFAULT', + priority: 0 + } + } + }, + { + id: 'e2-3', + source: '2', + target: '3', + type: 'default', + animated: true, + data: { + label: '通过', + condition: { + type: 'DEFAULT', + priority: 0 + } + } + } + ]; // 工具栏事件处理 - const handleBack = () => navigate('/workflow/definition'); - const handleSave = () => { - if (!graph) { - console.error('Graph 实例不存在'); + const handleSave = useCallback(async () => { + const nodes = getNodes() as FlowNode[]; + const edges = getEdges() as FlowEdge[]; + + const success = await saveWorkflow({ + nodes, + edges, + workflowId: currentWorkflowId, + name: workflowTitle, + description: workflowDefinition?.description || '', + definitionData: workflowDefinition // 传递原始定义数据 + }); + + if (success) { + console.log('保存工作流成功:', { nodes, edges }); + } + }, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]); + + const handleBack = useCallback(() => { + navigate('/workflow/definition'); + }, [navigate]); + + // 撤销操作 + const handleUndo = useCallback(() => { + const state = history.undo(); + if (state) { + history.pauseRecording(); + setNodes(state.nodes); + setEdges(state.edges); + setTimeout(() => history.resumeRecording(), 100); + message.success('已撤销'); + } else { + message.info('没有可撤销的操作'); + } + }, [history, setNodes, setEdges]); + + // 重做操作 + const handleRedo = useCallback(() => { + const state = history.redo(); + if (state) { + history.pauseRecording(); + setNodes(state.nodes); + setEdges(state.edges); + setTimeout(() => history.resumeRecording(), 100); + message.success('已重做'); + } else { + message.info('没有可重做的操作'); + } + }, [history, setNodes, setEdges]); + + // 复制选中的节点和边 + const handleCopy = useCallback(() => { + const selectedNodes = getNodes().filter(node => node.selected); + const selectedEdges = getEdges().filter(edge => { + // 只复制两端都被选中的边 + return selectedNodes.some(n => n.id === edge.source) && + selectedNodes.some(n => n.id === edge.target); + }); + + if (selectedNodes.length > 0) { + clipboard.current = { + nodes: JSON.parse(JSON.stringify(selectedNodes)), + edges: JSON.parse(JSON.stringify(selectedEdges)) + }; + message.success(`已复制 ${selectedNodes.length} 个节点`); + } else { + message.warning('请先选择要复制的节点'); + } + }, [getNodes, getEdges]); + + // 粘贴节点 + const handlePaste = useCallback(() => { + if (!clipboard.current || clipboard.current.nodes.length === 0) { + message.info('剪贴板为空'); return; } - saveWorkflow(graph); - }; - const handleZoomIn = () => graphOperations.zoom(0.1); - const handleZoomOut = () => graphOperations.zoom(-0.1); + + const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current; + const offset = 50; // 粘贴偏移量 + const idMap = new Map(); + + // 创建新节点(带偏移) + const newNodes = copiedNodes.map(node => { + const newId = `${node.type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + idMap.set(node.id, newId); + + return { + ...node, + id: newId, + position: { + x: node.position.x + offset, + y: node.position.y + offset + }, + selected: true + }; + }); + + // 创建新边(更新source和target为新ID) + const newEdges = copiedEdges.map(edge => { + const newSource = idMap.get(edge.source); + const newTarget = idMap.get(edge.target); + + if (!newSource || !newTarget) return null; + + return { + ...edge, + id: `e${newSource}-${newTarget}`, + source: newSource, + target: newTarget, + selected: true + }; + }).filter(edge => edge !== null) as FlowEdge[]; + + // 取消其他元素的选中状态 + setNodes(nodes => [ + ...nodes.map(n => ({ ...n, selected: false })), + ...newNodes + ]); + setEdges(edges => [ + ...edges.map(e => ({ ...e, selected: false })), + ...newEdges + ]); + + message.success(`已粘贴 ${newNodes.length} 个节点`); + }, [setNodes, setEdges]); + + const handleDelete = useCallback(() => { + const selectedNodes = getNodes().filter(node => node.selected); + const selectedEdges = getEdges().filter(edge => edge.selected); + + if (selectedNodes.length > 0 || selectedEdges.length > 0) { + setNodes(nodes => nodes.filter(node => !node.selected)); + setEdges(edges => edges.filter(edge => !edge.selected)); + message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`); + markUnsaved(); + } else { + message.warning('请先选择要删除的元素'); + } + }, [getNodes, getEdges, setNodes, setEdges, markUnsaved]); + + const handleSelectAll = useCallback(() => { + setNodes(nodes => nodes.map(node => ({ ...node, selected: true }))); + setEdges(edges => edges.map(edge => ({ ...edge, selected: true }))); + message.info('已全选所有元素'); + }, [setNodes, setEdges]); + + const handleFitView = useCallback(() => { + fitView({ padding: 0.2, duration: 800 }); + // 延迟更新zoom值以获取最新的缩放比例 + setTimeout(() => setCurrentZoom(getZoom()), 850); + }, [fitView, getZoom]); + + const handleZoomIn = useCallback(() => { + zoomIn({ duration: 300 }); + // 延迟更新zoom值以获取最新的缩放比例 + setTimeout(() => setCurrentZoom(getZoom()), 350); + }, [zoomIn, getZoom]); + + const handleZoomOut = useCallback(() => { + zoomOut({ duration: 300 }); + // 延迟更新zoom值以获取最新的缩放比例 + setTimeout(() => setCurrentZoom(getZoom()), 350); + }, [zoomOut, getZoom]); + + // 处理节点拖拽放置 - 使用官方推荐的screenToFlowPosition方法 + const handleDrop = useCallback((event: React.DragEvent) => { + event.preventDefault(); + + const dragData = event.dataTransfer.getData('application/reactflow'); + if (!dragData) return; + + try { + const { nodeType, nodeDefinition }: { nodeType: string; nodeDefinition: WorkflowNodeDefinition } = JSON.parse(dragData); + + // 根据React Flow官方文档,screenToFlowPosition会自动处理所有边界计算 + // 不需要手动减去容器边界! + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + + const newNode: FlowNode = { + id: `${nodeType}-${Date.now()}`, + type: nodeType, + position, + data: { + label: nodeDefinition.nodeName, + nodeType: nodeDefinition.nodeType, + category: nodeDefinition.category, + icon: nodeDefinition.uiConfig.style.icon, + color: nodeDefinition.uiConfig.style.fill, + // 保存原始节点定义引用,用于配置 + nodeDefinition, + // 初始化配置数据 + configs: { + nodeName: nodeDefinition.nodeName, + nodeCode: nodeDefinition.nodeCode, + description: nodeDefinition.description + }, + inputMapping: {}, + outputMapping: {} + } + }; + + setNodes(nodes => [...nodes, newNode]); + message.success(`已添加 ${nodeDefinition.nodeName} 节点`); + } catch (error) { + console.error('解析拖拽数据失败:', error); + message.error('添加节点失败'); + } + }, [screenToFlowPosition, setNodes]); + + // 处理节点双击 - 打开配置面板 + const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => { + // 只处理双击事件 + if (event.detail === 2) { + console.log('双击节点,打开配置:', node); + setConfigNode(node); + setConfigModalVisible(true); + } + }, []); + + // 处理边双击 - 打开条件配置弹窗 + const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => { + if (event.detail === 2) { + console.log('双击边,打开配置:', edge); + setConfigEdge(edge); + setEdgeConfigModalVisible(true); + } + }, []); + + // 处理节点配置更新 + const handleNodeConfigUpdate = useCallback((nodeId: string, updatedData: Partial) => { + setNodes((nodes) => + nodes.map((node) => + node.id === nodeId + ? { + ...node, + data: { + ...node.data, + ...updatedData, + }, + } + : node + ) + ); + markUnsaved(); // 标记有未保存更改 + }, [setNodes, markUnsaved]); + + // 关闭节点配置模态框 + const handleCloseConfigModal = useCallback(() => { + setConfigModalVisible(false); + setConfigNode(null); + }, []); + + // 处理边条件更新 + const handleEdgeConditionUpdate = useCallback((edgeId: string, condition: EdgeCondition) => { + setEdges((edges) => + edges.map((edge) => { + if (edge.id === edgeId) { + // 根据条件类型生成显示文本 + const label = condition.type === 'EXPRESSION' + ? condition.expression + : '默认路径'; + + return { + ...edge, + data: { + ...edge.data, + condition, + }, + label, + }; + } + return edge; + }) + ); + markUnsaved(); // 标记有未保存更改 + message.success('边条件配置已更新'); + setEdgeConfigModalVisible(false); + setConfigEdge(null); + }, [setEdges, markUnsaved]); + + // 关闭边配置模态框 + const handleCloseEdgeConfigModal = useCallback(() => { + setEdgeConfigModalVisible(false); + setConfigEdge(null); + }, []); + + // 监听视图变化(缩放、平移等) + const handleViewportChange = useCallback(() => { + const zoom = getZoom(); + setCurrentZoom(zoom); + }, [getZoom]); + + // 监听节点和边的变化,记录到历史 + useEffect(() => { + const nodes = getNodes() as FlowNode[]; + const edges = getEdges() as FlowEdge[]; + + // 只在有节点或边时记录(避免空状态) + if (nodes.length > 0 || edges.length > 0) { + history.record(nodes, edges); + } + }, [getNodes, getEdges, history]); + + // 键盘快捷键支持 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // 检查焦点是否在输入框、文本域或可编辑元素内 + const target = e.target as HTMLElement; + const tagName = target.tagName?.toUpperCase(); + const isInputElement = tagName === 'INPUT' || + tagName === 'TEXTAREA' || + target.isContentEditable || + target.getAttribute('contenteditable') === 'true'; + const isInDrawer = target.closest('.ant-drawer-body') !== null; + const isInModal = target.closest('.ant-modal') !== null; + + // 在抽屉或模态框内,且在输入元素中时,允许原生行为 + const shouldSkipShortcut = isInputElement || isInDrawer || isInModal; + + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + const ctrlKey = isMac ? e.metaKey : e.ctrlKey; + + // Ctrl+Z / Cmd+Z - 撤销(仅在画布区域) + if (ctrlKey && e.key === 'z' && !e.shiftKey) { + if (!shouldSkipShortcut) { + e.preventDefault(); + handleUndo(); + } + } + // Ctrl+Shift+Z / Cmd+Shift+Z - 重做(仅在画布区域) + else if (ctrlKey && e.key === 'z' && e.shiftKey) { + if (!shouldSkipShortcut) { + e.preventDefault(); + handleRedo(); + } + } + // Ctrl+C / Cmd+C - 复制节点(仅在画布区域) + else if (ctrlKey && e.key === 'c') { + if (!shouldSkipShortcut) { + e.preventDefault(); + handleCopy(); + } + } + // Ctrl+V / Cmd+V - 粘贴节点(仅在画布区域) + else if (ctrlKey && e.key === 'v') { + if (!shouldSkipShortcut) { + e.preventDefault(); + handlePaste(); + } + } + // Ctrl+A / Cmd+A - 全选节点(仅在画布区域) + else if (ctrlKey && e.key === 'a') { + if (!shouldSkipShortcut) { + e.preventDefault(); + handleSelectAll(); + } + } + // Delete / Backspace - 删除节点(仅在画布区域) + else if (e.key === 'Delete' || e.key === 'Backspace') { + if (!shouldSkipShortcut) { + e.preventDefault(); + handleDelete(); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete]); return ( -
+
+ {/* 工具栏 */} + + {/* 主要内容区域 */} +
+ {/* 节点面板 */} + + + {/* 画布区域 */} +
+ +
+
+ + {/* 节点配置弹窗 */} + - - - {configModalVisible && selectedNode && selectedNodeDefinition && ( - - )} - - {selectedEdge && ( - - )}
); }; +const WorkflowDesign: React.FC = () => { + return ( + + + + ); +}; + export default WorkflowDesign; diff --git a/frontend/src/pages/Workflow2/Design/nodes/EndEventNode.tsx b/frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx similarity index 100% rename from frontend/src/pages/Workflow2/Design/nodes/EndEventNode.tsx rename to frontend/src/pages/Workflow/Design/nodes/EndEventNode.tsx diff --git a/frontend/src/pages/Workflow2/Design/nodes/JenkinsBuildNode.tsx b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx similarity index 100% rename from frontend/src/pages/Workflow2/Design/nodes/JenkinsBuildNode.tsx rename to frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx diff --git a/frontend/src/pages/Workflow2/Design/nodes/StartEventNode.tsx b/frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx similarity index 100% rename from frontend/src/pages/Workflow2/Design/nodes/StartEventNode.tsx rename to frontend/src/pages/Workflow/Design/nodes/StartEventNode.tsx diff --git a/frontend/src/pages/Workflow/Design/nodes/definitions/DeployNode.ts b/frontend/src/pages/Workflow/Design/nodes/definitions/DeployNode.ts deleted file mode 100644 index 1f2712c4..00000000 --- a/frontend/src/pages/Workflow/Design/nodes/definitions/DeployNode.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types'; - -/** - * 部署任务节点定义 - * 可配置节点,支持配置、输入映射、输出映射 - */ -export const DeployNode: ConfigurableNodeDefinition = { - nodeCode: "DEPLOY_NODE", - nodeName: "构建任务", - nodeType: NodeType.DEPLOY_NODE, - category: NodeCategory.TASK, - description: "执行应用构建和部署任务", - - // UI 配置 - 节点在画布上的显示样式(现代化设计) - uiConfig: { - shape: 'rect', - size: { - width: 160, - height: 80 - }, - style: { - fill: '#1890ff', - stroke: '#0050b3', - strokeWidth: 2, - icon: 'build', - iconColor: '#fff' - }, - ports: { - groups: { - // 输入端口 - 现代化样式 - in: { - position: 'left', - attrs: { - circle: { - r: 7, - fill: '#ffffff', - stroke: '#3b82f6', - strokeWidth: 2.5, - // 现代化端口样式 - filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))', - // 端口悬浮效果 - ':hover': { - r: 8, - fill: '#dbeafe', - stroke: '#2563eb' - } - } - } - }, - // 输出端口 - 现代化样式 - out: { - position: 'right', - attrs: { - circle: { - r: 7, - fill: '#ffffff', - stroke: '#3b82f6', - strokeWidth: 2.5, - // 现代化端口样式 - filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))', - // 端口悬浮效果 - ':hover': { - r: 8, - fill: '#dbeafe', - stroke: '#2563eb' - } - } - } - } - } - } - }, - - // 基本配置Schema - 设计时用于生成表单,保存时转为key/value(包含基本信息+节点配置) - configSchema: { - type: "object", - title: "基本配置", - description: "节点的基本信息和构建任务的配置参数", - properties: { - // 基本信息 - nodeName: { - type: "string", - title: "节点名称", - description: "节点在流程图中显示的名称", - default: "构建任务" - }, - nodeCode: { - type: "string", - title: "节点编码", - description: "节点的唯一标识符", - default: "DEPLOY_NODE" - }, - description: { - type: "string", - title: "节点描述", - description: "节点的详细说明", - default: "执行应用构建和部署任务" - }, - // 节点配置 - buildCommand: { - type: "string", - title: "构建命令", - description: "执行构建的命令", - default: "npm run build" - }, - timeout: { - type: "number", - title: "超时时间(秒)", - description: "构建超时时间", - default: 300, - minimum: 30, - maximum: 3600 - }, - retryCount: { - type: "number", - title: "重试次数", - description: "构建失败时的重试次数", - default: 2, - minimum: 0, - maximum: 5 - }, - environment: { - type: "string", - title: "运行环境", - description: "构建运行的环境", - enum: ["development", "staging", "production"], - default: "production" - }, - dockerImage: { - type: "string", - title: "Docker镜像", - description: "构建使用的Docker镜像", - default: "node:18-alpine" - }, - workingDirectory: { - type: "string", - title: "工作目录", - description: "构建的工作目录", - default: "/app" - } - }, - required: ["nodeName", "nodeCode", "buildCommand", "timeout"] - }, - - // 输入映射Schema - 定义从上游节点接收的数据 - inputMappingSchema: { - type: "object", - title: "输入映射", - description: "从上游节点接收的数据映射配置", - properties: { - sourceCodePath: { - type: "string", - title: "源代码路径", - description: "源代码在存储中的路径", - default: "${upstream.outputPath}" - }, - buildArgs: { - type: "array", - title: "构建参数", - description: "额外的构建参数列表", - items: { - type: "string" - }, - default: [] - }, - envVariables: { - type: "object", - title: "环境变量", - description: "构建时的环境变量", - properties: {}, - additionalProperties: true - }, - dependencies: { - type: "string", - title: "依赖文件", - description: "依赖配置文件路径", - default: "${upstream.dependenciesFile}" - } - }, - required: ["sourceCodePath"] - }, - - // 输出映射Schema - 定义传递给下游节点的数据 - outputMappingSchema: { - type: "object", - title: "输出映射", - description: "传递给下游节点的数据映射配置", - properties: { - buildArtifactPath: { - type: "string", - title: "构建产物路径", - description: "构建完成后的产物存储路径", - default: "/artifacts/${buildId}" - }, - buildLog: { - type: "string", - title: "构建日志", - description: "构建过程的日志文件路径", - default: "/logs/build-${buildId}.log" - }, - buildStatus: { - type: "string", - title: "构建状态", - description: "构建完成状态", - enum: ["SUCCESS", "FAILED", "TIMEOUT"], - default: "SUCCESS" - }, - buildTime: { - type: "number", - title: "构建耗时", - description: "构建耗时(秒)", - default: 0 - }, - dockerImageTag: { - type: "string", - title: "Docker镜像标签", - description: "构建生成的Docker镜像标签", - default: "${imageRegistry}/${projectName}:${buildId}" - }, - metadata: { - type: "object", - title: "构建元数据", - description: "构建过程中的元数据信息", - properties: { - buildId: { type: "string" }, - buildTime: { type: "string" }, - gitCommit: { type: "string" }, - gitBranch: { type: "string" } - } - } - }, - required: ["buildArtifactPath", "buildStatus"] - } -}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/nodes/definitions/EndEventNode.ts b/frontend/src/pages/Workflow/Design/nodes/definitions/EndEventNode.ts deleted file mode 100644 index 45ffb3ed..00000000 --- a/frontend/src/pages/Workflow/Design/nodes/definitions/EndEventNode.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { BaseNodeDefinition, NodeType, NodeCategory } from '../types'; - -/** - * 结束事件节点定义 - * 简单节点,无需额外配置 - */ -export const EndEventNode: BaseNodeDefinition = { - nodeCode: "END_EVENT", - nodeName: "结束", - nodeType: NodeType.END_EVENT, - category: NodeCategory.EVENT, - description: "工作流的结束节点", - - // UI 配置 - 节点在画布上的显示样式(现代化设计) - uiConfig: { - shape: 'ellipse', - size: { - width: 80, - height: 50 - }, - style: { - fill: '#f5222d', - stroke: '#cf1322', - strokeWidth: 2, - icon: 'stop', - iconColor: '#fff' - }, - ports: { - groups: { - // 结束节点只有输入端口 - 现代化样式 - in: { - position: 'left', - attrs: { - circle: { - r: 4, - fill: '#fff', - stroke: '#f5222d' - } - } - } - } - } - }, - - // 基本配置Schema - 用于生成"基本配置"TAB(包含基本信息) - configSchema: { - type: "object", - title: "基本配置", - description: "节点的基本配置信息", - properties: { - nodeName: { - type: "string", - title: "节点名称", - description: "节点在流程图中显示的名称", - default: "结束" - }, - nodeCode: { - type: "string", - title: "节点编码", - description: "节点的唯一标识符", - default: "END_EVENT" - }, - description: { - type: "string", - title: "节点描述", - description: "节点的详细说明", - default: "工作流的结束节点" - } - }, - required: ["nodeName", "nodeCode"] - } -}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/nodes/definitions/JenkinsBuildNode.ts b/frontend/src/pages/Workflow/Design/nodes/definitions/JenkinsBuildNode.ts deleted file mode 100644 index 896f70ab..00000000 --- a/frontend/src/pages/Workflow/Design/nodes/definitions/JenkinsBuildNode.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types'; - -/** - * Jenkins构建节点定义 - * 可配置节点,支持配置、输入映射、输出映射 - */ -export const JenkinsBuildNode: ConfigurableNodeDefinition = { - nodeCode: "JENKINS_BUILD", - nodeName: "Jenkins构建", - nodeType: NodeType.JENKINS_BUILD, - category: NodeCategory.TASK, - description: "通过Jenkins执行构建任务", - - // UI 配置 - 节点在画布上的显示样式 - uiConfig: { - shape: 'rect', - size: { - width: 160, - height: 80 - }, - style: { - fill: '#52c41a', - stroke: '#389e08', - strokeWidth: 2, - icon: 'jenkins', - iconColor: '#fff' - }, - ports: { - groups: { - // 输入端口 - in: { - position: 'left', - attrs: { - circle: { - r: 4, - fill: '#fff', - stroke: '#52c41a' - } - } - }, - // 输出端口 - out: { - position: 'right', - attrs: { - circle: { - r: 4, - fill: '#fff', - stroke: '#52c41a' - } - } - } - } - } - }, - - // 基本配置Schema - 设计时用于生成表单,保存时转为key/value(包含基本信息和Jenkins配置) - configSchema: { - type: "object", - title: "基本配置", - description: "节点的基本信息和Jenkins构建配置参数", - properties: { - nodeName: { - type: "string", - title: "节点名称", - description: "节点在流程图中显示的名称", - default: "Jenkins构建" - }, - nodeCode: { - type: "string", - title: "节点编码", - description: "节点的唯一标识符", - default: "JENKINS_BUILD" - }, - description: { - type: "string", - title: "节点描述", - description: "节点的详细说明", - default: "通过Jenkins执行构建任务" - }, - jenkinsUrl: { - type: "string", - title: "Jenkins服务器地址", - description: "Jenkins服务器的完整URL地址", - default: "http://jenkins.example.com:8080", - format: "uri" - }, - jobName: { - type: "string", - title: "Job名称", - description: "要执行的Jenkins Job名称", - default: "" - }, - username: { - type: "string", - title: "用户名", - description: "Jenkins认证用户名", - default: "" - }, - apiToken: { - type: "string", - title: "API Token", - description: "Jenkins API Token或密码", - default: "", - format: "password" - }, - timeout: { - type: "number", - title: "超时时间(秒)", - description: "构建超时时间", - default: 600, - minimum: 60, - maximum: 7200 - }, - waitForCompletion: { - type: "boolean", - title: "等待构建完成", - description: "是否等待Jenkins构建完成", - default: true - }, - retryCount: { - type: "number", - title: "重试次数", - description: "构建失败时的重试次数", - default: 1, - minimum: 0, - maximum: 5 - }, - branchName: { - type: "string", - title: "分支名称", - description: "要构建的Git分支名称", - default: "main" - } - }, - required: ["nodeName", "nodeCode", "jenkinsUrl", "jobName", "username", "apiToken"] - }, - - // 输入映射Schema - 定义从上游节点接收的数据 - inputMappingSchema: { - type: "object", - title: "输入映射", - description: "从上游节点接收的数据映射配置", - properties: { - sourceCodeUrl: { - type: "string", - title: "源代码仓库地址", - description: "Git仓库的URL地址", - default: "${upstream.gitUrl}" - }, - buildParameters: { - type: "object", - title: "构建参数", - description: "传递给Jenkins Job的构建参数", - properties: {}, - additionalProperties: true, - default: {} - }, - environmentVariables: { - type: "object", - title: "环境变量", - description: "构建时的环境变量", - properties: {}, - additionalProperties: true, - default: {} - }, - commitId: { - type: "string", - title: "提交ID", - description: "要构建的Git提交ID", - default: "${upstream.commitId}" - }, - projectName: { - type: "string", - title: "项目名称", - description: "项目的名称标识", - default: "${upstream.projectName}" - }, - buildVersion: { - type: "string", - title: "构建版本", - description: "构建版本号", - default: "${upstream.version}" - } - }, - required: ["sourceCodeUrl", "buildParameters"] - }, - - // 输出映射Schema - 定义传递给下游节点的数据 - outputMappingSchema: { - type: "object", - title: "输出映射", - description: "传递给下游节点的数据映射配置", - properties: { - buildId: { - type: "string", - title: "构建ID", - description: "Jenkins构建的唯一标识符", - default: "${jenkins.buildNumber}" - }, - buildStatus: { - type: "string", - title: "构建状态", - description: "构建完成状态", - enum: ["SUCCESS", "FAILURE", "UNSTABLE", "ABORTED", "NOT_BUILT"], - default: "SUCCESS" - }, - buildUrl: { - type: "string", - title: "构建URL", - description: "Jenkins构建页面的URL", - default: "${jenkins.buildUrl}" - }, - buildDuration: { - type: "number", - title: "构建耗时", - description: "构建耗时(毫秒)", - default: 0 - }, - artifactsUrl: { - type: "string", - title: "构建产物URL", - description: "构建产物的下载地址", - default: "${jenkins.artifactsUrl}" - }, - consoleLog: { - type: "string", - title: "控制台日志", - description: "构建的控制台输出日志", - default: "${jenkins.consoleText}" - }, - testResults: { - type: "object", - title: "测试结果", - description: "构建中的测试结果统计", - properties: { - totalCount: { type: "number", title: "总测试数", default: 0 }, - passCount: { type: "number", title: "通过数", default: 0 }, - failCount: { type: "number", title: "失败数", default: 0 }, - skipCount: { type: "number", title: "跳过数", default: 0 } - }, - default: { - totalCount: 0, - passCount: 0, - failCount: 0, - skipCount: 0 - } - }, - buildParameters: { - type: "object", - title: "构建参数", - description: "实际使用的构建参数", - properties: {}, - additionalProperties: true, - default: {} - }, - commitInfo: { - type: "object", - title: "提交信息", - description: "构建对应的Git提交信息", - properties: { - commitId: { type: "string", title: "提交ID" }, - commitMessage: { type: "string", title: "提交消息" }, - commitAuthor: { type: "string", title: "提交作者" }, - commitTimestamp: { type: "string", title: "提交时间" } - }, - default: {} - } - }, - required: ["buildId", "buildStatus", "buildUrl"] - } -}; diff --git a/frontend/src/pages/Workflow/Design/nodes/definitions/StartEventNode.ts b/frontend/src/pages/Workflow/Design/nodes/definitions/StartEventNode.ts deleted file mode 100644 index ce6e6194..00000000 --- a/frontend/src/pages/Workflow/Design/nodes/definitions/StartEventNode.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {BaseNodeDefinition, NodeType, NodeCategory} from '../types'; - -/** - * 开始事件节点定义 - * 简单节点,无需额外配置 - */ -export const StartEventNode: BaseNodeDefinition = { - nodeCode: "START_EVENT", - nodeName: "开始", - nodeType: NodeType.START_EVENT, - category: NodeCategory.EVENT, - description: "工作流的起始节点", - - // UI 配置 - 节点在画布上的显示样式(现代化设计) - uiConfig: { - shape: 'ellipse', - size: { - width: 80, - height: 50 - }, - style: { - fill: '#52c41a', - stroke: '#389e08', - strokeWidth: 2, - icon: 'play-circle', - iconColor: '#fff' - }, - ports: { - groups: { - // 开始节点只有输出端口 - 现代化样式 - out: { - position: 'right', - attrs: { - circle: { - r: 4, - fill: '#fff', - stroke: '#52c41a' - } - } - } - } - } - }, - - // 基本配置Schema - 用于生成"基本配置"TAB(包含基本信息) - configSchema: { - type: "object", - title: "基本配置", - description: "节点的基本配置信息", - properties: { - nodeName: { - type: "string", - title: "节点名称", - description: "节点在流程图中显示的名称", - default: "开始" - }, - nodeCode: { - type: "string", - title: "节点编码", - description: "节点的唯一标识符", - default: "START_EVENT" - }, - description: { - type: "string", - title: "节点描述", - description: "节点的详细说明", - default: "工作流的起始节点" - } - }, - required: ["nodeName", "nodeCode"] - } -}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/nodes/definitions/index.ts b/frontend/src/pages/Workflow/Design/nodes/definitions/index.ts deleted file mode 100644 index bf77c824..00000000 --- a/frontend/src/pages/Workflow/Design/nodes/definitions/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {WorkflowNodeDefinition} from '../types'; -import {DeployNode} from './DeployNode'; -import {JenkinsBuildNode} from './JenkinsBuildNode'; -import {StartEventNode} from './StartEventNode'; -import {EndEventNode} from './EndEventNode'; - -/** - * 所有节点定义的注册表 - */ -export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [ - StartEventNode, - EndEventNode, - DeployNode, - JenkinsBuildNode, - // 在这里添加更多节点定义 -]; - -/** - * 导出节点定义 - */ -export { - StartEventNode, - EndEventNode, - DeployNode, - JenkinsBuildNode -}; diff --git a/frontend/src/pages/Workflow2/Design/nodes/index.ts b/frontend/src/pages/Workflow/Design/nodes/index.ts similarity index 100% rename from frontend/src/pages/Workflow2/Design/nodes/index.ts rename to frontend/src/pages/Workflow/Design/nodes/index.ts diff --git a/frontend/src/pages/Workflow/Design/nodes/nodeService.ts b/frontend/src/pages/Workflow/Design/nodes/nodeService.ts deleted file mode 100644 index 69b29078..00000000 --- a/frontend/src/pages/Workflow/Design/nodes/nodeService.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 节点服务 - 提供节点定义的访问接口 - */ - -import {NODE_DEFINITIONS} from './definitions'; -import {WorkflowNodeDefinition} from './types'; - -/** - * 获取所有节点定义列表 - */ -export const getNodeDefinitionList = async (): Promise => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(NODE_DEFINITIONS); - }, 10); - }); -}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/nodes/types.ts b/frontend/src/pages/Workflow/Design/nodes/types.ts index ada71ebc..ae702bc5 100644 --- a/frontend/src/pages/Workflow/Design/nodes/types.ts +++ b/frontend/src/pages/Workflow/Design/nodes/types.ts @@ -1,5 +1,4 @@ - -// 节点分类 (从 ../types.ts 合并) +// 节点分类 export enum NodeCategory { EVENT = 'EVENT', TASK = 'TASK', @@ -7,100 +6,7 @@ export enum NodeCategory { CONTAINER = 'CONTAINER' } -// 条件类型 -export type ConditionType = 'EXPRESSION' | 'SCRIPT' | 'DEFAULT'; - -// 边的条件配置 -export interface EdgeCondition { - type: ConditionType; - expression?: string; - script?: string; - priority: number; -} - -// JSON Schema 定义 -export interface JSONSchema { - type: string; - properties?: Record; - required?: string[]; - title?: string; - description?: string; -} - -// UI 变量配置 -export interface NodeSize { - width: number; - height: number; -} - -export interface PortStyle { - r: number; - fill: string; - stroke: string; - strokeWidth?: number; // 新增:端口描边宽度 - filter?: string; // 新增:端口滤镜效果 - ':hover'?: { // 新增:端口悬浮状态 - r?: number; - fill?: string; - stroke?: string; - }; -} - -export interface PortAttributes { - circle: PortStyle; -} - -export type PortPosition = 'left' | 'right' | 'top' | 'bottom'; - -export interface PortConfig { - attrs: PortAttributes; - position: PortPosition; -} - -export interface PortGroups { - in?: PortConfig; - out?: PortConfig; -} - -export type NodeShape = 'rect' | 'circle' | 'ellipse' | 'polygon'; - -export interface NodeStyle { - fill: string; - icon: string; - stroke: string; - iconColor: string; - strokeWidth: number; - // 现代化样式属性 - iconSize?: number; // 新增:图标大小 - borderRadius?: string; // 新增:圆角 - boxShadow?: string; // 新增:阴影 - transition?: string; // 新增:过渡效果 - fontSize?: string; // 新增:字体大小 - fontWeight?: string; // 新增:字体粗细 - fontFamily?: string; // 新增:字体族 - ':hover'?: { // 新增:悬浮状态 - fill?: string; - stroke?: string; - boxShadow?: string; - transform?: string; - }; - ':active'?: { // 新增:激活状态 - transform?: string; - boxShadow?: string; - }; -} - -export interface UIConfig { - size: NodeSize; - ports: { - groups: PortGroups; - }; - shape: NodeShape; - style: NodeStyle; - position?: { x: number; y: number }; -} - -// 节点类型和分类(保持原有的枚举格式) +// 节点类型(完整定义,包含所有可能的类型) export enum NodeType { START_EVENT = 'START_EVENT', END_EVENT = 'END_EVENT', @@ -114,6 +20,58 @@ export enum NodeType { CALL_ACTIVITY = 'CALL_ACTIVITY' } +// 节点类型到分类的映射 +export const NODE_CATEGORY_MAP: Record = { + [NodeType.START_EVENT]: NodeCategory.EVENT, + [NodeType.END_EVENT]: NodeCategory.EVENT, + [NodeType.USER_TASK]: NodeCategory.TASK, + [NodeType.SERVICE_TASK]: NodeCategory.TASK, + [NodeType.SCRIPT_TASK]: NodeCategory.TASK, + [NodeType.DEPLOY_NODE]: NodeCategory.TASK, + [NodeType.JENKINS_BUILD]: NodeCategory.TASK, + [NodeType.GATEWAY_NODE]: NodeCategory.GATEWAY, + [NodeType.SUB_PROCESS]: NodeCategory.CONTAINER, + [NodeType.CALL_ACTIVITY]: NodeCategory.CONTAINER, +}; + +// 获取节点分类的工具函数 +export const getNodeCategory = (nodeType: NodeType | string): NodeCategory => { + return NODE_CATEGORY_MAP[nodeType as NodeType] || NodeCategory.TASK; +}; + +// JSON Schema 定义 +/** + * JSON Schema 接口 - 直接使用 Formily 官方提供的 ISchema 类型 + * 从 @formily/react 导入(它会从 @formily/json-schema 重新导出) + */ +import type { ISchema } from '@formily/react'; +export type JSONSchema = ISchema; + +// UI 配置 +export interface NodeSize { + width: number; + height: number; +} + +export interface NodeStyle { + fill: string; + icon: string; + stroke: string; + iconColor: string; + strokeWidth: number; + iconSize?: number; + borderRadius?: string; + boxShadow?: string; + transition?: string; + fontSize?: string; + fontWeight?: string; + fontFamily?: string; +} + +export interface UIConfig { + size: NodeSize; + style: NodeStyle; +} // 基础节点定义(只有基本配置) export interface BaseNodeDefinition { @@ -150,5 +108,10 @@ export interface NodeInstanceData { // UI位置信息 position: { x: number; y: number }; - uiConfig: UIConfig; // 包含运行时可能更新的UI配置,如位置 + uiConfig: UIConfig; } + +// 判断是否为可配置节点 +export const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => { + return 'inputMappingSchema' in def || 'outputMappingSchema' in def; +}; diff --git a/frontend/src/pages/Workflow2/Design/types.ts b/frontend/src/pages/Workflow/Design/types.ts similarity index 100% rename from frontend/src/pages/Workflow2/Design/types.ts rename to frontend/src/pages/Workflow/Design/types.ts diff --git a/frontend/src/pages/Workflow/Design/utils/graph/eventHandlers.ts b/frontend/src/pages/Workflow/Design/utils/graph/eventHandlers.ts deleted file mode 100644 index 9a82b657..00000000 --- a/frontend/src/pages/Workflow/Design/utils/graph/eventHandlers.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { Graph, Cell, Edge } from '@antv/x6'; -import { message, Modal } from 'antd'; -import { createRoot } from 'react-dom/client'; -import { Dropdown } from 'antd'; -import React from 'react'; -import type { NodeDefinitionResponse } from "../../nodes/nodeService"; - -/** - * 节点样式管理 - */ -export class NodeStyleManager { - private static hoverStyle = { - strokeWidth: 2, - stroke: '#52c41a' // 绿色 - }; - - // 保存节点的原始样式 - static saveNodeOriginalStyle(node: any) { - const data = node.getData(); - if (!data?.originalStyle) { - const originalStyle = { - stroke: node.getAttrByPath('body/stroke') || '#5F95FF', - strokeWidth: node.getAttrByPath('body/strokeWidth') || 1 - }; - node.setData({ - ...data, - originalStyle - }); - } - } - - // 获取节点的原始样式 - static getNodeOriginalStyle(node: any) { - const data = node.getData(); - return data?.originalStyle || { - stroke: '#5F95FF', - strokeWidth: 2 - }; - } - - // 恢复节点的原始样式 - static resetNodeStyle(node: any) { - const originalStyle = this.getNodeOriginalStyle(node); - node.setAttrByPath('body/stroke', originalStyle.stroke); - node.setAttrByPath('body/strokeWidth', originalStyle.strokeWidth); - } - - // 应用悬停样式 - static applyHoverStyle(node: any) { - this.saveNodeOriginalStyle(node); - node.setAttrByPath('body/stroke', this.hoverStyle.stroke); - node.setAttrByPath('body/strokeWidth', this.hoverStyle.strokeWidth); - } -} - -/** - * 连接桩管理 - */ -export class PortManager { - static showPorts(nodeId: string) { - const ports = document.querySelectorAll(`[data-cell-id="${nodeId}"] .x6-port-body`); - ports.forEach((port) => { - port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;'); - }); - } - - static hidePorts(nodeId: string) { - const ports = document.querySelectorAll(`[data-cell-id="${nodeId}"] .x6-port-body`); - ports.forEach((port) => { - port.setAttribute('style', 'visibility: hidden'); - }); - } - - static hideAllPorts() { - const ports = document.querySelectorAll('.x6-port-body'); - ports.forEach((port) => { - port.setAttribute('style', 'visibility: hidden'); - }); - } - - static showAllPorts() { - const ports = document.querySelectorAll('.x6-port-body'); - ports.forEach((port) => { - const portGroup = port.getAttribute('port-group'); - if (portGroup !== 'top') { - port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;'); - } - }); - } -} - -/** - * 右键菜单管理 - */ -export class ContextMenuManager { - static createNodeContextMenu( - e: MouseEvent, - cell: Cell, - graph: Graph, - nodeDefinitions: NodeDefinitionResponse[], - onEdit: (cell: Cell, nodeDefinition?: NodeDefinitionResponse) => void - ) { - e.preventDefault(); - graph.cleanSelection(); - graph.select(cell); - - const dropdownContainer = document.createElement('div'); - dropdownContainer.style.position = 'absolute'; - dropdownContainer.style.left = `${e.clientX}px`; - dropdownContainer.style.top = `${e.clientY}px`; - document.body.appendChild(dropdownContainer); - - const root = createRoot(dropdownContainer); - let isOpen = true; - - const closeMenu = () => { - isOpen = false; - root.render( - React.createElement(Dropdown, { - menu: { items }, - open: false, - onOpenChange: (open: boolean) => { - if (!open) { - setTimeout(() => { - root.unmount(); - document.body.removeChild(dropdownContainer); - }, 100); - } - } - }, React.createElement('div')) - ); - }; - - const items = [ - { - key: '1', - label: '编辑', - onClick: () => { - closeMenu(); - const nodeDefinition = nodeDefinitions.find(def => def.nodeType === cell.getProp('nodeType')); - onEdit(cell, nodeDefinition); - } - }, - { - key: '2', - label: '删除', - onClick: () => { - closeMenu(); - Modal.confirm({ - title: '确认删除', - content: '确定要删除该节点吗?', - onOk: () => { - cell.remove(); - } - }); - } - } - ]; - - root.render( - React.createElement(Dropdown, { - menu: { items }, - open: isOpen - }, React.createElement('div')) - ); - - const handleClickOutside = (e: MouseEvent) => { - if (!dropdownContainer.contains(e.target as Node)) { - closeMenu(); - document.removeEventListener('click', handleClickOutside); - } - }; - document.addEventListener('click', handleClickOutside); - } - - static createEdgeContextMenu( - e: MouseEvent, - edge: Edge, - graph: Graph, - onEditCondition: (edge: Edge) => void - ) { - e.preventDefault(); - graph.cleanSelection(); - graph.select(edge); - - const sourceNode = graph.getCellById(edge.getSourceCellId()); - const isFromGateway = sourceNode.getProp('nodeType') === 'GATEWAY_NODE'; - - const dropdownContainer = document.createElement('div'); - dropdownContainer.style.position = 'absolute'; - dropdownContainer.style.left = `${e.clientX}px`; - dropdownContainer.style.top = `${e.clientY}px`; - document.body.appendChild(dropdownContainer); - - const root = createRoot(dropdownContainer); - let isOpen = true; - - const closeMenu = () => { - isOpen = false; - root.render( - React.createElement(Dropdown, { - menu: { items }, - open: false, - onOpenChange: (open: boolean) => { - if (!open) { - setTimeout(() => { - root.unmount(); - document.body.removeChild(dropdownContainer); - }, 100); - } - } - }, React.createElement('div')) - ); - }; - - const items = [ - // 只有网关节点的出线才显示编辑条件选项 - ...(isFromGateway ? [{ - key: 'edit', - label: '编辑条件', - onClick: () => { - closeMenu(); - onEditCondition(edge); - } - }] : []), - { - key: 'delete', - label: '删除', - danger: true, - onClick: () => { - closeMenu(); - Modal.confirm({ - title: '确认删除', - content: '确定要删除该连接线吗?', - onOk: () => { - edge.remove(); - } - }); - } - } - ]; - - root.render( - React.createElement(Dropdown, { - menu: { items }, - open: isOpen - }, React.createElement('div')) - ); - - const handleClickOutside = (e: MouseEvent) => { - if (!dropdownContainer.contains(e.target as Node)) { - closeMenu(); - document.removeEventListener('click', handleClickOutside); - } - }; - document.addEventListener('click', handleClickOutside); - } -} - -/** - * 边样式管理 - */ -export class EdgeStyleManager { - static readonly defaultEdgeAttrs = { - line: { - stroke: '#5F95FF', - strokeWidth: 2, - targetMarker: { - name: 'classic', - size: 7, - }, - }, - }; - - static readonly hoverEdgeAttrs = { - line: { - stroke: '#52c41a', - strokeWidth: 2, - }, - }; - - static applyDefaultStyle(edge: Edge) { - edge.setAttrs(this.defaultEdgeAttrs); - } - - static applyHoverStyle(edge: Edge) { - edge.setAttrs(this.hoverEdgeAttrs); - } - - static addEdgeTools(edge: Edge) { - edge.addTools([ - { - name: 'vertices', // 顶点工具 - args: { - padding: 4, - attrs: { - fill: '#fff', - stroke: '#5F95FF', - strokeWidth: 2, - } - } - }, - { - name: 'segments', // 线段工具 - args: { - attrs: { - fill: '#fff', - stroke: '#5F95FF', - strokeWidth: 2, - } - } - } - ]); - } -} - -/** - * 选择状态管理 - */ -export class SelectionManager { - static applySelectionStyle(cell: Cell) { - if (cell.isNode()) { - cell.setAttrByPath('body/stroke', '#1890ff'); - cell.setAttrByPath('body/strokeWidth', 2); - cell.setAttrByPath('body/strokeDasharray', '5 5'); - } else if (cell.isEdge()) { - cell.setAttrByPath('line/stroke', '#1890ff'); - cell.setAttrByPath('line/strokeWidth', 2); - cell.setAttrByPath('line/strokeDasharray', '5 5'); - } - } - - static removeSelectionStyle(cell: Cell) { - if (cell.isNode()) { - cell.setAttrByPath('body/stroke', '#5F95FF'); - cell.setAttrByPath('body/strokeWidth', 2); - cell.setAttrByPath('body/strokeDasharray', null); - } else if (cell.isEdge()) { - cell.setAttrByPath('line/stroke', '#5F95FF'); - cell.setAttrByPath('line/strokeWidth', 2); - cell.setAttrByPath('line/strokeDasharray', null); - } - } - - static selectAll(graph: Graph) { - const cells = graph.getCells(); - if (cells.length === 0) { - message.info('当前没有可选择的元素'); - return; - } - graph.resetSelection(); - graph.select(cells); - // 为选中的元素添加高亮样式 - cells.forEach(cell => { - this.applySelectionStyle(cell); - }); - } -} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/utils/graph/eventRegistrar.ts b/frontend/src/pages/Workflow/Design/utils/graph/eventRegistrar.ts deleted file mode 100644 index d05683df..00000000 --- a/frontend/src/pages/Workflow/Design/utils/graph/eventRegistrar.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Graph, Edge } from '@antv/x6'; -import type { WorkflowNodeDefinition } from "../../nodes/types"; -import { NodeStyleManager, PortManager, ContextMenuManager, EdgeStyleManager, SelectionManager } from './eventHandlers'; - -/** - * 事件注册器 - 负责注册所有图形事件 - */ -export class EventRegistrar { - private graph: Graph; - private nodeDefinitions: WorkflowNodeDefinition[]; - private onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void; - private onEdgeEdit: (edge: Edge) => void; - - constructor( - graph: Graph, - nodeDefinitions: WorkflowNodeDefinition[], - onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void, - onEdgeEdit: (edge: Edge) => void - ) { - this.graph = graph; - this.nodeDefinitions = nodeDefinitions; - this.onNodeEdit = onNodeEdit; - this.onEdgeEdit = onEdgeEdit; - } - - /** - * 注册所有事件 - */ - registerAllEvents() { - this.registerNodeEvents(); - this.registerEdgeEvents(); - this.registerSelectionEvents(); - this.registerCanvasEvents(); - this.registerHistoryEvents(); - } - - /** - * 注册节点相关事件 - */ - private registerNodeEvents() { - // 节点悬停事件 - this.graph.on('node:mouseenter', ({node}) => { - NodeStyleManager.applyHoverStyle(node); - PortManager.showPorts(node.id); - }); - - this.graph.on('node:mouseleave', ({node}) => { - NodeStyleManager.resetNodeStyle(node); - PortManager.hidePorts(node.id); - }); - - // 节点拖动事件 - this.graph.on('node:drag:start', ({node}) => { - NodeStyleManager.saveNodeOriginalStyle(node); - }); - - this.graph.on('node:moved', ({node}) => { - NodeStyleManager.resetNodeStyle(node); - PortManager.hidePorts(node.id); - }); - - // 节点点击事件 - this.graph.on('node:click', ({node}) => { - const selectedNode = this.graph.getSelectedCells()[0]; - if (selectedNode && selectedNode.isNode() && selectedNode.id !== node.id) { - NodeStyleManager.resetNodeStyle(selectedNode); - } - this.graph.resetSelection(); - this.graph.select(node); - }); - - // 节点双击事件 - this.graph.on('node:dblclick', ({node}) => { - const nodeType = node.getProp('nodeType'); - const nodeDefinition = this.nodeDefinitions.find(def => def.nodeType === nodeType); - if (nodeDefinition) { - this.onNodeEdit(node, nodeDefinition); - } - }); - - // 节点右键菜单 - this.graph.on('node:contextmenu', ({cell, e}) => { - ContextMenuManager.createNodeContextMenu( - e as MouseEvent, - cell, - this.graph, - this.nodeDefinitions, - this.onNodeEdit - ); - }); - } - - /** - * 注册边相关事件 - */ - private registerEdgeEvents() { - // 边连接事件 - this.graph.on('edge:connected', ({edge}) => { - EdgeStyleManager.applyDefaultStyle(edge); - }); - - // 边悬停事件 - this.graph.on('edge:mouseenter', ({edge}) => { - EdgeStyleManager.applyHoverStyle(edge); - PortManager.showAllPorts(); - }); - - this.graph.on('edge:mouseleave', ({edge}) => { - EdgeStyleManager.applyDefaultStyle(edge); - PortManager.hideAllPorts(); - }); - - // 边选择事件 - this.graph.on('edge:selected', ({edge}) => { - EdgeStyleManager.addEdgeTools(edge); - }); - - this.graph.on('edge:unselected', ({edge}) => { - edge.removeTools(); - }); - - // 边移动事件 - this.graph.on('edge:moved', ({edge, terminal}) => { - if (!edge || !terminal) return; - - const isSource = terminal.type === 'source'; - const source = isSource ? terminal : edge.getSource(); - const target = isSource ? edge.getTarget() : terminal; - - if (source && target) { - edge.remove(); - this.graph.addEdge({ - source: { - cell: source.cell, - port: source.port, - }, - target: { - cell: target.cell, - port: target.port, - }, - attrs: EdgeStyleManager.defaultEdgeAttrs, - }); - } - }); - - // 边改变事件 - this.graph.on('edge:change:source edge:change:target', ({edge}) => { - if (edge) { - EdgeStyleManager.applyDefaultStyle(edge); - } - }); - - // 边右键菜单 - this.graph.on('edge:contextmenu', ({cell, e}) => { - ContextMenuManager.createEdgeContextMenu( - e as MouseEvent, - cell as Edge, - this.graph, - this.onEdgeEdit - ); - }); - } - - /** - * 注册选择相关事件 - */ - private registerSelectionEvents() { - this.graph.on('selection:changed', ({selected, removed}) => { - // 处理新选中的元素 - selected.forEach(cell => { - SelectionManager.applySelectionStyle(cell); - }); - - // 处理取消选中的元素 - removed.forEach(cell => { - SelectionManager.removeSelectionStyle(cell); - }); - }); - } - - /** - * 注册画布事件 - */ - private registerCanvasEvents() { - // 点击空白处事件 - this.graph.on('blank:click', () => { - const selectedNode = this.graph.getSelectedCells()[0]; - if (selectedNode && selectedNode.isNode()) { - NodeStyleManager.resetNodeStyle(selectedNode); - } - this.graph.resetSelection(); - }); - - // 禁用默认右键菜单 - this.graph.on('blank:contextmenu', ({e}) => { - e.preventDefault(); - }); - } - - /** - * 注册历史记录事件 - */ - private registerHistoryEvents() { - this.graph.on('cell:added', () => { - // 可以在这里添加额外的逻辑 - }); - - this.graph.on('cell:removed', () => { - // 可以在这里添加额外的逻辑 - }); - - this.graph.on('cell:changed', () => { - // 可以在这里添加额外的逻辑 - }); - } -} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/utils/graph/graphConfig.ts b/frontend/src/pages/Workflow/Design/utils/graph/graphConfig.ts deleted file mode 100644 index 05f28344..00000000 --- a/frontend/src/pages/Workflow/Design/utils/graph/graphConfig.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Graph } from '@antv/x6'; - -/** - * X6 图形基础配置 - */ -export const createGraphConfig = (container: HTMLElement): Graph.Options => ({ - container, - grid: { - size: 10, - visible: true, - type: 'dot', - args: { - color: '#a0a0a0', - thickness: 1, - }, - }, - connecting: { - snap: true, // 连线时自动吸附 - allowBlank: false, // 禁止连线到空白位置 - allowLoop: false, // 禁止自环 - allowNode: false, // 禁止连接到节点(只允许连接到连接桩) - allowEdge: false, // 禁止边连接到边 - connector: { - name: 'rounded', - args: { - radius: 8 - } - }, - router: { - name: 'manhattan', - args: { - padding: 1 - } - }, - validateMagnet({magnet}) { - return magnet.getAttribute('port-group') !== 'top'; - }, - validateEdge() { - return true; - } - }, - highlighting: { - magnetAvailable: { - name: 'stroke', - args: { - padding: 4, - attrs: { - strokeWidth: 4, - stroke: '#52c41a', - }, - }, - }, - }, - // clipboard: { - // enabled: true, - // }, - selecting: { - enabled: true, - multiple: true, - rubberband: true, - movable: true, - showNodeSelectionBox: false, // 禁用节点选择框 - showEdgeSelectionBox: false, // 禁用边选择框 - selectNodeOnMoved: false, - selectEdgeOnMoved: false, - }, - snapline: true, - keyboard: { - enabled: true, - global: false, - }, - panning: { - enabled: true, - eventTypes: ['rightMouseDown'], // 右键按下时启用画布拖拽 - }, - mousewheel: { - enabled: true, - modifiers: ['ctrl', 'meta'], - minScale: 0.2, - maxScale: 2, - }, - edgeMovable: true, // 允许边移动 - edgeLabelMovable: true, // 允许边标签移动 - vertexAddable: true, // 允许添加顶点 - vertexMovable: true, // 允许顶点移动 - vertexDeletable: true, // 允许删除顶点 -}); - -/** - * 连接验证逻辑 - */ -export const createValidateConnection = (graph: Graph) => { - return ({sourceCell, targetCell, sourceMagnet, targetMagnet}: any) => { - if (sourceCell === targetCell) { - return false; // 禁止自连接 - } - if (!sourceMagnet || !targetMagnet) { - return false; // 需要有效的连接点 - } - - // 获取源节点和目标节点的类型 - const sourceNodeType = sourceCell.getProp('nodeType'); - const targetNodeType = targetCell.getProp('nodeType'); - - // 如果源节点或目标节点是网关类型,允许多条连线 - if (sourceNodeType === 'GATEWAY_NODE' || targetNodeType === 'GATEWAY_NODE') { - return true; - } - - // 对于其他类型的节点,检查是否已存在连接 - const edges = graph.getEdges(); - const exists = edges.some(edge => { - const source = edge.getSource(); - const target = edge.getTarget(); - return ( - (source as any).cell === sourceCell.id && - (target as any).cell === targetCell.id && - (source as any).port === sourceMagnet.getAttribute('port') && - (target as any).port === targetMagnet.getAttribute('port') - ); - }); - return !exists; - }; -}; - -/** - * 小地图配置 - */ -export const createMiniMapConfig = ( - container: HTMLElement, - width: number, - height: number, - scale: number -) => ({ - container, - width, - height, - padding: 5, - scalable: false, - minScale: scale * 0.8, - maxScale: scale * 1.2, - graphOptions: { - connecting: { - connector: 'rounded', - connectionPoint: 'anchor', - router: { - name: 'manhattan', - }, - }, - async: true, - frozen: true, - interacting: false, - grid: false - }, - viewport: { - padding: 0, - fitToContent: false, - initialPosition: { - x: 0, - y: 0 - }, - initialScale: scale - } -}); diff --git a/frontend/src/pages/Workflow/Design/utils/graph/graphInitializer.ts b/frontend/src/pages/Workflow/Design/utils/graph/graphInitializer.ts deleted file mode 100644 index bd9eedb2..00000000 --- a/frontend/src/pages/Workflow/Design/utils/graph/graphInitializer.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Graph } from '@antv/x6'; -import { Selection } from '@antv/x6-plugin-selection'; -import { MiniMap } from '@antv/x6-plugin-minimap'; -import { Clipboard } from '@antv/x6-plugin-clipboard'; -import { History } from '@antv/x6-plugin-history'; -import { Transform } from '@antv/x6-plugin-transform'; -import { createGraphConfig, createValidateConnection, createMiniMapConfig } from './graphConfig'; - -/** - * X6 图形初始化器 - */ -export class GraphInitializer { - private graphContainer: HTMLElement; - private minimapContainer: HTMLElement; - - constructor(graphContainer: HTMLElement, minimapContainer: HTMLElement) { - this.graphContainer = graphContainer; - this.minimapContainer = minimapContainer; - } - - /** - * 初始化图形实例 - */ - initializeGraph(): Graph { - // 获取主画布容器尺寸 - const containerWidth = this.graphContainer.clientWidth; - const containerHeight = this.graphContainer.clientHeight; - - // 计算小地图尺寸 - const MINIMAP_BASE_WIDTH = 200; - const minimapWidth = MINIMAP_BASE_WIDTH; - const minimapHeight = Math.round((MINIMAP_BASE_WIDTH * containerHeight) / containerWidth); - const scale = minimapWidth / containerWidth; - - // 设置CSS变量 - this.setCSSVariables(minimapWidth, minimapHeight); - - // 创建图形配置 - const config = createGraphConfig(this.graphContainer); - const graph = new Graph(config); - - // 设置连接验证 - (graph as any).options.connecting.validateConnection = createValidateConnection(graph); - - // 注册插件 - this.registerPlugins(graph, minimapWidth, minimapHeight, scale); - - // 设置键盘事件 - this.setupKeyboardEvents(graph); - - return graph; - } - - /** - * 设置CSS变量 - */ - private setCSSVariables(minimapWidth: number, minimapHeight: number) { - document.documentElement.style.setProperty('--minimap-width', `${minimapWidth}px`); - document.documentElement.style.setProperty('--minimap-height', `${minimapHeight}px`); - } - - /** - * 注册插件 - */ - private registerPlugins(graph: Graph, minimapWidth: number, minimapHeight: number, scale: number) { - // History 插件 - const history = new History({ - enabled: true, - beforeAddCommand(event: any, args: any) { - return true; - }, - afterExecuteCommand: () => {}, - afterUndo: () => {}, - afterRedo: () => {}, - }); - - // Selection 插件 - const selection = new Selection({ - enabled: true, - multiple: true, - rubberband: true, - movable: true, - showNodeSelectionBox: false, - showEdgeSelectionBox: false, - selectNodeOnMoved: false, - selectEdgeOnMoved: false, - multipleSelectionModifiers: ['ctrl', 'meta'], - pointerEvents: 'auto' - }); - - // MiniMap 插件 - const minimapConfig = createMiniMapConfig(this.minimapContainer, minimapWidth, minimapHeight, scale); - const minimap = new MiniMap(minimapConfig); - - // Transform 插件 - const transform = new Transform({ - resizing: false, - rotating: false, - }); - - // 注册插件 - graph.use(selection); - graph.use(minimap); - graph.use(new Clipboard()); - graph.use(history); - graph.use(transform); - - // 扩展 graph 对象,添加 history 属性 - (graph as any).history = history; - - // 设置变换事件监听 - this.setupTransformListener(graph, minimap, scale); - } - - /** - * 设置变换监听 - */ - private setupTransformListener(graph: Graph, minimap: MiniMap, scale: number) { - graph.on('transform', () => { - if (minimap) { - const mainViewport = graph.getView().getVisibleArea(); - (minimap as any).viewport.scale = scale; - (minimap as any).viewport.center(mainViewport.center); - } - }); - } - - /** - * 设置键盘事件 - */ - private setupKeyboardEvents(graph: Graph) { - this.graphContainer?.addEventListener('keydown', (e: KeyboardEvent) => { - // 只有当画布或其子元素被聚焦时才处理快捷键 - if (!this.graphContainer?.contains(document.activeElement)) { - return; - } - - // Ctrl+A 或 Command+A (Mac) - if ((e.ctrlKey || e.metaKey) && e.key === 'a') { - e.preventDefault(); // 阻止浏览器默认的全选行为 - if (!graph) return; - - const cells = graph.getCells(); - if (cells.length > 0) { - graph.resetSelection(); - graph.select(cells); - // 为选中的元素添加高亮样式 - cells.forEach(cell => { - if (cell.isNode()) { - cell.setAttrByPath('body/stroke', '#1890ff'); - cell.setAttrByPath('body/strokeWidth', 3); - cell.setAttrByPath('body/strokeDasharray', '5 5'); - } else if (cell.isEdge()) { - cell.setAttrByPath('line/stroke', '#1890ff'); - cell.setAttrByPath('line/strokeWidth', 3); - cell.setAttrByPath('line/strokeDasharray', '5 5'); - } - }); - } - } - }); - - // 确保画布可以接收键盘事件 - this.graphContainer?.setAttribute('tabindex', '0'); - } -} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/utils/nodeUtils.ts b/frontend/src/pages/Workflow/Design/utils/nodeUtils.ts deleted file mode 100644 index 1f5c533c..00000000 --- a/frontend/src/pages/Workflow/Design/utils/nodeUtils.ts +++ /dev/null @@ -1,125 +0,0 @@ -import {Graph} from '@antv/x6'; -import {WorkflowNodeDefinition, NodeInstanceData} from '../nodes/types'; - -/** - * 从节点定义创建新节点 - */ -export const createNodeFromDefinition = ( - graph: Graph, - nodeDefinition: WorkflowNodeDefinition, - position: { x: number; y: number } -) => { - const {uiConfig} = nodeDefinition; - - // 创建节点配置 - 直接使用X6原生形状名称 - const nodeConfig = { - shape: uiConfig.shape, - x: position.x, - y: position.y, - width: uiConfig.size.width, - height: uiConfig.size.height, - attrs: { - body: { - ...uiConfig.style, - ...(uiConfig.shape === 'polygon' ? { - refPoints: '0,10 10,0 20,10 10,20', - } : {}) - }, - label: { - text: nodeDefinition.nodeName, - fontSize: 12, - fill: '#000' - }, - }, - ports: convertPortConfig(uiConfig.ports), - // 同时设置为props和data,方便访问 - nodeType: nodeDefinition.nodeType, - nodeCode: nodeDefinition.nodeCode, - data: { - nodeType: nodeDefinition.nodeType, - nodeCode: nodeDefinition.nodeCode, - nodeName: nodeDefinition.nodeName, - // 新创建的节点,configs等数据为空 - configs: {}, - inputMapping: {}, - outputMapping: {} - } - }; - - return graph.addNode(nodeConfig); -}; - -/** - * 从保存的数据恢复节点 - */ -export const restoreNodeFromData = ( - graph: Graph, - nodeData: NodeInstanceData, - nodeDefinition: WorkflowNodeDefinition -) => { - // 从节点定义中获取UI配置,兼容旧数据中的uiConfig - const uiConfig = nodeData.uiConfig || nodeDefinition.uiConfig; - - // 创建节点配置 - 直接使用X6原生形状名称 - const nodeConfig = { - id: nodeData.nodeCode, // 使用保存的ID - shape: uiConfig.shape, - x: nodeData.position.x, - y: nodeData.position.y, - width: uiConfig.size.width, - height: uiConfig.size.height, - attrs: { - body: { - ...uiConfig.style, - ...(uiConfig.shape === 'polygon' ? { - refPoints: '0,10 10,0 20,10 10,20', - } : {}) - }, - label: { - text: nodeData.nodeName, - fontSize: 12, - fill: '#000' - }, - }, - ports: convertPortConfig(uiConfig.ports), - // 同时设置为props和data,方便访问 - nodeType: nodeData.nodeType, - nodeCode: nodeData.nodeCode, - data: { - nodeType: nodeData.nodeType, - nodeCode: nodeData.nodeCode, - nodeName: nodeData.nodeName, - // 统一使用configs字段名 - configs: nodeData.configs || {}, - inputMapping: nodeData.inputMapping || {}, - outputMapping: nodeData.outputMapping || {} - } - }; - - return graph.addNode(nodeConfig); -}; - -/** - * 转换端口配置为X6格式 - */ -const convertPortConfig = (ports: any) => { - if (!ports?.groups) return {items: []}; - - const groups: any = {}; - const items: any[] = []; - - Object.entries(ports.groups).forEach(([key, group]: [string, any]) => { - groups[key] = { - position: group.position, - attrs: { - circle: { - ...group.attrs.circle, - magnet: true, - } - } - }; - items.push({group: key}); - }); - - return {groups, items}; -}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Design/utils/validator.ts b/frontend/src/pages/Workflow/Design/utils/validator.ts index 8df3c1fc..c69ad562 100644 --- a/frontend/src/pages/Workflow/Design/utils/validator.ts +++ b/frontend/src/pages/Workflow/Design/utils/validator.ts @@ -1,72 +1,33 @@ -import { Graph, Cell } from '@antv/x6'; +import type { FlowNode, FlowEdge } from '../types'; +import { NodeType } from '../types'; -interface ValidationResult { +/** + * 验证结果接口 + */ +export interface ValidationResult { valid: boolean; message?: string; } /** - * 校验节点配置 - * @param node 节点 - * @param nodeDefinition 节点定义 + * 验证工作流是否为空 */ -const validateNodeConfig = (node: Cell, nodeDefinition: any): ValidationResult => { - const panelVariables = node.getProp('panelVariables'); - const localVariables = node.getProp('localVariables'); - - // 校验面板变量 - if (nodeDefinition?.panelVariablesSchema?.required) { - for (const field of nodeDefinition.panelVariablesSchema.required) { - if (!panelVariables?.[field]) { - const fieldTitle = nodeDefinition.panelVariablesSchema.properties[field]?.title || field; - return { - valid: false, - message: `节点 "${node.attr('label/text')}" 的面板变量 "${fieldTitle}" 是必填项` - }; - } - } - } - - // 校验环境变量 - if (nodeDefinition?.localVariablesSchema?.required) { - for (const field of nodeDefinition.localVariablesSchema.required) { - if (!localVariables?.[field]) { - const fieldTitle = nodeDefinition.localVariablesSchema.properties[field]?.title || field; - return { - valid: false, - message: `节点 "${node.attr('label/text')}" 的环境变量 "${fieldTitle}" 是必填项` - }; - } - } - } - - return { valid: true }; -}; - -/** - * 校验流程图是否为空 - * @param graph 流程图实例 - */ -const validateGraphNotEmpty = (graph: Graph): ValidationResult => { - const nodes = graph.getNodes(); +const validateNotEmpty = (nodes: FlowNode[]): ValidationResult => { if (nodes.length === 0) { return { valid: false, - message: '流程图中没有任何节点' + message: '流程图中没有任何节点,请至少添加一个节点' }; } return { valid: true }; }; /** - * 校验必要节点 - * @param graph 流程图实例 + * 验证必需的开始和结束节点 */ -const validateRequiredNodes = (graph: Graph): ValidationResult => { - const nodes = graph.getNodes(); - +const validateRequiredNodes = (nodes: FlowNode[]): ValidationResult => { // 检查开始节点 - const hasStartNode = nodes.some(node => node.getProp('nodeType') === 'START_EVENT'); + const hasStartNode = nodes.some(node => node.data.nodeType === NodeType.START_EVENT); if (!hasStartNode) { return { valid: false, @@ -75,7 +36,7 @@ const validateRequiredNodes = (graph: Graph): ValidationResult => { } // 检查结束节点 - const hasEndNode = nodes.some(node => node.getProp('nodeType') === 'END_EVENT'); + const hasEndNode = nodes.some(node => node.data.nodeType === NodeType.END_EVENT); if (!hasEndNode) { return { valid: false, @@ -87,35 +48,66 @@ const validateRequiredNodes = (graph: Graph): ValidationResult => { }; /** - * 校验节点连接 - * @param graph 流程图实例 + * 验证节点连接完整性 + * 检查是否有孤立的节点(除了开始和结束节点可能只有单向连接) */ -const validateNodeConnections = (graph: Graph): ValidationResult => { - const nodes = graph.getNodes(); - const edges = graph.getEdges(); - - if (edges.length < nodes.length - 1) { - return { - valid: false, - message: '存在未连接的节点,请确保所有节点都已正确连接' - }; +const validateNodeConnections = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => { + if (nodes.length <= 1) { + return { valid: true }; // 单节点或空流程不需要验证连接 } - return { valid: true }; -}; - -/** - * 校验所有节点配置 - * @param graph 流程图实例 - */ -const validateAllNodesConfig = (graph: Graph): ValidationResult => { - const nodes = graph.getNodes(); + // 构建节点连接映射 + const nodeConnections = new Map(); - for (const node of nodes) { - const nodeDefinition = node.getProp('nodeDefinition'); - const result = validateNodeConfig(node, nodeDefinition); - if (!result.valid) { - return result; + nodes.forEach(node => { + nodeConnections.set(node.id, { incoming: 0, outgoing: 0 }); + }); + + edges.forEach(edge => { + const source = nodeConnections.get(edge.source); + const target = nodeConnections.get(edge.target); + + if (source) source.outgoing++; + if (target) target.incoming++; + }); + + // 检查每个节点的连接情况 + for (const [nodeId, connections] of nodeConnections.entries()) { + const node = nodes.find(n => n.id === nodeId); + if (!node) continue; + + const nodeType = node.data.nodeType; + + // 开始节点必须有出边 + if (nodeType === NodeType.START_EVENT && connections.outgoing === 0) { + return { + valid: false, + message: `开始节点"${node.data.label}"没有连接到任何其他节点` + }; + } + + // 结束节点必须有入边 + if (nodeType === NodeType.END_EVENT && connections.incoming === 0) { + return { + valid: false, + message: `结束节点"${node.data.label}"没有被任何节点连接` + }; + } + + // 中间节点必须既有入边又有出边 + if (nodeType !== NodeType.START_EVENT && nodeType !== NodeType.END_EVENT) { + if (connections.incoming === 0) { + return { + valid: false, + message: `节点"${node.data.label}"没有输入连接,请确保它被其他节点连接` + }; + } + if (connections.outgoing === 0) { + return { + valid: false, + message: `节点"${node.data.label}"没有输出连接,请确保它连接到其他节点` + }; + } } } @@ -123,20 +115,88 @@ const validateAllNodesConfig = (graph: Graph): ValidationResult => { }; /** - * 校验整个流程图 - * @param graph 流程图实例 + * 验证节点配置完整性 + * 检查节点是否有必填配置 */ -export const validateWorkflow = (graph: Graph): ValidationResult => { - // 按顺序执行所有验证 +const validateNodeConfigs = (nodes: FlowNode[]): ValidationResult => { + for (const node of nodes) { + // 检查节点是否有配置数据 + if (!node.data.configs) { + return { + valid: false, + message: `节点"${node.data.label}"缺少配置信息,请配置该节点` + }; + } + + // 检查节点名称 + if (!node.data.configs.nodeName || node.data.configs.nodeName.trim() === '') { + return { + valid: false, + message: `节点"${node.data.label}"的名称不能为空` + }; + } + + // 检查节点编码 + if (!node.data.configs.nodeCode || node.data.configs.nodeCode.trim() === '') { + return { + valid: false, + message: `节点"${node.data.label}"的编码不能为空` + }; + } + } + + return { valid: true }; +}; + +/** + * 验证边条件配置 + * 如果边配置了条件,确保条件有效 + */ +const validateEdgeConditions = (edges: FlowEdge[]): ValidationResult => { + for (const edge of edges) { + const condition = edge.data?.condition; + + if (condition) { + // 如果是表达式类型,检查表达式是否为空 + if (condition.type === 'EXPRESSION') { + if (!condition.expression || condition.expression.trim() === '') { + return { + valid: false, + message: `边"${edge.id}"配置了表达式条件,但表达式为空` + }; + } + } + + // 检查优先级是否有效 + if (condition.priority < 1 || condition.priority > 999) { + return { + valid: false, + message: `边"${edge.id}"的优先级必须在1-999之间` + }; + } + } + } + + return { valid: true }; +}; + +/** + * 完整的工作流验证 + * 依次执行所有验证规则,任何一个失败就返回失败 + */ +export const validateWorkflow = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => { + // 定义验证规则链 const validators = [ - validateGraphNotEmpty, - validateRequiredNodes, - validateAllNodesConfig, - validateNodeConnections + () => validateNotEmpty(nodes), + () => validateRequiredNodes(nodes), + () => validateNodeConnections(nodes, edges), + () => validateNodeConfigs(nodes), + () => validateEdgeConditions(edges), ]; + // 依次执行验证 for (const validator of validators) { - const result = validator(graph); + const result = validator(); if (!result.valid) { return result; } @@ -144,3 +204,4 @@ export const validateWorkflow = (graph: Graph): ValidationResult => { return { valid: true }; }; + diff --git a/frontend/src/pages/Workflow2/Definition/components/EditModal.tsx b/frontend/src/pages/Workflow2/Definition/components/EditModal.tsx deleted file mode 100644 index 658b5a50..00000000 --- a/frontend/src/pages/Workflow2/Definition/components/EditModal.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { useEffect } from 'react'; -import { Modal, Form, Input, Select, message } from 'antd'; -import * as service from '../service'; -import type { WorkflowDefinition, WorkflowCategory } from '../types'; - -interface EditModalProps { - visible: boolean; - onClose: () => void; - onSuccess: () => void; - record?: WorkflowDefinition; -} - -const EditModal: React.FC = ({ - visible, - onClose, - onSuccess, - record -}) => { - const [form] = Form.useForm(); - const [loading, setLoading] = React.useState(false); - const [categories, setCategories] = React.useState([]); - - // 加载工作流分类 - useEffect(() => { - const loadCategories = async () => { - try { - const data = await service.getWorkflowCategories(); - setCategories(data); - } catch (error) { - console.error('加载工作流分类失败:', error); - } - }; - - if (visible) { - loadCategories(); - } - }, [visible]); - - // 设置表单初始值 - useEffect(() => { - if (visible && record) { - form.setFieldsValue({ - name: record.name, - key: record.key, - description: record.description, - category: record.category, - triggers: record.triggers || [] - }); - } else if (visible) { - form.resetFields(); - } - }, [visible, record, form]); - - const handleSubmit = async () => { - try { - const values = await form.validateFields(); - setLoading(true); - - if (record) { - await service.updateDefinition(record.id, values); - message.success('更新成功'); - } else { - await service.createDefinition({ - ...values, - status: 'DRAFT', - graph: { nodes: [], edges: [] }, - formConfig: { formItems: [] } - }); - message.success('创建成功'); - } - - onSuccess(); - onClose(); - } catch (error) { - console.error('保存失败:', error); - if (error instanceof Error) { - message.error(error.message); - } - } finally { - setLoading(false); - } - }; - - return ( - -
- - - - - - - - - - - - - -