This commit is contained in:
dengqichen 2025-10-20 16:12:41 +08:00
parent b26216a619
commit 978a24790a
16 changed files with 862 additions and 919 deletions

View File

@ -1,207 +1,182 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import {Tabs, TabsProps} from 'antd'; import { Tabs } from 'antd';
import {Cell} from '@antv/x6'; import { Cell } from '@antv/x6';
import type {NodeDefinitionResponse} from "../nodes/nodeService"; import type { WorkflowNodeDefinition, ConfigurableNodeDefinition } from "../nodes/types";
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
SheetFooter,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import {Button} from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {Input} from "@/components/ui/input";
import {Label} from "@/components/ui/label";
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils'; import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
import { Textarea } from "@/components/ui/textarea"; import { BetaSchemaForm } from '@ant-design/pro-components';
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface NodeConfigDrawerProps { interface NodeConfigModalProps {
visible: boolean; visible: boolean;
node: Cell | null; node: Cell | null;
nodeDefinition: NodeDefinitionResponse | null; nodeDefinition: WorkflowNodeDefinition | null;
onOk: (values: any) => void; onOk: (values: any) => void;
onCancel: () => void; onCancel: () => void;
} }
interface Variables { interface FormData {
[key: string]: any; config?: Record<string, any>;
inputMapping?: Record<string, any>;
outputMapping?: Record<string, any>;
} }
const NodeConfigDrawer: React.FC<NodeConfigDrawerProps> = ({ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible, visible,
node, node,
nodeDefinition, nodeDefinition,
onOk, onOk,
onCancel, onCancel,
}) => { }) => {
const [panelValues, setPanelValues] = React.useState<Variables>({}); const [formData, setFormData] = useState<FormData>({});
const [localValues, setLocalValues] = React.useState<Variables>({}); const [activeTab, setActiveTab] = useState('config');
// 判断是否为可配置节点
const isConfigurableNode = (def: WorkflowNodeDefinition): def is ConfigurableNodeDefinition => {
return 'inputMappingSchema' in def || 'outputMappingSchema' in def;
};
useEffect(() => { useEffect(() => {
if (nodeDefinition) { if (nodeDefinition && node) {
// 使用修改后的 schema 获取字段配置 // 从节点数据中获取现有配置
const panelColumns = convertJsonSchemaToColumns(nodeDefinition.panelVariablesSchema || { type: 'object', properties: {} }); const nodeData = node.getData() || {};
const localColumns = convertJsonSchemaToColumns(nodeDefinition.localVariablesSchema || { type: 'object', properties: {} }); setFormData({
// 初始化表单值,包括默认值 config: nodeData.config || {},
const initialPanelValues = panelColumns.reduce((acc, column) => { inputMapping: nodeData.inputMapping || {},
if (column.initialValue !== undefined) { outputMapping: nodeData.outputMapping || {},
acc[column.dataIndex] = column.initialValue;
}
return acc;
}, {} as Variables);
const initialLocalValues = localColumns.reduce((acc, column) => {
if (column.initialValue !== undefined) {
acc[column.dataIndex] = column.initialValue;
}
return acc;
}, {} as Variables);
// 设置初始值
setPanelValues({
...initialPanelValues,
...(nodeDefinition.panelVariables || {}),
// 如果是新节点,使用节点的 nodeType 和 nodeName 作为默认值
...(node && {
code: nodeDefinition.nodeType,
name: nodeDefinition.nodeName
})
});
setLocalValues({
...initialLocalValues,
...(nodeDefinition.localVariables || {})
}); });
} else {
setFormData({});
} }
}, [nodeDefinition]); }, [nodeDefinition, node]);
const handleSubmit = () => { const handleSubmit = () => {
const updatedNodeDefinition = { onOk(formData);
...nodeDefinition,
panelVariables: panelValues,
localVariables: localValues
};
onOk(updatedNodeDefinition);
}; };
const renderFormField = (column: any, value: any, onChange: (value: any) => void) => { const handleConfigChange = (values: Record<string, any>) => {
console.log('renderFormField 被调用:', { column, value }); setFormData(prev => ({
...prev,
// 特殊处理 code 字段 config: values
const isCodeField = column.dataIndex === 'code'; }));
const isReadOnly = isCodeField || column.readonly === true;
const props = {
value: value || '',
onChange: (e: any) => !isReadOnly && onChange(e.target?.value ?? e),
placeholder: column.fieldProps?.placeholder,
disabled: isReadOnly,
readOnly: isReadOnly,
className: `${isReadOnly ? "bg-gray-100" : ""} ${column.fieldProps?.className || ""}`,
}; };
switch (column.valueType) { const handleInputMappingChange = (values: Record<string, any>) => {
case 'select': setFormData(prev => ({
return ( ...prev,
<Select inputMapping: values
value={value} }));
onValueChange={onChange} };
disabled={column.readonly === true}
> const handleOutputMappingChange = (values: Record<string, any>) => {
<SelectTrigger> setFormData(prev => ({
<SelectValue placeholder={column.fieldProps?.placeholder} /> ...prev,
</SelectTrigger> outputMapping: values
<SelectContent> }));
{column.fieldProps?.options?.map((option: any) => ( };
<SelectItem key={option.value} value={option.value}>
{option.label} if (!nodeDefinition) {
</SelectItem> return null;
))} }
</SelectContent>
</Select> // 构建Tabs配置
); const tabItems = [
case 'switch': {
return ( key: 'config',
<Switch label: '基本配置',
checked={value} children: (
onCheckedChange={onChange} <div style={{ padding: '16px 0' }}>
disabled={column.readonly === true} <BetaSchemaForm
layoutType="Form"
columns={convertJsonSchemaToColumns(nodeDefinition.configSchema as any)}
initialValues={formData.config}
onValuesChange={(_, allValues) => handleConfigChange(allValues)}
submitter={false}
/> />
);
case 'textarea':
case 'code':
return <Textarea {...props} />;
case 'digit':
return <Input {...props} type="number" />;
default:
return <Input {...props} />;
}
};
const renderFormFields = (schema: any, values: Variables, onChange: (key: string, value: any) => void) => {
if (!schema?.properties) return null;
const columns = convertJsonSchemaToColumns(schema);
return columns.map(column => (
<div key={column.dataIndex} className="space-y-2">
<Label>{column.title}</Label>
{renderFormField(
column,
values[column.dataIndex],
(value) => onChange(column.dataIndex, value)
)}
</div> </div>
)); ),
};
const handlePanelChange = (key: string, value: any) => {
setPanelValues(prev => ({...prev, [key]: value}));
};
const handleLocalChange = (key: string, value: any) => {
setLocalValues(prev => ({...prev, [key]: value}));
};
const items: TabsProps['items'] = [
nodeDefinition?.panelVariablesSchema && {
key: 'panel',
label: '面板变量',
children: (
<div className="space-y-4">
{renderFormFields(nodeDefinition.panelVariablesSchema, panelValues, handlePanelChange)}
</div>
)
}, },
nodeDefinition?.localVariablesSchema && { ];
key: 'local',
label: '环境变量', // 如果是可配置节点添加输入和输出映射TAB
if (isConfigurableNode(nodeDefinition)) {
if (nodeDefinition.inputMappingSchema) {
tabItems.push({
key: 'input',
label: '输入映射',
children: ( children: (
<div className="space-y-4"> <div style={{ padding: '16px 0' }}>
{renderFormFields(nodeDefinition.localVariablesSchema, localValues, handleLocalChange)} <BetaSchemaForm
layoutType="Form"
columns={convertJsonSchemaToColumns(nodeDefinition.inputMappingSchema as any)}
initialValues={formData.inputMapping}
onValuesChange={(_, allValues) => handleInputMappingChange(allValues)}
submitter={false}
/>
</div> </div>
) ),
});
}
if (nodeDefinition.outputMappingSchema) {
tabItems.push({
key: 'output',
label: '输出映射',
children: (
<div style={{ padding: '16px 0' }}>
<BetaSchemaForm
layoutType="Form"
columns={convertJsonSchemaToColumns(nodeDefinition.outputMappingSchema as any)}
initialValues={formData.outputMapping}
onValuesChange={(_, allValues) => handleOutputMappingChange(allValues)}
submitter={false}
/>
</div>
),
});
}
} }
].filter(Boolean) as TabsProps['items'];
return ( return (
<Sheet open={visible} onOpenChange={(open) => !open && onCancel()}> <Sheet open={visible} onOpenChange={(open) => !open && onCancel()}>
<SheetContent side="right" className="w-[480px] sm:w-[480px]"> <SheetContent
<div className="flex flex-col h-full"> style={{
width: '600px',
maxWidth: '90vw',
display: 'flex',
flexDirection: 'column'
}}
>
<SheetHeader> <SheetHeader>
<SheetTitle> - {nodeDefinition?.nodeName || ''}</SheetTitle> <SheetTitle>
- {nodeDefinition.nodeName}
</SheetTitle>
</SheetHeader> </SheetHeader>
<div className="flex-1 overflow-y-auto py-6">
<Tabs items={items} className="border-none"/> <div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
className="flex-tabs"
/>
</div> </div>
<div className="shrink-0 border-t bg-background p-4">
<div className="flex justify-end gap-2"> <div
style={{
borderTop: '1px solid #f0f0f0',
padding: '16px 0 0 0',
display: 'flex',
justifyContent: 'flex-end',
gap: '8px'
}}
>
<Button variant="outline" onClick={onCancel}> <Button variant="outline" onClick={onCancel}>
</Button> </Button>
@ -209,11 +184,9 @@ const NodeConfigDrawer: React.FC<NodeConfigDrawerProps> = ({
</Button> </Button>
</div> </div>
</div>
</div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
); );
}; };
export default NodeConfigDrawer; export default NodeConfigModal;

View File

@ -13,7 +13,7 @@ import {
BranchesOutlined BranchesOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import {getNodeDefinitionList} from "../nodes/nodeService"; import {getNodeDefinitionList} from "../nodes/nodeService";
import type {NodeDefinitionResponse} from "../nodes/nodeService"; import type {WorkflowNodeDefinition} from "../nodes/types";
// 使用 Collapse 组件,不需要解构 Panel // 使用 Collapse 组件,不需要解构 Panel
@ -56,13 +56,13 @@ const categoryConfig: Record<NodeCategory, {
}; };
interface NodePanelProps { interface NodePanelProps {
onNodeDragStart?: (node: NodeDefinitionResponse, e: React.DragEvent) => void, onNodeDragStart?: (node: WorkflowNodeDefinition, e: React.DragEvent) => void,
nodeDefinitions?: NodeDefinitionResponse[] nodeDefinitions?: WorkflowNodeDefinition[]
} }
const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => { const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [nodeDefinitions, setNodeDefinitions] = useState<NodeDefinitionResponse[]>([]); const [nodeDefinitions, setNodeDefinitions] = useState<WorkflowNodeDefinition[]>([]);
// 加载节点定义列表 // 加载节点定义列表
const loadNodeDefinitions = async () => { const loadNodeDefinitions = async () => {
@ -91,17 +91,17 @@ const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
} }
acc[node.category].push(node); acc[node.category].push(node);
return acc; return acc;
}, {} as Record<NodeCategory, NodeDefinitionResponse[]>); }, {} as Record<NodeCategory, WorkflowNodeDefinition[]>);
// 处理节点拖拽开始事件 // 处理节点拖拽开始事件
const handleDragStart = (node: NodeDefinitionResponse, e: React.DragEvent) => { const handleDragStart = (node: WorkflowNodeDefinition, e: React.DragEvent) => {
e.dataTransfer.setData('node', JSON.stringify(node)); e.dataTransfer.setData('node', JSON.stringify(node));
onNodeDragStart?.(node, e); onNodeDragStart?.(node, e);
}; };
// 渲染节点图标 // 渲染节点图标
const renderNodeIcon = (node: NodeDefinitionResponse) => { const renderNodeIcon = (node: WorkflowNodeDefinition) => {
const iconName = node.uiVariables?.style.icon; const iconName = node.uiConfig?.style.icon;
// 首先尝试使用配置的图标 // 首先尝试使用配置的图标
let IconComponent = iconMap[iconName]; let IconComponent = iconMap[iconName];
@ -113,7 +113,7 @@ const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
return ( return (
<IconComponent <IconComponent
style={{ style={{
color: node.uiVariables?.style.iconColor || '#1890ff', color: node.uiConfig?.style.iconColor || '#1890ff',
fontSize: '16px', fontSize: '16px',
marginRight: '6px' marginRight: '6px'
}} }}
@ -121,17 +121,17 @@ const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
); );
}; };
const getNodeItemStyle = (node: NodeDefinitionResponse) => ({ const getNodeItemStyle = (node: WorkflowNodeDefinition) => ({
width: '100%', width: '100%',
padding: '10px 12px', padding: '10px 12px',
border: `1px solid ${node.uiVariables?.style.stroke}`, border: `1px solid ${node.uiConfig?.style.stroke}`,
borderRadius: '6px', borderRadius: '6px',
cursor: 'move', cursor: 'move',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: '10px', gap: '10px',
background: node.uiVariables?.style.fill, background: node.uiConfig?.style.fill,
transition: 'all 0.3s', transition: 'all 0.3s',
boxShadow: '0 1px 2px rgba(0,0,0,0.05)', boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
'&:hover': { '&:hover': {
@ -163,7 +163,7 @@ const NodePanel: React.FC<NodePanelProps> = ({onNodeDragStart}) => {
}}> }}>
{groupedNodes[category as NodeCategory]?.map(node => ( {groupedNodes[category as NodeCategory]?.map(node => (
<Tooltip <Tooltip
key={node.id} key={node.nodeCode}
title={ title={
<div> <div>
<div style={{fontSize: '14px', fontWeight: 500}}>{node.description}</div> <div style={{fontSize: '14px', fontWeight: 500}}>{node.description}</div>

View File

@ -91,8 +91,6 @@ export const CONNECTING_CONFIG = {
name: 'manhattan' name: 'manhattan'
}, },
validateConnection({ validateConnection({
sourceView,
targetView,
sourceMagnet, sourceMagnet,
targetMagnet targetMagnet
}: any) { }: any) {

View File

@ -4,14 +4,14 @@ import { Graph } from '@antv/x6';
import { getDefinitionDetail, saveDefinition } from '../../Definition/service'; import { getDefinitionDetail, saveDefinition } from '../../Definition/service';
import { getNodeDefinitionList } from '../nodes/nodeService'; import { getNodeDefinitionList } from '../nodes/nodeService';
import { validateWorkflow } from '../utils/validator'; import { validateWorkflow } from '../utils/validator';
import { addNodeToGraph } from '../utils/nodeUtils'; import { restoreNodeFromData } from '../utils/nodeUtils';
import type { NodeDefinitionResponse } from '../nodes/nodeService'; import type { WorkflowNodeDefinition } from '../nodes/types';
/** /**
* Hook * Hook
*/ */
export const useWorkflowData = () => { export const useWorkflowData = () => {
const [nodeDefinitions, setNodeDefinitions] = useState<NodeDefinitionResponse[]>([]); const [nodeDefinitions, setNodeDefinitions] = useState<WorkflowNodeDefinition[]>([]);
const [isNodeDefinitionsLoaded, setIsNodeDefinitionsLoaded] = useState(false); const [isNodeDefinitionsLoaded, setIsNodeDefinitionsLoaded] = useState(false);
const [definitionData, setDefinitionData] = useState<any>(null); const [definitionData, setDefinitionData] = useState<any>(null);
const [title, setTitle] = useState<string>('工作流设计'); const [title, setTitle] = useState<string>('工作流设计');
@ -56,19 +56,21 @@ export const useWorkflowData = () => {
// 创建节点 // 创建节点
response.graph?.nodes?.forEach((existingNode: any) => { response.graph?.nodes?.forEach((existingNode: any) => {
console.log('正在还原节点:', existingNode.nodeType, existingNode); console.log('正在还原节点:', existingNode.nodeType, existingNode);
const node = addNodeToGraph(false, graphInstance, existingNode, nodeDefinitions);
// 查找节点定义
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === existingNode.nodeType);
if (!nodeDefinition) {
console.error('找不到节点定义:', existingNode.nodeType);
return;
}
// 恢复节点
const node = restoreNodeFromData(graphInstance, existingNode, nodeDefinition);
if (!node) { if (!node) {
console.error('节点创建失败:', existingNode); console.error('节点创建失败:', existingNode);
return; return;
} }
// 只设置 graph 属性
node.setProp('graph', {
uiVariables: existingNode.uiVariables,
panelVariables: existingNode.panelVariables,
localVariables: existingNode.localVariables
});
nodeMap.set(existingNode.id, node); nodeMap.set(existingNode.id, node);
console.log('节点创建成功ID:', node.id, '映射 ID:', existingNode.id); console.log('节点创建成功ID:', node.id, '映射 ID:', existingNode.id);
}); });
@ -199,27 +201,23 @@ export const useWorkflowData = () => {
// 获取所有节点和边的数据 // 获取所有节点和边的数据
const nodes = graph.getNodes().map(node => { const nodes = graph.getNodes().map(node => {
const nodeType = node.getProp('nodeType'); const nodeData = node.getData();
const graphData = node.getProp('graph') || {}; const nodeDefinition = nodeDefinitions.find(def => def.nodeType === nodeData.nodeType);
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === nodeType);
const position = node.getPosition(); const position = node.getPosition();
const {
uiVariables,
panelVariables
} = graphData;
return { return {
id: node.id, id: node.id,
nodeCode: nodeType, nodeCode: nodeData.nodeCode,
nodeType: nodeType, nodeType: nodeData.nodeType,
nodeName: node.attr('label/text'), nodeName: nodeData.nodeName,
uiVariables: { position,
...uiVariables, uiConfig: {
position: position ...(nodeDefinition?.uiConfig || {}),
position
}, },
panelVariables, config: nodeData.config || {},
localVariables: nodeDefinition?.localVariablesSchema || {} inputMapping: nodeData.inputMapping || {},
outputMapping: nodeData.outputMapping || {}
}; };
}); });
@ -240,14 +238,15 @@ export const useWorkflowData = () => {
}; };
}); });
// 收集并合并所有节点的 localVariablesSchema // 收集并合并所有节点的输入映射Schema (用于全局变量)
const allLocalSchemas = nodes const allInputSchemas = nodes
.map(node => { .map(node => {
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === node.nodeType); const nodeDefinition = nodeDefinitions.find(def => def.nodeType === node.nodeType);
return nodeDefinition?.localVariablesSchema; // 检查是否为可配置节点并有输入映射Schema
return 'inputMappingSchema' in nodeDefinition! ? nodeDefinition.inputMappingSchema : null;
}) })
.filter(schema => schema); // 过滤掉空值 .filter(schema => schema); // 过滤掉空值
const mergedLocalSchema = mergeLocalVariablesSchemas(allLocalSchemas); const mergedLocalSchema = mergeLocalVariablesSchemas(allInputSchemas);
// 构建保存数据 // 构建保存数据
const saveData = { const saveData = {

View File

@ -1,17 +1,17 @@
import { Graph } from '@antv/x6'; import { Graph } from '@antv/x6';
import { message } from 'antd'; import { message } from 'antd';
import { addNodeToGraph } from '../utils/nodeUtils'; import { createNodeFromDefinition } from '../utils/nodeUtils';
import type { NodeDefinitionResponse } from '../nodes/nodeService'; import type { WorkflowNodeDefinition } from '../nodes/types';
/** /**
* Hook * Hook
*/ */
export const useWorkflowDragDrop = ( export const useWorkflowDragDrop = (
graph: Graph | null, graph: Graph | null,
nodeDefinitions: NodeDefinitionResponse[] nodeDefinitions: WorkflowNodeDefinition[]
) => { ) => {
// 处理节点拖拽开始 // 处理节点拖拽开始
const handleNodeDragStart = (node: NodeDefinitionResponse, e: React.DragEvent) => { const handleNodeDragStart = (node: WorkflowNodeDefinition, e: React.DragEvent) => {
e.dataTransfer.setData('node', JSON.stringify(node)); e.dataTransfer.setData('node', JSON.stringify(node));
}; };
@ -24,10 +24,10 @@ export const useWorkflowDragDrop = (
const nodeData = e.dataTransfer.getData('node'); const nodeData = e.dataTransfer.getData('node');
if (!nodeData) return; if (!nodeData) return;
const node = JSON.parse(nodeData); const nodeDefinition = JSON.parse(nodeData);
const {clientX, clientY} = e; const {clientX, clientY} = e;
const point = graph.clientToLocal({x: clientX, y: clientY}); const point = graph.clientToLocal({x: clientX, y: clientY});
addNodeToGraph(true, graph, node, nodeDefinitions, point); createNodeFromDefinition(graph, nodeDefinition, point);
} catch (error) { } catch (error) {
console.error('创建节点失败:', error); console.error('创建节点失败:', error);
message.error('创建节点失败'); message.error('创建节点失败');

View File

@ -3,15 +3,15 @@ import { Graph, Edge } from '@antv/x6';
import { message } from 'antd'; import { message } from 'antd';
import { GraphInitializer } from '../utils/graph/graphInitializer'; import { GraphInitializer } from '../utils/graph/graphInitializer';
import { EventRegistrar } from '../utils/graph/eventRegistrar'; import { EventRegistrar } from '../utils/graph/eventRegistrar';
import type { NodeDefinitionResponse } from '../nodes/nodeService'; import type { WorkflowNodeDefinition } from '../nodes/types';
/** /**
* Hook * Hook
*/ */
export const useWorkflowGraph = ( export const useWorkflowGraph = (
nodeDefinitions: NodeDefinitionResponse[], nodeDefinitions: WorkflowNodeDefinition[],
isNodeDefinitionsLoaded: boolean, isNodeDefinitionsLoaded: boolean,
onNodeEdit: (cell: any, nodeDefinition?: NodeDefinitionResponse) => void, onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void,
onEdgeEdit: (edge: Edge) => void onEdgeEdit: (edge: Edge) => void
) => { ) => {
const [graph, setGraph] = useState<Graph | null>(null); const [graph, setGraph] = useState<Graph | null>(null);

View File

@ -1,7 +1,7 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { Cell, Edge } from '@antv/x6'; import { Cell, Edge } from '@antv/x6';
import { message } from 'antd'; import { message } from 'antd';
import type { NodeDefinitionResponse } from '../nodes/nodeService'; import type { WorkflowNodeDefinition } from '../nodes/types';
import type { EdgeCondition } from '../types'; import type { EdgeCondition } from '../types';
/** /**
@ -9,13 +9,13 @@ import type { EdgeCondition } from '../types';
*/ */
export const useWorkflowModals = () => { export const useWorkflowModals = () => {
const [selectedNode, setSelectedNode] = useState<Cell | null>(null); const [selectedNode, setSelectedNode] = useState<Cell | null>(null);
const [selectedNodeDefinition, setSelectedNodeDefinition] = useState<NodeDefinitionResponse | null>(null); const [selectedNodeDefinition, setSelectedNodeDefinition] = useState<WorkflowNodeDefinition | null>(null);
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null); const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
const [configModalVisible, setConfigModalVisible] = useState(false); const [configModalVisible, setConfigModalVisible] = useState(false);
const [expressionModalVisible, setExpressionModalVisible] = useState(false); const [expressionModalVisible, setExpressionModalVisible] = useState(false);
// 处理节点编辑 // 处理节点编辑
const handleNodeEdit = useCallback((cell: Cell, nodeDefinition?: NodeDefinitionResponse) => { const handleNodeEdit = useCallback((cell: Cell, nodeDefinition?: WorkflowNodeDefinition) => {
setSelectedNode(cell); setSelectedNode(cell);
setSelectedNodeDefinition(nodeDefinition || null); setSelectedNodeDefinition(nodeDefinition || null);
setConfigModalVisible(true); setConfigModalVisible(true);
@ -28,15 +28,20 @@ export const useWorkflowModals = () => {
}, []); }, []);
// 处理节点配置更新 // 处理节点配置更新
const handleNodeConfigUpdate = useCallback((updatedNodeDefinition: any) => { const handleNodeConfigUpdate = useCallback((formData: any) => {
if (!selectedNode) return; if (!selectedNode) return;
// 设置节点的 graph 属性,将所有数据统一放在 graph 下 // 更新节点数据
selectedNode.setProp('graph', updatedNodeDefinition); const nodeData = selectedNode.getData() || {};
const updatedData = {
...nodeData,
...formData
};
selectedNode.setData(updatedData);
// 更新节点显示名称(如果存在) // 更新节点显示名称(如果配置中有nodeName
if (updatedNodeDefinition.panelVariables?.name) { if (formData.config?.nodeName) {
selectedNode.attr('label/text', updatedNodeDefinition.panelVariables.name); selectedNode.attr('label/text', formData.config.nodeName);
} }
setConfigModalVisible(false); setConfigModalVisible(false);

View File

@ -0,0 +1,76 @@
/**
*
* Schema格式和运行时key/value格式之间转换
*/
import {
ConfigurableNodeDefinition,
NodeInstanceData,
NodeFormData,
WorkflowNodeDefinition,
UIConfig
} from './types';
export class NodeDataConverter {
/**
*
*/
static isConfigurableNode(node: WorkflowNodeDefinition): node is ConfigurableNodeDefinition {
return 'inputMappingSchema' in node || 'outputMappingSchema' in node;
}
/**
*
*/
static convertToSaveFormat(
nodeDefinition: WorkflowNodeDefinition,
formData: NodeFormData,
position: { x: number; y: number },
uiConfig: UIConfig
): NodeInstanceData {
const baseData = {
nodeCode: nodeDefinition.nodeCode,
nodeName: nodeDefinition.nodeName,
nodeType: nodeDefinition.nodeType,
category: nodeDefinition.category,
description: nodeDefinition.description,
position,
uiConfig,
config: formData.config || {},
};
// 如果是可配置节点,添加输入输出映射
if (this.isConfigurableNode(nodeDefinition)) {
return {
...baseData,
inputMapping: formData.inputMapping || {},
outputMapping: formData.outputMapping || {}
};
}
return baseData;
}
/**
*
*/
static convertToFormFormat(
nodeDefinition: WorkflowNodeDefinition,
savedData: NodeInstanceData
): NodeFormData {
const formData: NodeFormData = {
config: savedData.config || {}
};
// 如果是可配置节点,添加输入输出映射
if (this.isConfigurableNode(nodeDefinition)) {
return {
...formData,
inputMapping: savedData.inputMapping || {},
outputMapping: savedData.outputMapping || {}
};
}
return formData;
}
}

View File

@ -1,106 +1,18 @@
import { WorkflowNodeDefinition, NodeType, NodeCategory } from '../types'; import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/** /**
* *
* *
*/ */
export const DeployNode: WorkflowNodeDefinition = { export const DeployNode: ConfigurableNodeDefinition = {
nodeCode: "DEPLOY_NODE", nodeCode: "DEPLOY_NODE",
nodeName: "构建任务", nodeName: "构建任务",
nodeType: NodeType.DEPLOY_NODE, nodeType: NodeType.DEPLOY_NODE,
category: NodeCategory.TASK, category: NodeCategory.TASK,
description: "执行应用构建和部署任务,支持多种构建工具和部署环境", description: "执行应用构建和部署任务",
// 面板变量配置 - 用户在节点配置面板中可以设置的参数 // UI 配置 - 节点在画布上的显示样式
panelVariablesSchema: { uiConfig: {
type: "object",
title: "构建任务配置",
description: "配置构建任务的基本信息和参数",
properties: {
delegate: {
type: "string",
title: "委派者",
description: "执行构建任务的委派者标识",
readOnly: true,
default: "${deployNodeDelegate}"
},
code: {
type: "string",
title: "节点编码",
description: "工作流节点的唯一编码标识",
minLength: 2,
maxLength: 50,
pattern: "^[A-Z0-9_]+$"
},
name: {
type: "string",
title: "节点名称",
description: "工作流节点的显示名称",
minLength: 1,
maxLength: 100
},
description: {
type: "string",
title: "节点描述",
description: "详细描述该节点的功能和用途",
maxLength: 500
},
buildType: {
type: "string",
title: "构建类型",
description: "选择构建工具类型",
enum: ["MAVEN", "GRADLE", "NPM", "DOCKER"],
enumNames: ["Maven", "Gradle", "NPM", "Docker"],
default: "MAVEN"
},
timeout: {
type: "number",
title: "超时时间(分钟)",
description: "构建任务的超时时间,超过该时间将自动终止",
minimum: 1,
maximum: 180,
default: 30
}
},
required: ["code", "name"]
},
// 本地变量配置 - 节点执行过程中的内部变量
localVariablesSchema: {
type: "object",
title: "本地变量",
description: "节点执行过程中使用的内部变量",
properties: {
buildStartTime: {
type: "string",
title: "构建开始时间",
description: "记录构建任务开始的时间戳"
},
buildEndTime: {
type: "string",
title: "构建结束时间",
description: "记录构建任务结束的时间戳"
},
buildResult: {
type: "string",
title: "构建结果",
description: "构建任务的执行结果",
enum: ["SUCCESS", "FAILED", "TIMEOUT"],
enumNames: ["成功", "失败", "超时"]
},
buildLog: {
type: "string",
title: "构建日志",
description: "构建过程的详细日志信息"
}
}
},
// 表单变量配置 - 用户触发流程时需要填写的表单字段
formVariablesSchema: null,
// UI 变量配置 - 节点在画布上的显示样式
uiVariables: {
shape: 'rect', shape: 'rect',
size: { size: {
width: 120, width: 120,
@ -111,7 +23,7 @@ export const DeployNode: WorkflowNodeDefinition = {
stroke: '#0050b3', stroke: '#0050b3',
strokeWidth: 2, strokeWidth: 2,
icon: 'build', icon: 'build',
iconColor: '#ffffff' iconColor: '#fff'
}, },
ports: { ports: {
groups: { groups: {
@ -120,7 +32,7 @@ export const DeployNode: WorkflowNodeDefinition = {
attrs: { attrs: {
circle: { circle: {
r: 4, r: 4,
fill: '#ffffff', fill: '#fff',
stroke: '#1890ff' stroke: '#1890ff'
} }
} }
@ -130,7 +42,7 @@ export const DeployNode: WorkflowNodeDefinition = {
attrs: { attrs: {
circle: { circle: {
r: 4, r: 4,
fill: '#ffffff', fill: '#fff',
stroke: '#1890ff' stroke: '#1890ff'
} }
} }
@ -139,6 +51,161 @@ export const DeployNode: WorkflowNodeDefinition = {
} }
}, },
orderNum: 10, // 基本配置Schema - 设计时用于生成表单保存时转为key/value包含基本信息+节点配置)
enabled: true configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息和构建任务的配置参数",
properties: {
// 基本信息
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明"
},
// 节点配置
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"]
}
}; };

View File

@ -1,94 +1,40 @@
import { WorkflowNodeDefinition, NodeType, NodeCategory } from '../types'; import { BaseNodeDefinition, NodeType, NodeCategory } from '../types';
/** /**
* *
* *
*/ */
export const EndEventNode: WorkflowNodeDefinition = { export const EndEventNode: BaseNodeDefinition = {
nodeCode: "END_EVENT", nodeCode: "END_EVENT",
nodeName: "结束", nodeName: "结束",
nodeType: NodeType.END_EVENT, nodeType: NodeType.END_EVENT,
category: NodeCategory.EVENT, category: NodeCategory.EVENT,
description: "工作流的终止节点,标识流程的结束点", description: "工作流的结束点",
panelVariablesSchema: { // UI 配置 - 节点在画布上的显示样式
type: "object", uiConfig: {
title: "结束事件配置",
properties: {
code: {
type: "string",
title: "节点编码",
description: "结束事件节点的编码",
default: "end"
},
name: {
type: "string",
title: "节点名称",
description: "结束事件节点的显示名称",
default: "结束"
},
description: {
type: "string",
title: "节点描述",
description: "描述该结束事件的触发条件或结果"
},
resultType: {
type: "string",
title: "结束类型",
description: "流程结束的类型",
enum: ["SUCCESS", "FAILED", "CANCELLED"],
enumNames: ["成功结束", "失败结束", "取消结束"],
default: "SUCCESS"
}
},
required: ["code", "name"]
},
localVariablesSchema: {
type: "object",
properties: {
endTime: {
type: "string",
title: "结束时间",
description: "流程实例的结束时间"
},
duration: {
type: "number",
title: "执行时长",
description: "流程实例的总执行时长(毫秒)"
},
finalResult: {
type: "string",
title: "最终结果",
description: "流程执行的最终结果"
}
}
},
formVariablesSchema: null,
uiVariables: {
shape: 'circle', shape: 'circle',
size: { size: {
width: 50, width: 40,
height: 50 height: 40
}, },
style: { style: {
fill: '#ff4d4f', fill: '#f5222d',
stroke: '#cf1322', stroke: '#cf1322',
strokeWidth: 2, strokeWidth: 1,
icon: 'stop', icon: 'stop',
iconColor: '#ffffff' iconColor: '#fff'
}, },
ports: { ports: {
groups: { groups: {
// 结束节点只有输入端口
in: { in: {
position: 'left', position: 'left',
attrs: { attrs: {
circle: { circle: {
r: 4, r: 4,
fill: '#ffffff', fill: '#fff',
stroke: '#ff4d4f' stroke: '#f5222d'
} }
} }
} }
@ -96,6 +42,28 @@ export const EndEventNode: WorkflowNodeDefinition = {
} }
}, },
orderNum: 99, // 基本配置Schema - 用于生成"基本配置"TAB包含基本信息
enabled: true configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本配置信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明"
}
},
required: ["nodeName", "nodeCode"]
}
}; };

View File

@ -1,80 +1,39 @@
import { WorkflowNodeDefinition, NodeType, NodeCategory } from '../types'; import { BaseNodeDefinition, NodeType, NodeCategory } from '../types';
/** /**
* *
* *
*/ */
export const StartEventNode: WorkflowNodeDefinition = { export const StartEventNode: BaseNodeDefinition = {
nodeCode: "START_EVENT", nodeCode: "START_EVENT",
nodeName: "开始", nodeName: "开始",
nodeType: NodeType.START_EVENT, nodeType: NodeType.START_EVENT,
category: NodeCategory.EVENT, category: NodeCategory.EVENT,
description: "工作流的起始节点,标识流程的开始点", description: "工作流的起始节点",
panelVariablesSchema: { // UI 配置 - 节点在画布上的显示样式
type: "object", uiConfig: {
title: "开始事件配置",
properties: {
code: {
type: "string",
title: "节点编码",
description: "开始事件节点的编码",
default: "start"
},
name: {
type: "string",
title: "节点名称",
description: "开始事件节点的显示名称",
default: "开始"
},
description: {
type: "string",
title: "节点描述",
description: "描述该开始事件的触发条件或场景"
}
},
required: ["code", "name"]
},
localVariablesSchema: {
type: "object",
properties: {
startTime: {
type: "string",
title: "开始时间",
description: "流程实例的开始时间"
},
processInstanceId: {
type: "string",
title: "流程实例ID",
description: "当前流程实例的唯一标识"
}
}
},
formVariablesSchema: null,
uiVariables: {
shape: 'circle', shape: 'circle',
size: { size: {
width: 50, width: 40,
height: 50 height: 40
}, },
style: { style: {
fill: '#52c41a', fill: '#52c41a',
stroke: '#389e0d', stroke: '#389e08',
strokeWidth: 2, strokeWidth: 1,
icon: 'play-circle', icon: 'play-circle',
iconColor: '#ffffff' iconColor: '#fff'
}, },
ports: { ports: {
groups: { groups: {
// 开始节点只有输出端口
out: { out: {
position: 'right', position: 'right',
attrs: { attrs: {
circle: { circle: {
r: 4, r: 4,
fill: '#ffffff', fill: '#fff',
stroke: '#52c41a' stroke: '#52c41a'
} }
} }
@ -83,6 +42,28 @@ export const StartEventNode: WorkflowNodeDefinition = {
} }
}, },
orderNum: 1, // 基本配置Schema - 用于生成"基本配置"TAB包含基本信息
enabled: true configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本配置信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明"
}
},
required: ["nodeName", "nodeCode"]
}
}; };

View File

@ -1,159 +1,76 @@
// 工作流节点定义注册中心 import { WorkflowNodeDefinition, NodeCategory, NodeType } from '../types';
import { NodeDataConverter } from '../NodeDataConverter';
import { DeployNode } from './DeployNode'; import { DeployNode } from './DeployNode';
import { StartEventNode } from './StartEventNode'; import { StartEventNode } from './StartEventNode';
import { EndEventNode } from './EndEventNode'; import { EndEventNode } from './EndEventNode';
import { WorkflowNodeDefinition, NodeType, NodeCategory } from '../types';
/** /**
* *
*
*/ */
export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
StartEventNode, StartEventNode,
EndEventNode, EndEventNode,
DeployNode, DeployNode,
// 在此处添加新的节点定义... // 在这里添加更多节点定义
]; ];
/** /**
* *
* @param nodeType
* @returns undefined
*/ */
export const getNodeDefinition = (nodeType: NodeType | string): WorkflowNodeDefinition | undefined => { export const getNodeDefinition = (nodeType: NodeType): WorkflowNodeDefinition | undefined => {
return NODE_DEFINITIONS.find(node => node.nodeType === nodeType); return NODE_DEFINITIONS.find(node => node.nodeType === nodeType);
}; };
/** /**
* *
* @param nodeCode
* @returns undefined
*/ */
export const getNodeDefinitionByCode = (nodeCode: string): WorkflowNodeDefinition | undefined => { export const getNodeDefinitionByCode = (nodeCode: string): WorkflowNodeDefinition | undefined => {
return NODE_DEFINITIONS.find(node => node.nodeCode === nodeCode); return NODE_DEFINITIONS.find(node => node.nodeCode === nodeCode);
}; };
/** /**
* *
* @param category
* @returns
*/ */
export const getNodesByCategory = (category: NodeCategory): WorkflowNodeDefinition[] => { export const getNodesByCategory = (category: NodeCategory): WorkflowNodeDefinition[] => {
return NODE_DEFINITIONS.filter(node => node.category === category); return NODE_DEFINITIONS.filter(node => node.category === category);
}; };
/**
*
* @returns
*/
export const getEnabledNodes = (): WorkflowNodeDefinition[] => {
return NODE_DEFINITIONS.filter(node => node.enabled !== false);
};
/** /**
* *
* @returns
*/ */
export const getNodesByCategories = (): Record<NodeCategory, WorkflowNodeDefinition[]> => { export const getNodesByCategories = (): Record<NodeCategory, WorkflowNodeDefinition[]> => {
const grouped = NODE_DEFINITIONS.reduce((acc, node) => { return NODE_DEFINITIONS.reduce((acc, node) => {
if (!acc[node.category]) { if (!acc[node.category]) {
acc[node.category] = []; acc[node.category] = [];
} }
acc[node.category].push(node); acc[node.category].push(node);
return acc; return acc;
}, {} as Record<NodeCategory, WorkflowNodeDefinition[]>); }, {} as Record<NodeCategory, WorkflowNodeDefinition[]>);
// 按 orderNum 排序
Object.keys(grouped).forEach(category => {
grouped[category as NodeCategory].sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
});
return grouped;
}; };
/** /**
* *
* @param nodeDefinition
* @returns
*/ */
export const validateNodeDefinition = (nodeDefinition: WorkflowNodeDefinition): { export const getEnabledNodes = (): WorkflowNodeDefinition[] => {
isValid: boolean; // 目前所有节点都是启用的后续可以添加enabled字段
errors: string[]; return NODE_DEFINITIONS;
} => {
const errors: string[] = [];
// 基础字段验证
if (!nodeDefinition.nodeCode) {
errors.push('节点编码不能为空');
}
if (!nodeDefinition.nodeName) {
errors.push('节点名称不能为空');
}
if (!nodeDefinition.nodeType) {
errors.push('节点类型不能为空');
}
if (!nodeDefinition.category) {
errors.push('节点分类不能为空');
}
// Schema 验证
if (!nodeDefinition.panelVariablesSchema) {
errors.push('面板变量配置不能为空');
} else {
if (!nodeDefinition.panelVariablesSchema.type) {
errors.push('面板变量配置缺少类型定义');
}
if (!nodeDefinition.panelVariablesSchema.properties) {
errors.push('面板变量配置缺少属性定义');
}
}
// UI变量验证
if (!nodeDefinition.uiVariables) {
errors.push('UI变量配置不能为空');
} else {
const ui = nodeDefinition.uiVariables;
if (!ui.shape) {
errors.push('UI配置缺少形状定义');
}
if (!ui.size || !ui.size.width || !ui.size.height) {
errors.push('UI配置缺少尺寸定义');
}
if (!ui.style) {
errors.push('UI配置缺少样式定义');
}
}
return {
isValid: errors.length === 0,
errors
};
}; };
/** /**
* *
* @returns
*/ */
export const getNodeStatistics = () => { export const isConfigurableNode = NodeDataConverter.isConfigurableNode;
const total = NODE_DEFINITIONS.length;
const enabled = getEnabledNodes().length;
const byCategory = getNodesByCategories();
return { /**
total, *
enabled, */
disabled: total - enabled, export {
categories: Object.keys(byCategory).length, StartEventNode,
byCategory: Object.entries(byCategory).map(([category, nodes]) => ({ EndEventNode,
category, DeployNode
count: nodes.length
}))
};
}; };
// 导出所有节点定义(向后兼容) /**
export { DeployNode, StartEventNode, EndEventNode }; *
*/
// 导出类型 export { NodeDataConverter };
export type { WorkflowNodeDefinition, NodeType, NodeCategory } from '../types';

View File

@ -1,173 +1,59 @@
// 节点服务适配器 /**
// 提供与原有API兼容的接口但使用本地节点定义 * - 访
*/
import { import {
NODE_DEFINITIONS, NODE_DEFINITIONS,
getNodeDefinition, getNodeDefinition,
getNodesByCategory, getNodesByCategory,
getEnabledNodes,
getNodesByCategories getNodesByCategories
} from './definitions'; } from './definitions';
import { WorkflowNodeDefinition, NodeType, NodeCategory } from './types'; import {
WorkflowNodeDefinition,
// 兼容原有的响应格式 NodeType,
export interface NodeDefinitionResponse { NodeCategory
id: number; } from './types';
nodeCode: string;
nodeName: string;
nodeType: NodeType;
category: NodeCategory;
description?: string;
panelVariablesSchema: any;
uiVariables: any;
localVariablesSchema?: any;
formVariablesSchema?: any;
panelVariables?: Record<string, any>;
localVariables?: Record<string, any>;
orderNum?: number;
enabled?: boolean;
createTime?: string;
updateTime?: string;
version?: number;
}
/** /**
* *
*/ */
const convertToResponse = (node: WorkflowNodeDefinition, index: number): NodeDefinitionResponse => { export const getNodeDefinitionList = async (): Promise<WorkflowNodeDefinition[]> => {
return { return new Promise((resolve) => {
id: index + 1, // 生成虚拟ID setTimeout(() => {
nodeCode: node.nodeCode, resolve(NODE_DEFINITIONS);
nodeName: node.nodeName, }, 10);
nodeType: node.nodeType, });
category: node.category,
description: node.description,
panelVariablesSchema: node.panelVariablesSchema,
uiVariables: node.uiVariables,
localVariablesSchema: node.localVariablesSchema,
formVariablesSchema: node.formVariablesSchema,
orderNum: node.orderNum || 0,
enabled: node.enabled !== false,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
version: 1
};
}; };
/** /**
* API调用 *
* @returns Promise<NodeDefinitionResponse[]>
*/ */
export const getNodeDefinitionList = async (): Promise<NodeDefinitionResponse[]> => { export const getNodeDefinitionsGroupedByCategory = async (): Promise<Record<NodeCategory, WorkflowNodeDefinition[]>> => {
// 模拟异步操作
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
const nodes = getEnabledNodes(); resolve(getNodesByCategories());
const response = nodes.map((node, index) => convertToResponse(node, index)); }, 10);
resolve(response);
}, 10); // 很短的延迟,模拟网络请求
}); });
}; };
/** /**
* *
* @param nodeType
* @returns Promise<NodeDefinitionResponse | null>
*/ */
export const getNodeDefinitionByType = async (nodeType: NodeType | string): Promise<NodeDefinitionResponse | null> => { export const getNodeDefinitionByType = async (nodeType: NodeType): Promise<WorkflowNodeDefinition | undefined> => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
const node = getNodeDefinition(nodeType); resolve(getNodeDefinition(nodeType));
if (node) {
const index = NODE_DEFINITIONS.findIndex(n => n.nodeType === nodeType);
resolve(convertToResponse(node, index));
} else {
resolve(null);
}
}, 10); }, 10);
}); });
}; };
/** /**
* *
* @returns Promise<Record<NodeCategory, NodeDefinitionResponse[]>>
*/ */
export const getNodeDefinitionsByCategories = async (): Promise<Record<NodeCategory, NodeDefinitionResponse[]>> => { export const getNodeDefinitionsByCategory = async (category: NodeCategory): Promise<WorkflowNodeDefinition[]> => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
const grouped = getNodesByCategories(); resolve(getNodesByCategory(category));
const result = {} as Record<NodeCategory, NodeDefinitionResponse[]>;
Object.entries(grouped).forEach(([category, nodes]) => {
result[category as NodeCategory] = nodes.map((node) =>
convertToResponse(node, NODE_DEFINITIONS.findIndex(n => n.nodeCode === node.nodeCode))
);
});
resolve(result);
}, 10); }, 10);
}); });
}; };
/**
*
* @param category
* @returns Promise<NodeDefinitionResponse[]>
*/
export const getNodeDefinitionsByCategory = async (category: NodeCategory): Promise<NodeDefinitionResponse[]> => {
return new Promise((resolve) => {
setTimeout(() => {
const nodes = getNodesByCategory(category);
const response = nodes.map((node) =>
convertToResponse(node, NODE_DEFINITIONS.findIndex(n => n.nodeCode === node.nodeCode))
);
resolve(response);
}, 10);
});
};
/**
*
* @param nodeType
* @param config
* @returns Promise<{ isValid: boolean; errors: string[] }>
*/
export const validateNodeConfig = async (
nodeType: NodeType | string,
config: Record<string, any>
): Promise<{ isValid: boolean; errors: string[] }> => {
return new Promise((resolve) => {
setTimeout(() => {
const node = getNodeDefinition(nodeType);
if (!node) {
resolve({ isValid: false, errors: ['未知的节点类型'] });
return;
}
const errors: string[] = [];
const required = node.panelVariablesSchema.required || [];
// 验证必填字段
required.forEach(field => {
if (!config[field] || config[field] === '') {
const property = node.panelVariablesSchema.properties[field];
const title = property?.title || field;
errors.push(`${title} 是必填字段`);
}
});
resolve({ isValid: errors.length === 0, errors });
}, 10);
});
};
// 导出节点定义相关工具
export {
NODE_DEFINITIONS,
getNodeDefinition,
getNodesByCategory,
getEnabledNodes,
getNodesByCategories
} from './definitions';
export type { WorkflowNodeDefinition, NodeType, NodeCategory } from './types';

View File

@ -1,6 +1,63 @@
// 工作流节点定义相关类型 import { BaseResponse } from "@/types/base";
// 节点类型枚举 // JSON Schema 定义
export interface JSONSchema {
type: string;
properties?: Record<string, any>;
required?: string[];
title?: string;
description?: string;
}
// UI 变量配置
export interface NodeSize {
width: number;
height: number;
}
export interface PortStyle {
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' | 'diamond';
export interface NodeStyle {
fill: string;
icon: string;
stroke: string;
iconColor: string;
strokeWidth: number;
}
export interface UIConfig {
size: NodeSize;
ports: {
groups: PortGroups;
};
shape: NodeShape;
style: NodeStyle;
position?: { x: number; y: number };
}
// 节点类型和分类(保持原有的枚举格式)
export enum NodeType { export enum NodeType {
START_EVENT = 'START_EVENT', START_EVENT = 'START_EVENT',
END_EVENT = 'END_EVENT', END_EVENT = 'END_EVENT',
@ -13,7 +70,6 @@ export enum NodeType {
CALL_ACTIVITY = 'CALL_ACTIVITY' CALL_ACTIVITY = 'CALL_ACTIVITY'
} }
// 节点分类枚举
export enum NodeCategory { export enum NodeCategory {
EVENT = 'EVENT', EVENT = 'EVENT',
TASK = 'TASK', TASK = 'TASK',
@ -21,98 +77,64 @@ export enum NodeCategory {
CONTAINER = 'CONTAINER' CONTAINER = 'CONTAINER'
} }
// JSON Schema 定义 // 基础节点定义(只有基本配置)
export interface JsonSchemaProperty { export interface BaseNodeDefinition {
type: string; nodeCode: string;
title?: string; nodeName: string;
description?: string; nodeType: NodeType;
default?: any; category: NodeCategory;
readOnly?: boolean; description: string;
enum?: any[]; uiConfig: UIConfig;
enumNames?: string[]; configSchema: JSONSchema; // 基本配置Schema包含基本信息+节点配置)
format?: string;
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
} }
export interface JsonSchema { // 可配置节点定义有3个TAB基本配置、输入、输出
type: string; export interface ConfigurableNodeDefinition extends BaseNodeDefinition {
properties: Record<string, JsonSchemaProperty>; inputMappingSchema?: JSONSchema; // 输入映射的Schema定义
required?: string[]; outputMappingSchema?: JSONSchema; // 输出映射的Schema定义
title?: string;
description?: string;
} }
// 节点样式配置 // 工作流节点定义(联合类型)
export interface NodeStyle { export type WorkflowNodeDefinition = BaseNodeDefinition | ConfigurableNodeDefinition;
fill: string;
stroke: string;
strokeWidth: number;
icon: string;
iconColor: string;
}
// 节点尺寸 // 节点实例数据(运行时)
export interface NodeSize { export interface NodeInstanceData {
width: number;
height: number;
}
// 端口配置
export interface PortConfig {
r: number;
fill: string;
stroke: string;
}
export interface PortGroup {
position: 'left' | 'right' | 'top' | 'bottom';
attrs: {
circle: PortConfig;
};
}
export interface PortGroups {
in?: PortGroup;
out?: PortGroup;
}
// UI 变量配置
export interface UIVariables {
shape: 'rect' | 'circle' | 'diamond';
size: NodeSize;
style: NodeStyle;
ports: {
groups: PortGroups;
};
}
// 工作流节点定义接口
export interface WorkflowNodeDefinition {
nodeCode: string; nodeCode: string;
nodeName: string; nodeName: string;
nodeType: NodeType; nodeType: NodeType;
category: NodeCategory; category: NodeCategory;
description?: string; description?: string;
panelVariablesSchema: JsonSchema;
localVariablesSchema?: JsonSchema; // 运行时数据key/value格式
formVariablesSchema?: JsonSchema | null; config?: Record<string, any>; // 基本配置数据(包含基本信息+节点配置)
uiVariables: UIVariables; inputMapping?: Record<string, any>;
orderNum?: number; outputMapping?: Record<string, any>;
enabled?: boolean;
// UI位置信息
position: { x: number; y: number };
uiConfig: UIConfig; // 包含运行时可能更新的UI配置如位置
} }
// 节点实例数据(运行时) // 兼容现有API的响应格式
export interface WorkflowNodeInstance { export interface NodeDefinitionResponse extends BaseResponse {
id: string;
nodeCode: string; nodeCode: string;
nodeType: NodeType;
nodeName: string; nodeName: string;
position?: { x: number; y: number }; nodeType: NodeType;
category: NodeCategory;
description: string;
uiConfig: UIConfig | null;
configSchema?: JSONSchema | null; // 基本配置Schema
// 兼容字段(保持现有组件正常工作)
panelVariablesSchema?: JSONSchema | null;
localVariablesSchema?: JSONSchema | null;
panelVariables?: Record<string, any>; panelVariables?: Record<string, any>;
localVariables?: Record<string, any>; localVariables?: Record<string, any>;
formVariables?: Record<string, any>; }
// 数据转换器工具类型
export interface NodeFormData {
config?: Record<string, any>; // 基本配置表单数据(包含基本信息+节点配置)
inputMapping?: Record<string, any>;
outputMapping?: Record<string, any>;
} }

View File

@ -1,5 +1,5 @@
import { Graph, Edge } from '@antv/x6'; import { Graph, Edge } from '@antv/x6';
import type { NodeDefinitionResponse } from "../../nodes/nodeService"; import type { WorkflowNodeDefinition } from "../../nodes/types";
import { NodeStyleManager, PortManager, ContextMenuManager, EdgeStyleManager, SelectionManager } from './eventHandlers'; import { NodeStyleManager, PortManager, ContextMenuManager, EdgeStyleManager, SelectionManager } from './eventHandlers';
/** /**
@ -7,14 +7,14 @@ import { NodeStyleManager, PortManager, ContextMenuManager, EdgeStyleManager, Se
*/ */
export class EventRegistrar { export class EventRegistrar {
private graph: Graph; private graph: Graph;
private nodeDefinitions: NodeDefinitionResponse[]; private nodeDefinitions: WorkflowNodeDefinition[];
private onNodeEdit: (cell: any, nodeDefinition?: NodeDefinitionResponse) => void; private onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void;
private onEdgeEdit: (edge: Edge) => void; private onEdgeEdit: (edge: Edge) => void;
constructor( constructor(
graph: Graph, graph: Graph,
nodeDefinitions: NodeDefinitionResponse[], nodeDefinitions: WorkflowNodeDefinition[],
onNodeEdit: (cell: any, nodeDefinition?: NodeDefinitionResponse) => void, onNodeEdit: (cell: any, nodeDefinition?: WorkflowNodeDefinition) => void,
onEdgeEdit: (edge: Edge) => void onEdgeEdit: (edge: Edge) => void
) { ) {
this.graph = graph; this.graph = graph;
@ -44,7 +44,7 @@ export class EventRegistrar {
PortManager.showPorts(node.id); PortManager.showPorts(node.id);
}); });
this.graph.on('node:mouseleave', ({node}) => { this.graph.on('node:mouseleave', ({node}: any) => {
NodeStyleManager.resetNodeStyle(node); NodeStyleManager.resetNodeStyle(node);
PortManager.hidePorts(node.id); PortManager.hidePorts(node.id);
}); });
@ -74,13 +74,7 @@ export class EventRegistrar {
const nodeType = node.getProp('nodeType'); const nodeType = node.getProp('nodeType');
const nodeDefinition = this.nodeDefinitions.find(def => def.nodeType === nodeType); const nodeDefinition = this.nodeDefinitions.find(def => def.nodeType === nodeType);
if (nodeDefinition) { if (nodeDefinition) {
const savedConfig = node.getProp('workflowDefinitionNode'); this.onNodeEdit(node, nodeDefinition);
const mergedNodeDefinition = {
...nodeDefinition,
...savedConfig,
...node.getProp('graph') || {}
};
this.onNodeEdit(node, mergedNodeDefinition);
} }
}); });

View File

@ -1,78 +1,135 @@
import {Graph} from '@antv/x6'; import { Graph } from '@antv/x6';
import {convertPortConfig} from '../constants'; import { WorkflowNodeDefinition, NodeInstanceData } from '../nodes/types';
/** /**
* *
* @param isNew
* @param graph X6 Graph实例
* @param currentNodeDefinition
* @param allNodeDefinitions
* @param position
* @returns
*/ */
export const addNodeToGraph = ( export const createNodeFromDefinition = (
isNew: boolean,
graph: Graph, graph: Graph,
currentNodeDefinition: any, nodeDefinition: WorkflowNodeDefinition,
allNodeDefinitions: any, position: { x: number; y: number }
position?: { x: number; y: number }
) => { ) => {
let nodeDefinition = allNodeDefinitions.find((def: any) => def.nodeType === currentNodeDefinition.nodeType); const { uiConfig } = nodeDefinition;
if (!nodeDefinition) {
console.error('找不到节点定义:', currentNodeDefinition.nodeType);
return null;
}
// 合并UI变量使用节点定义中的完整配置但保留保存数据中的位置信息
let uiVariables = isNew
? nodeDefinition.uiVariables
: {
...nodeDefinition.uiVariables, // 完整的UI配置
...currentNodeDefinition.uiVariables // 保存的位置信息(如果有的话)
};
// 根据形状类型设置正确的 shape // 根据形状类型设置正确的 shape
let shape = 'rect'; // 默认使用矩形 let shape = 'rect';
if (uiVariables.shape === 'circle') { if (uiConfig.shape === 'circle') {
shape = 'circle'; shape = 'circle';
} else if (uiVariables.shape === 'diamond') { } else if (uiConfig.shape === 'diamond') {
shape = 'polygon'; shape = 'polygon';
} }
// 创建节点配置 // 创建节点配置
const nodeConfig: any = { const nodeConfig = {
inherit: 'rect', shape,
width: uiVariables.size.width, x: position.x,
height: uiVariables.size.height, y: position.y,
width: uiConfig.size.width,
height: uiConfig.size.height,
attrs: { attrs: {
body: { body: {
...uiVariables.style, ...uiConfig.style,
...(uiVariables.shape === 'diamond' ? { ...(uiConfig.shape === 'diamond' ? {
refPoints: '0,10 10,0 20,10 10,20', refPoints: '0,10 10,0 20,10 10,20',
} : {}) } : {})
}, },
label: { label: {
text: isNew ? nodeDefinition.nodeName : currentNodeDefinition.nodeName text: nodeDefinition.nodeName,
fontSize: 12,
fill: '#000'
}, },
}, },
shape, ports: convertPortConfig(uiConfig.ports),
nodeType: isNew ? nodeDefinition.nodeType : currentNodeDefinition.nodeType, // 同时设置为props和data方便访问
nodeType: nodeDefinition.nodeType,
nodeCode: nodeDefinition.nodeCode, nodeCode: nodeDefinition.nodeCode,
ports: convertPortConfig(uiVariables.ports) data: {
nodeType: nodeDefinition.nodeType,
nodeCode: nodeDefinition.nodeCode,
nodeName: nodeDefinition.nodeName
}
}; };
// 为还原的节点设置ID保持与保存数据的一致性 return graph.addNode(nodeConfig);
if (!isNew && currentNodeDefinition.id) { };
nodeConfig.id = currentNodeDefinition.id;
} /**
*
const nodePosition = isNew ? position : currentNodeDefinition.uiVariables?.position; */
if (nodePosition) { export const restoreNodeFromData = (
Object.assign(nodeConfig, nodePosition); graph: Graph,
} nodeData: NodeInstanceData,
_nodeDefinition: WorkflowNodeDefinition
console.log('创建节点配置:', nodeConfig); ) => {
const node = graph.addNode(nodeConfig); const { uiConfig } = nodeData;
return node; // 根据形状类型设置正确的 shape
let shape = 'rect';
if (uiConfig.shape === 'circle') {
shape = 'circle';
} else if (uiConfig.shape === 'diamond') {
shape = 'polygon';
}
// 创建节点配置
const nodeConfig = {
id: nodeData.nodeCode, // 使用保存的ID
shape,
x: nodeData.position.x,
y: nodeData.position.y,
width: uiConfig.size.width,
height: uiConfig.size.height,
attrs: {
body: {
...uiConfig.style,
...(uiConfig.shape === 'diamond' ? {
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,
config: nodeData.config,
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 };
}; };